@hackcove/ui 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -10
- package/dist/AppShell.d.ts +19 -4
- package/dist/AppShell.js +45 -3
- package/dist/Avatar.d.ts +9 -4
- package/dist/Avatar.js +9 -3
- package/dist/BulletList.d.ts +3 -0
- package/dist/BulletList.js +6 -0
- package/dist/Code.d.ts +3 -0
- package/dist/Code.js +6 -0
- package/dist/Container.d.ts +3 -1
- package/dist/Container.js +2 -2
- package/dist/Divider.d.ts +3 -0
- package/dist/Divider.js +6 -0
- package/dist/InlineLinks.d.ts +11 -0
- package/dist/InlineLinks.js +11 -0
- package/dist/Markdown.js +1 -1
- package/dist/PageLayouts.d.ts +22 -1
- package/dist/PageLayouts.js +20 -2
- package/dist/Stack.d.ts +18 -6
- package/dist/Stack.js +20 -6
- package/dist/TextLink.d.ts +12 -0
- package/dist/TextLink.js +8 -0
- package/dist/Video.d.ts +10 -0
- package/dist/Video.js +10 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.js +7 -1
- package/dist/typography.d.ts +32 -24
- package/dist/typography.js +39 -9
- package/package.json +19 -1
package/README.md
CHANGED
|
@@ -40,11 +40,13 @@ export const Example = () => (
|
|
|
40
40
|
## What is exported
|
|
41
41
|
|
|
42
42
|
- Primitives: `Card`, `EpicHeader`, `Empty`, `PageTitle` (deprecated: use `PageHeader`), `Pill`, `groupByEpic`, `Markdown`.
|
|
43
|
-
- Layout (see Layout): `AppShell`, `appShellRootClass`, `Container`, `PageHeader`, `Stack`, `Section`, and the page layouts `ListLayout`, `DetailLayout`, `FormLayout`, `CardGrid`, `SidebarLayout`, `FullBleedLayout`, `CenteredLayout`.
|
|
43
|
+
- Layout (see Layout): `AppShell`, `appShellRootClass`, `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
|
-
`Body`, `BodyStrong`, `Caption`.
|
|
47
|
-
|
|
46
|
+
`Body`, `BodyStrong`, `Caption`. Three opt-in props, none changing today's look: `SectionHeading rule`
|
|
47
|
+
(a hairline under it, with its spacing), `SubsectionHeading size` (`sm` default, `base` bold, `lg`),
|
|
48
|
+
`Body preserveNewlines` (keeps a note's line breaks).
|
|
49
|
+
- Controls: `Button`, `buttonClass` (the classes of a `Button`, for a router's own `<Link>`), `IconButton` (a square button holding one icon; `label` is required and becomes its `aria-label` and `title`), `ColourSchemeToggle` (a light/dark switch on `<html>`; see Light and dark), `Input` (a text input; `icon` adds a leading icon inside the field), `Textarea` (a multi-line text input, `rows` defaults to 3), `Select` (a native `<select>` in the control look; takes `<option>` children and keeps the browser's arrow). `Checkbox` (a native checkbox with its label; `className` goes on the label, other props on the input). `CopyButton` (a small text button that writes `text` to the clipboard and swaps `label` ("copy") for `copiedLabel` ("copied"); every other button prop, `disabled` included, goes on the `<button>`). `CopyLine` (one line of mono text in a box with a `CopyButton` beside it; other props go on the root `<div>`). `Field` (a label above a control, `htmlFor` names the control's id; a `hint` sits under it, and an `error` replaces the hint with a `role="alert"` line). `Callout` (a tinted notice box; `tone` is `neutral`, `info` (default), `positive`, `warn` or `danger`; an optional `title` sits in bold above the body). `NavTabs` (a row of pill links in a `<nav>`; `items` are `{ label, href, active? }`, `label` names the landmark, the active one is filled and gets `aria-current="page"`). `navTabClass` (the classes of one tab, for a router's own `<Link>`). `Breadcrumbs` (a `<nav aria-label="Breadcrumb">` over an ordered list; `crumbs` are nodes, the last is the current page and gets `aria-current="page"`, and each crumb truncates). `crumbLinkClass` (the classes of a crumb that links, for a router's own `<Link>`). `KeyValueList` (a `<dl>` of label/value rows; each `KeyValue` takes a `label` and its value as children, the value right-aligned in tabular figures). `Avatar` (a round badge of the initials of `name`, titled with the name and `aria-hidden`; with `src` (the app's own local image path, never a remote one) it is a round, cropped `<img alt={name}>` instead; `size` is `sm`, `md` (default), `lg`, `xl` (56px) or `2xl` (96px); `ring` adds a page-colour border and a negative margin for an overlapping stack; the colour is one of seven stock background classes picked by a hash of the name, remapped by each theme, and `avatarColourClass(name)` returns it to colour something else the same way; `initialsOf(name)` is the two letters). `TextLink` (an inline text link in the accent colour; `href` is required, `external` adds `target="_blank"` and `rel="noreferrer"`, `className` and every other anchor prop pass through). `linkClass` (the classes of one, for a router's own `<Link>`). `InlineLinks` (a wrapping row of small links joined by an `aria-hidden` `separator`: `·` (default), `•` or `none`; `className` goes on the row). `Divider` (a hairline `<hr>` in the theme's line colour; spacing is the caller's, on `className`). `BulletList` (a `<ul>` with discs, an indent and a small gap; children are `<li>`s). `Code` (inline code: mono, on a subtle background). `Video` (a full-width, rounded `<video>` with controls that loads metadata only; `src` is required, `poster` and a WebVTT `captions` track are optional, children are the fallback). `Kbd` (a keyboard key in mono; its border takes the surrounding text colour, so it reads on a filled button too). `ToggleChip` (an on/off filter pill; `pressed` sets `aria-pressed` and the filled look, `dot` adds a leading colour dot, and the caller toggles it in `onClick`). `FilterGroup` (a `<fieldset>` with a small uppercase `legend` over a wrapped row of its children, e.g. `ToggleChip`s; `className` goes on the fieldset). `DatePicker` and `DateField` (see Dates).
|
|
48
50
|
- Dates: `toISODate`, `fromISODate`, and the `CalendarDate` type.
|
|
49
51
|
|
|
50
52
|
## Dates
|
|
@@ -95,7 +97,7 @@ import { AppShell, PageHeader, Section, Stack, appShellRootClass } from "@hackco
|
|
|
95
97
|
// short page and a long one centre their content at the same x. AppShell cannot set it itself.
|
|
96
98
|
<html className={appShellRootClass}>
|
|
97
99
|
<body>
|
|
98
|
-
<AppShell
|
|
100
|
+
<AppShell brand={<Logo />} nav={<NavTabs label="Main" items={nav} />} footer="…">
|
|
99
101
|
<PageHeader title="Builds" subtitle="Every build" actions={<Button>New</Button>} />
|
|
100
102
|
<Stack>
|
|
101
103
|
<Section title="Recent">…</Section>
|
|
@@ -106,8 +108,13 @@ import { AppShell, PageHeader, Section, Stack, appShellRootClass } from "@hackco
|
|
|
106
108
|
```
|
|
107
109
|
|
|
108
110
|
- **`AppShell`** is the page frame: a sticky header that is always one `h-14` row (a nav that does
|
|
109
|
-
not fit scrolls sideways, it never wraps
|
|
110
|
-
|
|
111
|
+
not fit scrolls sideways, it never wraps, and the edge with more to scroll to fades out until it
|
|
112
|
+
is reached), an optional `subnav` second row (same rule), `main`
|
|
113
|
+
with fixed `py-8`, and an optional `footer`. The header row is filled by slots, left to right:
|
|
114
|
+
`brand` (logo and name, at the brand type with the logo gap), `nav` (a `NavTabs`), `header` (a
|
|
115
|
+
free node, the escape hatch) and `controls` (end-aligned, `gap-2`, centred): no wrapper, no
|
|
116
|
+
`className`. `hideChromeInPrint` is opt-in: the header and footer are hidden in print and main
|
|
117
|
+
drops its padding (the page margin is then your `@page`); without it a print is unchanged. `bleed` drops main's `Container` for a full-bleed
|
|
111
118
|
page; the header keeps its own, so put a `Container` back around anything that should line up.
|
|
112
119
|
- **`Container`** is the only place the content width (`max-w-6xl`) and side padding
|
|
113
120
|
(`px-4 sm:px-6`) are defined. **Pages never set their own width or outer padding**: no `max-w-*`,
|
|
@@ -115,10 +122,13 @@ import { AppShell, PageHeader, Section, Stack, appShellRootClass } from "@hackco
|
|
|
115
122
|
- **`PageHeader`** is the top of every page: optional `breadcrumbs`, the `title` (a `PageHeading`),
|
|
116
123
|
`subtitle`, `actions` (right of the title from `sm`, under it on a phone) and `tabs` under it. Its
|
|
117
124
|
fixed bottom margin puts every page's first block at the same y.
|
|
118
|
-
- **`Stack`** spaces blocks in a column: `gap="
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
- **`Stack`** spaces blocks in a column: `gap="xs"` (`gap-2`) between lines of one text block (a
|
|
126
|
+
heading, the line under it), `sm` (`gap-4`) inside a group, `md` (`gap-6`, the default) between
|
|
127
|
+
blocks, `lg` (`gap-8`) between groups. Use it instead of `space-y-*`. `width` caps the column:
|
|
128
|
+
`narrow` (`max-w-3xl`, a reading measure), `wide` (`max-w-5xl`), `full` (`w-full`) or `default`
|
|
129
|
+
(nothing). A capped Stack starts at the Container's left edge and is never re-centred.
|
|
130
|
+
**`Section`** is a `<section>` Stack (`sm` by default) with an optional `SectionHeading` `title`,
|
|
131
|
+
`actions` beside it and the same `width`.
|
|
122
132
|
|
|
123
133
|
### Page layouts
|
|
124
134
|
|
|
@@ -133,6 +143,11 @@ except the last four.
|
|
|
133
143
|
- **`FormLayout`**: use this for a create or edit page. A `max-w-3xl` `<form>` that starts at the
|
|
134
144
|
Container's left edge (it is not re-centred, so the title does not jump), with an `actions` row at
|
|
135
145
|
the end. Other props (`onSubmit`, `action`) go on the `<form>`.
|
|
146
|
+
- **`ProseLayout`**: use this for a text page (privacy, terms, a CV, an about page). One `narrow`
|
|
147
|
+
(`max-w-3xl`) column that starts at the Container's left edge (not re-centred, so the title does
|
|
148
|
+
not jump): the header, then `children` in a `Stack` (`gap` prop, `md` by default). Unlike
|
|
149
|
+
`FormLayout` the header sits inside the column, so its `actions` stay beside the text.
|
|
150
|
+
`className` goes on the column.
|
|
136
151
|
- **`CardGrid`**: use this for stat tiles or a gallery of cards. `columns` is `2`, `3` (default) or
|
|
137
152
|
`4`: one column on a phone, two from `sm`, `columns` from `lg`.
|
|
138
153
|
- **`SidebarLayout`**: use this when a `rail` of navigation sits beside the content. Render it in
|
|
@@ -142,6 +157,18 @@ except the last four.
|
|
|
142
157
|
- **`FullBleedLayout`**: use this for a viewer that needs the whole width. Render it in
|
|
143
158
|
`<AppShell bleed>`; `children` fill the width, and an optional `panel` takes `lg:w-72` on the
|
|
144
159
|
right and stacks below under `lg`.
|
|
160
|
+
|
|
161
|
+
**`SidebarLayout` and `FullBleedLayout` deliberately break the shared left edge from `md` and
|
|
162
|
+
`lg` up.** A full-height rail and an edge-to-edge viewer need the viewport's edges, so they cannot
|
|
163
|
+
sit inside the Container the header uses. At 1280 px the header logo and every other layout start
|
|
164
|
+
at x=88, `SidebarLayout`'s rail at x=0 and its canvas at x=264 (rail 240 + the Container's 24
|
|
165
|
+
padding), `FullBleedLayout`'s `children` at x=0 (they add their own padding). Below `md` (`lg` for
|
|
166
|
+
`FullBleedLayout`) everything stacks and all layouts agree at the Container's padding. To avoid
|
|
167
|
+
the shift between pages, give a whole section of an app the same layout (every page under a
|
|
168
|
+
project on `SidebarLayout`) rather than mixing it with `ListLayout` pages that a user moves
|
|
169
|
+
between; the header, which stays inside the Container, is the constant. The title's y also
|
|
170
|
+
differs from `ListLayout` only when one page passes `breadcrumbs` and the other does not:
|
|
171
|
+
`PageHeader` puts them above the title in every layout.
|
|
145
172
|
- **`CenteredLayout`**: use this for sign-in, landing, not-found and error pages. A `max-w-md` card
|
|
146
173
|
centred at a fixed offset from the top; the one layout that does not share the left edge.
|
|
147
174
|
|
package/dist/AppShell.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
2
2
|
/**
|
|
3
3
|
* The class an app puts on its own `<html>`: it always reserves the scrollbar, so centred content
|
|
4
4
|
* does not shift between a short page and a long one. AppShell never touches `<html>` itself.
|
|
@@ -9,14 +9,29 @@ export declare const appShellRootClass = "overflow-y-scroll";
|
|
|
9
9
|
* optional footer. Header, main and footer each sit in a `Container`, so pages never set their own
|
|
10
10
|
* width or padding. `bleed` drops main's Container (the header keeps its own). Main is a flex
|
|
11
11
|
* column, so a full-height child (`SidebarLayout`) is `flex-1 min-h-0` and needs no header height.
|
|
12
|
+
*
|
|
13
|
+
* The header row is laid out by its slots, left to right: `brand`, `nav`, then `header` (anything
|
|
14
|
+
* the slots do not cover), then `controls`, pushed to the end. An app writes no wrapper or
|
|
15
|
+
* `className` for any of them.
|
|
12
16
|
*/
|
|
13
|
-
export declare function AppShell({ header, subnav, footer, bleed, children, }: {
|
|
14
|
-
/** The
|
|
15
|
-
|
|
17
|
+
export declare function AppShell({ brand, nav, controls, header, subnav, footer, bleed, hideChromeInPrint, children, }: {
|
|
18
|
+
/** The brand: a logo and a name, in the header's brand type, with the gap between them. */
|
|
19
|
+
brand?: ReactNode;
|
|
20
|
+
/** The main navigation, e.g. `NavTabs` (it is already a `<nav>`). */
|
|
21
|
+
nav?: ReactNode;
|
|
22
|
+
/** The end of the row: a picker, a theme toggle. Gap-2, vertically centred. */
|
|
23
|
+
controls?: ReactNode;
|
|
24
|
+
/** The escape hatch: a free node in the header row, after `brand` and `nav`. */
|
|
25
|
+
header?: ReactNode;
|
|
16
26
|
/** A second fixed-height row under the header, e.g. `NavTabs`. */
|
|
17
27
|
subnav?: ReactNode;
|
|
18
28
|
footer?: ReactNode;
|
|
19
29
|
/** Main without a Container, for a full-bleed layout. */
|
|
20
30
|
bleed?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Print only the page: the header and footer are hidden, and main's Container drops its padding
|
|
33
|
+
* (the page margin is then `@page`'s). Off by default, so a print is unchanged unless an app asks.
|
|
34
|
+
*/
|
|
35
|
+
hideChromeInPrint?: boolean;
|
|
21
36
|
children: ReactNode;
|
|
22
37
|
}): ReactElement;
|
package/dist/AppShell.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
2
3
|
import { Container } from "./Container.js";
|
|
3
4
|
import { cx } from "./cx.js";
|
|
4
5
|
/**
|
|
@@ -8,12 +9,53 @@ import { cx } from "./cx.js";
|
|
|
8
9
|
export const appShellRootClass = "overflow-y-scroll";
|
|
9
10
|
// One fixed-height row that scrolls sideways and never wraps; its children keep their width.
|
|
10
11
|
const ROW = "flex items-center gap-4 overflow-x-auto whitespace-nowrap *:shrink-0";
|
|
12
|
+
// While a row has more to scroll to on a side, that edge fades out (a mask, so it follows any theme).
|
|
13
|
+
// `ScrollRow` sets `data-more-start` / `data-more-end` (and `data-more` for either); a row that fits
|
|
14
|
+
// has none and is untouched.
|
|
15
|
+
const FADE = "data-more-start:[--fade-start:2rem] data-more-end:[--fade-end:2rem] data-more:[mask-image:linear-gradient(to_right,transparent,#000_var(--fade-start,0px),#000_calc(100%_-_var(--fade-end,0px)),transparent)]";
|
|
16
|
+
/** A row in a Container, with the fade on whichever side still has content to scroll to. */
|
|
17
|
+
function ScrollRow({ className, children }) {
|
|
18
|
+
const row = useRef(null);
|
|
19
|
+
// Every render: the children may have changed, and each needs observing.
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const el = row.current;
|
|
22
|
+
if (!el)
|
|
23
|
+
return undefined;
|
|
24
|
+
const update = () => {
|
|
25
|
+
const start = el.scrollLeft > 1;
|
|
26
|
+
const end = el.scrollWidth - el.clientWidth - el.scrollLeft > 1;
|
|
27
|
+
el.toggleAttribute("data-more-start", start);
|
|
28
|
+
el.toggleAttribute("data-more-end", end);
|
|
29
|
+
el.toggleAttribute("data-more", start || end);
|
|
30
|
+
};
|
|
31
|
+
update();
|
|
32
|
+
el.addEventListener("scroll", update, { passive: true });
|
|
33
|
+
// jsdom (a consumer's own tests) has no ResizeObserver.
|
|
34
|
+
const resize = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(update);
|
|
35
|
+
resize?.observe(el);
|
|
36
|
+
for (const child of el.children)
|
|
37
|
+
resize?.observe(child);
|
|
38
|
+
return () => {
|
|
39
|
+
el.removeEventListener("scroll", update);
|
|
40
|
+
resize?.disconnect();
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
return (_jsx(Container, { elementRef: (el) => void (row.current = el), className: cx(ROW, FADE, className), children: children }));
|
|
44
|
+
}
|
|
45
|
+
/** The header row's slots in order: brand, nav, the free `children`, then controls at the end. */
|
|
46
|
+
function HeaderSlots({ brand, nav, controls, children, }) {
|
|
47
|
+
return (_jsxs(_Fragment, { children: [brand && (_jsx("div", { className: "flex items-center gap-2 text-base font-bold tracking-tight text-slate-900", children: brand })), nav, children, controls && _jsx("div", { className: "ml-auto flex items-center gap-2", children: controls })] }));
|
|
48
|
+
}
|
|
11
49
|
/**
|
|
12
50
|
* The page frame: a sticky one-row header, an optional second `subnav` row, the main area and an
|
|
13
51
|
* optional footer. Header, main and footer each sit in a `Container`, so pages never set their own
|
|
14
52
|
* width or padding. `bleed` drops main's Container (the header keeps its own). Main is a flex
|
|
15
53
|
* column, so a full-height child (`SidebarLayout`) is `flex-1 min-h-0` and needs no header height.
|
|
54
|
+
*
|
|
55
|
+
* The header row is laid out by its slots, left to right: `brand`, `nav`, then `header` (anything
|
|
56
|
+
* the slots do not cover), then `controls`, pushed to the end. An app writes no wrapper or
|
|
57
|
+
* `className` for any of them.
|
|
16
58
|
*/
|
|
17
|
-
export function AppShell({ header, subnav, footer, bleed = false, children, }) {
|
|
18
|
-
return (_jsxs("div", { className: "flex min-h-dvh flex-col", children: [_jsxs("header", { className: "sticky top-0 z-10 border-b border-slate-200 bg-white", children: [_jsx(
|
|
59
|
+
export function AppShell({ brand, nav, controls, header, subnav, footer, bleed = false, hideChromeInPrint = false, children, }) {
|
|
60
|
+
return (_jsxs("div", { className: "flex min-h-dvh flex-col", children: [_jsxs("header", { className: cx("sticky top-0 z-10 border-b border-slate-200 bg-white", hideChromeInPrint && "print:hidden"), children: [_jsx(ScrollRow, { className: "h-14", children: _jsx(HeaderSlots, { brand: brand, nav: nav, controls: controls, children: header }) }), subnav && _jsx(ScrollRow, { className: "h-12", children: subnav })] }), _jsx("main", { className: "flex flex-1 flex-col", children: bleed ? (children) : (_jsx(Container, { className: cx("py-8", hideChromeInPrint && "print:p-0"), children: children })) }), footer && (_jsx("footer", { className: cx("border-t border-slate-200", hideChromeInPrint && "print:hidden"), children: _jsx(Container, { className: "py-6 text-sm text-slate-500", children: footer }) }))] }));
|
|
19
61
|
}
|
package/dist/Avatar.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ declare const SIZE_CLASSES: {
|
|
|
3
3
|
readonly sm: "size-6 text-xs";
|
|
4
4
|
readonly md: "size-7 text-xs";
|
|
5
5
|
readonly lg: "size-10 text-base";
|
|
6
|
+
readonly xl: "size-14 text-xl";
|
|
7
|
+
readonly "2xl": "size-24 text-3xl";
|
|
6
8
|
};
|
|
7
9
|
export type AvatarSize = keyof typeof SIZE_CLASSES;
|
|
8
10
|
/** The background class for a name: the same name gets the same colour everywhere. */
|
|
@@ -10,13 +12,16 @@ export declare function avatarColourClass(name: string): string;
|
|
|
10
12
|
/** Up to two letters or digits of a name, upper-cased; `?` when it has none. */
|
|
11
13
|
export declare function initialsOf(name: string): string;
|
|
12
14
|
/**
|
|
13
|
-
* A person as a round badge of initials
|
|
14
|
-
* another server for decoration, and it makes a
|
|
15
|
+
* A person as a round badge of initials, or as their photo when `src` is given. It never loads a
|
|
16
|
+
* remote image on its own: that is a request to another server for decoration, and it makes a
|
|
17
|
+
* screenshot depend on the network. `src` is the app's own local path.
|
|
15
18
|
*/
|
|
16
|
-
export declare function Avatar({ name, size, ring, className, ...rest }: {
|
|
19
|
+
export declare function Avatar({ name, src, size, ring, className, ...rest }: {
|
|
17
20
|
name: string;
|
|
21
|
+
/** The person's photo, a local path. It replaces the initials and is announced as `name`. */
|
|
22
|
+
src?: string;
|
|
18
23
|
size?: AvatarSize;
|
|
19
24
|
/** A ring in the page colour, for a stack of overlapping avatars. */
|
|
20
25
|
ring?: boolean;
|
|
21
|
-
} & Omit<HTMLAttributes<
|
|
26
|
+
} & Omit<HTMLAttributes<HTMLElement>, "children">): ReactElement;
|
|
22
27
|
export {};
|
package/dist/Avatar.js
CHANGED
|
@@ -14,6 +14,8 @@ const SIZE_CLASSES = {
|
|
|
14
14
|
sm: "size-6 text-xs",
|
|
15
15
|
md: "size-7 text-xs",
|
|
16
16
|
lg: "size-10 text-base",
|
|
17
|
+
xl: "size-14 text-xl",
|
|
18
|
+
"2xl": "size-24 text-3xl",
|
|
17
19
|
};
|
|
18
20
|
/** The background class for a name: the same name gets the same colour everywhere. */
|
|
19
21
|
export function avatarColourClass(name) {
|
|
@@ -30,9 +32,13 @@ export function initialsOf(name) {
|
|
|
30
32
|
.toUpperCase() || "?");
|
|
31
33
|
}
|
|
32
34
|
/**
|
|
33
|
-
* A person as a round badge of initials
|
|
34
|
-
* another server for decoration, and it makes a
|
|
35
|
+
* A person as a round badge of initials, or as their photo when `src` is given. It never loads a
|
|
36
|
+
* remote image on its own: that is a request to another server for decoration, and it makes a
|
|
37
|
+
* screenshot depend on the network. `src` is the app's own local path.
|
|
35
38
|
*/
|
|
36
|
-
export function Avatar({ name, size = "md", ring = false, className, ...rest }) {
|
|
39
|
+
export function Avatar({ name, src, size = "md", ring = false, className, ...rest }) {
|
|
40
|
+
if (src) {
|
|
41
|
+
return (_jsx("img", { src: src, alt: name, title: name, className: cx("shrink-0 rounded-full object-cover", SIZE_CLASSES[size], ring && "-ml-1.5 border-2 border-slate-50 first:ml-0", className), ...rest }));
|
|
42
|
+
}
|
|
37
43
|
return (_jsx("span", { "aria-hidden": "true", title: name, className: cx("inline-grid shrink-0 place-items-center rounded-full font-semibold text-violet-50", SIZE_CLASSES[size], avatarColourClass(name), ring && "-ml-1.5 border-2 border-slate-50 first:ml-0", className), ...rest, children: initialsOf(name) }));
|
|
38
44
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactElement } from "react";
|
|
2
|
+
/** A bulleted `<ul>`: discs, indented, a small gap between items. Its children are `<li>`s (e.g. `Body as="li"`). */
|
|
3
|
+
export declare function BulletList({ className, ...rest }: HTMLAttributes<HTMLUListElement>): ReactElement;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/** A bulleted `<ul>`: discs, indented, a small gap between items. Its children are `<li>`s (e.g. `Body as="li"`). */
|
|
4
|
+
export function BulletList({ className, ...rest }) {
|
|
5
|
+
return _jsx("ul", { className: cx("list-disc space-y-1 pl-5", className), ...rest });
|
|
6
|
+
}
|
package/dist/Code.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactElement } from "react";
|
|
2
|
+
/** Inline code: mono, on a subtle inset background. It takes the surrounding text colour and size (a touch smaller, for the mono face). */
|
|
3
|
+
export declare function Code({ className, ...rest }: HTMLAttributes<HTMLElement>): ReactElement;
|
package/dist/Code.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/** Inline code: mono, on a subtle inset background. It takes the surrounding text colour and size (a touch smaller, for the mono face). */
|
|
4
|
+
export function Code({ className, ...rest }) {
|
|
5
|
+
return (_jsx("code", { className: cx("rounded bg-slate-100 px-1 py-0.5 font-mono text-[0.9em]", className), ...rest }));
|
|
6
|
+
}
|
package/dist/Container.d.ts
CHANGED
|
@@ -3,8 +3,10 @@ import type { ReactElement, ReactNode } from "react";
|
|
|
3
3
|
* The one content edge: every page's width and side padding. This file is the only place they are
|
|
4
4
|
* defined, so a header, a page and a footer inside a Container start at the same x.
|
|
5
5
|
*/
|
|
6
|
-
export declare function Container({ as: Tag, className, children, }: {
|
|
6
|
+
export declare function Container({ as: Tag, className, elementRef, children, }: {
|
|
7
7
|
as?: "div" | "main" | "header" | "footer" | "section" | "nav";
|
|
8
8
|
className?: string;
|
|
9
|
+
/** The rendered element, for a parent that measures or scrolls it. */
|
|
10
|
+
elementRef?: (element: HTMLElement | null) => void;
|
|
9
11
|
children?: ReactNode;
|
|
10
12
|
}): ReactElement;
|
package/dist/Container.js
CHANGED
|
@@ -4,6 +4,6 @@ import { cx } from "./cx.js";
|
|
|
4
4
|
* The one content edge: every page's width and side padding. This file is the only place they are
|
|
5
5
|
* defined, so a header, a page and a footer inside a Container start at the same x.
|
|
6
6
|
*/
|
|
7
|
-
export function Container({ as: Tag = "div", className, children, }) {
|
|
8
|
-
return _jsx(Tag, { className: cx("mx-auto w-full max-w-6xl px-4 sm:px-6", className), children: children });
|
|
7
|
+
export function Container({ as: Tag = "div", className, elementRef, children, }) {
|
|
8
|
+
return (_jsx(Tag, { ref: elementRef, className: cx("mx-auto w-full max-w-6xl px-4 sm:px-6", className), children: children }));
|
|
9
9
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactElement } from "react";
|
|
2
|
+
/** A hairline rule in the theme's line colour: between a heading and its content, or above a footer. Spacing is the caller's, on `className`. */
|
|
3
|
+
export declare function Divider({ className, ...rest }: Omit<HTMLAttributes<HTMLHRElement>, "children">): ReactElement;
|
package/dist/Divider.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/** A hairline rule in the theme's line colour: between a heading and its content, or above a footer. Spacing is the caller's, on `className`. */
|
|
4
|
+
export function Divider({ className, ...rest }) {
|
|
5
|
+
return _jsx("hr", { className: cx("border-t border-slate-200/70", className), ...rest });
|
|
6
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
+
export type InlineLinksSeparator = "·" | "•" | "none";
|
|
3
|
+
/**
|
|
4
|
+
* A wrapping row of links, small text, joined by a separator. The separator is `aria-hidden` and
|
|
5
|
+
* leads each link but the first, held to its link so a wrap never leaves one at a line's end.
|
|
6
|
+
*/
|
|
7
|
+
export declare function InlineLinks({ separator, className, children, }: {
|
|
8
|
+
separator?: InlineLinksSeparator;
|
|
9
|
+
className?: string;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}): ReactElement;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Children } from "react";
|
|
3
|
+
import { cx } from "./cx.js";
|
|
4
|
+
import { Caption } from "./typography.js";
|
|
5
|
+
/**
|
|
6
|
+
* A wrapping row of links, small text, joined by a separator. The separator is `aria-hidden` and
|
|
7
|
+
* leads each link but the first, held to its link so a wrap never leaves one at a line's end.
|
|
8
|
+
*/
|
|
9
|
+
export function InlineLinks({ separator = "·", className, children, }) {
|
|
10
|
+
return (_jsx("div", { className: cx("flex flex-wrap items-center gap-x-3 gap-y-1 text-sm", className), children: Children.toArray(children).map((child, index) => (_jsxs("span", { className: "inline-flex items-center gap-x-3 whitespace-nowrap", children: [index > 0 && separator !== "none" && _jsx(Caption, { "aria-hidden": "true", children: separator }), child] }, index))) }));
|
|
11
|
+
}
|
package/dist/Markdown.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
// A minimal, SAFE markdown renderer
|
|
2
|
+
// A minimal, SAFE markdown renderer (headings / bullets / bold). No
|
|
3
3
|
// dangerouslySetInnerHTML — text is rendered as React nodes, so it can't inject HTML.
|
|
4
4
|
function inline(text) {
|
|
5
5
|
return text.split(/(\*\*[^*]+\*\*)/g).map((p, i) => p.startsWith("**") && p.endsWith("**") ? (_jsx("strong", { className: "font-semibold text-slate-900", children: p.slice(2, -2) }, i)) : (_jsx("span", { children: p }, i)));
|
package/dist/PageLayouts.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
2
2
|
import { type PageHeaderProps } from "./PageHeader.js";
|
|
3
|
+
import { type StackGap } from "./Stack.js";
|
|
3
4
|
/** A list page: header, an optional `filters` slot, then the list, or `empty` when `isEmpty`. */
|
|
4
5
|
export declare function ListLayout({ filters, empty, isEmpty, children, ...header }: PageHeaderProps & {
|
|
5
6
|
filters?: ReactNode;
|
|
@@ -18,6 +19,17 @@ export declare function FormLayout({ actions, children, breadcrumbs, title, subt
|
|
|
18
19
|
actions?: ReactNode;
|
|
19
20
|
children: ReactNode;
|
|
20
21
|
} & Omit<ComponentPropsWithoutRef<"form">, "title">): ReactElement;
|
|
22
|
+
/**
|
|
23
|
+
* A text page: header, then `children` stacked `gap` apart (`md` by default), all in one `narrow`
|
|
24
|
+
* (`max-w-3xl`) reading column that starts at the Container's left edge (it is not re-centred, so
|
|
25
|
+
* the title does not jump). Unlike `FormLayout` the header sits inside the column, so its `actions`
|
|
26
|
+
* stay beside the text instead of at the Container's far edge. `className` goes on the column.
|
|
27
|
+
*/
|
|
28
|
+
export declare function ProseLayout({ gap, className, children, ...header }: PageHeaderProps & {
|
|
29
|
+
gap?: StackGap | undefined;
|
|
30
|
+
className?: string;
|
|
31
|
+
children?: ReactNode;
|
|
32
|
+
}): ReactElement;
|
|
21
33
|
declare const COLUMNS: {
|
|
22
34
|
readonly 2: "lg:grid-cols-2";
|
|
23
35
|
readonly 3: "lg:grid-cols-3";
|
|
@@ -33,12 +45,21 @@ export declare function CardGrid({ columns, className, children, }: {
|
|
|
33
45
|
* A full-height `rail` beside the `children` canvas. Render it in `<AppShell bleed>`: it fills what
|
|
34
46
|
* the shell's flex column leaves under the header, so no header height is written down. The rail is
|
|
35
47
|
* `md:w-60` and stacks above the canvas below `md`; the canvas keeps the Container's padding.
|
|
48
|
+
*
|
|
49
|
+
* It deliberately breaks the shared left edge from `md` up: a full-height rail needs the viewport's
|
|
50
|
+
* left edge, so the rail starts at x=0 and the canvas at the rail's width plus the Container's
|
|
51
|
+
* padding (264 at 1280), not at the header logo's x. Below `md` every layout agrees.
|
|
36
52
|
*/
|
|
37
53
|
export declare function SidebarLayout({ rail, children, }: {
|
|
38
54
|
rail: ReactNode;
|
|
39
55
|
children: ReactNode;
|
|
40
56
|
}): ReactElement;
|
|
41
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Edge-to-edge `children` with an optional right `panel` (`lg:w-72`) that stacks below on a smaller
|
|
59
|
+
* screen. Render it in `<AppShell bleed>`. It deliberately breaks the shared left edge: a viewer
|
|
60
|
+
* that needs the whole width has no Container of its own, so `children` start at the viewport's
|
|
61
|
+
* left edge and add their own padding.
|
|
62
|
+
*/
|
|
42
63
|
export declare function FullBleedLayout({ panel, children, }: {
|
|
43
64
|
panel?: ReactNode;
|
|
44
65
|
children: ReactNode;
|
package/dist/PageLayouts.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Card } from "./ui.js";
|
|
|
3
3
|
import { Container } from "./Container.js";
|
|
4
4
|
import { cx } from "./cx.js";
|
|
5
5
|
import { PageHeader, PageHeaderContent } from "./PageHeader.js";
|
|
6
|
-
import { Stack } from "./Stack.js";
|
|
6
|
+
import { STACK_WIDTH, Stack } from "./Stack.js";
|
|
7
7
|
// Compositions of the layout foundation. None sets a page width or outer padding of its own; the
|
|
8
8
|
// ones that narrow their content do it inside the Container they are rendered in.
|
|
9
9
|
/** A list page: header, an optional `filters` slot, then the list, or `empty` when `isEmpty`. */
|
|
@@ -18,6 +18,15 @@ export function DetailLayout({ aside, children, ...header }) {
|
|
|
18
18
|
export function FormLayout({ actions, children, breadcrumbs, title, subtitle, tabs, className, ...form }) {
|
|
19
19
|
return (_jsxs(_Fragment, { children: [_jsx(PageHeader, { breadcrumbs: breadcrumbs, title: title, subtitle: subtitle, tabs: tabs }), _jsx("form", { className: cx("max-w-3xl", className), ...form, children: _jsxs(Stack, { children: [children, actions && _jsx("div", { className: "flex flex-wrap items-center gap-2", children: actions })] }) })] }));
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* A text page: header, then `children` stacked `gap` apart (`md` by default), all in one `narrow`
|
|
23
|
+
* (`max-w-3xl`) reading column that starts at the Container's left edge (it is not re-centred, so
|
|
24
|
+
* the title does not jump). Unlike `FormLayout` the header sits inside the column, so its `actions`
|
|
25
|
+
* stay beside the text instead of at the Container's far edge. `className` goes on the column.
|
|
26
|
+
*/
|
|
27
|
+
export function ProseLayout({ gap, className, children, ...header }) {
|
|
28
|
+
return (_jsxs("div", { className: cx(STACK_WIDTH.narrow, className), children: [_jsx(PageHeader, { ...header }), _jsx(Stack, { gap: gap, children: children })] }));
|
|
29
|
+
}
|
|
21
30
|
const COLUMNS = {
|
|
22
31
|
2: "lg:grid-cols-2",
|
|
23
32
|
3: "lg:grid-cols-3",
|
|
@@ -31,11 +40,20 @@ export function CardGrid({ columns = 3, className, children, }) {
|
|
|
31
40
|
* A full-height `rail` beside the `children` canvas. Render it in `<AppShell bleed>`: it fills what
|
|
32
41
|
* the shell's flex column leaves under the header, so no header height is written down. The rail is
|
|
33
42
|
* `md:w-60` and stacks above the canvas below `md`; the canvas keeps the Container's padding.
|
|
43
|
+
*
|
|
44
|
+
* It deliberately breaks the shared left edge from `md` up: a full-height rail needs the viewport's
|
|
45
|
+
* left edge, so the rail starts at x=0 and the canvas at the rail's width plus the Container's
|
|
46
|
+
* padding (264 at 1280), not at the header logo's x. Below `md` every layout agrees.
|
|
34
47
|
*/
|
|
35
48
|
export function SidebarLayout({ rail, children, }) {
|
|
36
49
|
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col md:flex-row", children: [_jsx("aside", { className: "w-full shrink-0 border-b border-slate-200 bg-slate-50 p-4 md:w-60 md:border-r md:border-b-0", children: rail }), _jsx("div", { className: "min-w-0 flex-1", children: _jsx(Container, { className: "py-8", children: children }) })] }));
|
|
37
50
|
}
|
|
38
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Edge-to-edge `children` with an optional right `panel` (`lg:w-72`) that stacks below on a smaller
|
|
53
|
+
* screen. Render it in `<AppShell bleed>`. It deliberately breaks the shared left edge: a viewer
|
|
54
|
+
* that needs the whole width has no Container of its own, so `children` start at the viewport's
|
|
55
|
+
* left edge and add their own padding.
|
|
56
|
+
*/
|
|
39
57
|
export function FullBleedLayout({ panel, children, }) {
|
|
40
58
|
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col lg:flex-row", children: [_jsx("div", { className: "min-w-0 flex-1", children: children }), panel && (_jsx("aside", { className: "w-full shrink-0 border-t border-slate-200 bg-slate-50 p-4 lg:w-72 lg:border-t-0 lg:border-l", children: panel }))] }));
|
|
41
59
|
}
|
package/dist/Stack.d.ts
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
-
export type StackGap = "sm" | "md" | "lg";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
export type StackGap = "xs" | "sm" | "md" | "lg";
|
|
3
|
+
export type StackWidth = "narrow" | "default" | "wide" | "full";
|
|
4
|
+
/** `default` adds nothing: the Stack is as wide as its parent. */
|
|
5
|
+
export declare const STACK_WIDTH: Record<StackWidth, string | undefined>;
|
|
6
|
+
/**
|
|
7
|
+
* A vertical column of blocks a fixed `gap` apart: `xs` between lines of text (a heading and the
|
|
8
|
+
* line under it), `sm` inside a group, `md` (default) between blocks, `lg` between groups.
|
|
9
|
+
*
|
|
10
|
+
* `width` caps the column: `narrow` is a reading measure (`max-w-3xl`), `wide` is `max-w-5xl`, `full`
|
|
11
|
+
* is the parent's whole width and `default` sets nothing. A capped Stack starts at its parent's left
|
|
12
|
+
* edge (it is never re-centred), so its first line lines up with every other block on the page.
|
|
13
|
+
*/
|
|
14
|
+
export declare function Stack({ gap, width, as: Tag, className, children, ...rest }: {
|
|
15
|
+
gap?: StackGap | undefined;
|
|
16
|
+
width?: StackWidth | undefined;
|
|
6
17
|
as?: "div" | "section";
|
|
7
18
|
className?: string | undefined;
|
|
8
19
|
children?: ReactNode;
|
|
9
20
|
"aria-labelledby"?: string | undefined;
|
|
10
21
|
}): ReactElement;
|
|
11
22
|
/** A `<section>` Stack with an optional `SectionHeading` title (which names it) and `actions` beside it. */
|
|
12
|
-
export declare function Section({ title, actions, gap, className, children, }: {
|
|
23
|
+
export declare function Section({ title, actions, gap, width, className, children, }: {
|
|
13
24
|
title?: ReactNode;
|
|
14
25
|
actions?: ReactNode;
|
|
15
|
-
gap?: StackGap;
|
|
26
|
+
gap?: StackGap | undefined;
|
|
27
|
+
width?: StackWidth | undefined;
|
|
16
28
|
className?: string;
|
|
17
29
|
children?: ReactNode;
|
|
18
30
|
}): ReactElement;
|
package/dist/Stack.js
CHANGED
|
@@ -2,13 +2,27 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useId } from "react";
|
|
3
3
|
import { cx } from "./cx.js";
|
|
4
4
|
import { SectionHeading } from "./typography.js";
|
|
5
|
-
const GAP = { sm: "gap-4", md: "gap-6", lg: "gap-8" };
|
|
6
|
-
/**
|
|
7
|
-
export
|
|
8
|
-
|
|
5
|
+
const GAP = { xs: "gap-2", sm: "gap-4", md: "gap-6", lg: "gap-8" };
|
|
6
|
+
/** `default` adds nothing: the Stack is as wide as its parent. */
|
|
7
|
+
export const STACK_WIDTH = {
|
|
8
|
+
narrow: "max-w-3xl",
|
|
9
|
+
default: undefined,
|
|
10
|
+
wide: "max-w-5xl",
|
|
11
|
+
full: "w-full",
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* A vertical column of blocks a fixed `gap` apart: `xs` between lines of text (a heading and the
|
|
15
|
+
* line under it), `sm` inside a group, `md` (default) between blocks, `lg` between groups.
|
|
16
|
+
*
|
|
17
|
+
* `width` caps the column: `narrow` is a reading measure (`max-w-3xl`), `wide` is `max-w-5xl`, `full`
|
|
18
|
+
* is the parent's whole width and `default` sets nothing. A capped Stack starts at its parent's left
|
|
19
|
+
* edge (it is never re-centred), so its first line lines up with every other block on the page.
|
|
20
|
+
*/
|
|
21
|
+
export function Stack({ gap = "md", width = "default", as: Tag = "div", className, children, ...rest }) {
|
|
22
|
+
return (_jsx(Tag, { className: cx("flex flex-col", GAP[gap], STACK_WIDTH[width], className), ...rest, children: children }));
|
|
9
23
|
}
|
|
10
24
|
/** A `<section>` Stack with an optional `SectionHeading` title (which names it) and `actions` beside it. */
|
|
11
|
-
export function Section({ title, actions, gap = "sm", className, children, }) {
|
|
25
|
+
export function Section({ title, actions, gap = "sm", width, className, children, }) {
|
|
12
26
|
const titleId = useId();
|
|
13
|
-
return (_jsxs(Stack, { as: "section", gap: gap, className: className, "aria-labelledby": title ? titleId : undefined, children: [(title || actions) && (_jsxs("div", { className: "flex items-center justify-between gap-4", children: [title && _jsx(SectionHeading, { id: titleId, children: title }), actions && _jsx("div", { className: "ml-auto flex items-center gap-2", children: actions })] })), children] }));
|
|
27
|
+
return (_jsxs(Stack, { as: "section", gap: gap, width: width, className: className, "aria-labelledby": title ? titleId : undefined, children: [(title || actions) && (_jsxs("div", { className: "flex items-center justify-between gap-4", children: [title && _jsx(SectionHeading, { id: titleId, children: title }), actions && _jsx("div", { className: "ml-auto flex items-center gap-2", children: actions })] })), children] }));
|
|
14
28
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement } from "react";
|
|
2
|
+
/** The classes of an inline text link, for a router's own <Link> that should look like one. */
|
|
3
|
+
export declare const linkClass: string;
|
|
4
|
+
type TextLinkProps = {
|
|
5
|
+
/** Opens in a new tab: adds `target="_blank"` and `rel="noreferrer"`. */
|
|
6
|
+
external?: boolean | undefined;
|
|
7
|
+
} & ComponentPropsWithoutRef<"a"> & {
|
|
8
|
+
href: string;
|
|
9
|
+
};
|
|
10
|
+
/** An inline text link in the accent colour. Every anchor prop goes on the `<a>`. */
|
|
11
|
+
export declare function TextLink({ external, className, children, ...rest }: TextLinkProps): ReactElement;
|
|
12
|
+
export {};
|
package/dist/TextLink.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx, focusRing } from "./cx.js";
|
|
3
|
+
/** The classes of an inline text link, for a router's own <Link> that should look like one. */
|
|
4
|
+
export const linkClass = cx("rounded font-medium text-violet-600 underline-offset-2 transition-colors hover:text-violet-700", focusRing);
|
|
5
|
+
/** An inline text link in the accent colour. Every anchor prop goes on the `<a>`. */
|
|
6
|
+
export function TextLink({ external = false, className, children, ...rest }) {
|
|
7
|
+
return (_jsx("a", { className: cx(linkClass, className), ...(external ? { target: "_blank", rel: "noreferrer" } : {}), ...rest, children: children }));
|
|
8
|
+
}
|
package/dist/Video.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactElement, VideoHTMLAttributes } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A `<video>` with controls, full width and rounded; it loads metadata only, so the page does not
|
|
4
|
+
* pull the file until it is played. `src` is the app's own video; `captions` is a WebVTT file,
|
|
5
|
+
* offered as the default track. `children` is the fallback for a browser that cannot play it.
|
|
6
|
+
*/
|
|
7
|
+
export declare function Video({ src, captions, className, children, ...rest }: {
|
|
8
|
+
src: string;
|
|
9
|
+
captions?: string | undefined;
|
|
10
|
+
} & Omit<VideoHTMLAttributes<HTMLVideoElement>, "src" | "controls">): ReactElement;
|
package/dist/Video.js
ADDED
|
@@ -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 `<video>` with controls, full width and rounded; it loads metadata only, so the page does not
|
|
5
|
+
* pull the file until it is played. `src` is the app's own video; `captions` is a WebVTT file,
|
|
6
|
+
* offered as the default track. `children` is the fallback for a browser that cannot play it.
|
|
7
|
+
*/
|
|
8
|
+
export function Video({ src, captions, className, children, ...rest }) {
|
|
9
|
+
return (_jsxs("video", { controls: true, preload: "metadata", src: src, className: cx("w-full rounded-lg", className), ...rest, children: [captions && _jsx("track", { kind: "captions", src: captions, default: true }), children] }));
|
|
10
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,12 +3,15 @@ export { Markdown } from "./Markdown.js";
|
|
|
3
3
|
export { AppShell, appShellRootClass } from "./AppShell.js";
|
|
4
4
|
export { Container } from "./Container.js";
|
|
5
5
|
export { PageHeader, type PageHeaderProps } from "./PageHeader.js";
|
|
6
|
-
export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, SidebarLayout, } from "./PageLayouts.js";
|
|
7
|
-
export { Section, Stack, type StackGap } from "./Stack.js";
|
|
6
|
+
export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, SidebarLayout, } from "./PageLayouts.js";
|
|
7
|
+
export { Section, Stack, type StackGap, type StackWidth } from "./Stack.js";
|
|
8
8
|
export { Button, buttonClass, type ButtonVariant } from "./Button.js";
|
|
9
9
|
export { Avatar, avatarColourClass, initialsOf, type AvatarSize } from "./Avatar.js";
|
|
10
10
|
export { Breadcrumbs, crumbLinkClass } from "./Breadcrumbs.js";
|
|
11
|
+
export { BulletList } from "./BulletList.js";
|
|
11
12
|
export { Callout, type CalloutTone } from "./Callout.js";
|
|
13
|
+
export { Code } from "./Code.js";
|
|
14
|
+
export { Divider } from "./Divider.js";
|
|
12
15
|
export { Dialog, type DialogProps } from "./Dialog.js";
|
|
13
16
|
export { IconButton } from "./IconButton.js";
|
|
14
17
|
export { ColourSchemeToggle } from "./ColourSchemeToggle.js";
|
|
@@ -22,11 +25,14 @@ export { CopyLine } from "./CopyLine.js";
|
|
|
22
25
|
export { Field } from "./Field.js";
|
|
23
26
|
export { FieldDialog, type FieldDialogProps } from "./FieldDialog.js";
|
|
24
27
|
export { Input } from "./Input.js";
|
|
28
|
+
export { InlineLinks, type InlineLinksSeparator } from "./InlineLinks.js";
|
|
25
29
|
export { Kbd } from "./Kbd.js";
|
|
26
30
|
export { KeyValue, KeyValueList } from "./KeyValueList.js";
|
|
27
31
|
export { Select } from "./Select.js";
|
|
28
32
|
export { Textarea } from "./Textarea.js";
|
|
29
|
-
export {
|
|
33
|
+
export { Video } from "./Video.js";
|
|
34
|
+
export { linkClass, TextLink } from "./TextLink.js";
|
|
35
|
+
export { Body, BodyStrong, Caption, Display, Lead, ModalHeading, Overline, PageHeading, SectionHeading, SubsectionHeading, type SubsectionHeadingSize, Subtitle, type TypographyColor, type TypographyProps, } from "./typography.js";
|
|
30
36
|
export { DateField, type DateFieldProps } from "./DateField.js";
|
|
31
37
|
export { DatePicker, type DatePickerProps } from "./DatePicker.js";
|
|
32
38
|
export { fromISODate, toISODate } from "./isoDate.js";
|
package/dist/index.js
CHANGED
|
@@ -3,12 +3,15 @@ export { Markdown } from "./Markdown.js";
|
|
|
3
3
|
export { AppShell, appShellRootClass } from "./AppShell.js";
|
|
4
4
|
export { Container } from "./Container.js";
|
|
5
5
|
export { PageHeader } from "./PageHeader.js";
|
|
6
|
-
export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, SidebarLayout, } from "./PageLayouts.js";
|
|
6
|
+
export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, SidebarLayout, } from "./PageLayouts.js";
|
|
7
7
|
export { Section, Stack } from "./Stack.js";
|
|
8
8
|
export { Button, buttonClass } from "./Button.js";
|
|
9
9
|
export { Avatar, avatarColourClass, initialsOf } from "./Avatar.js";
|
|
10
10
|
export { Breadcrumbs, crumbLinkClass } from "./Breadcrumbs.js";
|
|
11
|
+
export { BulletList } from "./BulletList.js";
|
|
11
12
|
export { Callout } from "./Callout.js";
|
|
13
|
+
export { Code } from "./Code.js";
|
|
14
|
+
export { Divider } from "./Divider.js";
|
|
12
15
|
export { Dialog } from "./Dialog.js";
|
|
13
16
|
export { IconButton } from "./IconButton.js";
|
|
14
17
|
export { ColourSchemeToggle } from "./ColourSchemeToggle.js";
|
|
@@ -22,10 +25,13 @@ export { CopyLine } from "./CopyLine.js";
|
|
|
22
25
|
export { Field } from "./Field.js";
|
|
23
26
|
export { FieldDialog } from "./FieldDialog.js";
|
|
24
27
|
export { Input } from "./Input.js";
|
|
28
|
+
export { InlineLinks } from "./InlineLinks.js";
|
|
25
29
|
export { Kbd } from "./Kbd.js";
|
|
26
30
|
export { KeyValue, KeyValueList } from "./KeyValueList.js";
|
|
27
31
|
export { Select } from "./Select.js";
|
|
28
32
|
export { Textarea } from "./Textarea.js";
|
|
33
|
+
export { Video } from "./Video.js";
|
|
34
|
+
export { linkClass, TextLink } from "./TextLink.js";
|
|
29
35
|
export { Body, BodyStrong, Caption, Display, Lead, ModalHeading, Overline, PageHeading, SectionHeading, SubsectionHeading, Subtitle, } from "./typography.js";
|
|
30
36
|
export { DateField } from "./DateField.js";
|
|
31
37
|
export { DatePicker } from "./DatePicker.js";
|
package/dist/typography.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentPropsWithoutRef, ElementType, ReactNode } from "react";
|
|
1
|
+
import type { ComponentPropsWithoutRef, ElementType, ReactElement, ReactNode } from "react";
|
|
2
2
|
export type TypographyColor = "default" | "secondary" | "tertiary" | "muted" | "brand";
|
|
3
3
|
/** Own props shared by every typography component; the rest follow the rendered element. */
|
|
4
4
|
type OwnProps = {
|
|
@@ -19,57 +19,65 @@ export type TypographyProps<E extends ElementType> = OwnProps & {
|
|
|
19
19
|
* overriding PageHeading's size.
|
|
20
20
|
*/
|
|
21
21
|
export declare const Display: {
|
|
22
|
-
<E extends ElementType = "h1">(
|
|
22
|
+
<E extends ElementType = "h1">(props: TypographyProps<E>): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
23
23
|
displayName: string;
|
|
24
24
|
};
|
|
25
25
|
/** The supporting sentence under a `Display`. `p`, text-lg/7 normal, secondary. */
|
|
26
26
|
export declare const Lead: {
|
|
27
|
-
<E extends ElementType = "p">(
|
|
27
|
+
<E extends ElementType = "p">(props: TypographyProps<E>): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
28
28
|
displayName: string;
|
|
29
29
|
};
|
|
30
30
|
/** Page title. `h1`, text-2xl/8 semibold, primary text. */
|
|
31
31
|
export declare const PageHeading: {
|
|
32
|
-
<E extends ElementType = "h1">(
|
|
33
|
-
displayName: string;
|
|
34
|
-
};
|
|
35
|
-
/** Top-level section header. `h2`, text-base/6 semibold, primary text. */
|
|
36
|
-
export declare const SectionHeading: {
|
|
37
|
-
<E extends ElementType = "h2">({ as, color, className, children, ...rest }: TypographyProps<E>): import("react").JSX.Element;
|
|
38
|
-
displayName: string;
|
|
39
|
-
};
|
|
40
|
-
/** Nested header. `h3`, text-sm/5 semibold, primary text. */
|
|
41
|
-
export declare const SubsectionHeading: {
|
|
42
|
-
<E extends ElementType = "h3">({ as, color, className, children, ...rest }: TypographyProps<E>): import("react").JSX.Element;
|
|
32
|
+
<E extends ElementType = "h1">(props: TypographyProps<E>): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
43
33
|
displayName: string;
|
|
44
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* Top-level section header. `h2`, text-base/6 semibold, primary text. `rule` sets
|
|
37
|
+
* a hairline under it (the `Divider` line colour) with the space either side of
|
|
38
|
+
* the line built in.
|
|
39
|
+
*/
|
|
40
|
+
export declare function SectionHeading<E extends ElementType = "h2">({ rule, ...props }: TypographyProps<E> & {
|
|
41
|
+
rule?: boolean;
|
|
42
|
+
}): ReactElement;
|
|
43
|
+
export type SubsectionHeadingSize = "sm" | "base" | "lg";
|
|
44
|
+
/**
|
|
45
|
+
* Nested header. `h3`, text-sm/5 semibold, primary text. `size` steps it up for
|
|
46
|
+
* a card, drawer or form-group title: `sm` (default), `base` (bold) or `lg`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function SubsectionHeading<E extends ElementType = "h3">({ size, ...props }: TypographyProps<E> & {
|
|
49
|
+
size?: SubsectionHeadingSize;
|
|
50
|
+
}): ReactElement;
|
|
45
51
|
/** Dialog/modal title. `h2`, text-base/6 semibold, primary text. */
|
|
46
52
|
export declare const ModalHeading: {
|
|
47
|
-
<E extends ElementType = "h2">(
|
|
53
|
+
<E extends ElementType = "h2">(props: TypographyProps<E>): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
48
54
|
displayName: string;
|
|
49
55
|
};
|
|
50
56
|
/** Supporting line under a heading. `p`, text-sm/5 normal, secondary. */
|
|
51
57
|
export declare const Subtitle: {
|
|
52
|
-
<E extends ElementType = "p">(
|
|
58
|
+
<E extends ElementType = "p">(props: TypographyProps<E>): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
53
59
|
displayName: string;
|
|
54
60
|
};
|
|
55
61
|
/** Small all-caps label above a group. `span`, text-xs semibold uppercase, tertiary. */
|
|
56
62
|
export declare const Overline: {
|
|
57
|
-
<E extends ElementType = "span">(
|
|
58
|
-
displayName: string;
|
|
59
|
-
};
|
|
60
|
-
/** Default running text. `span` (inline; `as="p"` for a paragraph), text-sm/5 normal, secondary. */
|
|
61
|
-
export declare const Body: {
|
|
62
|
-
<E extends ElementType = "span">({ as, color, className, children, ...rest }: TypographyProps<E>): import("react").JSX.Element;
|
|
63
|
+
<E extends ElementType = "span">(props: TypographyProps<E>): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
63
64
|
displayName: string;
|
|
64
65
|
};
|
|
66
|
+
/**
|
|
67
|
+
* Default running text. `span` (inline; `as="p"` for a paragraph), text-sm/5 normal, secondary.
|
|
68
|
+
* `preserveNewlines` keeps the line breaks in the text (a user-typed note); runs of spaces still collapse.
|
|
69
|
+
*/
|
|
70
|
+
export declare function Body<E extends ElementType = "span">({ preserveNewlines, ...props }: TypographyProps<E> & {
|
|
71
|
+
preserveNewlines?: boolean;
|
|
72
|
+
}): ReactElement;
|
|
65
73
|
/** Emphasised run of body text. `span`, text-sm/5 medium, primary text. */
|
|
66
74
|
export declare const BodyStrong: {
|
|
67
|
-
<E extends ElementType = "span">(
|
|
75
|
+
<E extends ElementType = "span">(props: TypographyProps<E>): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
68
76
|
displayName: string;
|
|
69
77
|
};
|
|
70
78
|
/** Fine print / metadata. `span` (inline; `as="p"` for a block), text-xs/4 normal, tertiary. */
|
|
71
79
|
export declare const Caption: {
|
|
72
|
-
<E extends ElementType = "span">(
|
|
80
|
+
<E extends ElementType = "span">(props: TypographyProps<E>): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
73
81
|
displayName: string;
|
|
74
82
|
};
|
|
75
83
|
export {};
|
package/dist/typography.js
CHANGED
|
@@ -15,13 +15,20 @@ const COLOR = {
|
|
|
15
15
|
* layout classes — never the pinned size/weight.
|
|
16
16
|
*/
|
|
17
17
|
function typography(displayName, defaultAs, variantClass, defaultColor) {
|
|
18
|
-
function Typography(
|
|
19
|
-
|
|
20
|
-
return (_jsx(Tag, { className: cx(variantClass, COLOR[color ?? defaultColor], className), ...rest, children: children }));
|
|
18
|
+
function Typography(props) {
|
|
19
|
+
return pinned(defaultAs, variantClass, defaultColor, props);
|
|
21
20
|
}
|
|
22
21
|
Typography.displayName = displayName;
|
|
23
22
|
return Typography;
|
|
24
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* The one place a pinned role renders. The few roles with an opt-in prop of their own are
|
|
26
|
+
* written by hand, not by `typography`, and call this too.
|
|
27
|
+
*/
|
|
28
|
+
function pinned(defaultAs, variantClass, defaultColor, { as, color, className, children, ...rest }) {
|
|
29
|
+
const Tag = as ?? defaultAs;
|
|
30
|
+
return (_jsx(Tag, { className: cx(variantClass, COLOR[color ?? defaultColor], className), ...rest, children: children }));
|
|
31
|
+
}
|
|
25
32
|
// --- Structural roles (block by default) -----------------------------------
|
|
26
33
|
/**
|
|
27
34
|
* Marketing-scale headline — the one line at the top of a public page. `h1`,
|
|
@@ -35,10 +42,27 @@ export const Display = typography("Display", "h1", "text-4xl/[1.1] font-semibold
|
|
|
35
42
|
export const Lead = typography("Lead", "p", "text-lg/7 font-normal sm:text-xl/8", "secondary");
|
|
36
43
|
/** Page title. `h1`, text-2xl/8 semibold, primary text. */
|
|
37
44
|
export const PageHeading = typography("PageHeading", "h1", "text-2xl/8 font-semibold tracking-tight", "default");
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Top-level section header. `h2`, text-base/6 semibold, primary text. `rule` sets
|
|
47
|
+
* a hairline under it (the `Divider` line colour) with the space either side of
|
|
48
|
+
* the line built in.
|
|
49
|
+
*/
|
|
50
|
+
export function SectionHeading({ rule, ...props }) {
|
|
51
|
+
const variant = cx(rule && "mb-4 border-b border-slate-200/70 pb-2", "text-base/6 font-semibold");
|
|
52
|
+
return pinned("h2", variant, "default", props);
|
|
53
|
+
}
|
|
54
|
+
const SUBSECTION_SIZE = {
|
|
55
|
+
sm: "text-sm/5 font-semibold",
|
|
56
|
+
base: "text-base/6 font-bold",
|
|
57
|
+
lg: "text-lg/7 font-semibold",
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Nested header. `h3`, text-sm/5 semibold, primary text. `size` steps it up for
|
|
61
|
+
* a card, drawer or form-group title: `sm` (default), `base` (bold) or `lg`.
|
|
62
|
+
*/
|
|
63
|
+
export function SubsectionHeading({ size = "sm", ...props }) {
|
|
64
|
+
return pinned("h3", SUBSECTION_SIZE[size], "default", props);
|
|
65
|
+
}
|
|
42
66
|
/** Dialog/modal title. `h2`, text-base/6 semibold, primary text. */
|
|
43
67
|
export const ModalHeading = typography("ModalHeading", "h2", "text-base/6 font-semibold", "default");
|
|
44
68
|
/** Supporting line under a heading. `p`, text-sm/5 normal, secondary. */
|
|
@@ -46,8 +70,14 @@ export const Subtitle = typography("Subtitle", "p", "text-sm/5 font-normal", "se
|
|
|
46
70
|
// --- Inline roles (inline by default) --------------------------------------
|
|
47
71
|
/** Small all-caps label above a group. `span`, text-xs semibold uppercase, tertiary. */
|
|
48
72
|
export const Overline = typography("Overline", "span", "text-xs font-semibold uppercase tracking-wide", "tertiary");
|
|
49
|
-
/**
|
|
50
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Default running text. `span` (inline; `as="p"` for a paragraph), text-sm/5 normal, secondary.
|
|
75
|
+
* `preserveNewlines` keeps the line breaks in the text (a user-typed note); runs of spaces still collapse.
|
|
76
|
+
*/
|
|
77
|
+
export function Body({ preserveNewlines, ...props }) {
|
|
78
|
+
const variant = cx(preserveNewlines && "whitespace-pre-line", "text-sm/5 font-normal");
|
|
79
|
+
return pinned("span", variant, "secondary", props);
|
|
80
|
+
}
|
|
51
81
|
/** Emphasised run of body text. `span`, text-sm/5 medium, primary text. */
|
|
52
82
|
export const BodyStrong = typography("BodyStrong", "span", "text-sm/5 font-medium", "default");
|
|
53
83
|
/** Fine print / metadata. `span` (inline; `as="p"` for a block), text-xs/4 normal, tertiary. */
|
package/package.json
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hackcove/ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"components",
|
|
7
|
+
"design-system",
|
|
8
|
+
"hackcove",
|
|
9
|
+
"markdown",
|
|
10
|
+
"react",
|
|
11
|
+
"tailwind",
|
|
12
|
+
"tailwindcss",
|
|
13
|
+
"ui"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/madangryscientist/hackcove/tree/main/packages/ui#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/madangryscientist/hackcove/issues"
|
|
18
|
+
},
|
|
5
19
|
"license": "MIT",
|
|
20
|
+
"author": {
|
|
21
|
+
"name": "Jacobus Langner",
|
|
22
|
+
"url": "https://github.com/madangryscientist"
|
|
23
|
+
},
|
|
6
24
|
"repository": {
|
|
7
25
|
"type": "git",
|
|
8
26
|
"url": "git+https://github.com/madangryscientist/hackcove.git",
|