@mistriwork/ui 0.0.0-stage → 1.0.0-rc.10

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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -2
  3. package/dist/Avatar.d.ts +11 -0
  4. package/dist/Avatar.js +28 -0
  5. package/dist/Button.d.ts +36 -0
  6. package/dist/Button.js +22 -0
  7. package/dist/Carousel.d.ts +52 -0
  8. package/dist/Carousel.js +110 -0
  9. package/dist/Choices.d.ts +77 -0
  10. package/dist/Choices.js +88 -0
  11. package/dist/DateField.d.ts +66 -0
  12. package/dist/DateField.js +255 -0
  13. package/dist/Drawer.d.ts +33 -0
  14. package/dist/Drawer.js +12 -0
  15. package/dist/DropMenu.d.ts +59 -0
  16. package/dist/DropMenu.js +52 -0
  17. package/dist/Dropdown.d.ts +73 -0
  18. package/dist/Dropdown.js +85 -0
  19. package/dist/Field.d.ts +53 -0
  20. package/dist/Field.js +58 -0
  21. package/dist/FormParts.d.ts +68 -0
  22. package/dist/FormParts.js +53 -0
  23. package/dist/HeroBlocks.d.ts +37 -0
  24. package/dist/HeroBlocks.js +26 -0
  25. package/dist/Icon.d.ts +29 -0
  26. package/dist/Icon.js +78 -0
  27. package/dist/Modal.d.ts +48 -0
  28. package/dist/Modal.js +57 -0
  29. package/dist/Pagination.d.ts +33 -0
  30. package/dist/Pagination.js +25 -0
  31. package/dist/Popover.d.ts +21 -0
  32. package/dist/Popover.js +20 -0
  33. package/dist/Resizable.d.ts +48 -0
  34. package/dist/Resizable.js +122 -0
  35. package/dist/ScrollArea.d.ts +29 -0
  36. package/dist/ScrollArea.js +62 -0
  37. package/dist/Select.d.ts +26 -0
  38. package/dist/Select.js +20 -0
  39. package/dist/SensitiveField.d.ts +26 -0
  40. package/dist/SensitiveField.js +27 -0
  41. package/dist/Slider.d.ts +17 -0
  42. package/dist/Slider.js +10 -0
  43. package/dist/Switch.d.ts +28 -0
  44. package/dist/Switch.js +24 -0
  45. package/dist/Tabs.d.ts +25 -0
  46. package/dist/Tabs.js +5 -0
  47. package/dist/TextField.d.ts +44 -0
  48. package/dist/TextField.js +19 -0
  49. package/dist/Tooltip.d.ts +29 -0
  50. package/dist/Tooltip.js +39 -0
  51. package/dist/Typeahead.d.ts +63 -0
  52. package/dist/Typeahead.js +90 -0
  53. package/dist/fieldFrame.d.ts +37 -0
  54. package/dist/fieldFrame.js +16 -0
  55. package/dist/forms.css +192 -0
  56. package/dist/hero.css +23 -0
  57. package/dist/index.d.ts +38 -0
  58. package/dist/index.js +40 -0
  59. package/dist/internal/ComponentStyleScope.d.ts +1 -0
  60. package/dist/internal/ComponentStyleScope.js +4 -0
  61. package/dist/internal/anchor.d.ts +19 -0
  62. package/dist/internal/anchor.js +52 -0
  63. package/dist/internal/datePickerModel.d.ts +53 -0
  64. package/dist/internal/datePickerModel.js +175 -0
  65. package/dist/internal/popups.d.ts +13 -0
  66. package/dist/internal/popups.js +16 -0
  67. package/dist/internal/ruler.d.ts +37 -0
  68. package/dist/internal/ruler.js +55 -0
  69. package/dist/masks.d.ts +32 -0
  70. package/dist/masks.js +62 -0
  71. package/dist/preferences.css +26 -0
  72. package/dist/primitives.css +188 -0
  73. package/dist/standalone.css +413 -0
  74. package/dist/style.css +19 -0
  75. package/dist/tokens.css +233 -0
  76. package/mark.svg +1 -0
  77. package/package.json +84 -4
