@hackcove/ui 0.2.4 → 0.2.6
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 +65 -6
- package/dist/AppShell.d.ts +27 -2
- package/dist/AppShell.js +25 -2
- package/dist/Badge.d.ts +9 -0
- package/dist/Badge.js +10 -0
- package/dist/Breadcrumbs.d.ts +22 -3
- package/dist/Breadcrumbs.js +23 -2
- package/dist/Button.d.ts +18 -8
- package/dist/Button.js +54 -22
- package/dist/ButtonGroup.d.ts +14 -0
- package/dist/ButtonGroup.js +10 -0
- package/dist/ButtonLink.d.ts +20 -0
- package/dist/ButtonLink.js +18 -0
- package/dist/Callout.d.ts +10 -2
- package/dist/Callout.js +8 -3
- package/dist/Checkbox.d.ts +10 -4
- package/dist/Checkbox.js +8 -3
- package/dist/ColourSchemeToggle.d.ts +14 -3
- package/dist/ColourSchemeToggle.js +13 -9
- package/dist/DateField.d.ts +15 -2
- package/dist/DateField.js +8 -4
- package/dist/DatePicker.d.ts +1 -1
- package/dist/DatePicker.js +3 -3
- package/dist/Drawer.d.ts +29 -0
- package/dist/Drawer.js +26 -0
- package/dist/FileDrop.d.ts +10 -0
- package/dist/FileDrop.js +11 -0
- package/dist/Input.d.ts +3 -2
- package/dist/Input.js +5 -4
- package/dist/KeyValueList.d.ts +11 -4
- package/dist/KeyValueList.js +14 -5
- package/dist/ListRow.d.ts +36 -0
- package/dist/ListRow.js +31 -0
- package/dist/NavTabs.d.ts +15 -4
- package/dist/NavTabs.js +13 -2
- package/dist/PageFade.d.ts +25 -0
- package/dist/PageFade.js +39 -0
- package/dist/ProgressBar.d.ts +26 -0
- package/dist/ProgressBar.js +32 -0
- package/dist/SegmentedControl.d.ts +11 -0
- package/dist/SegmentedControl.js +22 -0
- package/dist/Select.d.ts +3 -2
- package/dist/Select.js +4 -4
- package/dist/SplitBar.d.ts +21 -0
- package/dist/SplitBar.js +15 -0
- package/dist/StatTile.d.ts +16 -0
- package/dist/StatTile.js +21 -0
- package/dist/Table.d.ts +48 -0
- package/dist/Table.js +42 -0
- package/dist/Tabs.d.ts +13 -0
- package/dist/Tabs.js +23 -0
- package/dist/TextLink.d.ts +19 -4
- package/dist/TextLink.js +19 -4
- package/dist/Textarea.d.ts +3 -2
- package/dist/Textarea.js +4 -4
- package/dist/Timeline.d.ts +21 -0
- package/dist/Timeline.js +19 -0
- package/dist/colourScheme.d.ts +11 -2
- package/dist/colourScheme.js +16 -3
- package/dist/control.d.ts +19 -2
- package/dist/control.js +25 -2
- package/dist/index.d.ts +22 -6
- package/dist/index.js +19 -4
- package/dist/link.d.ts +22 -0
- package/dist/link.js +11 -0
- package/dist/switcher.d.ts +66 -0
- package/dist/switcher.js +53 -0
- package/dist/ui.d.ts +25 -5
- package/dist/ui.js +51 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,14 +39,14 @@ export const Example = () => (
|
|
|
39
39
|
|
|
40
40
|
## What is exported
|
|
41
41
|
|
|
42
|
-
- Primitives: `Card
|
|
43
|
-
- Layout (see Layout): `AppShell`, `appShellRootClass`, `Container`, `PageHeader`, `Stack`, `Section`, and the page layouts `ListLayout`, `DetailLayout`, `FormLayout`, `ProseLayout`, `CardGrid`, `SidebarLayout`, `FullBleedLayout`, `CenteredLayout`.
|
|
42
|
+
- Primitives: `Card` (a bordered panel; `title` and `count` make a small heading; `flush` drops the padding and clips the corners, for a table that runs edge to edge; `bar` moves the title, count and `actions` into a tinted strip over the body; `dashed` is an empty-state shell with centred muted text; `as` is `section` (default), `article` or `div`), `EpicHeader`, `Empty`, `PageTitle` (deprecated: use `PageHeader`), `Pill`, `groupByEpic`, `Markdown`.
|
|
43
|
+
- Layout (see Layout): `AppShell`, `AppShellBrand`, `appShellRootClass`, `appShellRootClassFor`, `Container`, `PageHeader`, `Stack`, `Section`, and the page layouts `ListLayout`, `DetailLayout`, `FormLayout`, `ProseLayout`, `CardGrid`, `SidebarLayout`, `FullBleedLayout`, `CenteredLayout`.
|
|
44
44
|
- Typography roles (each pins size + weight, takes a `color` and an `as`): `Display`, `Lead`,
|
|
45
45
|
`PageHeading`, `SectionHeading`, `SubsectionHeading`, `ModalHeading`, `Subtitle`, `Overline`,
|
|
46
46
|
`Body`, `BodyStrong`, `Caption`. Three opt-in props, none changing today's look: `SectionHeading rule`
|
|
47
47
|
(a hairline under it, with its spacing), `SubsectionHeading size` (`sm` default, `base` bold, `lg`),
|
|
48
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
|
|
49
|
+
- Controls: `Button` (`variant` is `primary` (default), `outline`, `ghost` or `link`, the link having no box: text with an underline on hover, for an in-line action; `size="sm"` is compact, `text-xs`, for table rows and toolbars; `tone="danger"` is red, for delete and clear), `buttonClass(variant, { size, tone })` (the classes of a `Button`, for a router's own `<Link>`), `ButtonLink` (a link that looks like a `Button`: `href`, or `render` to draw the app's own router `<Link>`; takes `variant`, `size`, `tone`, `external`), `ButtonGroup` (a wrapping row of buttons; `end` is one item pushed to the far side, `label` makes it a named `role="group"`), `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). `Input`, `Select` and `Textarea` take `size` (`md` default, or `sm`: `text-xs`, less padding, for dense forms and table rows) and `width` (`full` default, the fixed `xs` 5rem, `sm` 7rem, `md` 9rem, `lg` 12rem, or `auto`: no width class, as wide as the content or as a `className` width such as `w-24`); a fixed width replaces `w-full`, so there is no class to collide with. `size` replaces the native `size` attribute, so a multi-row `<select size={4}>` is not reachable through `Select`. `Checkbox` (a native checkbox with its label; `className` goes on the label, other props on the input; `size="sm"` is `text-xs` with a smaller box, and with no `label` it is a bare box for a table cell, named by `aria-label`). `FileDrop` (a dashed drop zone, a `<label>` around a file input that covers it, so a click opens the dialog and a dropped file lands on the input; `children` is the prompt, `className` goes on the label, `accept`, `multiple`, `name`, `disabled` and `onChange` 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; `actions` puts a button or link at the end of the row; `banner` makes it a flat strip, bottom border only and no radius, for the top of a `Card flush`). `ListRow` (a full-width row that is a `<button>`, or a link with `href` or `render`, with a hover fill, a left-aligned label and `trailing` content; `expanded` makes it the header of a collapsible section: `aria-expanded` and a chevron, the caller owning the state and the section). `NavTabs` (a row of pill links in a `<nav>`; `items` are `{ label, href, active?, external? }` or `{ label, render, active? }`, `label` names the landmark, the active one is filled and gets `aria-current="page"`; `external` opens in a new tab; see Router links). `navTabClass` (the classes of one tab, for a router's own `<Link>`). `Tabs` (underline tabs for switching a view in place: a row over a bottom rule, the current tab underlined in the accent; `items` are `{ value, label }` buttons, with `value` the current one's `value` and `onChange(value)` the caller's state, one tab stop and the arrow keys, Home and End moving between enabled tabs, or `{ value, label, href | render }` links in a `<nav>` with `aria-current="page"` on the current one; `label` names the group; a button tab takes `disabled` and `controls`, the id of its `role="tabpanel"`, which is the caller's). `SegmentedControl` (a joined pill group, one option filled; the same `items`, `value`, `onChange` and `label` as `Tabs`, as a `radiogroup`). `Drawer` (a modal slide-over on the right edge, on a native `<dialog>` opened on mount like `Dialog`, so render it only while open: `title` is its name, a close button sits beside it, `children` scroll between the header and an optional pinned `footer`; Esc, the scrim and the close button call `onClose`; `size` is `sm`, `md` (default) or `lg`; `closeLabel` names the close button). `Breadcrumbs` (a `<nav aria-label="Breadcrumb">` over an ordered list; `crumbs` are nodes, or `{ label, href | render, external? }` for one that links; the last is the current page and is never a link and gets `aria-current="page"`, and each crumb truncates). `crumbLinkClass` (the classes of a crumb that links, for a router's own `<Link>`). `Table` (a list of records in a rounded, bordered card around a full-width `<table>` that scrolls sideways on a narrow screen; `caption` names it for assistive tech and is not drawn; compose it from `TableHead`, `TableBody`, `TableRow`, `TableHeaderCell` (`align` `start` or `end`) and `TableCell` (`align`, `tone` `default`, `strong` or `muted`, `numeric` for tabular figures, `mono` for a small mono face)). `rowLinkClass` (the classes of the one link in a row whose hit area covers the row, so the whole row is the click target; for a router's own `<Link>`). `Timeline` (a named `<ol>` of events; `label` is its `aria-label`, `empty` is drawn instead when there are no items) with `TimelineItem` (a `title`, an already-formatted `time` and the `children` as a note). `KeyValueList` (a `<dl>` of label/value rows; each `KeyValue` takes a `label` and its value as children, the value right-aligned in tabular figures; `align="start"` puts the values left-aligned in one column beside the labels instead, using CSS subgrid: in Chrome before 117 a row stacks its label over its value). `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 unless `render` draws the app's own router `<Link>`, `external` adds `target="_blank"` and `rel="noreferrer"`, `stretch` covers the nearest `relative` ancestor so a whole row or card is the click target, `className` and every other anchor prop pass through). `linkClass` (the classes of one, for a router's own `<Link>`) and `linkStretchClass` (the stretch). `InlineLinks` (a wrapping row of small links joined by an `aria-hidden` `separator`: `·` (default), `•` or `none`; `className` goes on the row). `PageFade` (the page's content area, named for the View Transitions API so a route change cross-fades it while the frame around it stays put; wrap what changes between pages, once per page, or give each a `name` (a CSS identifier) when there are two; it fades when the navigation runs in a view transition, through `startPageTransition(update)` (flushes a synchronous state change inside `document.startViewTransition`) or a router's own option such as TanStack Router `viewTransition`, and otherwise, in a browser without View Transitions, or under `prefers-reduced-motion: reduce`, it swaps with no fade; it renders a small `<style>`, with no colour in it, and the div is a stacking context, so portal an overlay that must rise above the frame). `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
50
|
- Dates: `toISODate`, `fromISODate`, and the `CalendarDate` type.
|
|
51
51
|
|
|
52
52
|
## Dates
|
|
@@ -85,6 +85,30 @@ import { DatePicker, fromISODate, toISODate, type CalendarDate } from "@hackcove
|
|
|
85
85
|
error once the field loses focus.
|
|
86
86
|
- An impossible date such as 31/02/1990 shows "February 1990 has 28 days." and stays exactly as
|
|
87
87
|
typed, focus leaving included, until it is fixed. `errorMessage` replaces either message.
|
|
88
|
+
- `error` is an error from outside the field, a server's say: it shows in place of the description,
|
|
89
|
+
in red with `role="alert"` (as `Field`'s `error` does), and marks the field invalid. It wins over
|
|
90
|
+
the built-in messages; clear it when the date changes. `isInvalid` marks the field invalid with no
|
|
91
|
+
message of its own; pair it with `errorMessage`.
|
|
92
|
+
|
|
93
|
+
## Router links
|
|
94
|
+
|
|
95
|
+
`@hackcove/ui` has no router dependency. `NavTabs` (a tab), `Tabs` and `SegmentedControl` (an item with `href` or `render`), `Breadcrumbs` (a crumb), `TextLink` and
|
|
96
|
+
`ButtonLink` take a `render` function that draws the app's own `<Link>`, handed the component's
|
|
97
|
+
classes (and `aria-current` on the active tab) to spread on it. The call site carries no `className`:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
<NavTabs
|
|
101
|
+
label="Main"
|
|
102
|
+
items={[
|
|
103
|
+
{ label: "Quotes", active: true, render: (props) => <Link to="/quotes" {...props} /> },
|
|
104
|
+
{ label: "Traces", href: "https://traces.example.com", external: true },
|
|
105
|
+
]}
|
|
106
|
+
/>
|
|
107
|
+
<ButtonLink variant="ghost" render={(props) => <Link to="/quotes" {...props} />}>Back</ButtonLink>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
With `render`, `href` is not used. `buttonClass`, `navTabClass`, `crumbLinkClass` and `linkClass` stay
|
|
111
|
+
exported for an app that would rather put the class on its own link.
|
|
88
112
|
|
|
89
113
|
## Layout
|
|
90
114
|
|
|
@@ -113,9 +137,25 @@ import { AppShell, PageHeader, Section, Stack, appShellRootClass } from "@hackco
|
|
|
113
137
|
with fixed `py-8`, and an optional `footer`. The header row is filled by slots, left to right:
|
|
114
138
|
`brand` (logo and name, at the brand type with the logo gap), `nav` (a `NavTabs`), `header` (a
|
|
115
139
|
free node, the escape hatch) and `controls` (end-aligned, `gap-2`, centred): no wrapper, no
|
|
116
|
-
`className`.
|
|
140
|
+
`className`. Nothing in `controls` shrinks, and a full-width control there (a `Select` at its
|
|
141
|
+
default `width="full"`) sizes to its content, so a `Select` next to a `ColourSchemeToggle` leaves
|
|
142
|
+
the toggle its square. `hideChromeInPrint` is opt-in: the header and footer are hidden in print and main
|
|
117
143
|
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
144
|
page; the header keeps its own, so put a `Container` back around anything that should line up.
|
|
145
|
+
- **`AppShellBrand`** is the `brand` slot as one link: `logo` and `label` inside the same `<a>`, so
|
|
146
|
+
the whole brand goes home (the label names the link; the logo is hidden from assistive tech). With
|
|
147
|
+
`render` it draws your router's `<Link>`, as `NavTabs`, `Breadcrumbs` and `TextLink` do:
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
<AppShell brand={<AppShellBrand logo={<Avatar name="Quotes" size="sm" />} label="Quotes"
|
|
151
|
+
render={(props) => <Link to="/" {...props} />} />} …>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
A plain node in `brand` still works. Stories: `Layout/AppShell › LinkedBrand`, `RouterBrand`.
|
|
155
|
+
- **`appShellRootClassFor(scheme, schemes?)`** is what goes on `<html>` when the server knows the
|
|
156
|
+
scheme: that scheme's class and `appShellRootClass`, joined, so there is no string concatenation:
|
|
157
|
+
`<html className={appShellRootClassFor(scheme, schemes)}>`. `appShellRootClass` and `schemeClass`
|
|
158
|
+
stay exported.
|
|
119
159
|
- **`Container`** is the only place the content width (`max-w-6xl`) and side padding
|
|
120
160
|
(`px-4 sm:px-6`) are defined. **Pages never set their own width or outer padding**: no `max-w-*`,
|
|
121
161
|
no `mx-auto`, no `px-*` on a page. AppShell already gives them the edge.
|
|
@@ -218,8 +258,27 @@ Two states, light and dark. The stored choice wins; with nothing stored the syst
|
|
|
218
258
|
mode) is swallowed and the page still switches. It reads the current state from the `dark` class
|
|
219
259
|
when it mounts, so it agrees with the script and renders the same on the server and the client.
|
|
220
260
|
|
|
221
|
-
|
|
222
|
-
|
|
261
|
+
4. **A light theme with a class of its own, or a scheme the server knows.** An app that picks the
|
|
262
|
+
scheme from a cookie and whose light theme has a scope class passes both:
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
import { ColourSchemeToggle, schemeClass } from "@hackcove/ui";
|
|
266
|
+
|
|
267
|
+
const schemes = { light: "my-light", dark: "dark" }; // default: { light: "", dark: "dark" }
|
|
268
|
+
|
|
269
|
+
<html className={schemeClass(scheme, schemes)}>
|
|
270
|
+
<ColourSchemeToggle schemes={schemes} initial={scheme} onChange={(next) => saveCookie(next)} />
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Leave `colourSchemeScript` out here: it knows only the `dark` class and the stored choice, so it would fight the server. `schemes` is the class each scheme puts on `<html>`; a click swaps them. `initial` is the scheme the
|
|
274
|
+
server rendered: the server markup, the hydrating client and the `<html>` class (from
|
|
275
|
+
`schemeClass`) then agree, with no flash. Persist the choice from `onChange`; the toggle still
|
|
276
|
+
writes `localStorage`, which an app reading a cookie simply ignores. With neither prop nothing
|
|
277
|
+
changes: `dark` on and off, light on the server.
|
|
278
|
+
|
|
279
|
+
Without React, `applyColourScheme(scheme, root?, schemes?)`, `readColourScheme(fallback?)`,
|
|
280
|
+
`storeColourScheme(scheme)` and `schemeClass(scheme, schemes?)` are exported too; `ColourScheme` is
|
|
281
|
+
`"light" | "dark"` and `ColourSchemeClasses` is the `schemes` shape.
|
|
223
282
|
|
|
224
283
|
## Palette
|
|
225
284
|
|
package/dist/AppShell.d.ts
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
1
1
|
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import { type ColourScheme, type ColourSchemeClasses } from "./colourScheme.js";
|
|
3
|
+
import { type LinkRender } from "./link.js";
|
|
2
4
|
/**
|
|
3
5
|
* The class an app puts on its own `<html>`: it always reserves the scrollbar, so centred content
|
|
4
6
|
* does not shift between a short page and a long one. AppShell never touches `<html>` itself.
|
|
5
7
|
*/
|
|
6
8
|
export declare const appShellRootClass = "overflow-y-scroll";
|
|
9
|
+
/**
|
|
10
|
+
* Everything an app puts on its own `<html>` from the scheme the server knows: that scheme's class
|
|
11
|
+
* (`schemeClass`) and `appShellRootClass`, joined. `<html className={appShellRootClassFor(scheme,
|
|
12
|
+
* schemes)}>` replaces `schemeClass(scheme, schemes) + " " + appShellRootClass`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function appShellRootClassFor(scheme: ColourScheme, schemes?: ColourSchemeClasses): string;
|
|
15
|
+
/**
|
|
16
|
+
* The `brand` slot as one link: a logo and a label inside the same `<a>`, so the whole brand is the
|
|
17
|
+
* way home. The label names the link; the logo is decoration and hidden from assistive tech. With
|
|
18
|
+
* `render` it draws the app's own router `<Link>` (`render={(p) => <Link to="/" {...p} />}`).
|
|
19
|
+
*/
|
|
20
|
+
export declare function AppShellBrand({ logo, label, href, render, }: {
|
|
21
|
+
/** The mark before the label, e.g. an `Avatar` or an `<img>`. */
|
|
22
|
+
logo?: ReactNode;
|
|
23
|
+
/** The name beside the logo. It is the link's accessible name. */
|
|
24
|
+
label: ReactNode;
|
|
25
|
+
} & ({
|
|
26
|
+
href: string;
|
|
27
|
+
render?: LinkRender | undefined;
|
|
28
|
+
} | {
|
|
29
|
+
href?: string | undefined;
|
|
30
|
+
render: LinkRender;
|
|
31
|
+
})): ReactElement;
|
|
7
32
|
/**
|
|
8
33
|
* The page frame: a sticky one-row header, an optional second `subnav` row, the main area and an
|
|
9
34
|
* optional footer. Header, main and footer each sit in a `Container`, so pages never set their own
|
|
@@ -15,11 +40,11 @@ export declare const appShellRootClass = "overflow-y-scroll";
|
|
|
15
40
|
* `className` for any of them.
|
|
16
41
|
*/
|
|
17
42
|
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. */
|
|
43
|
+
/** The brand: a logo and a name, in the header's brand type, with the gap between them. `AppShellBrand` makes both one link. */
|
|
19
44
|
brand?: ReactNode;
|
|
20
45
|
/** The main navigation, e.g. `NavTabs` (it is already a `<nav>`). */
|
|
21
46
|
nav?: ReactNode;
|
|
22
|
-
/** The end of the row: a picker, a theme toggle. Gap-2, vertically centred. */
|
|
47
|
+
/** The end of the row: a picker, a theme toggle. Gap-2, vertically centred; no child shrinks, and a full-width control (a `Select` at its default) sizes to its content. */
|
|
23
48
|
controls?: ReactNode;
|
|
24
49
|
/** The escape hatch: a free node in the header row, after `brand` and `nav`. */
|
|
25
50
|
header?: ReactNode;
|
package/dist/AppShell.js
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
import { Container } from "./Container.js";
|
|
4
|
-
import {
|
|
4
|
+
import { schemeClass } from "./colourScheme.js";
|
|
5
|
+
import { cx, focusRing } from "./cx.js";
|
|
6
|
+
import { renderLink } from "./link.js";
|
|
5
7
|
/**
|
|
6
8
|
* The class an app puts on its own `<html>`: it always reserves the scrollbar, so centred content
|
|
7
9
|
* does not shift between a short page and a long one. AppShell never touches `<html>` itself.
|
|
8
10
|
*/
|
|
9
11
|
export const appShellRootClass = "overflow-y-scroll";
|
|
12
|
+
/**
|
|
13
|
+
* Everything an app puts on its own `<html>` from the scheme the server knows: that scheme's class
|
|
14
|
+
* (`schemeClass`) and `appShellRootClass`, joined. `<html className={appShellRootClassFor(scheme,
|
|
15
|
+
* schemes)}>` replaces `schemeClass(scheme, schemes) + " " + appShellRootClass`.
|
|
16
|
+
*/
|
|
17
|
+
export function appShellRootClassFor(scheme, schemes) {
|
|
18
|
+
return cx(schemeClass(scheme, schemes), appShellRootClass);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The `brand` slot as one link: a logo and a label inside the same `<a>`, so the whole brand is the
|
|
22
|
+
* way home. The label names the link; the logo is decoration and hidden from assistive tech. With
|
|
23
|
+
* `render` it draws the app's own router `<Link>` (`render={(p) => <Link to="/" {...p} />}`).
|
|
24
|
+
*/
|
|
25
|
+
export function AppShellBrand({ logo, label, href, render, }) {
|
|
26
|
+
return renderLink({
|
|
27
|
+
href,
|
|
28
|
+
render,
|
|
29
|
+
className: cx("flex items-center gap-2 rounded", focusRing),
|
|
30
|
+
children: (_jsxs(_Fragment, { children: [logo && (_jsx("span", { "aria-hidden": "true", className: "flex shrink-0 items-center", children: logo })), _jsx("span", { children: label })] })),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
10
33
|
// One fixed-height row that scrolls sideways and never wraps; its children keep their width.
|
|
11
34
|
const ROW = "flex items-center gap-4 overflow-x-auto whitespace-nowrap *:shrink-0";
|
|
12
35
|
// While a row has more to scroll to on a side, that edge fades out (a mask, so it follows any theme).
|
|
@@ -44,7 +67,7 @@ function ScrollRow({ className, children }) {
|
|
|
44
67
|
}
|
|
45
68
|
/** The header row's slots in order: brand, nav, the free `children`, then controls at the end. */
|
|
46
69
|
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 })] }));
|
|
70
|
+
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 *:shrink-0 [&>.w-full]:w-auto", children: controls }))] }));
|
|
48
71
|
}
|
|
49
72
|
/**
|
|
50
73
|
* The page frame: a sticky one-row header, an optional second `subnav` row, the main area and an
|
package/dist/Badge.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small red count, 10px, for sitting on an icon. Position it from outside with `className`
|
|
3
|
+
* (`absolute -right-1 -top-1` over a `relative` parent); a count above 99 reads `99+`. Name the
|
|
4
|
+
* control it sits on (`aria-label` on the button) for a screen reader.
|
|
5
|
+
*/
|
|
6
|
+
export declare function Badge({ count, className }: {
|
|
7
|
+
count: number;
|
|
8
|
+
className?: string;
|
|
9
|
+
}): import("react").JSX.Element;
|
package/dist/Badge.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/**
|
|
4
|
+
* A small red count, 10px, for sitting on an icon. Position it from outside with `className`
|
|
5
|
+
* (`absolute -right-1 -top-1` over a `relative` parent); a count above 99 reads `99+`. Name the
|
|
6
|
+
* control it sits on (`aria-label` on the button) for a screen reader.
|
|
7
|
+
*/
|
|
8
|
+
export function Badge({ count, className }) {
|
|
9
|
+
return (_jsx("span", { className: cx("inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-red-600 px-1 text-[10px] font-semibold leading-none text-red-50", className), children: count > 99 ? "99+" : count }));
|
|
10
|
+
}
|
package/dist/Breadcrumbs.d.ts
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import { type LinkRender } from "./link.js";
|
|
2
3
|
/** The classes of a crumb that links somewhere, for a router's own <Link>. The current page is plain text, not a link. */
|
|
3
4
|
export declare const crumbLinkClass: string;
|
|
4
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* A crumb that links: a plain `<a href>`, or with `render` the app's own router `<Link>`
|
|
7
|
+
* (`render: (p) => <Link to="/quotes" {...p} />`; `href` is then not used). `external` opens in a
|
|
8
|
+
* new tab. The last crumb is the current page and is never a link.
|
|
9
|
+
*/
|
|
10
|
+
export type Crumb = {
|
|
11
|
+
label: ReactNode;
|
|
12
|
+
external?: boolean;
|
|
13
|
+
} & ({
|
|
14
|
+
href: string;
|
|
15
|
+
render?: LinkRender;
|
|
16
|
+
} | {
|
|
17
|
+
href?: string;
|
|
18
|
+
render: LinkRender;
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* `org / project / build / snapshot`: each crumb truncates, and the last is the current page. A
|
|
22
|
+
* crumb is a node, or a `Crumb` object when it links.
|
|
23
|
+
*/
|
|
5
24
|
export declare function Breadcrumbs({ crumbs, className, }: {
|
|
6
|
-
crumbs: ReactNode
|
|
25
|
+
crumbs: Array<Crumb | ReactNode>;
|
|
7
26
|
className?: string;
|
|
8
27
|
}): ReactElement;
|
package/dist/Breadcrumbs.js
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { isValidElement } from "react";
|
|
2
3
|
import { cx, focusRing } from "./cx.js";
|
|
4
|
+
import { renderLink } from "./link.js";
|
|
3
5
|
/** The classes of a crumb that links somewhere, for a router's own <Link>. The current page is plain text, not a link. */
|
|
4
6
|
export const crumbLinkClass = cx("rounded text-slate-500 hover:underline", focusRing);
|
|
5
|
-
|
|
7
|
+
function isCrumb(crumb) {
|
|
8
|
+
return typeof crumb === "object" && crumb !== null && !isValidElement(crumb) && "label" in crumb;
|
|
9
|
+
}
|
|
10
|
+
function CrumbContent({ crumb, current, }) {
|
|
11
|
+
if (!isCrumb(crumb))
|
|
12
|
+
return crumb;
|
|
13
|
+
if (current)
|
|
14
|
+
return crumb.label;
|
|
15
|
+
return renderLink({
|
|
16
|
+
href: crumb.href,
|
|
17
|
+
render: crumb.render,
|
|
18
|
+
external: crumb.external,
|
|
19
|
+
className: crumbLinkClass,
|
|
20
|
+
children: crumb.label,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `org / project / build / snapshot`: each crumb truncates, and the last is the current page. A
|
|
25
|
+
* crumb is a node, or a `Crumb` object when it links.
|
|
26
|
+
*/
|
|
6
27
|
export function Breadcrumbs({ crumbs, className, }) {
|
|
7
28
|
const last = crumbs.length - 1;
|
|
8
|
-
return (_jsx("nav", { "aria-label": "Breadcrumb", className: cx("min-w-0", className), children: _jsx("ol", { className: "flex min-w-0 items-center gap-2 text-sm", children: crumbs.map((crumb, index) => (_jsxs("li", { "aria-current": index === last ? "page" : undefined, className: "flex min-w-0 items-center gap-2", children: [index > 0 && (_jsx("span", { "aria-hidden": "true", className: "text-slate-400", children: "/" })), _jsx("span", { className: cx("min-w-0 truncate", index === last ? "font-medium text-slate-900" : "text-slate-500"), children: crumb })] }, index))) }) }));
|
|
29
|
+
return (_jsx("nav", { "aria-label": "Breadcrumb", className: cx("min-w-0", className), children: _jsx("ol", { className: "flex min-w-0 items-center gap-2 text-sm", children: crumbs.map((crumb, index) => (_jsxs("li", { "aria-current": index === last ? "page" : undefined, className: "flex min-w-0 items-center gap-2", children: [index > 0 && (_jsx("span", { "aria-hidden": "true", className: "text-slate-400", children: "/" })), _jsx("span", { className: cx("min-w-0 truncate", index === last ? "font-medium text-slate-900" : "text-slate-500"), children: _jsx(CrumbContent, { crumb: crumb, current: index === last }) })] }, index))) }) }));
|
|
9
30
|
}
|
package/dist/Button.d.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef, ReactElement } from "react";
|
|
2
|
-
export type ButtonVariant = "primary" | "outline" | "ghost";
|
|
2
|
+
export type ButtonVariant = "primary" | "outline" | "ghost" | "link";
|
|
3
|
+
export type ButtonSize = "md" | "sm";
|
|
4
|
+
export type ButtonTone = "default" | "danger";
|
|
5
|
+
/** Size, tone and variant: the look of a button, apart from its state. */
|
|
6
|
+
export type ButtonLook = {
|
|
7
|
+
variant?: ButtonVariant | undefined;
|
|
8
|
+
/** `sm` is compact, for table rows and toolbars. */
|
|
9
|
+
size?: ButtonSize | undefined;
|
|
10
|
+
/** `danger` is red, for delete and clear. */
|
|
11
|
+
tone?: ButtonTone | undefined;
|
|
12
|
+
};
|
|
3
13
|
/** The classes of a Button, for a router's own <Link> that should look like one. */
|
|
4
|
-
export declare function buttonClass(variant?: ButtonVariant): string;
|
|
5
|
-
type ButtonProps = {
|
|
6
|
-
variant?: ButtonVariant;
|
|
14
|
+
export declare function buttonClass(variant?: ButtonVariant, { size, tone }?: Omit<ButtonLook, "variant">): string;
|
|
15
|
+
type ButtonProps = ButtonLook & {
|
|
7
16
|
/** Why the button cannot be pressed: disables it, sets it as the `title`, and greys it flat. */
|
|
8
17
|
disabledReason?: string | undefined;
|
|
9
18
|
/** The work is in flight: disabled, `aria-busy`, keeps its colour. Wins over `disabledReason`. */
|
|
@@ -12,9 +21,10 @@ type ButtonProps = {
|
|
|
12
21
|
busyLabel?: string | undefined;
|
|
13
22
|
} & ComponentPropsWithoutRef<"button">;
|
|
14
23
|
/**
|
|
15
|
-
* A button in the primary, outline or
|
|
16
|
-
*
|
|
17
|
-
*
|
|
24
|
+
* A button in the primary, outline, ghost or link style (the link has no box: text with an
|
|
25
|
+
* underline on hover, for an in-line action). `size="sm"` is compact; `tone="danger"` is red.
|
|
26
|
+
* Defaults to `type="button"`. `disabledReason` and `busy` are the two disabled states that say
|
|
27
|
+
* more than a plain `disabled`, which keeps its half-opacity look.
|
|
18
28
|
*/
|
|
19
|
-
export declare function Button({ variant, disabledReason, busy, busyLabel, className, children, title, disabled, ...rest }: ButtonProps): ReactElement;
|
|
29
|
+
export declare function Button({ variant, size, tone, disabledReason, busy, busyLabel, className, children, title, disabled, ...rest }: ButtonProps): ReactElement;
|
|
20
30
|
export {};
|
package/dist/Button.js
CHANGED
|
@@ -1,39 +1,71 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx, focusRing } from "./cx.js";
|
|
3
|
-
const SHAPE = "inline-flex items-center justify-center
|
|
3
|
+
const SHAPE = "inline-flex items-center justify-center rounded-lg border font-medium transition";
|
|
4
|
+
// Size sets the padding and the text. The link variant has no box, so it takes the text size only.
|
|
5
|
+
const SIZE = {
|
|
6
|
+
md: { box: "gap-2 px-4 py-2", text: "text-sm" },
|
|
7
|
+
sm: { box: "gap-1.5 px-2.5 py-1", text: "text-xs" },
|
|
8
|
+
};
|
|
4
9
|
// A plain `disabled` washes out and ignores the pointer. Blocked and busy buttons are disabled too,
|
|
5
10
|
// but they keep pointer events (the title shows, the cursor says why) and do not take this.
|
|
6
11
|
const DISABLED_PLAIN = "disabled:pointer-events-none disabled:opacity-50";
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
const LINK = "underline-offset-2";
|
|
13
|
+
const TONE = {
|
|
14
|
+
default: {
|
|
15
|
+
primary: {
|
|
16
|
+
color: "border-violet-600 bg-violet-600 text-violet-50",
|
|
17
|
+
hover: "hover:bg-violet-700",
|
|
18
|
+
},
|
|
19
|
+
outline: { color: "border-violet-600 text-violet-600", hover: "hover:bg-violet-50" },
|
|
20
|
+
ghost: { color: "border-transparent text-slate-600", hover: "hover:bg-slate-100" },
|
|
21
|
+
link: {
|
|
22
|
+
color: `border-transparent text-violet-600 ${LINK}`,
|
|
23
|
+
hover: "hover:text-violet-700 hover:underline",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
danger: {
|
|
27
|
+
primary: { color: "border-red-600 bg-red-600 text-red-50", hover: "hover:bg-red-700" },
|
|
28
|
+
outline: { color: "border-red-200 text-red-700", hover: "hover:bg-red-50" },
|
|
29
|
+
ghost: { color: "border-transparent text-red-700", hover: "hover:bg-red-50" },
|
|
30
|
+
link: { color: `border-transparent text-red-700 ${LINK}`, hover: "hover:underline" },
|
|
11
31
|
},
|
|
12
|
-
outline: { color: "border-violet-600 text-violet-600", hover: "hover:bg-violet-50" },
|
|
13
|
-
ghost: { color: "border-transparent text-slate-600", hover: "hover:bg-slate-100" },
|
|
14
32
|
};
|
|
15
33
|
// Blocked replaces the variant's colour and hover rather than overriding them, so no `!` is needed.
|
|
16
34
|
const BLOCKED = "cursor-not-allowed border-slate-200 bg-slate-100 text-slate-400";
|
|
35
|
+
const BLOCKED_LINK = "cursor-not-allowed border-transparent text-slate-400";
|
|
17
36
|
const BUSY = "cursor-progress opacity-85";
|
|
37
|
+
function shape(variant, size) {
|
|
38
|
+
const { box, text } = SIZE[size];
|
|
39
|
+
return cx(SHAPE, variant !== "link" && box, variant === "link" && "gap-1.5", text, focusRing);
|
|
40
|
+
}
|
|
18
41
|
/** The classes of a Button, for a router's own <Link> that should look like one. */
|
|
19
|
-
export function buttonClass(variant = "primary") {
|
|
20
|
-
|
|
42
|
+
export function buttonClass(variant = "primary", { size = "md", tone = "default" } = {}) {
|
|
43
|
+
const look = TONE[tone][variant];
|
|
44
|
+
return cx(shape(variant, size), DISABLED_PLAIN, look.color, look.hover);
|
|
21
45
|
}
|
|
22
|
-
/**
|
|
23
|
-
function
|
|
24
|
-
const { color } = VARIANT[variant];
|
|
25
|
-
if (blocked)
|
|
26
|
-
return cx(SHAPE, focusRing, BLOCKED);
|
|
46
|
+
/** Busy wins over blocked. */
|
|
47
|
+
function stateOf(busy, disabledReason) {
|
|
27
48
|
if (busy)
|
|
28
|
-
return
|
|
29
|
-
return
|
|
49
|
+
return "busy";
|
|
50
|
+
return disabledReason === undefined ? "idle" : "blocked";
|
|
51
|
+
}
|
|
52
|
+
/** The classes of a Button in a given state: blocked swaps the colour out, busy keeps it. */
|
|
53
|
+
function stateClass({ variant = "primary", size = "md", tone = "default" }, state) {
|
|
54
|
+
if (state === "blocked") {
|
|
55
|
+
return cx(shape(variant, size), variant === "link" ? BLOCKED_LINK : BLOCKED);
|
|
56
|
+
}
|
|
57
|
+
if (state === "busy")
|
|
58
|
+
return cx(shape(variant, size), TONE[tone][variant].color, BUSY);
|
|
59
|
+
return buttonClass(variant, { size, tone });
|
|
30
60
|
}
|
|
31
61
|
/**
|
|
32
|
-
* A button in the primary, outline or
|
|
33
|
-
*
|
|
34
|
-
*
|
|
62
|
+
* A button in the primary, outline, ghost or link style (the link has no box: text with an
|
|
63
|
+
* underline on hover, for an in-line action). `size="sm"` is compact; `tone="danger"` is red.
|
|
64
|
+
* Defaults to `type="button"`. `disabledReason` and `busy` are the two disabled states that say
|
|
65
|
+
* more than a plain `disabled`, which keeps its half-opacity look.
|
|
35
66
|
*/
|
|
36
|
-
export function Button({ variant
|
|
37
|
-
const
|
|
38
|
-
|
|
67
|
+
export function Button({ variant, size, tone, disabledReason, busy = false, busyLabel, className, children, title, disabled, ...rest }) {
|
|
68
|
+
const state = stateOf(busy, disabledReason);
|
|
69
|
+
const blocked = state === "blocked";
|
|
70
|
+
return (_jsx("button", { type: "button", className: cx(stateClass({ variant, size, tone }, state), className), disabled: disabled || busy || blocked, "aria-busy": busy || undefined, title: blocked ? disabledReason : title, ...rest, children: busy && busyLabel ? busyLabel : children }));
|
|
39
71
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A wrapping row of buttons with one optional item pushed to the far end (a Back link, say). On a
|
|
4
|
+
* narrow viewport the row wraps and `end` stays at the end of the last line. `label` makes it a
|
|
5
|
+
* named `role="group"`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ButtonGroup({ children, end, label, className, }: {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
/** One item, pushed to the end of the row. */
|
|
10
|
+
end?: ReactNode;
|
|
11
|
+
/** The accessible name of the group: sets `role="group"` and `aria-label`. */
|
|
12
|
+
label?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
}): ReactElement;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/**
|
|
4
|
+
* A wrapping row of buttons with one optional item pushed to the far end (a Back link, say). On a
|
|
5
|
+
* narrow viewport the row wraps and `end` stays at the end of the last line. `label` makes it a
|
|
6
|
+
* named `role="group"`.
|
|
7
|
+
*/
|
|
8
|
+
export function ButtonGroup({ children, end, label, className, }) {
|
|
9
|
+
return (_jsxs("div", { ...(label === undefined ? {} : { role: "group", "aria-label": label }), className: cx("flex flex-wrap items-center gap-2", className), children: [children, end ? _jsx("div", { className: "ml-auto", children: end }) : null] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement } from "react";
|
|
2
|
+
import { type ButtonLook } from "./Button.js";
|
|
3
|
+
import { type LinkRender } from "./link.js";
|
|
4
|
+
type ButtonLinkProps = ButtonLook & {
|
|
5
|
+
/** Opens in a new tab: adds `target="_blank"` and `rel="noreferrer"`. */
|
|
6
|
+
external?: boolean | undefined;
|
|
7
|
+
} & Omit<ComponentPropsWithoutRef<"a">, "href"> & ({
|
|
8
|
+
href: string;
|
|
9
|
+
render?: LinkRender | undefined;
|
|
10
|
+
} | {
|
|
11
|
+
href?: string | undefined;
|
|
12
|
+
render: LinkRender;
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* A link that looks like a Button. A plain `<a href>` by default; with `render` it draws the app's
|
|
16
|
+
* own router `<Link>` instead (`render={(p) => <Link to="/quotes" {...p} />}`), already carrying the
|
|
17
|
+
* button classes. `variant`, `size` and `tone` are the Button's.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ButtonLink({ variant, size, tone, href, render, external, className, children, ...rest }: ButtonLinkProps): ReactElement;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { buttonClass } from "./Button.js";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
import { renderLink } from "./link.js";
|
|
4
|
+
/**
|
|
5
|
+
* A link that looks like a Button. A plain `<a href>` by default; with `render` it draws the app's
|
|
6
|
+
* own router `<Link>` instead (`render={(p) => <Link to="/quotes" {...p} />}`), already carrying the
|
|
7
|
+
* button classes. `variant`, `size` and `tone` are the Button's.
|
|
8
|
+
*/
|
|
9
|
+
export function ButtonLink({ variant = "primary", size = "md", tone = "default", href, render, external = false, className, children, ...rest }) {
|
|
10
|
+
return renderLink({
|
|
11
|
+
...rest,
|
|
12
|
+
href,
|
|
13
|
+
render,
|
|
14
|
+
external,
|
|
15
|
+
className: cx(buttonClass(variant, { size, tone }), className),
|
|
16
|
+
children,
|
|
17
|
+
});
|
|
18
|
+
}
|
package/dist/Callout.d.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import type { HTMLAttributes, ReactElement, ReactNode } from "react";
|
|
2
2
|
/** The colour family of a callout. */
|
|
3
3
|
export type CalloutTone = "neutral" | "info" | "positive" | "warn" | "danger";
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* A tinted notice box, with an optional bold title above its body. `actions` puts a button or link
|
|
6
|
+
* at the end of the row; `banner` makes it a flat strip (bottom border only, no radius) for the
|
|
7
|
+
* top of a `Card flush` (a padded card would inset it).
|
|
8
|
+
*/
|
|
9
|
+
export declare function Callout({ tone, title, actions, banner, children, className, ...rest }: {
|
|
6
10
|
/** Defaults to "info". */
|
|
7
11
|
tone?: CalloutTone;
|
|
8
12
|
title?: ReactNode;
|
|
13
|
+
/** A button or link at the end of the row, beside the title and body. */
|
|
14
|
+
actions?: ReactNode;
|
|
15
|
+
/** A flat strip: `border-b` only and no radius, to sit above a card body. */
|
|
16
|
+
banner?: boolean;
|
|
9
17
|
children: ReactNode;
|
|
10
18
|
} & Omit<HTMLAttributes<HTMLDivElement>, "title">): ReactElement;
|
package/dist/Callout.js
CHANGED
|
@@ -7,7 +7,12 @@ const TONES = {
|
|
|
7
7
|
warn: "border-amber-200 bg-amber-50 text-amber-800",
|
|
8
8
|
danger: "border-red-200 bg-red-50 text-red-800",
|
|
9
9
|
};
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
/**
|
|
11
|
+
* A tinted notice box, with an optional bold title above its body. `actions` puts a button or link
|
|
12
|
+
* at the end of the row; `banner` makes it a flat strip (bottom border only, no radius) for the
|
|
13
|
+
* top of a `Card flush` (a padded card would inset it).
|
|
14
|
+
*/
|
|
15
|
+
export function Callout({ tone = "info", title, actions, banner = false, children, className, ...rest }) {
|
|
16
|
+
const content = title ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "font-semibold", children: title }), _jsx("div", { className: "mt-1", children: children })] })) : (children);
|
|
17
|
+
return (_jsx("div", { className: cx(banner ? "border-b px-5 py-3 text-sm" : "rounded-xl border p-4 text-sm", TONES[tone], className), ...rest, children: actions ? (_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("div", { className: "min-w-0", children: content }), _jsx("div", { className: "flex shrink-0 items-center gap-2", children: actions })] })) : (content) }));
|
|
13
18
|
}
|
package/dist/Checkbox.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import type { ControlSize } from "./control.js";
|
|
3
|
+
/**
|
|
4
|
+
* A native checkbox with its label. `className` goes on the label, every other prop on the input.
|
|
5
|
+
* `size="sm"` is `text-xs` and a smaller box, for a dense row. Leave `label` out for a bare box in
|
|
6
|
+
* a table cell, and name it with `aria-label`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function Checkbox({ label, size, className, ...rest }: {
|
|
9
|
+
label?: ReactNode;
|
|
10
|
+
size?: ControlSize | undefined;
|
|
11
|
+
} & Omit<ComponentPropsWithoutRef<"input">, "type" | "size" | "width">): ReactElement;
|
package/dist/Checkbox.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx, focusRing } from "./cx.js";
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* A native checkbox with its label. `className` goes on the label, every other prop on the input.
|
|
5
|
+
* `size="sm"` is `text-xs` and a smaller box, for a dense row. Leave `label` out for a bare box in
|
|
6
|
+
* a table cell, and name it with `aria-label`.
|
|
7
|
+
*/
|
|
8
|
+
export function Checkbox({ label, size = "md", className, ...rest }) {
|
|
9
|
+
const small = size === "sm";
|
|
10
|
+
return (_jsxs("label", { className: cx("inline-flex cursor-pointer items-center text-slate-700", small ? "gap-1.5 text-xs" : "gap-2 text-sm", className), children: [_jsx("input", { type: "checkbox", className: cx(small ? "h-3.5 w-3.5" : "h-4 w-4", "accent-violet-600", focusRing), ...rest }), label] }));
|
|
6
11
|
}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { type ReactElement } from "react";
|
|
2
|
-
import { type ColourScheme } from "./colourScheme.js";
|
|
3
|
-
/**
|
|
4
|
-
|
|
2
|
+
import { type ColourScheme, type ColourSchemeClasses } from "./colourScheme.js";
|
|
3
|
+
/**
|
|
4
|
+
* 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>`, unless you pass `schemes` or `initial`: the script knows only the `dark` class and the stored choice, so an app that sets the scheme itself leaves it out.
|
|
5
|
+
*
|
|
6
|
+
* An app whose light scheme has a class of its own passes `schemes` (`{ light: "paper", dark: "dark" }`);
|
|
7
|
+
* the toggle swaps those classes, and `schemeClass` gives the same one for the server to render on `<html>`.
|
|
8
|
+
* An app that knows the scheme on the server (from a cookie) passes it as `initial`, so the server
|
|
9
|
+
* markup, the hydrating client and the `<html>` class agree; persist a change from `onChange`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function ColourSchemeToggle({ className, onChange, schemes, initial, }: {
|
|
5
12
|
className?: string;
|
|
6
13
|
onChange?: (scheme: ColourScheme) => void;
|
|
14
|
+
/** The class each scheme puts on `<html>`. Default `{ light: "", dark: "dark" }`. */
|
|
15
|
+
schemes?: ColourSchemeClasses | undefined;
|
|
16
|
+
/** The scheme the server rendered. What the server and the hydrating client show; the real state follows right after. Default `"light"`. */
|
|
17
|
+
initial?: ColourScheme | undefined;
|
|
7
18
|
}): ReactElement;
|