@hackcove/ui 0.2.5 → 0.2.7
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 +46 -5
- package/dist/AppShell.d.ts +27 -2
- package/dist/AppShell.js +25 -2
- package/dist/Checkbox.d.ts +4 -2
- package/dist/Checkbox.js +4 -3
- package/dist/ColourSchemeToggle.d.ts +7 -3
- package/dist/ColourSchemeToggle.js +6 -3
- package/dist/NavTabs.d.ts +2 -2
- package/dist/NavTabs.js +3 -3
- package/dist/SegmentedControl.d.ts +3 -1
- package/dist/SegmentedControl.js +6 -3
- package/dist/Table.d.ts +28 -17
- package/dist/Table.js +26 -16
- package/dist/Tabs.d.ts +9 -3
- package/dist/Tabs.js +15 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/link.d.ts +25 -6
- package/dist/link.js +12 -2
- package/dist/switcher.d.ts +13 -2
- package/dist/switcher.js +19 -8
- package/dist/ui.d.ts +8 -7
- package/dist/ui.js +12 -8
- 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` (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`, `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, and `dashed="filled"` the same on a surface fill with more padding; `as` is `section` (default), `article` or `div`; `id`, `data-*` and `aria-*` go on the root), `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` (`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).
|
|
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>`; `id`, `data-*` and other button props go on the button, `store={false}` skips `localStorage`; 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, `align="start"` puts the box on the first line of a label that wraps, where the default `center` centres it on all of them, 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; `variant="pill"` fills the current tab like `NavTabs` does, with no rule, for `PageHeader`'s `tabs` slot; `disabled` on the row disables every tab, e.g. until hydration). `SegmentedControl` (a joined pill group, one option filled; the same `items`, `value`, `onChange`, `disabled` and `label` as `Tabs`, as a `radiogroup` with `aria-checked` on each `radio`). On both, an item takes `count` (a small muted number after the label, part of its name: "Shots 12") and `title`; a `disabled` button option stays in the page as `aria-disabled`, so its `title` is still the hover tooltip and is announced as the reason, it is out of the tab order and the arrow keys, Home and End skip it. `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, `secondary` for a second, muted line under the content); `variant="plain"` on `Table` drops the card border, fill and shadow, the head strip and the row hover and keeps the row dividers, for a table inside a card of its own; every table element passes `id`, `data-*` and `aria-*` through, and none takes a `className` except `Table`, whose goes on the wrapper). `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
|
|
@@ -107,7 +107,14 @@ classes (and `aria-current` on the active tab) to spread on it. The call site ca
|
|
|
107
107
|
<ButtonLink variant="ghost" render={(props) => <Link to="/quotes" {...props} />}>Back</ButtonLink>
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
With `render`, `href` is not used.
|
|
110
|
+
With `render`, `href` is not used. The props `render` is handed are the exported type `LinkRenderProps`:
|
|
111
|
+
`className` and `children`, plus `aria-current`, `target`, `rel` and `title` when there is one. Each
|
|
112
|
+
optional field is `?: T` and an absent one is left out, never set to `undefined`, so the spread
|
|
113
|
+
compiles on a router `<Link>` that declares its props the same way under `exactOptionalPropertyTypes`
|
|
114
|
+
(TanStack Router's does). Any other anchor prop you gave the component (`id`, `download`, a handler)
|
|
115
|
+
is handed over too.
|
|
116
|
+
|
|
117
|
+
`buttonClass`, `navTabClass`, `crumbLinkClass` and `linkClass` stay
|
|
111
118
|
exported for an app that would rather put the class on its own link.
|
|
112
119
|
|
|
113
120
|
## Layout
|
|
@@ -137,9 +144,25 @@ import { AppShell, PageHeader, Section, Stack, appShellRootClass } from "@hackco
|
|
|
137
144
|
with fixed `py-8`, and an optional `footer`. The header row is filled by slots, left to right:
|
|
138
145
|
`brand` (logo and name, at the brand type with the logo gap), `nav` (a `NavTabs`), `header` (a
|
|
139
146
|
free node, the escape hatch) and `controls` (end-aligned, `gap-2`, centred): no wrapper, no
|
|
140
|
-
`className`.
|
|
147
|
+
`className`. Nothing in `controls` shrinks, and a full-width control there (a `Select` at its
|
|
148
|
+
default `width="full"`) sizes to its content, so a `Select` next to a `ColourSchemeToggle` leaves
|
|
149
|
+
the toggle its square. `hideChromeInPrint` is opt-in: the header and footer are hidden in print and main
|
|
141
150
|
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
|
|
142
151
|
page; the header keeps its own, so put a `Container` back around anything that should line up.
|
|
152
|
+
- **`AppShellBrand`** is the `brand` slot as one link: `logo` and `label` inside the same `<a>`, so
|
|
153
|
+
the whole brand goes home (the label names the link; the logo is hidden from assistive tech). With
|
|
154
|
+
`render` it draws your router's `<Link>`, as `NavTabs`, `Breadcrumbs` and `TextLink` do:
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
<AppShell brand={<AppShellBrand logo={<Avatar name="Quotes" size="sm" />} label="Quotes"
|
|
158
|
+
render={(props) => <Link to="/" {...props} />} />} …>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
A plain node in `brand` still works. Stories: `Layout/AppShell › LinkedBrand`, `RouterBrand`.
|
|
162
|
+
- **`appShellRootClassFor(scheme, schemes?)`** is what goes on `<html>` when the server knows the
|
|
163
|
+
scheme: that scheme's class and `appShellRootClass`, joined, so there is no string concatenation:
|
|
164
|
+
`<html className={appShellRootClassFor(scheme, schemes)}>`. `appShellRootClass` and `schemeClass`
|
|
165
|
+
stay exported.
|
|
143
166
|
- **`Container`** is the only place the content width (`max-w-6xl`) and side padding
|
|
144
167
|
(`px-4 sm:px-6`) are defined. **Pages never set their own width or outer padding**: no `max-w-*`,
|
|
145
168
|
no `mx-auto`, no `px-*` on a page. AppShell already gives them the edge.
|
|
@@ -260,6 +283,24 @@ Two states, light and dark. The stored choice wins; with nothing stored the syst
|
|
|
260
283
|
writes `localStorage`, which an app reading a cookie simply ignores. With neither prop nothing
|
|
261
284
|
changes: `dark` on and off, light on the server.
|
|
262
285
|
|
|
286
|
+
**Persisting to a cookie instead of `localStorage`.** Pass `store={false}` (default `true`) and write the
|
|
287
|
+
choice from `onChange`; the toggle then touches no storage at all:
|
|
288
|
+
|
|
289
|
+
```tsx
|
|
290
|
+
<ColourSchemeToggle
|
|
291
|
+
store={false}
|
|
292
|
+
initial={scheme} // read from the cookie on the server
|
|
293
|
+
onChange={(next) => {
|
|
294
|
+
document.cookie = `scheme=${next}; path=/; max-age=31536000; samesite=lax`;
|
|
295
|
+
// or POST it to the server, which sets the cookie
|
|
296
|
+
}}
|
|
297
|
+
/>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The toggle takes `id`, `data-*` and the rest of a button's props, which go on its button. It has no
|
|
301
|
+
`aria-pressed`: its label already says what a click does ("Switch to dark theme"), and a pressed state on a
|
|
302
|
+
button whose label changes would say it twice.
|
|
303
|
+
|
|
263
304
|
Without React, `applyColourScheme(scheme, root?, schemes?)`, `readColourScheme(fallback?)`,
|
|
264
305
|
`storeColourScheme(scheme)` and `schemeClass(scheme, schemes?)` are exported too; `ColourScheme` is
|
|
265
306
|
`"light" | "dark"` and `ColourSchemeClasses` is the `schemes` shape.
|
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/Checkbox.d.ts
CHANGED
|
@@ -3,9 +3,11 @@ import type { ControlSize } from "./control.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* A native checkbox with its label. `className` goes on the label, every other prop on the input.
|
|
5
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`.
|
|
6
|
+
* a table cell, and name it with `aria-label`. `align="start"` puts the box on the label's first
|
|
7
|
+
* line, for a label that wraps onto several; the default centres it on the whole label.
|
|
7
8
|
*/
|
|
8
|
-
export declare function Checkbox({ label, size, className, ...rest }: {
|
|
9
|
+
export declare function Checkbox({ label, size, align, className, ...rest }: {
|
|
9
10
|
label?: ReactNode;
|
|
10
11
|
size?: ControlSize | undefined;
|
|
12
|
+
align?: "center" | "start" | undefined;
|
|
11
13
|
} & Omit<ComponentPropsWithoutRef<"input">, "type" | "size" | "width">): ReactElement;
|
package/dist/Checkbox.js
CHANGED
|
@@ -3,9 +3,10 @@ import { cx, focusRing } from "./cx.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* A native checkbox with its label. `className` goes on the label, every other prop on the input.
|
|
5
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`.
|
|
6
|
+
* a table cell, and name it with `aria-label`. `align="start"` puts the box on the label's first
|
|
7
|
+
* line, for a label that wraps onto several; the default centres it on the whole label.
|
|
7
8
|
*/
|
|
8
|
-
export function Checkbox({ label, size = "md", className, ...rest }) {
|
|
9
|
+
export function Checkbox({ label, size = "md", align = "center", className, ...rest }) {
|
|
9
10
|
const small = size === "sm";
|
|
10
|
-
return (_jsxs("label", { className: cx("inline-flex cursor-pointer
|
|
11
|
+
return (_jsxs("label", { className: cx("inline-flex cursor-pointer text-slate-700", align === "start" ? "items-start" : "items-center", 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", align === "start" && "shrink-0", align === "start" && (small ? "mt-px" : "mt-0.5"), "accent-violet-600", focusRing), ...rest }), label] }));
|
|
11
12
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ReactElement } from "react";
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ReactElement } from "react";
|
|
2
2
|
import { type ColourScheme, type ColourSchemeClasses } from "./colourScheme.js";
|
|
3
3
|
/**
|
|
4
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.
|
|
@@ -7,12 +7,16 @@ import { type ColourScheme, type ColourSchemeClasses } from "./colourScheme.js";
|
|
|
7
7
|
* the toggle swaps those classes, and `schemeClass` gives the same one for the server to render on `<html>`.
|
|
8
8
|
* An app that knows the scheme on the server (from a cookie) passes it as `initial`, so the server
|
|
9
9
|
* markup, the hydrating client and the `<html>` class agree; persist a change from `onChange`.
|
|
10
|
+
* `store={false}` skips `localStorage`, for an app that keeps the choice in a server cookie and
|
|
11
|
+
* writes it from `onChange`. Every other button prop (`id`, `data-*`, `aria-*`) goes on the button.
|
|
10
12
|
*/
|
|
11
|
-
export declare function ColourSchemeToggle({ className, onChange, schemes, initial, }: {
|
|
13
|
+
export declare function ColourSchemeToggle({ className, onChange, schemes, initial, store, ...rest }: {
|
|
12
14
|
className?: string;
|
|
13
15
|
onChange?: (scheme: ColourScheme) => void;
|
|
14
16
|
/** The class each scheme puts on `<html>`. Default `{ light: "", dark: "dark" }`. */
|
|
15
17
|
schemes?: ColourSchemeClasses | undefined;
|
|
16
18
|
/** The scheme the server rendered. What the server and the hydrating client show; the real state follows right after. Default `"light"`. */
|
|
17
19
|
initial?: ColourScheme | undefined;
|
|
18
|
-
|
|
20
|
+
/** Remember the choice in `localStorage`. `false` leaves persisting to `onChange`. Default `true`. */
|
|
21
|
+
store?: boolean | undefined;
|
|
22
|
+
} & Omit<ComponentPropsWithoutRef<"button">, "onChange" | "onClick" | "children" | "className" | "aria-label" | "title">): ReactElement;
|
|
@@ -17,13 +17,16 @@ function subscribe(onChange) {
|
|
|
17
17
|
* the toggle swaps those classes, and `schemeClass` gives the same one for the server to render on `<html>`.
|
|
18
18
|
* An app that knows the scheme on the server (from a cookie) passes it as `initial`, so the server
|
|
19
19
|
* markup, the hydrating client and the `<html>` class agree; persist a change from `onChange`.
|
|
20
|
+
* `store={false}` skips `localStorage`, for an app that keeps the choice in a server cookie and
|
|
21
|
+
* writes it from `onChange`. Every other button prop (`id`, `data-*`, `aria-*`) goes on the button.
|
|
20
22
|
*/
|
|
21
|
-
export function ColourSchemeToggle({ className, onChange, schemes = { light: "", dark: "dark" }, initial, }) {
|
|
23
|
+
export function ColourSchemeToggle({ className, onChange, schemes = { light: "", dark: "dark" }, initial, store = true, ...rest }) {
|
|
22
24
|
const scheme = useSyncExternalStore(subscribe, () => appliedColourScheme(document.documentElement, schemes), () => initial ?? "light");
|
|
23
25
|
const next = scheme === "dark" ? "light" : "dark";
|
|
24
|
-
return (_jsx(IconButton, { label: `Switch to ${next} theme`, className: className, onClick: () => {
|
|
26
|
+
return (_jsx(IconButton, { ...rest, label: `Switch to ${next} theme`, className: className, onClick: () => {
|
|
25
27
|
applyColourScheme(next, document.documentElement, schemes);
|
|
26
|
-
|
|
28
|
+
if (store)
|
|
29
|
+
storeColourScheme(next);
|
|
27
30
|
onChange?.(next);
|
|
28
31
|
}, children: next === "light" ? sun : moon }));
|
|
29
32
|
}
|
package/dist/NavTabs.d.ts
CHANGED
|
@@ -16,8 +16,8 @@ export type NavTab = {
|
|
|
16
16
|
href?: string;
|
|
17
17
|
render: LinkRender;
|
|
18
18
|
});
|
|
19
|
-
/** The classes of one tab, for a router's own <Link>. */
|
|
20
|
-
export declare function navTabClass(active: boolean): string;
|
|
19
|
+
/** The classes of one tab, for a router's own <Link>. A `disabled` one drops the hover fill. */
|
|
20
|
+
export declare function navTabClass(active: boolean, disabled?: boolean): string;
|
|
21
21
|
/** A row of pill links where the active one is filled. Plain <a> by default; a tab's `render` draws a router's Link. */
|
|
22
22
|
export declare function NavTabs({ items, label, className, }: {
|
|
23
23
|
items: NavTab[];
|
package/dist/NavTabs.js
CHANGED
|
@@ -2,9 +2,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { cx, focusRing } from "./cx.js";
|
|
3
3
|
import { renderLink } from "./link.js";
|
|
4
4
|
const BASE = `rounded-lg px-3 py-1.5 transition ${focusRing}`;
|
|
5
|
-
/** The classes of one tab, for a router's own <Link>. */
|
|
6
|
-
export function navTabClass(active) {
|
|
7
|
-
return cx(BASE, active ? "bg-slate-100 font-medium text-slate-900" : "text-slate-600 hover:bg-slate-100");
|
|
5
|
+
/** The classes of one tab, for a router's own <Link>. A `disabled` one drops the hover fill. */
|
|
6
|
+
export function navTabClass(active, disabled = false) {
|
|
7
|
+
return cx(BASE, active ? "bg-slate-100 font-medium text-slate-900" : "text-slate-600", !active && !disabled && "hover:bg-slate-100");
|
|
8
8
|
}
|
|
9
9
|
/** A row of pill links where the active one is filled. Plain <a> by default; a tab's `render` draws a router's Link. */
|
|
10
10
|
export function NavTabs({ items, label, className, }) {
|
|
@@ -6,6 +6,8 @@ export type Segment = SwitchItem;
|
|
|
6
6
|
* A joined pill group where one option is filled: a mode or a range chosen in place. `items` are
|
|
7
7
|
* `{ value, label }` buttons (`value` and `onChange` are the caller's state; the arrow keys, Home
|
|
8
8
|
* and End move the choice) or links (`href`, or `render` to draw the app's own router `<Link>`;
|
|
9
|
-
* `value` marks the current one, which gets `aria-current`). `
|
|
9
|
+
* `value` marks the current one, which gets `aria-current`). An item may carry a `count`, and a
|
|
10
|
+
* button one `disabled` (with a `title` for the reason); `disabled` on the group disables every
|
|
11
|
+
* option. `label` names the group.
|
|
10
12
|
*/
|
|
11
13
|
export declare function SegmentedControl(props: SwitchProps): ReactElement;
|
package/dist/SegmentedControl.js
CHANGED
|
@@ -4,12 +4,15 @@ import { Switcher } from "./switcher.js";
|
|
|
4
4
|
// The focus ring sits inside the option, since the group's rounded edge would clip an outer one.
|
|
5
5
|
const BASE = "border-l border-slate-300 px-3 py-1.5 transition first:rounded-l-full first:border-l-0 last:rounded-r-full focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-violet-600";
|
|
6
6
|
const ON = "bg-violet-600 font-medium text-violet-50";
|
|
7
|
-
const OFF = "bg-white text-slate-600
|
|
7
|
+
const OFF = "bg-white text-slate-600";
|
|
8
|
+
const OFF_LIVE = "hover:bg-violet-50 hover:text-violet-700";
|
|
8
9
|
/**
|
|
9
10
|
* A joined pill group where one option is filled: a mode or a range chosen in place. `items` are
|
|
10
11
|
* `{ value, label }` buttons (`value` and `onChange` are the caller's state; the arrow keys, Home
|
|
11
12
|
* and End move the choice) or links (`href`, or `render` to draw the app's own router `<Link>`;
|
|
12
|
-
* `value` marks the current one, which gets `aria-current`). `
|
|
13
|
+
* `value` marks the current one, which gets `aria-current`). An item may carry a `count`, and a
|
|
14
|
+
* button one `disabled` (with a `title` for the reason); `disabled` on the group disables every
|
|
15
|
+
* option. `label` names the group.
|
|
13
16
|
*/
|
|
14
17
|
export function SegmentedControl(props) {
|
|
15
18
|
return (_jsx(Switcher, { ...props, parts: {
|
|
@@ -17,6 +20,6 @@ export function SegmentedControl(props) {
|
|
|
17
20
|
item: "radio",
|
|
18
21
|
vertical: true,
|
|
19
22
|
root: "inline-flex rounded-full border border-slate-300 text-sm",
|
|
20
|
-
option: (active) => cx(BASE, active ? ON : OFF),
|
|
23
|
+
option: (active, disabled) => cx(BASE, active ? ON : OFF, !active && !disabled && OFF_LIVE),
|
|
21
24
|
} }));
|
|
22
25
|
}
|
package/dist/Table.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import type { ReactElement, ReactNode } from "react";
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
2
2
|
type Align = "start" | "end";
|
|
3
3
|
declare const TONE: {
|
|
4
4
|
readonly default: "text-slate-700";
|
|
5
5
|
readonly strong: "font-medium text-slate-900";
|
|
6
6
|
readonly muted: "text-slate-500";
|
|
7
7
|
};
|
|
8
|
+
type TableVariant = "card" | "plain";
|
|
9
|
+
/** What an element passes through (`data-*`, `aria-*`, `id`, …): the layout and the children stay the component's. */
|
|
10
|
+
type Rest<Tag extends "table" | "thead" | "tbody" | "tr" | "th" | "td", Own extends string = never> = Omit<ComponentPropsWithoutRef<Tag>, "className" | "style" | "children" | Own>;
|
|
8
11
|
/**
|
|
9
12
|
* The classes of the one link in a row whose hit area covers the row, so the whole row is the
|
|
10
13
|
* click target. For a router's own `<Link>`; `TableRow` is the `relative` ancestor it stretches to.
|
|
@@ -13,36 +16,44 @@ export declare const rowLinkClass: string;
|
|
|
13
16
|
/**
|
|
14
17
|
* A list of records in a rounded, bordered card that scrolls sideways on a narrow screen. Compose it
|
|
15
18
|
* from `TableHead`, `TableBody`, `TableRow`, `TableHeaderCell` and `TableCell`. `caption` names the
|
|
16
|
-
* table for assistive tech and is not drawn.
|
|
19
|
+
* table for assistive tech and is not drawn. `variant="plain"` drops the card (its border, fill and
|
|
20
|
+
* shadow), the head's tinted strip and the row hover, and keeps the row dividers; it is for a table
|
|
21
|
+
* that sits inside a card of its own. `className` goes on the scrolling wrapper; the other props
|
|
22
|
+
* (`id`, `data-*`, `aria-*`) on the `<table>`.
|
|
17
23
|
*/
|
|
18
|
-
export declare function Table({ caption, className, children, }: {
|
|
24
|
+
export declare function Table({ caption, variant, className, children, ...rest }: {
|
|
19
25
|
caption?: string | undefined;
|
|
26
|
+
variant?: TableVariant | undefined;
|
|
20
27
|
className?: string | undefined;
|
|
21
28
|
children: ReactNode;
|
|
22
|
-
}): ReactElement;
|
|
23
|
-
/** The `<thead>`: small uppercase column titles on a tinted strip
|
|
24
|
-
export declare function TableHead({ children }: {
|
|
29
|
+
} & Rest<"table">): ReactElement;
|
|
30
|
+
/** The `<thead>`: small uppercase column titles, on a tinted strip unless the table is `plain`. Holds one `TableRow` of `TableHeaderCell`s. */
|
|
31
|
+
export declare function TableHead({ children, ...rest }: {
|
|
25
32
|
children: ReactNode;
|
|
26
|
-
}): ReactElement;
|
|
27
|
-
/** The `<tbody>`: the rows, each with a line above it and a fill on hover. */
|
|
28
|
-
export declare function TableBody({ children }: {
|
|
33
|
+
} & Rest<"thead">): ReactElement;
|
|
34
|
+
/** The `<tbody>`: the rows, each with a line above it and, unless the table is `plain`, a fill on hover. */
|
|
35
|
+
export declare function TableBody({ children, ...rest }: {
|
|
29
36
|
children: ReactNode;
|
|
30
|
-
}): ReactElement;
|
|
37
|
+
} & Rest<"tbody">): ReactElement;
|
|
31
38
|
/** A `<tr>`, `relative`: the anchor a `rowLinkClass` link stretches to. Use it in the head and the body. */
|
|
32
|
-
export declare function TableRow({ children }: {
|
|
39
|
+
export declare function TableRow({ children, ...rest }: {
|
|
33
40
|
children: ReactNode;
|
|
34
|
-
}): ReactElement;
|
|
41
|
+
} & Rest<"tr">): ReactElement;
|
|
35
42
|
/** A column title, `<th scope="col">`. */
|
|
36
|
-
export declare function TableHeaderCell({ align, children, }: {
|
|
43
|
+
export declare function TableHeaderCell({ align, children, ...rest }: {
|
|
37
44
|
align?: Align | undefined;
|
|
38
45
|
children?: ReactNode;
|
|
39
|
-
}): ReactElement;
|
|
40
|
-
/**
|
|
41
|
-
|
|
46
|
+
} & Rest<"th", "align" | "scope">): ReactElement;
|
|
47
|
+
/**
|
|
48
|
+
* A `<td>`. `tone` is the text weight and colour; `numeric` sets tabular figures, `mono` a small mono
|
|
49
|
+
* face. `secondary` is a second, muted line under the content.
|
|
50
|
+
*/
|
|
51
|
+
export declare function TableCell({ align, tone, numeric, mono, secondary, children, ...rest }: {
|
|
42
52
|
align?: Align | undefined;
|
|
43
53
|
tone?: keyof typeof TONE | undefined;
|
|
44
54
|
numeric?: boolean | undefined;
|
|
45
55
|
mono?: boolean | undefined;
|
|
56
|
+
secondary?: ReactNode;
|
|
46
57
|
children?: ReactNode;
|
|
47
|
-
}): ReactElement;
|
|
58
|
+
} & Rest<"td", "align">): ReactElement;
|
|
48
59
|
export {};
|
package/dist/Table.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
2
3
|
import { cx } from "./cx.js";
|
|
3
4
|
import { linkClass, linkStretchClass } from "./TextLink.js";
|
|
4
5
|
const ALIGN = { start: "text-left", end: "text-right" };
|
|
@@ -7,6 +8,7 @@ const TONE = {
|
|
|
7
8
|
strong: "font-medium text-slate-900",
|
|
8
9
|
muted: "text-slate-500",
|
|
9
10
|
};
|
|
11
|
+
const VariantContext = createContext("card");
|
|
10
12
|
/**
|
|
11
13
|
* The classes of the one link in a row whose hit area covers the row, so the whole row is the
|
|
12
14
|
* click target. For a router's own `<Link>`; `TableRow` is the `relative` ancestor it stretches to.
|
|
@@ -15,28 +17,36 @@ export const rowLinkClass = cx(linkClass, linkStretchClass);
|
|
|
15
17
|
/**
|
|
16
18
|
* A list of records in a rounded, bordered card that scrolls sideways on a narrow screen. Compose it
|
|
17
19
|
* from `TableHead`, `TableBody`, `TableRow`, `TableHeaderCell` and `TableCell`. `caption` names the
|
|
18
|
-
* table for assistive tech and is not drawn.
|
|
20
|
+
* table for assistive tech and is not drawn. `variant="plain"` drops the card (its border, fill and
|
|
21
|
+
* shadow), the head's tinted strip and the row hover, and keeps the row dividers; it is for a table
|
|
22
|
+
* that sits inside a card of its own. `className` goes on the scrolling wrapper; the other props
|
|
23
|
+
* (`id`, `data-*`, `aria-*`) on the `<table>`.
|
|
19
24
|
*/
|
|
20
|
-
export function Table({ caption, className, children, }) {
|
|
21
|
-
return (_jsx("div", { className: cx("overflow-x-auto rounded-xl border border-slate-200/70 bg-white shadow-sm", className), children: _jsxs("table", { className: "w-full text-left text-sm", children: [caption && _jsx("caption", { className: "sr-only", children: caption }), children] }) }));
|
|
25
|
+
export function Table({ caption, variant = "card", className, children, ...rest }) {
|
|
26
|
+
return (_jsx(VariantContext.Provider, { value: variant, children: _jsx("div", { className: cx("overflow-x-auto", variant === "card" && "rounded-xl border border-slate-200/70 bg-white shadow-sm", className), children: _jsxs("table", { ...rest, className: "w-full text-left text-sm", children: [caption && _jsx("caption", { className: "sr-only", children: caption }), children] }) }) }));
|
|
22
27
|
}
|
|
23
|
-
/** The `<thead>`: small uppercase column titles on a tinted strip
|
|
24
|
-
export function TableHead({ children }) {
|
|
25
|
-
|
|
28
|
+
/** The `<thead>`: small uppercase column titles, on a tinted strip unless the table is `plain`. Holds one `TableRow` of `TableHeaderCell`s. */
|
|
29
|
+
export function TableHead({ children, ...rest }) {
|
|
30
|
+
const plain = useContext(VariantContext) === "plain";
|
|
31
|
+
return (_jsx("thead", { ...rest, className: cx("text-xs uppercase tracking-wide text-slate-500", !plain && "bg-slate-50"), children: children }));
|
|
26
32
|
}
|
|
27
|
-
/** The `<tbody>`: the rows, each with a line above it and a fill on hover. */
|
|
28
|
-
export function TableBody({ children }) {
|
|
29
|
-
|
|
33
|
+
/** The `<tbody>`: the rows, each with a line above it and, unless the table is `plain`, a fill on hover. */
|
|
34
|
+
export function TableBody({ children, ...rest }) {
|
|
35
|
+
const plain = useContext(VariantContext) === "plain";
|
|
36
|
+
return (_jsx("tbody", { ...rest, className: cx("[&>tr]:border-t [&>tr]:border-slate-200/70", !plain && "[&>tr:hover]:bg-slate-50"), children: children }));
|
|
30
37
|
}
|
|
31
38
|
/** A `<tr>`, `relative`: the anchor a `rowLinkClass` link stretches to. Use it in the head and the body. */
|
|
32
|
-
export function TableRow({ children }) {
|
|
33
|
-
return _jsx("tr", { className: "relative", children: children });
|
|
39
|
+
export function TableRow({ children, ...rest }) {
|
|
40
|
+
return (_jsx("tr", { ...rest, className: "relative", children: children }));
|
|
34
41
|
}
|
|
35
42
|
/** A column title, `<th scope="col">`. */
|
|
36
|
-
export function TableHeaderCell({ align = "start", children, }) {
|
|
37
|
-
return (_jsx("th", { scope: "col", className: cx("whitespace-nowrap px-4 py-2.5 font-medium", ALIGN[align]), children: children }));
|
|
43
|
+
export function TableHeaderCell({ align = "start", children, ...rest }) {
|
|
44
|
+
return (_jsx("th", { ...rest, scope: "col", className: cx("whitespace-nowrap px-4 py-2.5 font-medium", ALIGN[align]), children: children }));
|
|
38
45
|
}
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
/**
|
|
47
|
+
* A `<td>`. `tone` is the text weight and colour; `numeric` sets tabular figures, `mono` a small mono
|
|
48
|
+
* face. `secondary` is a second, muted line under the content.
|
|
49
|
+
*/
|
|
50
|
+
export function TableCell({ align = "start", tone = "default", numeric = false, mono = false, secondary, children, ...rest }) {
|
|
51
|
+
return (_jsxs("td", { ...rest, className: cx("whitespace-nowrap px-4 py-3", ALIGN[align], TONE[tone], numeric && "tabular-nums", mono && "font-mono text-xs"), children: [children, secondary != null && secondary !== false && (_jsx("div", { className: "text-xs font-normal text-slate-500", children: secondary }))] }));
|
|
42
52
|
}
|
package/dist/Tabs.d.ts
CHANGED
|
@@ -2,12 +2,18 @@ import type { ReactElement } from "react";
|
|
|
2
2
|
import { type SwitchItem, type SwitchProps } from "./switcher.js";
|
|
3
3
|
/** One tab of a Tabs row. See `SwitchItem`. */
|
|
4
4
|
export type Tab = SwitchItem;
|
|
5
|
+
export type TabsProps = SwitchProps & {
|
|
6
|
+
/** `underline` (default) is a row over a bottom rule; `pill` fills the current tab, as `NavTabs` does. */
|
|
7
|
+
variant?: "underline" | "pill";
|
|
8
|
+
};
|
|
5
9
|
/**
|
|
6
10
|
* Underline tabs for switching a view in place: a row over a bottom rule, the current tab
|
|
7
11
|
* underlined in the accent. `items` are `{ value, label }` buttons (`value` and `onChange` are the
|
|
8
12
|
* caller's state; the arrow keys, Home and End move between tabs) or links (`href`, or `render` to
|
|
9
13
|
* draw the app's own router `<Link>`; `value` marks the current one, which gets `aria-current`).
|
|
10
|
-
* `
|
|
11
|
-
* `
|
|
14
|
+
* `variant="pill"` fills the current tab instead, with no rule, for under a `PageHeader`. An item may
|
|
15
|
+
* carry a `count`, and a button one `disabled` (with a `title` for the reason); `disabled` on the
|
|
16
|
+
* row disables every tab. `label` names the tab list. The panel is the caller's: give it
|
|
17
|
+
* `role="tabpanel"`, and a tab `controls` to point at it.
|
|
12
18
|
*/
|
|
13
|
-
export declare function Tabs(props:
|
|
19
|
+
export declare function Tabs({ variant, ...props }: TabsProps): ReactElement;
|
package/dist/Tabs.js
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx, focusRing } from "./cx.js";
|
|
3
|
+
import { navTabClass } from "./NavTabs.js";
|
|
3
4
|
import { Switcher } from "./switcher.js";
|
|
4
5
|
const BASE = `-mb-px border-b-2 px-0.5 py-2 transition ${focusRing}`;
|
|
5
6
|
const ON = "border-violet-600 font-medium text-violet-700";
|
|
6
|
-
const OFF = "border-transparent text-slate-600
|
|
7
|
+
const OFF = "border-transparent text-slate-600";
|
|
8
|
+
const OFF_LIVE = "hover:text-violet-700";
|
|
7
9
|
/**
|
|
8
10
|
* Underline tabs for switching a view in place: a row over a bottom rule, the current tab
|
|
9
11
|
* underlined in the accent. `items` are `{ value, label }` buttons (`value` and `onChange` are the
|
|
10
12
|
* caller's state; the arrow keys, Home and End move between tabs) or links (`href`, or `render` to
|
|
11
13
|
* draw the app's own router `<Link>`; `value` marks the current one, which gets `aria-current`).
|
|
12
|
-
* `
|
|
13
|
-
* `
|
|
14
|
+
* `variant="pill"` fills the current tab instead, with no rule, for under a `PageHeader`. An item may
|
|
15
|
+
* carry a `count`, and a button one `disabled` (with a `title` for the reason); `disabled` on the
|
|
16
|
+
* row disables every tab. `label` names the tab list. The panel is the caller's: give it
|
|
17
|
+
* `role="tabpanel"`, and a tab `controls` to point at it.
|
|
14
18
|
*/
|
|
15
|
-
export function Tabs(props) {
|
|
19
|
+
export function Tabs({ variant = "underline", ...props }) {
|
|
20
|
+
const pill = variant === "pill";
|
|
16
21
|
return (_jsx(Switcher, { ...props, parts: {
|
|
17
22
|
group: "tablist",
|
|
18
23
|
item: "tab",
|
|
19
24
|
vertical: false,
|
|
20
|
-
root:
|
|
21
|
-
|
|
25
|
+
root: pill
|
|
26
|
+
? "flex flex-wrap gap-1 text-sm"
|
|
27
|
+
: "flex flex-wrap gap-x-5 border-b border-slate-200 text-sm",
|
|
28
|
+
option: (active, disabled) => pill
|
|
29
|
+
? navTabClass(active, disabled)
|
|
30
|
+
: cx(BASE, active ? ON : OFF, !active && !disabled && OFF_LIVE),
|
|
22
31
|
} }));
|
|
23
32
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { StatTile, type StatTileTone } from "./StatTile.js";
|
|
|
4
4
|
export { ProgressBar, type BarSize, type BarTone } from "./ProgressBar.js";
|
|
5
5
|
export { SplitBar } from "./SplitBar.js";
|
|
6
6
|
export { Markdown } from "./Markdown.js";
|
|
7
|
-
export { AppShell, appShellRootClass } from "./AppShell.js";
|
|
7
|
+
export { AppShell, AppShellBrand, appShellRootClass, appShellRootClassFor } from "./AppShell.js";
|
|
8
8
|
export { Container } from "./Container.js";
|
|
9
9
|
export { PageHeader, type PageHeaderProps } from "./PageHeader.js";
|
|
10
10
|
export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, SidebarLayout, } from "./PageLayouts.js";
|
|
@@ -29,7 +29,7 @@ export { IconButton } from "./IconButton.js";
|
|
|
29
29
|
export { ColourSchemeToggle } from "./ColourSchemeToggle.js";
|
|
30
30
|
export { applyColourScheme, schemeClass, type ColourSchemeClasses, colourSchemeScript, readColourScheme, storeColourScheme, type ColourScheme, } from "./colourScheme.js";
|
|
31
31
|
export { NavTabs, navTabClass, type NavTab } from "./NavTabs.js";
|
|
32
|
-
export { Tabs, type Tab } from "./Tabs.js";
|
|
32
|
+
export { Tabs, type Tab, type TabsProps } from "./Tabs.js";
|
|
33
33
|
export { SegmentedControl, type Segment } from "./SegmentedControl.js";
|
|
34
34
|
export { ToggleChip } from "./ToggleChip.js";
|
|
35
35
|
export { FilterGroup } from "./FilterGroup.js";
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { StatTile } from "./StatTile.js";
|
|
|
4
4
|
export { ProgressBar } from "./ProgressBar.js";
|
|
5
5
|
export { SplitBar } from "./SplitBar.js";
|
|
6
6
|
export { Markdown } from "./Markdown.js";
|
|
7
|
-
export { AppShell, appShellRootClass } from "./AppShell.js";
|
|
7
|
+
export { AppShell, AppShellBrand, appShellRootClass, appShellRootClassFor } from "./AppShell.js";
|
|
8
8
|
export { Container } from "./Container.js";
|
|
9
9
|
export { PageHeader } from "./PageHeader.js";
|
|
10
10
|
export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, SidebarLayout, } from "./PageLayouts.js";
|
package/dist/link.d.ts
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
2
|
+
type AriaCurrent = NonNullable<ComponentPropsWithoutRef<"a">["aria-current"]>;
|
|
2
3
|
/**
|
|
3
4
|
* What a `render` function receives to draw a link with the app's own router `<Link>`: the
|
|
4
|
-
* component's classes, its label as `children`, and (for a tab that is current
|
|
5
|
-
* external) `aria-current`, `target` and `
|
|
6
|
-
* `render: (props) => <Link to="/quotes" {...props} />`. `@hackcove/ui` has no
|
|
5
|
+
* component's classes, its label as `children`, and (for a tab that is current, a link that is
|
|
6
|
+
* external or an option with a tooltip) `aria-current`, `target`, `rel` and `title`. Spread it on
|
|
7
|
+
* the router's link: `render: (props) => <Link to="/quotes" {...props} />`. `@hackcove/ui` has no
|
|
8
|
+
* router dependency.
|
|
9
|
+
*
|
|
10
|
+
* An optional field is `?: T`, never `?: T | undefined`, and a key that has no value is left out
|
|
11
|
+
* rather than set to `undefined`. A router's `<Link>` declares its props that way, so under
|
|
12
|
+
* `exactOptionalPropertyTypes` the spread compiles. Any other anchor prop the caller gave the
|
|
13
|
+
* component (`id`, `data-*`, `download`, a handler) arrives too, untyped here, and `<Link>`
|
|
14
|
+
* takes it as its own.
|
|
7
15
|
*/
|
|
8
|
-
export type LinkRenderProps =
|
|
16
|
+
export type LinkRenderProps = {
|
|
9
17
|
className: string;
|
|
10
18
|
children: ReactNode;
|
|
19
|
+
"aria-current"?: AriaCurrent;
|
|
20
|
+
target?: string;
|
|
21
|
+
rel?: string;
|
|
22
|
+
title?: string;
|
|
11
23
|
};
|
|
12
24
|
/** Draws one link. The component hands it `LinkRenderProps`; it returns the app's own link element. */
|
|
13
25
|
export type LinkRender = (props: LinkRenderProps) => ReactNode;
|
|
@@ -15,8 +27,15 @@ export type LinkRender = (props: LinkRenderProps) => ReactNode;
|
|
|
15
27
|
* A link drawn by `render` when there is one, otherwise a plain `<a href>`. `external` adds
|
|
16
28
|
* `target="_blank"` and `rel="noreferrer"` to either, unless the caller passes its own.
|
|
17
29
|
*/
|
|
18
|
-
export declare function renderLink({ href, render, external, ...
|
|
30
|
+
export declare function renderLink({ href, render, external, className, children, "aria-current": current, target, rel, title, ...rest }: {
|
|
31
|
+
className: string;
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
"aria-current"?: AriaCurrent | undefined;
|
|
34
|
+
target?: string | undefined;
|
|
35
|
+
rel?: string | undefined;
|
|
36
|
+
title?: string | undefined;
|
|
19
37
|
href?: string | undefined;
|
|
20
38
|
render?: LinkRender | undefined;
|
|
21
39
|
external?: boolean | undefined;
|
|
22
|
-
}): ReactElement;
|
|
40
|
+
} & Omit<ComponentPropsWithoutRef<"a">, "href" | "className" | "children" | "aria-current">): ReactElement;
|
|
41
|
+
export {};
|
package/dist/link.js
CHANGED
|
@@ -3,8 +3,18 @@ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
* A link drawn by `render` when there is one, otherwise a plain `<a href>`. `external` adds
|
|
4
4
|
* `target="_blank"` and `rel="noreferrer"` to either, unless the caller passes its own.
|
|
5
5
|
*/
|
|
6
|
-
export function renderLink({ href, render, external = false, ...
|
|
7
|
-
const
|
|
6
|
+
export function renderLink({ href, render, external = false, className, children, "aria-current": current, target, rel, title, ...rest }) {
|
|
7
|
+
const opened = external ? { target: "_blank", rel: "noreferrer" } : {};
|
|
8
|
+
const all = {
|
|
9
|
+
...rest,
|
|
10
|
+
className,
|
|
11
|
+
children,
|
|
12
|
+
...(current === undefined ? {} : { "aria-current": current }),
|
|
13
|
+
...opened,
|
|
14
|
+
...(target === undefined ? {} : { target }),
|
|
15
|
+
...(rel === undefined ? {} : { rel }),
|
|
16
|
+
...(title === undefined ? {} : { title }),
|
|
17
|
+
};
|
|
8
18
|
if (render)
|
|
9
19
|
return _jsx(_Fragment, { children: render(all) });
|
|
10
20
|
return _jsx("a", { ...(href === undefined ? {} : { href }), ...all });
|
package/dist/switcher.d.ts
CHANGED
|
@@ -4,9 +4,17 @@ type Option = {
|
|
|
4
4
|
/** What `value` is set to when this option is current. */
|
|
5
5
|
value: string;
|
|
6
6
|
label: ReactNode;
|
|
7
|
+
/** A small muted count after the label, part of the accessible name ("Shots 12"). */
|
|
8
|
+
count?: number;
|
|
9
|
+
/** A native tooltip; on a disabled option it is the reason. */
|
|
10
|
+
title?: string;
|
|
7
11
|
};
|
|
8
12
|
/** An option that is a button: it calls `onChange(value)`. */
|
|
9
13
|
export type SwitchButton = Option & {
|
|
14
|
+
/**
|
|
15
|
+
* Not chosen by click or arrow key. The button stays in the page as `aria-disabled`, so its
|
|
16
|
+
* `title` still shows on hover and is read out.
|
|
17
|
+
*/
|
|
10
18
|
disabled?: boolean;
|
|
11
19
|
/** The id of the element this option controls (a tab's panel). */
|
|
12
20
|
controls?: string;
|
|
@@ -42,9 +50,12 @@ export type SwitchProps = Shared & ({
|
|
|
42
50
|
items: SwitchButton[];
|
|
43
51
|
/** Called with the option's `value` when one is chosen, by click or arrow key. */
|
|
44
52
|
onChange: (value: string) => void;
|
|
53
|
+
/** Every option is disabled, e.g. until the page has hydrated. */
|
|
54
|
+
disabled?: boolean;
|
|
45
55
|
} | {
|
|
46
56
|
items: SwitchLink[];
|
|
47
57
|
onChange?: undefined;
|
|
58
|
+
disabled?: undefined;
|
|
48
59
|
});
|
|
49
60
|
type Parts = {
|
|
50
61
|
/** The roles of a group of buttons; a group with links is a `<nav>` instead. */
|
|
@@ -53,14 +64,14 @@ type Parts = {
|
|
|
53
64
|
/** The vertical arrows move too, as in a radio group. */
|
|
54
65
|
vertical: boolean;
|
|
55
66
|
root: string;
|
|
56
|
-
option: (active: boolean) => string;
|
|
67
|
+
option: (active: boolean, disabled: boolean) => string;
|
|
57
68
|
};
|
|
58
69
|
/**
|
|
59
70
|
* The shared body of Tabs and SegmentedControl: a row of options with one current. Buttons form a
|
|
60
71
|
* `tablist` or `radiogroup` with a single tab stop and arrow-key movement (the arrow focuses and
|
|
61
72
|
* chooses); links form a `<nav>` with `aria-current="page"` on the current one.
|
|
62
73
|
*/
|
|
63
|
-
export declare function Switcher({ items, value, onChange, label, className, parts, }: SwitchProps & {
|
|
74
|
+
export declare function Switcher({ items, value, onChange, disabled: groupDisabled, label, className, parts, }: SwitchProps & {
|
|
64
75
|
parts: Parts;
|
|
65
76
|
}): ReactElement;
|
|
66
77
|
export {};
|
package/dist/switcher.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Fragment } from "react";
|
|
3
3
|
import { cx } from "./cx.js";
|
|
4
4
|
import { renderLink } from "./link.js";
|
|
@@ -14,28 +14,36 @@ function target(key, at, count, vertical) {
|
|
|
14
14
|
return count - 1;
|
|
15
15
|
return -1;
|
|
16
16
|
}
|
|
17
|
+
/** The label with its count after it; the plain-space text node keeps the name "Shots 12". */
|
|
18
|
+
function Label({ item }) {
|
|
19
|
+
if (item.count === undefined)
|
|
20
|
+
return _jsx(_Fragment, { children: item.label });
|
|
21
|
+
return (_jsxs(_Fragment, { children: [item.label, " ", _jsx("span", { className: "ml-0.5 text-xs tabular-nums opacity-70", children: item.count })] }));
|
|
22
|
+
}
|
|
17
23
|
const isLinks = (items) => items.some((item) => item.href !== undefined || item.render !== undefined);
|
|
18
24
|
/**
|
|
19
25
|
* The shared body of Tabs and SegmentedControl: a row of options with one current. Buttons form a
|
|
20
26
|
* `tablist` or `radiogroup` with a single tab stop and arrow-key movement (the arrow focuses and
|
|
21
27
|
* chooses); links form a `<nav>` with `aria-current="page"` on the current one.
|
|
22
28
|
*/
|
|
23
|
-
export function Switcher({ items, value, onChange, label, className, parts, }) {
|
|
29
|
+
export function Switcher({ items, value, onChange, disabled: groupDisabled, label, className, parts, }) {
|
|
24
30
|
const root = cx(parts.root, className);
|
|
25
31
|
if (isLinks(items)) {
|
|
26
32
|
return (_jsx("nav", { "aria-label": label, className: root, children: items.map((item) => (_jsx(Fragment, { children: renderLink({
|
|
27
33
|
href: item.href,
|
|
28
34
|
render: item.render,
|
|
29
35
|
external: item.external,
|
|
30
|
-
className: parts.option(item.value === value),
|
|
36
|
+
className: parts.option(item.value === value, false),
|
|
31
37
|
"aria-current": item.value === value ? "page" : undefined,
|
|
32
|
-
|
|
38
|
+
...(item.title === undefined ? {} : { title: item.title }),
|
|
39
|
+
children: _jsx(Label, { item: item }),
|
|
33
40
|
}) }, item.value))) }));
|
|
34
41
|
}
|
|
35
|
-
const
|
|
42
|
+
const off = (item) => groupDisabled === true || item.disabled === true;
|
|
43
|
+
const enabled = items.filter((item) => !off(item));
|
|
36
44
|
const stop = enabled.some((item) => item.value === value) ? value : enabled[0]?.value;
|
|
37
45
|
function onKeyDown(event) {
|
|
38
|
-
const buttons = [...event.currentTarget.querySelectorAll("button:not(
|
|
46
|
+
const buttons = [...event.currentTarget.querySelectorAll("button:not([aria-disabled=true])")];
|
|
39
47
|
const at = buttons.findIndex((button) => button === event.target);
|
|
40
48
|
if (at < 0)
|
|
41
49
|
return;
|
|
@@ -47,7 +55,10 @@ export function Switcher({ items, value, onChange, label, className, parts, }) {
|
|
|
47
55
|
button.focus();
|
|
48
56
|
button.click();
|
|
49
57
|
}
|
|
50
|
-
return (_jsx("div", { role: parts.group, "aria-label": label, onKeyDown: onKeyDown, className: root, children: items.map((item) => (_jsx("button", { type: "button", role: parts.item, ...(parts.item === "tab"
|
|
58
|
+
return (_jsx("div", { role: parts.group, "aria-label": label, ...(groupDisabled ? { "aria-disabled": true } : {}), onKeyDown: onKeyDown, className: root, children: items.map((item) => (_jsx("button", { type: "button", role: parts.item, ...(parts.item === "tab"
|
|
51
59
|
? { "aria-selected": item.value === value }
|
|
52
|
-
: { "aria-checked": item.value === value }), ...(item.controls === undefined ? {} : { "aria-controls": item.controls }), tabIndex: item.value === stop ? 0 : -1,
|
|
60
|
+
: { "aria-checked": item.value === value }), ...(item.controls === undefined ? {} : { "aria-controls": item.controls }), tabIndex: item.value === stop ? 0 : -1, ...(off(item) ? { "aria-disabled": true } : {}), ...(item.title === undefined ? {} : { title: item.title }), onClick: () => {
|
|
61
|
+
if (!off(item))
|
|
62
|
+
onChange?.(item.value);
|
|
63
|
+
}, className: cx(parts.option(item.value === value, off(item)), off(item) && "cursor-not-allowed opacity-50"), children: _jsx(Label, { item: item }) }, item.value))) }));
|
|
53
64
|
}
|
package/dist/ui.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
2
|
/** @deprecated use PageHeader */
|
|
3
3
|
export declare function PageTitle({ title, subtitle }: {
|
|
4
4
|
title: string;
|
|
@@ -9,10 +9,11 @@ export declare function PageTitle({ title, subtitle }: {
|
|
|
9
9
|
* Three opt-in forms for data views: `flush` drops the padding and clips the corners, for a table
|
|
10
10
|
* that runs edge to edge; `bar` puts the title, count and `actions` in a tinted strip across the
|
|
11
11
|
* top, over the body; `dashed` is an empty-state shell, a dashed outline with centred muted text.
|
|
12
|
-
* `dashed` stands alone: it ignores `flush` and `bar
|
|
13
|
-
*
|
|
12
|
+
* `dashed` stands alone: it ignores `flush` and `bar`; `dashed="filled"` is the same shell on a
|
|
13
|
+
* surface fill with more padding, for an empty state that must read as a panel. A flush card has no
|
|
14
|
+
* padding for a `title`, so give it a `bar`. Other props (`id`, `data-*`, `aria-*`) go on the root.
|
|
14
15
|
*/
|
|
15
|
-
export declare function Card({ title, count, actions, bar, flush, dashed, as: Tag, className, children, }: {
|
|
16
|
+
export declare function Card({ title, count, actions, bar, flush, dashed, as: Tag, className, children, ...rest }: {
|
|
16
17
|
title?: string;
|
|
17
18
|
count?: number;
|
|
18
19
|
/** Buttons or links at the end of the header: in the strip with `bar`, else beside the title. */
|
|
@@ -21,12 +22,12 @@ export declare function Card({ title, count, actions, bar, flush, dashed, as: Ta
|
|
|
21
22
|
bar?: boolean;
|
|
22
23
|
/** No padding and `overflow-hidden`: the body (a table, a list) fills the card. */
|
|
23
24
|
flush?: boolean;
|
|
24
|
-
/** An empty-state shell: dashed outline, no shadow, no fill, centred muted text. */
|
|
25
|
-
dashed?: boolean;
|
|
25
|
+
/** An empty-state shell: dashed outline, no shadow, no fill, centred muted text. `"filled"` puts it on a surface fill with more padding. */
|
|
26
|
+
dashed?: boolean | "filled";
|
|
26
27
|
as?: "section" | "article" | "div";
|
|
27
28
|
className?: string;
|
|
28
29
|
children: ReactNode;
|
|
29
|
-
}): import("react").JSX.Element;
|
|
30
|
+
} & Omit<ComponentPropsWithoutRef<"section">, "title" | "className" | "style" | "children">): import("react").JSX.Element;
|
|
30
31
|
/** A coloured epic header row: a dot in the epic's colour + its label. */
|
|
31
32
|
export declare function EpicHeader({ color, label }: {
|
|
32
33
|
color: string;
|
package/dist/ui.js
CHANGED
|
@@ -24,31 +24,35 @@ function CardHeader({ heading, actions }) {
|
|
|
24
24
|
}
|
|
25
25
|
/** `dashed` stands alone; `bar` needs a title or actions to fill it. */
|
|
26
26
|
function cardForm(asked, hasHeader) {
|
|
27
|
-
const dashed = asked.dashed
|
|
27
|
+
const dashed = Boolean(asked.dashed);
|
|
28
28
|
return {
|
|
29
29
|
flush: (asked.flush ?? false) && !dashed,
|
|
30
30
|
bar: (asked.bar ?? false) && !dashed && hasHeader,
|
|
31
31
|
dashed,
|
|
32
|
+
filled: asked.dashed === "filled",
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
function cardClass(form) {
|
|
35
|
-
const { flush, bar, dashed } = form;
|
|
36
|
+
const { flush, bar, dashed, filled } = form;
|
|
36
37
|
const clipped = flush || bar;
|
|
37
|
-
const padding = dashed
|
|
38
|
-
|
|
38
|
+
const padding = dashed
|
|
39
|
+
? cx(filled ? "p-10" : "p-8", "text-center text-sm text-slate-500")
|
|
40
|
+
: "p-5";
|
|
41
|
+
return cx("rounded-xl border", dashed ? "border-dashed border-slate-300" : "border-slate-200/70 bg-white", filled && "bg-white", !clipped && padding, !dashed && "shadow-sm", clipped && "overflow-hidden");
|
|
39
42
|
}
|
|
40
43
|
/**
|
|
41
44
|
* A bordered panel with an optional title and count; `as` picks the element (default `section`).
|
|
42
45
|
* Three opt-in forms for data views: `flush` drops the padding and clips the corners, for a table
|
|
43
46
|
* that runs edge to edge; `bar` puts the title, count and `actions` in a tinted strip across the
|
|
44
47
|
* top, over the body; `dashed` is an empty-state shell, a dashed outline with centred muted text.
|
|
45
|
-
* `dashed` stands alone: it ignores `flush` and `bar
|
|
46
|
-
*
|
|
48
|
+
* `dashed` stands alone: it ignores `flush` and `bar`; `dashed="filled"` is the same shell on a
|
|
49
|
+
* surface fill with more padding, for an empty state that must read as a panel. A flush card has no
|
|
50
|
+
* padding for a `title`, so give it a `bar`. Other props (`id`, `data-*`, `aria-*`) go on the root.
|
|
47
51
|
*/
|
|
48
|
-
export function Card({ title, count, actions, bar, flush, dashed, as: Tag = "section", className, children, }) {
|
|
52
|
+
export function Card({ title, count, actions, bar, flush, dashed, as: Tag = "section", className, children, ...rest }) {
|
|
49
53
|
const form = cardForm({ flush, bar, dashed }, Boolean(title || actions));
|
|
50
54
|
const heading = title && (_jsx(CardHeading, { title: title, count: count, bar: form.bar, spaced: !form.bar && !actions }));
|
|
51
|
-
return (_jsx(Tag, { className: cx(cardClass(form), className), children: form.bar ? (_jsxs(_Fragment, { children: [_jsx(CardBar, { heading: heading, actions: actions }), _jsx("div", { className: form.flush ? undefined : "p-5", children: children })] })) : (_jsxs(_Fragment, { children: [_jsx(CardHeader, { heading: heading, actions: actions }), children] })) }));
|
|
55
|
+
return (_jsx(Tag, { ...rest, className: cx(cardClass(form), className), children: form.bar ? (_jsxs(_Fragment, { children: [_jsx(CardBar, { heading: heading, actions: actions }), _jsx("div", { className: form.flush ? undefined : "p-5", children: children })] })) : (_jsxs(_Fragment, { children: [_jsx(CardHeader, { heading: heading, actions: actions }), children] })) }));
|
|
52
56
|
}
|
|
53
57
|
/** A coloured epic header row: a dot in the epic's colour + its label. */
|
|
54
58
|
export function EpicHeader({ color, label }) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hackcove/ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Hack Cove React components: primitives, typography roles and a safe Markdown renderer, written against Tailwind v4 utilities. Pair with a @hackcove/theme-* package.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"components",
|