@wtfalch/design 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef } from 'react';
3
+ export default function ScrollArea({ axis = 'y', fade = true, hideBar = false, className, children, ref, label, }) {
4
+ const own = useRef(null);
5
+ /* The edge state lives in data attributes rather than React state.
6
+
7
+ This runs on every scroll frame. Setting state there re-renders the
8
+ subtree -- which, for the thing this is usually wrapped around, is the
9
+ whole list -- sixty times a second while a finger is on the trackpad.
10
+ Writing two attributes touches one element and never re-renders, and CSS
11
+ reads them. */
12
+ const measure = useCallback(() => {
13
+ const el = own.current;
14
+ if (!el)
15
+ return;
16
+ const room = 1;
17
+ el.dataset.top = String(el.scrollTop > room);
18
+ el.dataset.bottom = String(el.scrollTop + el.clientHeight < el.scrollHeight - room);
19
+ el.dataset.left = String(el.scrollLeft > room);
20
+ el.dataset.right = String(el.scrollLeft + el.clientWidth < el.scrollWidth - room);
21
+ }, []);
22
+ useEffect(() => {
23
+ const el = own.current;
24
+ if (!el)
25
+ return;
26
+ measure();
27
+ el.addEventListener('scroll', measure, { passive: true });
28
+ /* Content arriving is the case a scroll listener alone misses: a mailbox
29
+ that loads its second page grows the box without anyone scrolling, and
30
+ the bottom fade has to appear for content nobody has touched. */
31
+ const resize = new ResizeObserver(measure);
32
+ resize.observe(el);
33
+ for (const child of Array.from(el.children))
34
+ resize.observe(child);
35
+ return () => {
36
+ el.removeEventListener('scroll', measure);
37
+ resize.disconnect();
38
+ };
39
+ }, [measure]);
40
+ return (_jsx("div", { ref: (node) => {
41
+ own.current = node;
42
+ if (typeof ref === 'function')
43
+ ref(node);
44
+ else if (ref)
45
+ ref.current = node;
46
+ }, className: `scroller axis-${axis}${fade ? ' faded' : ''}${hideBar ? ' barless' : ''}${className ? ` ${className}` : ''}`, ...(label ? { tabIndex: 0, role: 'region', 'aria-label': label } : {}), children: children }));
47
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Two panes and a handle between them.
3
+ *
4
+ * A mail client is a list beside a message, a forum is threads beside a
5
+ * thread, and in both the right width depends on the reader rather than on us:
6
+ * a wide list to scan senders, a narrow one to give the message room. So it is
7
+ * theirs to set, and it has to survive a reload or setting it was a waste of
8
+ * their time.
9
+ *
10
+ * **The handle is a control, not a decoration, and that is the part everybody
11
+ * skips.** Nearly every split view on the web is a `<div>` with a mousedown
12
+ * listener: no role, no tab stop, no way to move it without a pointer. The
13
+ * ARIA pattern for this is `separator` with a value, which makes it a real
14
+ * widget -- focusable, announced as "splitter, 30 percent", and moved with the
15
+ * arrow keys. Home and End take it to its limits, and Enter collapses the
16
+ * first pane and restores it, which is the thing a mouse does by dragging to
17
+ * the edge.
18
+ *
19
+ * **The size is a percentage of the container**, not pixels, because the
20
+ * window is resized more often than the split is. A pixel width chosen on a
21
+ * wide monitor is most of a laptop screen.
22
+ *
23
+ * **The panes are `min-width: 0`.** Without it a flex child refuses to shrink
24
+ * below the intrinsic width of its content, so one long unbroken subject line
25
+ * silently pins the list open and the handle stops halfway with no explanation.
26
+ * That is in `splitpane.css` and it is the reason the file exists.
27
+ */
28
+ export interface Props {
29
+ /** Exactly two: the first pane and the second. */
30
+ children: [React.ReactNode, React.ReactNode];
31
+ /** `row` puts them side by side with a vertical handle. `column` stacks
32
+ * them. */
33
+ direction?: 'row' | 'column';
34
+ /** The first pane's share, as a percentage, before anyone moves it. */
35
+ defaultSize?: number;
36
+ /** How small and how large the first pane may get, as percentages. The
37
+ * limits are the design's: a list narrower than its own row content is not
38
+ * a smaller list, it is a broken one. */
39
+ min?: number;
40
+ max?: number;
41
+ /**
42
+ * Remember the size under this key, per browser.
43
+ *
44
+ * Without it the handle resets on every reload, which makes moving it feel
45
+ * like it did not work. Storage can throw -- a private window, a browser set
46
+ * to block site data -- so a failure to remember is silent and the default
47
+ * stands.
48
+ */
49
+ storageKey?: string;
50
+ /** Names the handle. "Sidebar width", not "splitter": the name is announced
51
+ * and should say what moving it does. */
52
+ label: string;
53
+ /** Called as the handle moves, with the first pane's percentage. */
54
+ onResize?: (size: number) => void;
55
+ className?: string;
56
+ }
57
+ export default function SplitPane({ children, direction, defaultSize, min, max, storageKey, label, onResize, className, }: Props): import("react").JSX.Element;
@@ -0,0 +1,103 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useId, useRef, useState } from 'react';
3
+ function clamp(value, min, max) {
4
+ return Math.min(max, Math.max(min, value));
5
+ }
6
+ function remembered(key, fallback) {
7
+ if (!key)
8
+ return fallback;
9
+ try {
10
+ const stored = window.localStorage.getItem(`design.split.${key}`);
11
+ const parsed = stored === null ? Number.NaN : Number.parseFloat(stored);
12
+ return Number.isFinite(parsed) ? parsed : fallback;
13
+ }
14
+ catch {
15
+ return fallback;
16
+ }
17
+ }
18
+ export default function SplitPane({ children, direction = 'row', defaultSize = 30, min = 15, max = 70, storageKey, label, onResize, className, }) {
19
+ const [first, second] = children;
20
+ const frame = useRef(null);
21
+ const firstPaneId = useId();
22
+ const [size, setSize] = useState(() => clamp(remembered(storageKey, defaultSize), min, max));
23
+ /* Where the pane was before Enter collapsed it, so Enter puts it back where
24
+ it was rather than at the default. Collapsing and restoring should be the
25
+ same gesture undone, not a reset. */
26
+ const restore = useRef(size);
27
+ const move = useCallback((next) => {
28
+ const bounded = clamp(next, min, max);
29
+ setSize(bounded);
30
+ onResize?.(bounded);
31
+ if (!storageKey)
32
+ return;
33
+ try {
34
+ window.localStorage.setItem(`design.split.${storageKey}`, String(bounded));
35
+ }
36
+ catch {
37
+ // A browser that will not remember is not a reason to refuse to resize.
38
+ }
39
+ }, [min, max, onResize, storageKey]);
40
+ /* Pointer events rather than mouse events, so a pen and a touch drag work
41
+ with the same code; and pointer capture, so a drag that leaves the window
42
+ -- which is what dragging to the edge *is* -- keeps sending moves instead
43
+ of freezing the handle where the cursor left. */
44
+ const onPointerDown = (event) => {
45
+ if (event.button !== 0)
46
+ return;
47
+ const box = frame.current?.getBoundingClientRect();
48
+ if (!box)
49
+ return;
50
+ const handle = event.currentTarget;
51
+ handle.setPointerCapture(event.pointerId);
52
+ const onMove = (e) => {
53
+ const along = direction === 'row' ? e.clientX - box.left : e.clientY - box.top;
54
+ const total = direction === 'row' ? box.width : box.height;
55
+ if (total > 0)
56
+ move((along / total) * 100);
57
+ };
58
+ const onUp = (e) => {
59
+ handle.releasePointerCapture(e.pointerId);
60
+ handle.removeEventListener('pointermove', onMove);
61
+ handle.removeEventListener('pointerup', onUp);
62
+ handle.removeEventListener('pointercancel', onUp);
63
+ document.body.classList.remove('splitting');
64
+ };
65
+ handle.addEventListener('pointermove', onMove);
66
+ handle.addEventListener('pointerup', onUp);
67
+ handle.addEventListener('pointercancel', onUp);
68
+ /* The cursor is set on the body for the duration, because a drag that
69
+ passes over a text pane otherwise flickers to an I-beam the whole way
70
+ across. */
71
+ document.body.classList.add('splitting');
72
+ };
73
+ useEffect(() => () => document.body.classList.remove('splitting'), []);
74
+ const onKeyDown = (event) => {
75
+ const back = direction === 'row' ? 'ArrowLeft' : 'ArrowUp';
76
+ const forward = direction === 'row' ? 'ArrowRight' : 'ArrowDown';
77
+ const step = event.shiftKey ? 10 : 2;
78
+ if (event.key === back)
79
+ move(size - step);
80
+ else if (event.key === forward)
81
+ move(size + step);
82
+ else if (event.key === 'Home')
83
+ move(min);
84
+ else if (event.key === 'End')
85
+ move(max);
86
+ else if (event.key === 'Enter') {
87
+ if (size <= min)
88
+ move(restore.current);
89
+ else {
90
+ restore.current = size;
91
+ move(min);
92
+ }
93
+ }
94
+ else
95
+ return;
96
+ event.preventDefault();
97
+ };
98
+ return (_jsxs("div", { ref: frame, className: `split dir-${direction}${className ? ` ${className}` : ''}`, style: { '--split': `${size}%` }, children: [_jsx("div", { className: "split-pane split-first", id: firstPaneId, children: first }), _jsx("div", { role: "separator", tabIndex: 0, "aria-label": label, "aria-orientation": direction === 'row' ? 'vertical' : 'horizontal', "aria-controls": firstPaneId, "aria-valuenow": Math.round(size), "aria-valuemin": min, "aria-valuemax": max, className: "split-handle", onPointerDown: onPointerDown, onKeyDown: onKeyDown,
99
+ /* Back to where it started. A drag has no undo, and the size is
100
+ remembered, so without this a mis-drag is permanent until you get it
101
+ right by hand. */
102
+ onDoubleClick: () => move(defaultSize), children: _jsx("span", { className: "split-grip", "aria-hidden": "true" }) }), _jsx("div", { className: "split-pane split-second", children: second })] }));
103
+ }
@@ -65,6 +65,16 @@ export default function Toggle({ label, hint, checked, onChange, disabled, said,
65
65
  is stopped at the track a tap on the knob no longer reaches the input by
66
66
  itself (measured, in `keyboard.spec.ts`).
67
67
 
68
+ **The knob goes where the pointer is, not where the pointer has been.**
69
+ This measured the drag as a delta from the knob's resting side, which made
70
+ a click that drifted a few pixels answer the opposite of the click: press
71
+ the far side of an off switch, wander seven pixels, and the delta is about
72
+ zero, so it resolves to off -- it refuses the very thing you pressed. And
73
+ it refuses *silently*, because the knob is back where it started and
74
+ nothing on screen says a gesture was even seen. Reading the pointer's
75
+ position on the track instead means a press and a press-with-a-wobble give
76
+ the same answer, which is the one the pointer is over.
77
+
68
78
  `onChange` is called once per gesture, or not at all if the knob was put
69
79
  back where it started -- a consumer that saves on change must not see a
70
80
  drag as two saves. The keyboard is untouched: the input is still the
@@ -126,9 +136,16 @@ export default function Toggle({ label, hint, checked, onChange, disabled, said,
126
136
  const [held, setHeld] = useState(false);
127
137
  const gesture = useRef(null);
128
138
  const swallowClick = useRef(false);
129
- const position = (track, g, x) => {
130
- const travel = track.clientWidth - KNOB[size] - 4;
131
- return Math.min(1, Math.max(0, g.from + (x - g.startX) / travel));
139
+ /** Where the knob would sit, 0 to 1, if its centre were under `x`. The
140
+ * 2px and the knob's width are the inset `toggle.css` draws it at:
141
+ * `left: calc(2px + var(--knob-x) * var(--knob-travel))`. */
142
+ const position = (track, x) => {
143
+ const box = track.getBoundingClientRect();
144
+ const knobWidth = KNOB[size];
145
+ const travel = box.width - knobWidth - 4;
146
+ if (travel <= 0)
147
+ return 0;
148
+ return Math.min(1, Math.max(0, (x - box.left - 2 - knobWidth / 2) / travel));
132
149
  };
133
150
  return (_jsxs(Switch, { ref: rowRef, className: `switch-row switch-${size}${className ? ` ${className}` : ''}`, isSelected: shown, onChange: commit, isDisabled: disabled,
134
151
  /* Read-only, not disabled, while a request is out: focus stays where it
@@ -143,7 +160,6 @@ export default function Toggle({ label, hint, checked, onChange, disabled, said,
143
160
  id: e.pointerId,
144
161
  startX: e.clientX,
145
162
  startY: e.clientY,
146
- from: shown ? 1 : 0,
147
163
  moved: false,
148
164
  };
149
165
  setHeld(true);
@@ -154,16 +170,15 @@ export default function Toggle({ label, hint, checked, onChange, disabled, said,
154
170
  if (!g.moved) {
155
171
  const dx = e.clientX - g.startX;
156
172
  const dy = e.clientY - g.startY;
157
- /* Mostly sideways and past the slop, or it is still a press. A
158
- drag begins where the threshold was crossed, not where the
159
- pointer first landed, so the knob starts from rest instead of
160
- jumping the slop's width the moment it engages. */
173
+ /* Mostly sideways and past the slop, or it is still a press. The
174
+ slop is what keeps a click a click: under it nothing is drawn
175
+ and nothing moves, so a hand that is not quite still does not
176
+ turn a press into a drag. */
161
177
  if (Math.abs(dx) < SLOP || Math.abs(dx) <= Math.abs(dy))
162
178
  return;
163
179
  g.moved = true;
164
- g.startX = e.clientX;
165
180
  }
166
- setKnob(position(e.currentTarget, g, e.clientX));
181
+ setKnob(position(e.currentTarget, e.clientX));
167
182
  }, onPointerUp: (e) => {
168
183
  const g = gesture.current;
169
184
  if (!g || e.pointerId !== g.id)
@@ -172,7 +187,7 @@ export default function Toggle({ label, hint, checked, onChange, disabled, said,
172
187
  setHeld(false);
173
188
  setKnob(null);
174
189
  swallowClick.current = true;
175
- const on = g.moved ? position(e.currentTarget, g, e.clientX) > 0.5 : !shown;
190
+ const on = g.moved ? position(e.currentTarget, e.clientX) > 0.5 : !shown;
176
191
  if (on !== shown)
177
192
  commit(on);
178
193
  }, onPointerCancel: () => {
@@ -0,0 +1,36 @@
1
+ /**
2
+ * How an identity disc is drawn: its two letters and its hue.
3
+ *
4
+ * **Its own module so `Identity.tsx` stays a Fast Refresh boundary.** A
5
+ * component module that exports a value beside its component loses the
6
+ * boundary, and editing it re-runs every importer instead of swapping the
7
+ * component in place. `iconNames.ts` and `tourMarker.ts` exist for the same
8
+ * reason.
9
+ *
10
+ * Exported from the package as well, because anything else colouring by
11
+ * sender -- a thread list's left rule, a chart of who writes most -- has to
12
+ * agree with the discs or the second cue contradicts the first.
13
+ */
14
+ /**
15
+ * Up to two initials.
16
+ *
17
+ * From the name's first and last word, which is right for "Ada Lovelace" and
18
+ * for "Ada Byron King Lovelace"; from the address's local part when there is
19
+ * no name, which is most machine senders. Split on whitespace only --
20
+ * splitting on punctuation turns "O'Brien" into "OB" and "Smith-Jones" into
21
+ * "SJ", which are worse than the single letter they replace.
22
+ */
23
+ export declare function initialsOf(name: string | null | undefined, address: string): string;
24
+ /**
25
+ * A hue from the address.
26
+ *
27
+ * A cheap, stable string hash. Not a cryptographic one and not trying to be:
28
+ * the requirement is that the same address gives the same number everywhere
29
+ * and on every machine, which any deterministic function satisfies, and that
30
+ * neighbouring addresses do not land on the same hue, which multiplying by an
31
+ * odd prime handles well enough.
32
+ *
33
+ * Case- and whitespace-insensitive, because `Ada@Example.com ` and
34
+ * `ada@example.com` are one person and two colours would say they were two.
35
+ */
36
+ export declare function hueOf(address: string): number;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * How an identity disc is drawn: its two letters and its hue.
3
+ *
4
+ * **Its own module so `Identity.tsx` stays a Fast Refresh boundary.** A
5
+ * component module that exports a value beside its component loses the
6
+ * boundary, and editing it re-runs every importer instead of swapping the
7
+ * component in place. `iconNames.ts` and `tourMarker.ts` exist for the same
8
+ * reason.
9
+ *
10
+ * Exported from the package as well, because anything else colouring by
11
+ * sender -- a thread list's left rule, a chart of who writes most -- has to
12
+ * agree with the discs or the second cue contradicts the first.
13
+ */
14
+ /**
15
+ * Up to two initials.
16
+ *
17
+ * From the name's first and last word, which is right for "Ada Lovelace" and
18
+ * for "Ada Byron King Lovelace"; from the address's local part when there is
19
+ * no name, which is most machine senders. Split on whitespace only --
20
+ * splitting on punctuation turns "O'Brien" into "OB" and "Smith-Jones" into
21
+ * "SJ", which are worse than the single letter they replace.
22
+ */
23
+ export function initialsOf(name, address) {
24
+ const words = (name ?? '').trim().split(/\s+/).filter(Boolean);
25
+ if (words.length > 0) {
26
+ const first = words[0]?.[0] ?? '';
27
+ const last = words.length > 1 ? (words[words.length - 1]?.[0] ?? '') : '';
28
+ return (first + last).toUpperCase();
29
+ }
30
+ const local = address.split('@')[0] ?? address;
31
+ return (local[0] ?? '?').toUpperCase();
32
+ }
33
+ /**
34
+ * A hue from the address.
35
+ *
36
+ * A cheap, stable string hash. Not a cryptographic one and not trying to be:
37
+ * the requirement is that the same address gives the same number everywhere
38
+ * and on every machine, which any deterministic function satisfies, and that
39
+ * neighbouring addresses do not land on the same hue, which multiplying by an
40
+ * odd prime handles well enough.
41
+ *
42
+ * Case- and whitespace-insensitive, because `Ada@Example.com ` and
43
+ * `ada@example.com` are one person and two colours would say they were two.
44
+ */
45
+ export function hueOf(address) {
46
+ let hash = 0;
47
+ for (const character of address.trim().toLowerCase()) {
48
+ hash = (hash * 31 + (character.codePointAt(0) ?? 0)) % 360;
49
+ }
50
+ return hash;
51
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Which page buttons a pager draws.
3
+ *
4
+ * **Its own module so `Pagination.tsx` stays a Fast Refresh boundary**, the
5
+ * same rule `iconNames.ts` and `initials.ts` exist under.
6
+ *
7
+ * Never more than seven slots, with the first and last always present and a
8
+ * gap standing in for the run that is elided. A pager that lists twenty-six
9
+ * pages is wider than the table above it; one that lists only the current
10
+ * page's neighbours loses the jump to the end, which is the second most
11
+ * common thing anybody does with one.
12
+ *
13
+ * The width is held at seven even near an end, so the row does not change size
14
+ * as you move through it -- otherwise the button under the cursor shifts
15
+ * between clicks, which is how somebody lands two pages from where they meant.
16
+ *
17
+ * The gap is where the pager puts its "go to page" field, because that is
18
+ * exactly where the pages you cannot see are. What the gap *does* is the
19
+ * component's business; this only says where one goes.
20
+ */
21
+ export declare function pageWindow(current: number, pages: number): (number | 'gap')[];
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Which page buttons a pager draws.
3
+ *
4
+ * **Its own module so `Pagination.tsx` stays a Fast Refresh boundary**, the
5
+ * same rule `iconNames.ts` and `initials.ts` exist under.
6
+ *
7
+ * Never more than seven slots, with the first and last always present and a
8
+ * gap standing in for the run that is elided. A pager that lists twenty-six
9
+ * pages is wider than the table above it; one that lists only the current
10
+ * page's neighbours loses the jump to the end, which is the second most
11
+ * common thing anybody does with one.
12
+ *
13
+ * The width is held at seven even near an end, so the row does not change size
14
+ * as you move through it -- otherwise the button under the cursor shifts
15
+ * between clicks, which is how somebody lands two pages from where they meant.
16
+ *
17
+ * The gap is where the pager puts its "go to page" field, because that is
18
+ * exactly where the pages you cannot see are. What the gap *does* is the
19
+ * component's business; this only says where one goes.
20
+ */
21
+ export function pageWindow(current, pages) {
22
+ if (pages <= 7)
23
+ return Array.from({ length: pages }, (_, i) => i + 1);
24
+ const near = [current - 1, current, current + 1].filter((p) => p > 1 && p < pages);
25
+ const slots = new Set([1, ...near, pages]);
26
+ /* Near an end there is only one gap instead of two, so the run has to be one
27
+ longer to keep the row at seven. Four rather than three: 1 2 3 4 5 … 26,
28
+ not 1 2 3 4 … 26, which is six slots and a row that changes width as you
29
+ leave the first page. */
30
+ if (current <= 3)
31
+ for (const p of [2, 3, 4, 5])
32
+ slots.add(p);
33
+ if (current >= pages - 2) {
34
+ for (const p of [pages - 4, pages - 3, pages - 2, pages - 1])
35
+ slots.add(p);
36
+ }
37
+ const out = [];
38
+ let previous = 0;
39
+ for (const page of [...slots].filter((p) => p >= 1 && p <= pages).sort((a, b) => a - b)) {
40
+ if (previous && page - previous > 1)
41
+ out.push('gap');
42
+ out.push(page);
43
+ previous = page;
44
+ }
45
+ return out;
46
+ }
package/dist/index.d.ts CHANGED
@@ -26,26 +26,52 @@ export type { Props as ButtonProps } from './components/Button.js';
26
26
  export { default as Callout } from './components/Callout.js';
27
27
  export { default as Card } from './components/Card.js';
28
28
  export { default as Checkbox } from './components/Checkbox.js';
29
+ /** Type what you want to do: the keyboard's front door over the application. */
30
+ export { default as Command } from './components/Command.js';
31
+ export type { Command as CommandItem, Group as CommandGroup } from './components/Command.js';
29
32
  export { DangerAction, default as DangerZone } from './components/DangerZone.js';
30
33
  export { default as Dialog } from './components/Dialog.js';
31
34
  export { default as Empty } from './components/Empty.js';
32
35
  export { default as Field } from './components/Field.js';
33
36
  export type { FieldWiring } from './components/Field.js';
34
37
  export { default as Icon } from './components/Icon.js';
38
+ /** A person in one line -- initials, name, address -- and the two pure
39
+ * functions behind the disc, exported so a caller colouring something else
40
+ * by sender agrees with it. */
41
+ export { default as Identity } from './components/Identity.js';
42
+ export { hueOf, initialsOf } from './components/initials.js';
35
43
  export { ICON_NAMES } from './components/iconNames.js';
36
44
  export type { IconName } from './components/iconNames.js';
37
45
  export { default as Illustration } from './components/Illustration.js';
38
46
  export { default as Input } from './components/Input.js';
39
47
  export type { Props as InputProps } from './components/Input.js';
40
48
  export { default as Markdown } from './components/Markdown.js';
49
+ export { default as Menu } from './components/Menu.js';
50
+ export type { Item as MenuItem, Section as MenuSection } from './components/Menu.js';
41
51
  export { default as Modal } from './components/Modal.js';
52
+ /** Moving through a list that does not fit, counted in items rather than
53
+ * pages. `pageWindow` is the elision, exported because it is the arithmetic
54
+ * worth testing on its own. */
55
+ export { default as Pagination } from './components/Pagination.js';
56
+ export { pageWindow } from './components/pageWindow.js';
42
57
  export { default as Pill } from './components/Pill.js';
58
+ /** A small surface anchored to what opened it: the middle term between
59
+ * `Tooltip`, which only says something, and `Modal`, which takes the
60
+ * application away. */
61
+ export { default as Popover } from './components/Popover.js';
43
62
  export { default as Progress } from './components/Progress.js';
44
63
  export { Row, Rows } from './components/Rows.js';
64
+ /** A box that scrolls and says so. */
65
+ export { default as ScrollArea } from './components/ScrollArea.js';
66
+ export type { Props as ScrollAreaProps } from './components/ScrollArea.js';
45
67
  export { default as Select } from './components/Select.js';
46
68
  export { default as SizeGrid } from './components/SizeGrid.js';
47
69
  export { default as Skeleton } from './components/Skeleton.js';
48
70
  export { default as Slider } from './components/Slider.js';
71
+ /** Two panes and a handle between them, the handle being a real `separator`
72
+ * widget rather than a div with a mousedown listener. */
73
+ export { default as SplitPane } from './components/SplitPane.js';
74
+ export type { Props as SplitPaneProps } from './components/SplitPane.js';
49
75
  export { default as Table } from './components/Table.js';
50
76
  export type { Column } from './components/Table.js';
51
77
  export { default as Tabs } from './components/Tabs.js';
package/dist/index.js CHANGED
@@ -24,6 +24,8 @@ export { default as Button } from './components/Button.js';
24
24
  export { default as Callout } from './components/Callout.js';
25
25
  export { default as Card } from './components/Card.js';
26
26
  export { default as Checkbox } from './components/Checkbox.js';
27
+ /** Type what you want to do: the keyboard's front door over the application. */
28
+ export { default as Command } from './components/Command.js';
27
29
  /* `DangerAction` is a component -- a row inside the zone -- and was exported as
28
30
  a type until the first consumer wrote `<DangerAction>` and TypeScript refused
29
31
  it (TS1362). Nothing in this repo renders one, which is how it went unseen. */
@@ -32,18 +34,38 @@ export { default as Dialog } from './components/Dialog.js';
32
34
  export { default as Empty } from './components/Empty.js';
33
35
  export { default as Field } from './components/Field.js';
34
36
  export { default as Icon } from './components/Icon.js';
37
+ /** A person in one line -- initials, name, address -- and the two pure
38
+ * functions behind the disc, exported so a caller colouring something else
39
+ * by sender agrees with it. */
40
+ export { default as Identity } from './components/Identity.js';
41
+ export { hueOf, initialsOf } from './components/initials.js';
35
42
  export { ICON_NAMES } from './components/iconNames.js';
36
43
  export { default as Illustration } from './components/Illustration.js';
37
44
  export { default as Input } from './components/Input.js';
38
45
  export { default as Markdown } from './components/Markdown.js';
46
+ export { default as Menu } from './components/Menu.js';
39
47
  export { default as Modal } from './components/Modal.js';
48
+ /** Moving through a list that does not fit, counted in items rather than
49
+ * pages. `pageWindow` is the elision, exported because it is the arithmetic
50
+ * worth testing on its own. */
51
+ export { default as Pagination } from './components/Pagination.js';
52
+ export { pageWindow } from './components/pageWindow.js';
40
53
  export { default as Pill } from './components/Pill.js';
54
+ /** A small surface anchored to what opened it: the middle term between
55
+ * `Tooltip`, which only says something, and `Modal`, which takes the
56
+ * application away. */
57
+ export { default as Popover } from './components/Popover.js';
41
58
  export { default as Progress } from './components/Progress.js';
42
59
  export { Row, Rows } from './components/Rows.js';
60
+ /** A box that scrolls and says so. */
61
+ export { default as ScrollArea } from './components/ScrollArea.js';
43
62
  export { default as Select } from './components/Select.js';
44
63
  export { default as SizeGrid } from './components/SizeGrid.js';
45
64
  export { default as Skeleton } from './components/Skeleton.js';
46
65
  export { default as Slider } from './components/Slider.js';
66
+ /** Two panes and a handle between them, the handle being a real `separator`
67
+ * widget rather than a div with a mousedown listener. */
68
+ export { default as SplitPane } from './components/SplitPane.js';
47
69
  export { default as Table } from './components/Table.js';
48
70
  export { default as Tabs } from './components/Tabs.js';
49
71
  export { default as Textarea } from './components/Textarea.js';