package/dist/Field.js ADDED
@@ -0,0 +1,58 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * One field: its label over the control, a hint under it, and the problem
4
+ * it holds under that — in the control's own column, so saying a problem
5
+ * never narrows or moves the control (docs/design.md, Access).
6
+ *
7
+ * The wiring is the point. The control is handed its id, what describes it
8
+ * (the hint, the problem), `aria-invalid` while it holds a problem and
9
+ * `aria-required` beside the star, so no form has to remember to join them.
10
+ * A control that is one element takes them as props (`children` an element);
11
+ * one that is several — a listbox's button, a date and its calendar, a
12
+ * picker — takes them through `children` as a function and puts them where
13
+ * its focusable is.
14
+ */
15
+ import { cloneElement, isValidElement, useId } from 'react';
16
+ export function Field({ label, hint, problem, required = false, id, group = false, check = false, size = 'default', optional, labelAside, labelHidden = false, reserve = false, className = '', children, }) {
17
+ const made = useId();
18
+ const controlId = id ?? `field-${made}`;
19
+ const hintId = hint ? `${controlId}-hint` : undefined;
20
+ const problemId = `${controlId}-problem`;
21
+ const holdsProblem = problem !== undefined && problem !== null && problem !== false && problem !== '';
22
+ const labelId = `${controlId}-label`;
23
+ const labelClass = labelHidden ? 'form-field-label form-field-label-hidden' : 'form-field-label';
24
+ const caption = group ? (_jsx("span", { id: labelId, className: labelClass, children: label })) : (_jsx("label", { id: labelId, htmlFor: controlId, className: labelClass, children: label }));
25
+ const word = optional && !required ? _jsx("span", { className: "form-field-optional-word", children: optional }) : null;
26
+ const control = {
27
+ id: controlId,
28
+ ...(hintId || holdsProblem ? { 'aria-describedby': [hintId, holdsProblem ? problemId : undefined].filter(Boolean).join(' ') } : {}),
29
+ ...(holdsProblem ? { 'aria-invalid': true } : {}),
30
+ ...(required ? { 'aria-required': true } : {}),
31
+ ...(group ? { 'aria-labelledby': labelId } : {}),
32
+ };
33
+ const drawn = typeof children === 'function'
34
+ ? children(control)
35
+ : isValidElement(children)
36
+ ? cloneElement(children, { ...control, ...joined(children.props, control) })
37
+ : children;
38
+ /* The star is the stylesheet's (`.is-required`), not text: a label's words
39
+ stay its name, to a query by label as to a screen reader, which hears
40
+ "required" from `aria-required` instead. */
41
+ return (_jsxs("div", { className: [
42
+ 'form-field',
43
+ size === 'compact' ? 'form-field-compact' : '',
44
+ check ? 'form-field-check' : '',
45
+ reserve ? 'form-field-reserve' : '',
46
+ required ? 'is-required' : '',
47
+ holdsProblem ? 'is-invalid' : '',
48
+ className,
49
+ ]
50
+ .filter(Boolean)
51
+ .join(' '), children: [check ? (_jsxs("div", { className: "form-field-check-row", children: [drawn, _jsx("label", { id: labelId, htmlFor: controlId, className: "form-field-label", children: label }), word] })) : (_jsxs(_Fragment, { children: [labelAside || word ? (_jsxs("div", { className: "form-field-label-row", children: [caption, word, labelAside] })) : (caption), drawn] })), hint ? (_jsx("p", { id: hintId, className: "form-field-hint", children: hint })) : null, _jsx("p", { id: problemId, className: "form-field-problem", "aria-live": "polite", hidden: !holdsProblem && !reserve, children: holdsProblem ? problem : null })] }));
52
+ }
53
+ /** A control's own description joins the field's rather than being replaced by it. */
54
+ function joined(props, control) {
55
+ const own = typeof props['aria-describedby'] === 'string' ? props['aria-describedby'] : '';
56
+ const described = [own, control['aria-describedby']].filter(Boolean).join(' ');
57
+ return described ? { 'aria-describedby': described } : {};
58
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The small parts a form is drawn from: the help tip, the progress bar, a step's heading, the fixed action bar, the
3
+ * review group and a row of columns. Props only: every word and every picture arrives as a prop, and none of them
4
+ * knows a field, a step or an address. Preview: @mistriwork/ui is Preview at 1.0.
5
+ */
6
+ import { type ReactNode } from 'react';
7
+ export interface HelpTipProps {
8
+ /** The tip's text. */
9
+ text: ReactNode;
10
+ /** What the button is called ("More information"); the caller's word. */
11
+ label: string;
12
+ /** The mark on the button; without one, an "i". */
13
+ icon?: ReactNode;
14
+ }
15
+ /** The "i" beside a label: a button that opens its text under it. Escape and a press elsewhere close it. */
16
+ export declare function HelpTip({ text, label, icon }: HelpTipProps): ReactNode;
17
+ export interface ProgressBarProps {
18
+ value: number;
19
+ max: number;
20
+ /** The bottom of the range, announced as `aria-valuemin`: 0 by default, 1 for "step 1 of 5". */
21
+ min?: number;
22
+ /** What the bar is called to assistive technology ("Step 3 of 12"). Leave it out, with `decorative`, to hide the bar. */
23
+ label?: string;
24
+ decorative?: boolean;
25
+ }
26
+ /** How far along: a filled track. It is information that moves often, so the fill is not animated. */
27
+ export declare function ProgressBar({ value, max, min, label, decorative }: ProgressBarProps): ReactNode;
28
+ export interface StepHeaderProps {
29
+ title: ReactNode;
30
+ /** The heading's level: 1 (the room's own) by default, 2 where the form sits in a dialog that has its title. */
31
+ level?: 1 | 2;
32
+ /** The heading's id, for a region that is named by it. */
33
+ id?: string;
34
+ lead?: ReactNode;
35
+ /** A decorative glyph before the title. */
36
+ mark?: ReactNode;
37
+ }
38
+ /** A step's title and the quiet lead under it. */
39
+ export declare function StepHeader({ title, lead, mark, level, id }: StepHeaderProps): ReactNode;
40
+ export interface ActionBarProps {
41
+ /** Another class on the bar, for the form that draws it. */
42
+ className?: string;
43
+ back?: {
44
+ label: ReactNode;
45
+ onClick: () => void;
46
+ icon?: ReactNode;
47
+ };
48
+ primary: {
49
+ label: ReactNode;
50
+ onClick: () => void;
51
+ disabled?: boolean;
52
+ busy?: boolean;
53
+ busyLabel?: ReactNode;
54
+ icon?: ReactNode;
55
+ };
56
+ }
57
+ /** The bar fixed to the foot of its container: Back as a quiet word, and a filled primary that takes the rest of the width. */
58
+ export declare function ActionBar({ back, primary, className }: ActionBarProps): ReactNode;
59
+ /** Fields side by side; `cols` is a grid template such as "1fr 1.8fr". A row stacks where its fields no longer fit. */
60
+ export declare function Columns({ cols, children }: {
61
+ cols?: string;
62
+ children: ReactNode;
63
+ }): ReactNode;
64
+ /** The editable review: earlier steps' fields drawn again in one labelled group. */
65
+ export declare function ReviewList({ label, children }: {
66
+ label: string;
67
+ children: ReactNode;
68
+ }): ReactNode;
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The small parts a form is drawn from: the help tip, the progress bar, a step's heading, the fixed action bar, the
4
+ * review group and a row of columns. Props only: every word and every picture arrives as a prop, and none of them
5
+ * knows a field, a step or an address. Preview: @mistriwork/ui is Preview at 1.0.
6
+ */
7
+ import { useEffect, useId, useRef, useState } from 'react';
8
+ import { Button } from './Button.js';
9
+ /** The "i" beside a label: a button that opens its text under it. Escape and a press elsewhere close it. */
10
+ export function HelpTip({ text, label, icon }) {
11
+ const [open, setOpen] = useState(false);
12
+ const tipId = useId();
13
+ const root = useRef(null);
14
+ useEffect(() => {
15
+ if (!open)
16
+ return undefined;
17
+ const outside = (e) => {
18
+ if (root.current && !root.current.contains(e.target))
19
+ setOpen(false);
20
+ };
21
+ document.addEventListener('pointerdown', outside);
22
+ return () => document.removeEventListener('pointerdown', outside);
23
+ }, [open]);
24
+ return (_jsxs("span", { className: "help-tip", ref: root, children: [_jsx("button", { type: "button", className: "help-tip-button", "aria-label": label, "aria-expanded": open, "aria-describedby": open ? tipId : undefined, onClick: () => setOpen(!open), onKeyDown: (e) => {
25
+ if (e.key === 'Escape' && open) {
26
+ e.stopPropagation();
27
+ setOpen(false);
28
+ }
29
+ }, children: icon ?? _jsx("span", { "aria-hidden": "true", children: "i" }) }), open ? (_jsx("span", { role: "tooltip", id: tipId, className: "help-tip-text", children: text })) : null] }));
30
+ }
31
+ /** How far along: a filled track. It is information that moves often, so the fill is not animated. */
32
+ export function ProgressBar({ value, max, min = 0, label, decorative }) {
33
+ return (_jsx("div", { className: "progress-bar", ...(decorative
34
+ ? { 'aria-hidden': true }
35
+ : { role: 'progressbar', 'aria-label': label, 'aria-valuemin': min, 'aria-valuemax': max, 'aria-valuenow': value }), children: _jsx("span", { className: "progress-bar-fill", style: { width: `${max > 0 ? Math.min(100, (value / max) * 100) : 0}%` } }) }));
36
+ }
37
+ /** A step's title and the quiet lead under it. */
38
+ export function StepHeader({ title, lead, mark, level = 1, id }) {
39
+ const Heading = level === 1 ? 'h1' : 'h2';
40
+ return (_jsxs("header", { className: "step-header", children: [_jsxs(Heading, { className: "step-title", id: id, tabIndex: -1, children: [mark ? (_jsx("span", { className: "step-mark", "aria-hidden": "true", children: mark })) : null, title] }), lead ? _jsx("p", { className: "step-lead", children: lead }) : null] }));
41
+ }
42
+ /** The bar fixed to the foot of its container: Back as a quiet word, and a filled primary that takes the rest of the width. */
43
+ export function ActionBar({ back, primary, className }) {
44
+ return (_jsxs("footer", { className: className ? `action-bar ${className}` : 'action-bar', children: [back ? (_jsxs(Button, { intent: "quiet", size: "large", className: "action-bar-back", onClick: back.onClick, children: [back.icon, back.label] })) : null, _jsxs(Button, { intent: "primary", size: "large", className: "action-bar-primary", disabled: primary.disabled, busy: primary.busy, busyLabel: primary.busyLabel, onClick: primary.onClick, children: [primary.label, primary.icon] })] }));
45
+ }
46
+ /** Fields side by side; `cols` is a grid template such as "1fr 1.8fr". A row stacks where its fields no longer fit. */
47
+ export function Columns({ cols, children }) {
48
+ return (_jsx("div", { className: "form-columns", style: cols ? { '--form-columns': cols } : undefined, children: children }));
49
+ }
50
+ /** The editable review: earlier steps' fields drawn again in one labelled group. */
51
+ export function ReviewList({ label, children }) {
52
+ return (_jsx("div", { className: "review-list", role: "group", "aria-label": label, children: children }));
53
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The landing hero's blocks (Preview): a badge over the heading, two to four photo tiles where one photo would stand,
3
+ * and up to four rows of an icon and a line. Props only: the words, the icons (as nodes) and the image addresses are
4
+ * handed in, so the kit draws them with its own icons and the files an App holds. Their rules are hero.css.
5
+ */
6
+ import type { ReactNode } from 'react';
7
+ export interface BadgeProps {
8
+ /** The few words. */
9
+ children: ReactNode;
10
+ /** A picture before the words, already sized by the caller. */
11
+ icon?: ReactNode;
12
+ }
13
+ /** A pill of a few words with an optional icon, over a heading. Quiet: a note on the page, never a button. */
14
+ export declare function Badge({ children, icon }: BadgeProps): import("react").JSX.Element;
15
+ export interface PhotoTile {
16
+ src: string;
17
+ alt: string;
18
+ srcSet?: string;
19
+ sizes?: string;
20
+ }
21
+ export interface PhotoTilesProps {
22
+ tiles: readonly PhotoTile[];
23
+ }
24
+ /**
25
+ * Two to four photos in the one photo's 4:3 box, cut into a mosaic, so the box has its size before any photo arrives
26
+ * and nothing moves. The first is the one the page waits for; the rest load as they come near. Fewer than two is no
27
+ * mosaic, and more than four are cut off.
28
+ */
29
+ export declare function PhotoTiles({ tiles }: PhotoTilesProps): import("react").JSX.Element | null;
30
+ export interface IconRowsProps {
31
+ rows: readonly {
32
+ icon: ReactNode;
33
+ text: string;
34
+ }[];
35
+ }
36
+ /** Up to four rows, each an icon in the accent and a line in the muted ink. Nothing for no rows. */
37
+ export declare function IconRows({ rows }: IconRowsProps): import("react").JSX.Element | null;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** A pill of a few words with an optional icon, over a heading. Quiet: a note on the page, never a button. */
3
+ export function Badge({ children, icon }) {
4
+ return (_jsxs("p", { className: "front-badge", children: [icon, _jsx("span", { children: children })] }));
5
+ }
6
+ /** The most tiles a mosaic cuts the box into. */
7
+ const MOST_TILES = 4;
8
+ /**
9
+ * Two to four photos in the one photo's 4:3 box, cut into a mosaic, so the box has its size before any photo arrives
10
+ * and nothing moves. The first is the one the page waits for; the rest load as they come near. Fewer than two is no
11
+ * mosaic, and more than four are cut off.
12
+ */
13
+ export function PhotoTiles({ tiles }) {
14
+ if (tiles.length < 2)
15
+ return null;
16
+ const shown = tiles.slice(0, MOST_TILES);
17
+ return (_jsx("div", { className: `front-hero-media front-hero-tiles front-hero-tiles-${shown.length}`, children: shown.map((tile, index) => (_jsx("img", { src: tile.src, srcSet: tile.srcSet, sizes: tile.sizes, alt: tile.alt, decoding: "async", ...(index === 0 ? { fetchPriority: 'high' } : { loading: 'lazy' }) }, `${index}:${tile.src}`))) }));
18
+ }
19
+ /** The most rows the block draws. */
20
+ const MOST_ROWS = 4;
21
+ /** Up to four rows, each an icon in the accent and a line in the muted ink. Nothing for no rows. */
22
+ export function IconRows({ rows }) {
23
+ if (!rows.length)
24
+ return null;
25
+ return (_jsx("ul", { className: "front-points", children: rows.slice(0, MOST_ROWS).map((row, index) => (_jsxs("li", { children: [row.icon, _jsx("span", { children: row.text })] }, `${index}:${row.text}`))) }));
26
+ }
package/dist/Icon.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /** A glyph's drawings: its paths in the regular weight, and in bold (for a logo). */
2
+ export type IconGlyph = readonly [regular: readonly string[], bold: readonly string[]];
3
+ /** The two weights every glyph keeps. */
4
+ export type IconWeight = 'regular' | 'bold';
5
+ /** A page-wide icon weight: the one an icon that says none draws in, and that weight's drawing of a name when it is
6
+ not one every glyph carries (undefined draws the glyph's own). */
7
+ export interface IconWeighting {
8
+ weight: () => string;
9
+ drawing: (name: string) => readonly string[] | undefined;
10
+ subscribe: (listener: () => void) => () => void;
11
+ version: () => number;
12
+ }
13
+ /** Glyphs to draw, by name. A name already known keeps its glyph. */
14
+ export declare function registerIcons(sent: Readonly<Record<string, IconGlyph>> | undefined): void;
15
+ /** Where the glyphs not yet registered come from: called once, when an icon first names one. */
16
+ export declare function loadIconsWith(load: () => Promise<Readonly<Record<string, IconGlyph>> | undefined>): void;
17
+ /** The page's icon weight, from now on. */
18
+ export declare function weighIconsWith(weights: IconWeighting): void;
19
+ export interface IconProps<Name extends string = string> {
20
+ name: Name;
21
+ /** Omit for decoration. Icon-only controls still need their own name. */
22
+ label?: string;
23
+ /** Relative sizes follow the surrounding typography and density. */
24
+ size?: number | string;
25
+ /** Omitted, the page's weight (`weighIconsWith`), regular unless it says another. */
26
+ weight?: IconWeight;
27
+ className?: string;
28
+ }
29
+ export declare function Icon<Name extends string = string>({ name, label, size, weight, className }: IconProps<Name>): import("react").JSX.Element;
package/dist/Icon.js ADDED
@@ -0,0 +1,78 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * An icon: a glyph drawn from path data on a 256 box, in the text's colour.
4
+ *
5
+ * The glyphs are data rather than one component per icon (the kit's
6
+ * scripts/generate-icons.mjs says why), and this module knows none of them:
7
+ * whoever draws with it registers the set it uses. The kit registers the
8
+ * chrome's own few at once, an App's as its payload brings them, and a loader
9
+ * for the rest (`loadIconsWith`), which an icon whose name is not yet known
10
+ * calls once; every icon waiting on it draws when it lands, and keeps its box
11
+ * meanwhile, so nothing moves. A page's icon weight, when it wears one beyond
12
+ * the two every glyph carries, is lent the same way (`weighIconsWith`).
13
+ */
14
+ import { useEffect, useSyncExternalStore } from 'react';
15
+ const glyphs = new Map();
16
+ const listeners = new Set();
17
+ let known = 0;
18
+ let loader;
19
+ let loading;
20
+ const unweighted = { weight: () => 'regular', drawing: () => undefined, subscribe: () => () => { }, version: () => 0 };
21
+ let weighting = unweighted;
22
+ /** Glyphs to draw, by name. A name already known keeps its glyph. */
23
+ export function registerIcons(sent) {
24
+ if (!sent)
25
+ return;
26
+ let added = false;
27
+ for (const [name, glyph] of Object.entries(sent)) {
28
+ if (glyphs.has(name) || !Array.isArray(glyph))
29
+ continue;
30
+ glyphs.set(name, glyph);
31
+ added = true;
32
+ }
33
+ if (!added)
34
+ return;
35
+ known += 1;
36
+ for (const listener of listeners)
37
+ listener();
38
+ }
39
+ /** Where the glyphs not yet registered come from: called once, when an icon first names one. */
40
+ export function loadIconsWith(load) {
41
+ loader = load;
42
+ loading = undefined;
43
+ }
44
+ /** The page's icon weight, from now on. */
45
+ export function weighIconsWith(weights) {
46
+ weighting = weights;
47
+ }
48
+ function loadEveryGlyph() {
49
+ if (!loader)
50
+ return;
51
+ loading ??= loader()
52
+ .then(registerIcons)
53
+ // A failed load is tried again by the next icon that asks.
54
+ .catch(() => {
55
+ loading = undefined;
56
+ });
57
+ }
58
+ const subscribe = (listener) => {
59
+ listeners.add(listener);
60
+ return () => {
61
+ listeners.delete(listener);
62
+ };
63
+ };
64
+ const version = () => known;
65
+ export function Icon({ name, label, size = '1em', weight, className }) {
66
+ useSyncExternalStore(subscribe, version, version);
67
+ const weights = weighting;
68
+ useSyncExternalStore(weights.subscribe, weights.version, weights.version);
69
+ const glyph = glyphs.get(name);
70
+ const drawn = weight ?? weights.weight();
71
+ const drawing = (weight ? undefined : weights.drawing(name)) ?? glyph?.[drawn === 'bold' ? 1 : 0];
72
+ useEffect(() => {
73
+ if (!glyph)
74
+ loadEveryGlyph();
75
+ }, [glyph]);
76
+ const accessibleLabel = label?.trim() || undefined;
77
+ return (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, fill: "currentColor", viewBox: "0 0 256 256", className: `tw-icon${className ? ` ${className}` : ''}`, role: accessibleLabel ? 'img' : undefined, "aria-label": accessibleLabel, "aria-hidden": accessibleLabel ? undefined : true, focusable: "false", children: drawing?.map((d, index) => (_jsx("path", { d: d }, index))) }));
78
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * A modal, over Base UI's Dialog: what every dialog here used to get from
3
+ * the native `<dialog>` — the scrim, focus held inside, Escape and a click
4
+ * on the scrim to dismiss, focus handed back on close — in one place, and
5
+ * the one thing the native element could not give: popups drawn inside it
6
+ * (a listbox, a menu) that Base UI knows are its own.
7
+ *
8
+ * Open from the moment it mounts, gone when the caller unmounts it; the
9
+ * caller hears `onClose` once, whichever way it closed, and can close it
10
+ * itself through the handle. Wears the caller's classes — `confirm`,
11
+ * `prefs`, `cols` — so the stylesheet and the browser suite see the box
12
+ * they always saw, plus `modal` for the placement the element used to
13
+ * supply, and a backdrop named after the first class for its scrim.
14
+ */
15
+ import { type ReactNode, type Ref, type RefObject } from 'react';
16
+ export interface ModalHandle {
17
+ close: () => void;
18
+ }
19
+ interface ModalProps {
20
+ /** The classes the stylesheet knows the box by (`confirm`, `prefs`); the first also names the scrim (`confirm-backdrop`). Optional: a bare modal is centered and sized by `modal` alone. */
21
+ className?: string;
22
+ /** Told once, however the dialog closed. */
23
+ onClose: () => void;
24
+ /** A non-cancellable operation is awaiting acknowledgement. */
25
+ closeDisabled?: boolean;
26
+ /** Return false to keep the dialog open, for example while confirming discard. */
27
+ beforeClose?: () => boolean;
28
+ children: ReactNode;
29
+ /** The control to focus on open; Base UI's default (the first focusable) otherwise. */
30
+ initialFocus?: RefObject<HTMLElement | null> | false;
31
+ /** Where focus goes on close, once the page is no longer inert; the element focused at open otherwise. */
32
+ finalFocus?: RefObject<HTMLElement | null>;
33
+ /** The dialog's name; absent, its first heading names it. */
34
+ ariaLabel?: string;
35
+ /** The box places itself — a panel that is its own scrim, a box the stylesheet already centres — so the `modal` placement is left off. */
36
+ plain?: boolean;
37
+ /** No scrim element: the box covers the screen itself. */
38
+ backdrop?: boolean;
39
+ /** `'mobile'` — the default — takes the whole screen under 640px, because a
40
+ dialog that leaves a sliver of the page around it on a phone reads as a
41
+ mistake rather than as a layer. `true` takes the whole screen at every
42
+ width; `false` opts out, for a dialog small enough to be a confirmation
43
+ and nothing more. */
44
+ fullscreen?: boolean | 'mobile';
45
+ ref?: Ref<ModalHandle>;
46
+ }
47
+ export declare function Modal({ className, onClose, closeDisabled, beforeClose, children, initialFocus, finalFocus, ariaLabel, plain, backdrop, fullscreen, ref, }: ModalProps): import("react").JSX.Element;
48
+ export {};
package/dist/Modal.js ADDED
@@ -0,0 +1,57 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * A modal, over Base UI's Dialog: what every dialog here used to get from
4
+ * the native `<dialog>` — the scrim, focus held inside, Escape and a click
5
+ * on the scrim to dismiss, focus handed back on close — in one place, and
6
+ * the one thing the native element could not give: popups drawn inside it
7
+ * (a listbox, a menu) that Base UI knows are its own.
8
+ *
9
+ * Open from the moment it mounts, gone when the caller unmounts it; the
10
+ * caller hears `onClose` once, whichever way it closed, and can close it
11
+ * itself through the handle. Wears the caller's classes — `confirm`,
12
+ * `prefs`, `cols` — so the stylesheet and the browser suite see the box
13
+ * they always saw, plus `modal` for the placement the element used to
14
+ * supply, and a backdrop named after the first class for its scrim.
15
+ */
16
+ import { useCallback, useContext, useId, useImperativeHandle, useRef, useState } from 'react';
17
+ import { ComponentStyleScope } from './internal/ComponentStyleScope.js';
18
+ import { Dialog } from '@base-ui/react/dialog';
19
+ import { popupContainer } from './internal/popups.js';
20
+ export function Modal({ className = '', onClose, closeDisabled = false, beforeClose, children, initialFocus, finalFocus, ariaLabel, plain = false, backdrop = true, fullscreen = 'mobile', ref, }) {
21
+ const componentScope = useContext(ComponentStyleScope);
22
+ const [open, setOpen] = useState(true);
23
+ const closed = useRef(false);
24
+ const close = () => {
25
+ if (closed.current || closeDisabled || beforeClose?.() === false)
26
+ return false;
27
+ closed.current = true;
28
+ setOpen(false);
29
+ onClose();
30
+ return true;
31
+ };
32
+ useImperativeHandle(ref, () => ({ close }));
33
+ /* A dialog with no `aria-label` is named by its first heading, which every one of these opens with: a screen reader
34
+ announces "Delete invoice 7, dialog" rather than an unnamed dialog. Done when the box attaches, because the heading
35
+ is the caller's children and the box mounts in a portal after this component's first effect. */
36
+ const headingId = useId();
37
+ const [labelledBy, setLabelledBy] = useState();
38
+ const popup = useCallback((node) => {
39
+ if (ariaLabel || !node)
40
+ return;
41
+ const heading = node.querySelector('h1, h2, h3');
42
+ if (!heading)
43
+ return;
44
+ if (!heading.id)
45
+ heading.id = headingId;
46
+ setLabelledBy(heading.id);
47
+ }, [ariaLabel, headingId]);
48
+ const first = className.split(/\s+/)[0];
49
+ const scrim = first ? `modal-backdrop ${first}-backdrop` : 'modal-backdrop';
50
+ const size = fullscreen ? ` modal-fullscreen${fullscreen === 'mobile' ? '-mobile' : ''}` : '';
51
+ return (_jsx(Dialog.Root, { modal: true, open: open, onOpenChange: (next, details) => {
52
+ if (next)
53
+ return;
54
+ if (!close())
55
+ details.cancel();
56
+ }, children: _jsxs(Dialog.Portal, { container: popupContainer(), children: [backdrop ? _jsx(Dialog.Backdrop, { className: plain ? scrim.replace('modal-backdrop ', '') : scrim }) : null, _jsx(Dialog.Popup, { "data-component": componentScope, className: `${plain ? className : `modal ${className}`.trim()}${size}`, initialFocus: initialFocus, ...(finalFocus ? { finalFocus } : {}), ref: popup, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : labelledBy, "aria-modal": "true", children: children })] }) }));
57
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Pagination: the pages of a list, as a row of buttons. Plain React —
3
+ * Base UI has no pagination, and a row of buttons is all one is — with
4
+ * the rules a list's pages need: the current page is `aria-current`,
5
+ * never a button to itself; Previous and Next are disabled at the ends,
6
+ * never hidden, so the row keeps its shape; and a long run of pages is
7
+ * shortened with an ellipsis, never with a jump the reader cannot see.
8
+ * Pages are 1-based, as the reader counts them; `pages` is the count of
9
+ * pages, not of rows. With one page or none, the row renders nothing:
10
+ * a pager for a single page is noise.
11
+ */
12
+ import type { ReactNode } from 'react';
13
+ export interface PaginationProps {
14
+ /** The current page, 1-based. */
15
+ page: number;
16
+ /** The number of pages. */
17
+ pages: number;
18
+ onPageChange: (page: number) => void;
19
+ /** The list's accessible name, where the pager needs one ("Invoices, pages"). */
20
+ ariaLabel?: string;
21
+ /** Every word the pager says, in the caller's language; any left out are English. */
22
+ words?: Partial<PaginationWords>;
23
+ }
24
+ interface PaginationWords {
25
+ /** The nav's name when `ariaLabel` is not given. */
26
+ pages: string;
27
+ previous: ReactNode;
28
+ next: ReactNode;
29
+ }
30
+ /** The page numbers to draw: the first, the last, and a window of five around the current, with gaps as ellipses. */
31
+ export declare function paginationItems(page: number, pages: number): Array<number | '…'>;
32
+ export declare function Pagination({ page, pages, onPageChange, ariaLabel, words: given }: PaginationProps): import("react").JSX.Element | null;
33
+ export {};
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** The page numbers to draw: the first, the last, and a window of five around the current, with gaps as ellipses. */
3
+ export function paginationItems(page, pages) {
4
+ if (pages <= 7)
5
+ return Array.from({ length: pages }, (_, i) => i + 1);
6
+ const window = new Set([1, pages]);
7
+ for (let p = page - 2; p <= page + 2; p++)
8
+ if (p >= 1 && p <= pages)
9
+ window.add(p);
10
+ const sorted = [...window].sort((a, b) => a - b);
11
+ const items = [];
12
+ for (const p of sorted) {
13
+ if (items.length && typeof items[items.length - 1] === 'number' && p - items[items.length - 1] > 1)
14
+ items.push('…');
15
+ items.push(p);
16
+ }
17
+ return items;
18
+ }
19
+ export function Pagination({ page, pages, onPageChange, ariaLabel, words: given }) {
20
+ const words = { pages: 'Pages', previous: 'Previous', next: 'Next', ...given };
21
+ if (pages <= 1)
22
+ return null;
23
+ const current = Math.min(Math.max(page, 1), pages);
24
+ return (_jsxs("nav", { className: "ui-pagination", "aria-label": ariaLabel ?? words.pages, children: [_jsx("button", { type: "button", className: "ui-page ui-page-step", disabled: current === 1, onClick: () => onPageChange(current - 1), children: words.previous }), paginationItems(current, pages).map((item, i) => item === '…' ? (_jsx("span", { className: "ui-page-gap", "aria-hidden": "true", children: "\u2026" }, `gap-${i}`)) : (_jsx("button", { type: "button", className: "ui-page", "aria-current": item === current ? 'page' : undefined, onClick: () => onPageChange(item), children: item }, item))), _jsx("button", { type: "button", className: "ui-page ui-page-step", disabled: current === pages, onClick: () => onPageChange(current + 1), children: words.next })] }));
25
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * A popover: a small layer anchored to a trigger, over Base UI's Popover,
3
+ * which gives it the anchoring, Escape and a click outside to dismiss,
4
+ * and focus handed back on close. The trigger is the caller's element
5
+ * (a button, a word); the popup is the layer's children. Controlled with
6
+ * `open`/`onOpenChange`, uncontrolled with `defaultOpen`. Where the
7
+ * popup is a menu of actions, DropMenu is the right primitive instead:
8
+ * a popover is a layer, not a list of commands.
9
+ */
10
+ import { type ReactNode } from 'react';
11
+ export interface PopoverProps {
12
+ trigger: ReactNode;
13
+ children: ReactNode;
14
+ open?: boolean;
15
+ defaultOpen?: boolean;
16
+ onOpenChange?: (open: boolean) => void;
17
+ className?: string;
18
+ /** The popup's accessible name, where it needs one. */
19
+ ariaLabel?: string;
20
+ }
21
+ export declare function Popover({ trigger, children, open, defaultOpen, onOpenChange, className, ariaLabel }: PopoverProps): import("react").JSX.Element;
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * A popover: a small layer anchored to a trigger, over Base UI's Popover,
4
+ * which gives it the anchoring, Escape and a click outside to dismiss,
5
+ * and focus handed back on close. The trigger is the caller's element
6
+ * (a button, a word); the popup is the layer's children. Controlled with
7
+ * `open`/`onOpenChange`, uncontrolled with `defaultOpen`. Where the
8
+ * popup is a menu of actions, DropMenu is the right primitive instead:
9
+ * a popover is a layer, not a list of commands.
10
+ */
11
+ import { isValidElement } from 'react';
12
+ import { Popover as BasePopover } from '@base-ui/react/popover';
13
+ export function Popover({ trigger, children, open, defaultOpen, onOpenChange, className, ariaLabel }) {
14
+ /* The trigger is rendered as the trigger, not inside one: a button
15
+ passed as a child nested inside Base UI's own button, giving two
16
+ buttons for one control. The render prop merges onto the caller's
17
+ element (DropMenu.tsx does the same for its trigger). */
18
+ const triggerElement = isValidElement(trigger) ? trigger : _jsx("button", { type: "button", children: trigger });
19
+ return (_jsxs(BasePopover.Root, { open: open, defaultOpen: defaultOpen, onOpenChange: onOpenChange, children: [_jsx(BasePopover.Trigger, { className: "ui-popover-trigger", render: triggerElement }), _jsx(BasePopover.Portal, { children: _jsx(BasePopover.Positioner, { sideOffset: 8, children: _jsx(BasePopover.Popup, { className: ['ui-popover', className].filter(Boolean).join(' '), "aria-label": ariaLabel, children: children }) }) })] }));
20
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Resizable panels: two or more panes side by side (or stacked), divided
3
+ * by a handle the reader drags or walks with the keyboard. Plain React —
4
+ * Base UI has no resizable, and a flex row with a draggable divider is all
5
+ * one is. The panes are the caller's array; each names its own `minSize`
6
+ * in percent (10 by default) and its starting `defaultSize`, and sizes
7
+ * that do not add to 100 are scaled to it, so a caller who writes 1 and 2
8
+ * gets a third and two thirds. Controlled with `sizes`/`onSizesChange`,
9
+ * uncontrolled with the panels' `defaultSize`s.
10
+ *
11
+ * A handle is a separator, not a button: `role="separator"`,
12
+ * `aria-orientation`, `aria-valuenow`/`min`/`max` in percent, and the two
13
+ * panes it divides named by `aria-controls`. Arrow keys move it 5 points
14
+ * (10 with Shift), Home and End take it to its ends, and a pointer drag
15
+ * moves it by the pointer's share of the container — the same handle for
16
+ * mouse, pen and touch, so no reader needs a mouse to resize. The panes
17
+ * themselves never shrink below their minimum, and a drag that would push
18
+ * a neighbour below its minimum stops at that neighbour's edge.
19
+ */
20
+ import { type ReactNode } from 'react';
21
+ export interface ResizablePanel {
22
+ /** The pane's accessible name, said by its handle's controls. */
23
+ label?: string;
24
+ /** The pane's starting share, in percent; shares are scaled to 100. */
25
+ defaultSize?: number;
26
+ /** The pane's smallest share, in percent; 10 by default. */
27
+ minSize?: number;
28
+ children: ReactNode;
29
+ }
30
+ export interface ResizableProps {
31
+ panels: readonly ResizablePanel[];
32
+ direction?: 'horizontal' | 'vertical';
33
+ sizes?: readonly number[];
34
+ onSizesChange?: (sizes: number[]) => void;
35
+ /** The group's accessible name, where the panes need one ("Record and detail"). */
36
+ ariaLabel?: string;
37
+ /** Every word the handles say, in the caller's language; any left out are English. */
38
+ words?: Partial<ResizableWords>;
39
+ className?: string;
40
+ }
41
+ interface ResizableWords {
42
+ /** A handle's name, from the names of the two panes it parts (`before`, `after`). */
43
+ resize: (before: string, after: string) => string;
44
+ /** A pane's stand-in name when it has no `label`: "panel 2". */
45
+ panel: (number: number) => string;
46
+ }
47
+ export declare function Resizable({ panels, direction, sizes, onSizesChange, ariaLabel, words: given, className }: ResizableProps): import("react").JSX.Element | null;
48
+ export {};