@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Baljinder Randhawa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,51 @@
1
- # Temporary Holding Version
1
+ <img src="mark.svg" alt="mistri" width="48" height="48" />
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # @mistriwork/ui
4
+
5
+ The standalone React primitives both shells, mistri's and tablewalk's, are
6
+ drawn with: Modal, DropMenu, Dropdown, Typeahead, Button, Field, Icon and
7
+ DateField over [Base UI](https://base-ui.com), the tokens and stylesheet they
8
+ are drawn with, and the zoom-aware ruler their popups are placed by.
9
+
10
+ The form controls are built on them (**Preview**: @mistriwork/ui is Preview at 1.0): `TextField`, `MaskedField`,
11
+ `MoneyField`, `SensitiveField`, `Select`, `MultiSelect`, `YesNo`, `ChoiceCards`, `Checkbox`, `Consent`, `Slider`,
12
+ `HelpTip`, `ProgressBar`, `StepHeader`, `ActionBar`, `Columns` and `ReviewList`, with the `masks` they take
13
+ (`phone`, `date`, `month`, `ssn`, `zip`, `money`). Each takes a value and an `onChange` and draws what it is told. A
14
+ typed MM/DD/YYYY date is `MaskedField` with `masks.date` (and `isoDay` to read it); the calendar field is `DateField`.
15
+ The landing hero's blocks are in the same Preview: `Badge` (a few words and an optional icon in a pill), `PhotoTiles`
16
+ (two to four photos in a mosaic, sized before they arrive) and `IconRows` (up to four rows of an icon and a line), their
17
+ rules in `hero.css`; the icons, the words and the image addresses are props.
18
+
19
+ A control is a Field, so its label, hint and problem are wired into it, and its problem's line is kept in the flow so
20
+ saying it moves nothing. A theme restyles them all together through `--field-fill`, `--control-h`,
21
+ `--control-border-w` and `--field-radius`, and a field label's weight is the theme's label role.
22
+
23
+ A primitive reads no data, address or message catalog: its words, its icons,
24
+ its answers and its hooks are props, and it renders with no server behind it.
25
+ React, React DOM and Base UI are peers: a project drawing with these installs
26
+ them.
27
+
28
+ ```tsx
29
+ import '@mistriwork/ui/style.css';
30
+ import { Button, Modal } from '@mistriwork/ui';
31
+ ```
32
+
33
+ `style.css` is the tokens (`tokens.css`), the primitives' own rules
34
+ (`primitives.css`) and the form controls' (`forms.css`) in a `mistri-ui` cascade layer, so any rule of the page's
35
+ own wins over them. A page with a layered stylesheet of its own can take the
36
+ files into a layer it names instead (`@import '@mistriwork/ui/tokens.css'
37
+ layer(base);`). An Icon draws the glyphs registered with `registerIcons`; a
38
+ DateField says the `words` it is given, as do Carousel, Resizable and Pagination (a
39
+ `words` object over English defaults). One rule names the rest: a name no one sees is
40
+ `ariaLabel` (Modal, Tabs, Popover, Carousel, Resizable, ScrollArea, Pagination, Drawer),
41
+ and `label` is only ever text drawn on the page. A Field's `optional` is the word drawn
42
+ beside the label of a field that is not required.
43
+
44
+ ## Exports
45
+
46
+ Its exports are its public API, and nothing else is: `@mistriwork/ui` (every
47
+ primitive), each module by its path (`@mistriwork/ui/Button.js`,
48
+ `@mistriwork/ui/internal/datePickerModel.js`, …), and the four stylesheets.
49
+
50
+ Part of [mistri](https://mistriwork.dev);
51
+ [source](https://github.com/rbaljinder/mistri/tree/main/packages/ui).
@@ -0,0 +1,11 @@
1
+ export type AvatarSize = 'sm' | 'md' | 'lg';
2
+ export interface AvatarProps {
3
+ /** The person's name, for the fallback initials. */
4
+ name?: string;
5
+ /** The picture's address; absent, the fallback shows at once. */
6
+ src?: string;
7
+ size?: AvatarSize;
8
+ className?: string;
9
+ }
10
+ export declare function initialsOf(name: string): string;
11
+ export declare function Avatar({ name, src, size, className }: AvatarProps): import("react").JSX.Element;
package/dist/Avatar.js ADDED
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * An avatar: a person's picture, or their initials where there is none.
4
+ * Over Base UI's Avatar, which shows the fallback only while the picture
5
+ * loads or fails, so a slow picture never leaves a blank disc. The
6
+ * initials are the first letters of the first two words of the name
7
+ * ("Ada Lovelace" → "AL", "Ada" → "A"); the disc itself is decorative,
8
+ * and a name that matters is said beside it by the caller, never by the
9
+ * disc's title. Sizes are the house ones: `sm` 28px, `md` 36px, `lg` 48px.
10
+ */
11
+ import { Avatar as BaseAvatar } from '@base-ui/react/avatar';
12
+ const graphemes = typeof Intl !== 'undefined' && 'Segmenter' in Intl ? new Intl.Segmenter(undefined, { granularity: 'grapheme' }) : null;
13
+ /* A letter is a grapheme, not a UTF-16 unit: `slice(0, 1)` splits an emoji or an astral letter into a lone surrogate. */
14
+ function firstLetter(word) {
15
+ const first = graphemes ? graphemes.segment(word)[Symbol.iterator]().next().value?.segment : Array.from(word)[0];
16
+ return (first ?? '').toUpperCase();
17
+ }
18
+ export function initialsOf(name) {
19
+ const words = name.trim().split(/\s+/).filter(Boolean);
20
+ if (!words.length)
21
+ return '';
22
+ if (words.length === 1)
23
+ return firstLetter(words[0]);
24
+ return firstLetter(words[0]) + firstLetter(words[1]);
25
+ }
26
+ export function Avatar({ name, src, size = 'md', className }) {
27
+ return (_jsxs(BaseAvatar.Root, { className: ['ui-avatar', `ui-avatar-${size}`, className].filter(Boolean).join(' '), children: [src ? _jsx(BaseAvatar.Image, { className: "ui-avatar-image", src: src, alt: "" }) : null, _jsx(BaseAvatar.Fallback, { className: "ui-avatar-fallback", "aria-hidden": "true", children: name ? initialsOf(name) : '' })] }));
28
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The one button: an intent, a size, and the states the platform already
3
+ * has. Every look dresses the classes it renders — `primary`, `ghost` and
4
+ * `danger` are what the looks' rules already name, `btn` and its modifiers
5
+ * are the primitive's own — so a form, a dialog and the sign-in card draw the
6
+ * same control instead of each dressing a `<button>` of its own.
7
+ *
8
+ * Busy is not disabled (docs/design.md, Access): a disabled button drops the
9
+ * focus it holds, so a busy one says `aria-disabled` and refuses the press
10
+ * itself — a click, and the implicit submission Enter makes through it.
11
+ */
12
+ import type { ButtonHTMLAttributes, ReactNode, Ref } from 'react';
13
+ export type ButtonIntent = 'primary' | 'secondary' | 'quiet' | 'danger';
14
+ export type ButtonSize = 'compact' | 'default' | 'large';
15
+ interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {
16
+ /** Primary is the one filled action; secondary the outlined rest; quiet a word in the link ink; danger what cannot be taken back. */
17
+ intent?: ButtonIntent;
18
+ size?: ButtonSize;
19
+ /** Working: pressed once already. Says so and refuses another press, keeping the focus. */
20
+ busy?: boolean;
21
+ /** What the button says while busy ("Saving…"); its own words otherwise. */
22
+ busyLabel?: ReactNode;
23
+ /** Full width of its row: the phone's actions, the sign-in card's submit. */
24
+ block?: boolean;
25
+ type?: 'button' | 'submit' | 'reset';
26
+ ref?: Ref<HTMLButtonElement>;
27
+ }
28
+ /** The classes, for the few controls that must stay another element (a link dressed as a button). */
29
+ export declare function buttonClass({ intent, size, block, className, }?: {
30
+ intent?: ButtonIntent;
31
+ size?: ButtonSize;
32
+ block?: boolean;
33
+ className?: string;
34
+ }): string;
35
+ export declare function Button({ intent, size, busy, busyLabel, block, type, className, onClick, children, ref, ...rest }: ButtonProps): import("react").JSX.Element;
36
+ export {};
package/dist/Button.js ADDED
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /* The class a look already dresses for each intent, beside the primitive's own. */
3
+ const DRESS = {
4
+ primary: 'primary btn-primary',
5
+ secondary: 'ghost btn-secondary',
6
+ quiet: 'btn-quiet',
7
+ danger: 'danger btn-danger',
8
+ };
9
+ /** The classes, for the few controls that must stay another element (a link dressed as a button). */
10
+ export function buttonClass({ intent = 'secondary', size = 'default', block = false, className = '', } = {}) {
11
+ return ['btn', DRESS[intent], size === 'default' ? '' : `btn-${size}`, block ? 'btn-block' : '', className].filter(Boolean).join(' ');
12
+ }
13
+ export function Button({ intent = 'secondary', size = 'default', busy = false, busyLabel, block = false, type = 'button', className, onClick, children, ref, ...rest }) {
14
+ const press = (event) => {
15
+ if (busy) {
16
+ event.preventDefault();
17
+ return;
18
+ }
19
+ onClick?.(event);
20
+ };
21
+ return (_jsx("button", { ...rest, ref: ref, type: type, className: buttonClass({ intent, size, block, className }), "aria-disabled": busy || rest['aria-disabled'] || undefined, "data-busy": busy || undefined, onClick: press, children: busy && busyLabel !== undefined ? busyLabel : children }));
22
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * A carousel: one slide at a time from a row of them, chosen with Previous
3
+ * and Next or with a dot per slide. Plain React — Base UI has no carousel,
4
+ * and one slide, two steps and a dot row is all one is. The slides are the
5
+ * caller's array (the Tabs pattern), not children kept in step by hand.
6
+ * Controlled with `index`/`onIndexChange`, uncontrolled with
7
+ * `defaultIndex`. The ends do not wrap: Previous disables on the first
8
+ * slide and Next on the last, so the row keeps its shape and a reader is
9
+ * never spun back to the start without asking. Only the current slide is
10
+ * exposed to assistive tech (the rest are `aria-hidden`); the arrow keys
11
+ * on the region move, Home goes first, End goes last — only while focus
12
+ * is on the carousel or its own controls, so a widget inside a slide
13
+ * keeps its keys — and
14
+ * the slide's movement is silenced for a reader who asked for reduced
15
+ * motion (the track's transition lives under `no-preference` in
16
+ * standalone.css). A step button that disables under a focused finger
17
+ * hands focus to the other step, never to nowhere.
18
+ */
19
+ import { type ReactNode } from 'react';
20
+ export interface CarouselSlide {
21
+ /** The slide's short name, said after its number ("1 of 3: Studio"). */
22
+ label?: string;
23
+ children: ReactNode;
24
+ }
25
+ export interface CarouselProps {
26
+ slides: readonly CarouselSlide[];
27
+ index?: number;
28
+ defaultIndex?: number;
29
+ onIndexChange?: (index: number) => void;
30
+ /** The region's accessible name, where the carousel needs one ("Project photos"). */
31
+ ariaLabel?: string;
32
+ /** Every word the carousel says, in the caller's language; any left out are English. */
33
+ words?: Partial<CarouselWords>;
34
+ className?: string;
35
+ }
36
+ interface CarouselWords {
37
+ /** The region's name when `ariaLabel` is not given. */
38
+ region: string;
39
+ /** The step buttons' visible words. */
40
+ previous: ReactNode;
41
+ next: ReactNode;
42
+ /** The dot row's name. */
43
+ chooseSlide: string;
44
+ /** A slide's name: "1 of 3: Studio". */
45
+ slide: (number: number, total: number, label?: string) => string;
46
+ /** A dot's name: "Go to slide 2: Studio". */
47
+ goToSlide: (number: number, label?: string) => string;
48
+ /** The polite status line read as the slide changes. */
49
+ status: (number: number, total: number, label?: string) => string;
50
+ }
51
+ export declare function Carousel({ slides, index, defaultIndex, onIndexChange, ariaLabel, words: given, className }: CarouselProps): import("react").JSX.Element | null;
52
+ export {};
@@ -0,0 +1,110 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * A carousel: one slide at a time from a row of them, chosen with Previous
4
+ * and Next or with a dot per slide. Plain React — Base UI has no carousel,
5
+ * and one slide, two steps and a dot row is all one is. The slides are the
6
+ * caller's array (the Tabs pattern), not children kept in step by hand.
7
+ * Controlled with `index`/`onIndexChange`, uncontrolled with
8
+ * `defaultIndex`. The ends do not wrap: Previous disables on the first
9
+ * slide and Next on the last, so the row keeps its shape and a reader is
10
+ * never spun back to the start without asking. Only the current slide is
11
+ * exposed to assistive tech (the rest are `aria-hidden`); the arrow keys
12
+ * on the region move, Home goes first, End goes last — only while focus
13
+ * is on the carousel or its own controls, so a widget inside a slide
14
+ * keeps its keys — and
15
+ * the slide's movement is silenced for a reader who asked for reduced
16
+ * motion (the track's transition lives under `no-preference` in
17
+ * standalone.css). A step button that disables under a focused finger
18
+ * hands focus to the other step, never to nowhere.
19
+ */
20
+ import { useEffect, useRef, useState } from 'react';
21
+ const ENGLISH = {
22
+ region: 'Carousel',
23
+ previous: 'Previous slide',
24
+ next: 'Next slide',
25
+ chooseSlide: 'Choose a slide',
26
+ slide: (n, total, label) => `${n} of ${total}${label ? `: ${label}` : ''}`,
27
+ goToSlide: (n, label) => `Go to slide ${n}${label ? `: ${label}` : ''}`,
28
+ status: (n, total, label) => `Slide ${n} of ${total}${label ? `: ${label}` : ''}`,
29
+ };
30
+ export function Carousel({ slides, index, defaultIndex = 0, onIndexChange, ariaLabel, words: given, className }) {
31
+ const words = { ...ENGLISH, ...given };
32
+ const [uncontrolled, setUncontrolled] = useState(defaultIndex);
33
+ const prevRef = useRef(null);
34
+ const nextRef = useRef(null);
35
+ const regionRef = useRef(null);
36
+ const controlsRef = useRef(null);
37
+ /* The step last focused, so a step that disables under it is rescued
38
+ even where the browser already moved focus to the body on disable. */
39
+ const lastStepRef = useRef(null);
40
+ const current = slides.length ? Math.min(Math.max(index ?? uncontrolled, 0), slides.length - 1) : 0;
41
+ const go = (next) => {
42
+ const clamped = Math.min(Math.max(next, 0), slides.length - 1);
43
+ if (index === undefined)
44
+ setUncontrolled(clamped);
45
+ onIndexChange?.(clamped);
46
+ };
47
+ /* A step that disables under its own focus hands focus on: the other
48
+ step where it is live, the region itself where both ends are shut
49
+ (one slide). Rescue runs only while focus is still on that step or
50
+ has already fallen to the body — a reader who tabbed onward is never
51
+ pulled back, and a caller-driven index change moves nothing. */
52
+ useEffect(() => {
53
+ if (!slides.length)
54
+ return;
55
+ const active = document.activeElement;
56
+ const lost = active === document.body;
57
+ /* The rescued step becomes the last-focused one: the next disable
58
+ under it rescues again, even where the rescue focus itself is
59
+ what set the ref (the null-after-focus ordering bug, otherwise). */
60
+ const focusStep = (step) => {
61
+ const other = step === 'prev' ? prevRef.current : nextRef.current;
62
+ if (other && !other.disabled) {
63
+ other.focus();
64
+ lastStepRef.current = step;
65
+ }
66
+ else {
67
+ regionRef.current?.focus();
68
+ lastStepRef.current = null;
69
+ }
70
+ };
71
+ if (current === 0 && lastStepRef.current === 'prev' && (lost || active === prevRef.current))
72
+ focusStep('next');
73
+ else if (current === slides.length - 1 && lastStepRef.current === 'next' && (lost || active === nextRef.current))
74
+ focusStep('prev');
75
+ }, [current, slides.length]);
76
+ if (!slides.length)
77
+ return null;
78
+ const onRegionKeyDown = (event) => {
79
+ /* The keys are the carousel's only while focus is on the carousel itself or its own controls. A widget inside a
80
+ slide (an input's caret, a slider, a tab list, a menu) owns them, and so does anything drawn in a portal: React
81
+ delivers a portal's events up through this tree, so the DOM, not the React tree, says whose they are. */
82
+ const target = event.target;
83
+ if (target !== regionRef.current && !(target && controlsRef.current?.contains(target)))
84
+ return;
85
+ if (event.key === 'ArrowLeft') {
86
+ event.preventDefault();
87
+ go(current - 1);
88
+ }
89
+ else if (event.key === 'ArrowRight') {
90
+ event.preventDefault();
91
+ go(current + 1);
92
+ }
93
+ else if (event.key === 'Home') {
94
+ event.preventDefault();
95
+ go(0);
96
+ }
97
+ else if (event.key === 'End') {
98
+ event.preventDefault();
99
+ go(slides.length - 1);
100
+ }
101
+ };
102
+ return (_jsxs("section", { ref: regionRef, className: ['ui-carousel', className].filter(Boolean).join(' '), "aria-roledescription": "carousel", "aria-label": ariaLabel ?? words.region,
103
+ /* Programmatic focus only, for the step-disable rescue above:
104
+ -1 is no tab stop. */
105
+ tabIndex: -1, onKeyDown: onRegionKeyDown, children: [_jsx("div", { className: "ui-carousel-viewport", children: _jsx("div", { className: "ui-carousel-track", style: { transform: `translateX(-${current * 100}%)` }, children: slides.map((slide, i) => (_jsx("div", { className: "ui-carousel-slide", role: "group", "aria-roledescription": "slide", "aria-label": words.slide(i + 1, slides.length, slide.label), "aria-hidden": i !== current || undefined, inert: i !== current || undefined, children: slide.children }, i))) }) }), _jsxs("div", { className: "ui-carousel-controls", ref: controlsRef, children: [_jsx("button", { ref: prevRef, type: "button", className: "ui-carousel-step", disabled: current === 0, onFocus: () => {
106
+ lastStepRef.current = 'prev';
107
+ }, onClick: () => go(current - 1), children: words.previous }), _jsx("span", { className: "ui-carousel-dots", role: "group", "aria-label": words.chooseSlide, children: slides.map((slide, i) => (_jsx("button", { type: "button", className: "ui-carousel-dot", "aria-label": words.goToSlide(i + 1, slide.label), "aria-current": i === current || undefined, onClick: () => go(i) }, i))) }), _jsx("button", { ref: nextRef, type: "button", className: "ui-carousel-step", disabled: current === slides.length - 1, onFocus: () => {
108
+ lastStepRef.current = 'next';
109
+ }, onClick: () => go(current + 1), children: words.next })] }), _jsx("p", { className: "ui-carousel-status", "aria-live": "polite", children: words.status(current + 1, slides.length, slides[current]?.label) })] }));
110
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Choices drawn as buttons, cards and checkboxes: Yes/No, icon cards (one or several), a checkbox and a consent. One
3
+ * answer is a native radio dressed, so the arrows, Space and the form's own semantics are the browser's, and the focus
4
+ * ring shows on the face when the keyboard is on the input. Several cards are one group as the radios are (CheckCards):
5
+ * one Tab stop, the arrows move between them, Space turns one on or off, and each says `aria-checked`. Props only.
6
+ * Preview: @mistriwork/ui is Preview at 1.0.
7
+ */
8
+ import { type AriaAttributes, type ReactNode } from 'react';
9
+ import { type FieldFrame } from './fieldFrame.js';
10
+ export interface ChoiceOption {
11
+ value: string;
12
+ label: string;
13
+ /** A picture for a card. */
14
+ icon?: ReactNode;
15
+ }
16
+ export interface YesNoProps extends Omit<FieldFrame, 'label'> {
17
+ /** The question. */
18
+ label: ReactNode;
19
+ value: 'yes' | 'no' | '';
20
+ onChange: (value: 'yes' | 'no') => void;
21
+ /** The two words, in the caller's language. */
22
+ yes: string;
23
+ no: string;
24
+ }
25
+ /** Two buttons answering a question. */
26
+ export declare function YesNo(props: YesNoProps): ReactNode;
27
+ export interface ChoiceCardsProps extends FieldFrame {
28
+ options: ChoiceOption[];
29
+ value: string[];
30
+ onChange: (value: string[]) => void;
31
+ /** Several may be on. */
32
+ multiple?: boolean;
33
+ /** The card that stands alone ("None"): choosing it clears the rest, and choosing another clears it. */
34
+ exclusive?: string;
35
+ }
36
+ /** Cards with an icon, a label and a box. One choice is a radio group; `multiple` a group of checkboxes. */
37
+ export declare function ChoiceCards(props: ChoiceCardsProps): ReactNode;
38
+ /** One card of several: its words, its picture and whether it is on. */
39
+ export interface CheckCard {
40
+ key: string;
41
+ label: ReactNode;
42
+ icon?: ReactNode;
43
+ /** A line under the label. */
44
+ help?: ReactNode;
45
+ on: boolean;
46
+ }
47
+ export interface CheckCardsProps {
48
+ cards: readonly CheckCard[];
49
+ /** The card at `index` turned on or off. */
50
+ onToggle: (index: number, on: boolean) => void;
51
+ id?: string;
52
+ disabled?: boolean;
53
+ 'aria-labelledby'?: string;
54
+ 'aria-describedby'?: string;
55
+ 'aria-invalid'?: AriaAttributes['aria-invalid'];
56
+ /** How short every card's words are, so a wide page can set them side by side (`data-fit`; the page's stylesheet
57
+ * decides what that means): `short` is a label alone, `brief` a label and a line that fit half a column. */
58
+ fit?: 'short' | 'brief';
59
+ }
60
+ /**
61
+ * Several cards, any of them on: full-width cards, each an icon, its words and a square at its end that fills when it
62
+ * is on. They are one group to the keyboard, as radios are — one Tab stop (the card last moved to, else the first one
63
+ * on, else the first), the arrows and Home and End move between them, and Space turns the one in hand on or off — and
64
+ * each is a `checkbox` saying `aria-checked`, so a screen reader hears "checked" as it moves.
65
+ */
66
+ export declare function CheckCards(props: CheckCardsProps): ReactNode;
67
+ export interface CheckboxProps extends Omit<FieldFrame, 'label'> {
68
+ checked: boolean;
69
+ onChange: (on: boolean) => void;
70
+ /** The text beside the box; it is the label, so it is a click target too. */
71
+ label: ReactNode;
72
+ disabled?: boolean;
73
+ }
74
+ /** A checkbox beside its text. */
75
+ export declare function Checkbox(props: CheckboxProps): ReactNode;
76
+ /** A checkbox for a long notice: the paragraph is the label, in the body's weight. */
77
+ export declare function Consent(props: CheckboxProps): ReactNode;
@@ -0,0 +1,88 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Choices drawn as buttons, cards and checkboxes: Yes/No, icon cards (one or several), a checkbox and a consent. One
4
+ * answer is a native radio dressed, so the arrows, Space and the form's own semantics are the browser's, and the focus
5
+ * ring shows on the face when the keyboard is on the input. Several cards are one group as the radios are (CheckCards):
6
+ * one Tab stop, the arrows move between them, Space turns one on or off, and each says `aria-checked`. Props only.
7
+ * Preview: @mistriwork/ui is Preview at 1.0.
8
+ */
9
+ import { useId, useRef, useState } from 'react';
10
+ import { Field } from './Field.js';
11
+ import { frameOf } from './fieldFrame.js';
12
+ /** Two buttons answering a question. */
13
+ export function YesNo(props) {
14
+ const name = useId();
15
+ return (_jsx(Field, { ...frameOf(props), group: true, children: (control) => (_jsx("div", { className: "choice-pair", role: "radiogroup", "aria-labelledby": control['aria-labelledby'], "aria-describedby": control['aria-describedby'], "aria-invalid": control['aria-invalid'], children: ['yes', 'no'].map((v) => (_jsxs("label", { className: `choice-pick${props.value === v ? ' is-on' : ''}`, children: [_jsx("input", { type: "radio", name: name, className: "choice-input", checked: props.value === v, onChange: () => props.onChange(v) }), props[v]] }, v))) })) }));
16
+ }
17
+ /** Cards with an icon, a label and a box. One choice is a radio group; `multiple` a group of checkboxes. */
18
+ export function ChoiceCards(props) {
19
+ const name = useId();
20
+ const toggle = (v, on) => {
21
+ if (!props.multiple)
22
+ return props.onChange(on ? [v] : []);
23
+ if (v === props.exclusive)
24
+ return props.onChange(on ? [v] : []);
25
+ const rest = props.value.filter((x) => x !== v && x !== props.exclusive);
26
+ props.onChange(on ? props.options.map((o) => o.value).filter((x) => x === v || rest.includes(x)) : rest);
27
+ };
28
+ if (props.multiple)
29
+ return (_jsx(Field, { ...frameOf(props), group: true, children: (control) => (_jsx(CheckCards, { "aria-labelledby": control['aria-labelledby'], "aria-describedby": control['aria-describedby'], "aria-invalid": control['aria-invalid'], cards: props.options.map((o) => ({ key: o.value, label: o.label, icon: o.icon, on: props.value.includes(o.value) })), onToggle: (index, on) => toggle(props.options[index].value, on) })) }));
30
+ return (_jsx(Field, { ...frameOf(props), group: true, children: (control) => (_jsx("div", { className: "choice-cards", role: "radiogroup", "aria-labelledby": control['aria-labelledby'], "aria-describedby": control['aria-describedby'], "aria-invalid": control['aria-invalid'], children: props.options.map((o) => {
31
+ const on = props.value.includes(o.value);
32
+ return (_jsxs("label", { className: `choice-card${on ? ' is-on' : ''}`, children: [o.icon ? (_jsx("span", { className: "choice-card-icon", "aria-hidden": "true", children: o.icon })) : null, _jsx("span", { className: "choice-card-label", children: o.label }), _jsx("input", { type: "radio", name: name, className: "choice-input", checked: on, onChange: (e) => toggle(o.value, e.target.checked) }), _jsx("span", { className: "choice-box", "aria-hidden": "true" })] }, o.value));
33
+ }) })) }));
34
+ }
35
+ /**
36
+ * Several cards, any of them on: full-width cards, each an icon, its words and a square at its end that fills when it
37
+ * is on. They are one group to the keyboard, as radios are — one Tab stop (the card last moved to, else the first one
38
+ * on, else the first), the arrows and Home and End move between them, and Space turns the one in hand on or off — and
39
+ * each is a `checkbox` saying `aria-checked`, so a screen reader hears "checked" as it moves.
40
+ */
41
+ export function CheckCards(props) {
42
+ const { cards } = props;
43
+ const [held, setHeld] = useState(undefined);
44
+ const faces = useRef([]);
45
+ const first = cards.findIndex((card) => card.on);
46
+ const stop = held !== undefined && held < cards.length ? held : Math.max(first, 0);
47
+ const move = (to) => {
48
+ const next = (to + cards.length) % cards.length;
49
+ setHeld(next);
50
+ faces.current[next]?.focus();
51
+ };
52
+ const keys = (event, index) => {
53
+ const to = event.key === 'ArrowDown' || event.key === 'ArrowRight'
54
+ ? index + 1
55
+ : event.key === 'ArrowUp' || event.key === 'ArrowLeft'
56
+ ? index - 1
57
+ : event.key === 'Home'
58
+ ? 0
59
+ : event.key === 'End'
60
+ ? cards.length - 1
61
+ : undefined;
62
+ if (to !== undefined) {
63
+ event.preventDefault();
64
+ move(to);
65
+ }
66
+ else if (event.key === ' ') {
67
+ // Space is the box's own key; the page does not scroll under it.
68
+ event.preventDefault();
69
+ if (!props.disabled)
70
+ props.onToggle(index, !cards[index].on);
71
+ }
72
+ };
73
+ return (_jsx("div", { className: "choice-cards", role: "group", "data-fit": props.fit, id: props.id, "aria-labelledby": props['aria-labelledby'], "aria-describedby": props['aria-describedby'], "aria-invalid": props['aria-invalid'], children: cards.map((card, index) => (_jsxs("div", { ref: (face) => {
74
+ faces.current[index] = face;
75
+ }, role: "checkbox", "aria-checked": card.on, "aria-disabled": props.disabled || undefined, tabIndex: index === stop ? 0 : -1, className: `choice-card${card.on ? ' is-on' : ''}`, onClick: () => {
76
+ setHeld(index);
77
+ if (!props.disabled)
78
+ props.onToggle(index, !card.on);
79
+ }, onFocus: () => setHeld(index), onKeyDown: (event) => keys(event, index), children: [card.icon ? (_jsx("span", { className: "choice-card-icon", "aria-hidden": "true", children: card.icon })) : null, _jsxs("span", { className: "choice-card-label", children: [card.label, card.help ? _jsx("span", { className: "choice-card-help", children: card.help }) : null] }), _jsx("span", { className: "choice-box", "aria-hidden": "true" })] }, card.key))) }));
80
+ }
81
+ /** A checkbox beside its text. */
82
+ export function Checkbox(props) {
83
+ return (_jsx(Field, { ...frameOf(props), check: true, children: (control) => (_jsx("input", { ...control, type: "checkbox", className: "field-check", checked: props.checked, disabled: props.disabled, onChange: (e) => props.onChange(e.target.checked) })) }));
84
+ }
85
+ /** A checkbox for a long notice: the paragraph is the label, in the body's weight. */
86
+ export function Consent(props) {
87
+ return _jsx(Checkbox, { ...props, className: ['form-field-consent', props.className].filter(Boolean).join(' ') });
88
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The themed calendar field: editable ISO text beside a calendar popover,
3
+ * for a date, or with `withTime` for a date and a wall-clock time. The text
4
+ * is the value — typed drafts survive, the calendar only ever writes the
5
+ * date part — and nothing here converts a value through an instant. The
6
+ * arithmetic and the bounds are datePickerModel.ts's. Its words and the
7
+ * calendar button's icon are props: the kit lends its catalog's and its icon.
8
+ */
9
+ import { type AriaAttributes, type KeyboardEvent, type ReactNode, type Ref } from 'react';
10
+ /** What a date field says. A `format` is the locale's numeric date pattern ("MM/DD/YYYY"). */
11
+ export interface DateFieldWords {
12
+ /** The placeholder and the format a date and a time are typed in. */
13
+ dateTimePattern: string;
14
+ /** The box's complaint at a date and time it cannot take. */
15
+ enterDateAndTime: string;
16
+ /** The box's complaint at a date it cannot take. */
17
+ enterDate: (format: string) => string;
18
+ /** The hint read with the box: its format, for a date and a time. */
19
+ dateAndTimeFormat: string;
20
+ /** The hint read with the box: its format, for a date. */
21
+ dateFormat: (format: string) => string;
22
+ /** The hint's second sentence: the key that opens the calendar. */
23
+ openWithKeys: string;
24
+ /** The calendar button's name, with the field's label when it has one. */
25
+ chooseDate: (label?: string) => string;
26
+ /** The calendar's name, with the field's label when it has one. */
27
+ calendar: (label?: string) => string;
28
+ previousMonth: string;
29
+ nextMonth: string;
30
+ clear: string;
31
+ today: string;
32
+ }
33
+ export interface DateFieldProps {
34
+ id: string;
35
+ words: DateFieldWords;
36
+ /** Drawn in the calendar button. */
37
+ calendarIcon: ReactNode;
38
+ value: string;
39
+ /** Typed text is preserved, including an incomplete date; callers validate on commit. */
40
+ onChange: (value: string) => void;
41
+ /** Composite blur: never fired while focus moves between the input and calendar. */
42
+ onBlur?: () => void;
43
+ onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
44
+ disabled?: boolean;
45
+ required?: boolean;
46
+ min?: string;
47
+ max?: string;
48
+ locale?: string;
49
+ timezone?: string;
50
+ firstDay?: number;
51
+ className?: string;
52
+ name?: string;
53
+ inputRef?: Ref<HTMLInputElement>;
54
+ placeholder?: string;
55
+ /** A date and a wall-clock time (`YYYY-MM-DD HH:MM`, as the box says it; the `T` a datetime-local control writes is read too): the calendar
56
+ picks the date and keeps the time typed. `min` and `max` then bound the whole value. */
57
+ withTime?: boolean;
58
+ /** The calendar is drawn in a portal; an owner that watches focus (an autosaving field) is told where. */
59
+ popupRef?: (node: HTMLDivElement | null) => void;
60
+ onOpenChange?: (open: boolean) => void;
61
+ 'aria-label'?: string;
62
+ 'aria-invalid'?: AriaAttributes['aria-invalid'];
63
+ 'aria-describedby'?: string;
64
+ }
65
+ /** A date field, or a date and a wall-clock time. No input is parsed as an instant or shifted by a timezone. */
66
+ export declare function DateField({ id, words, calendarIcon, value, onChange, onBlur, onKeyDown, disabled, required, min, max, locale, timezone, firstDay, className, name, inputRef, placeholder, withTime, popupRef, onOpenChange, 'aria-label': label, 'aria-invalid': invalid, 'aria-describedby': describedBy, }: DateFieldProps): import("react").JSX.Element;