@mistriwork/ui 0.0.0 → 1.0.0-rc.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +49 -1
- package/dist/Avatar.d.ts +11 -0
- package/dist/Avatar.js +28 -0
- package/dist/Button.d.ts +36 -0
- package/dist/Button.js +22 -0
- package/dist/Carousel.d.ts +52 -0
- package/dist/Carousel.js +110 -0
- package/dist/Choices.d.ts +77 -0
- package/dist/Choices.js +88 -0
- package/dist/DateField.d.ts +66 -0
- package/dist/DateField.js +255 -0
- package/dist/Drawer.d.ts +33 -0
- package/dist/Drawer.js +12 -0
- package/dist/DropMenu.d.ts +59 -0
- package/dist/DropMenu.js +52 -0
- package/dist/Dropdown.d.ts +73 -0
- package/dist/Dropdown.js +85 -0
- package/dist/Field.d.ts +53 -0
- package/dist/Field.js +58 -0
- package/dist/FormParts.d.ts +68 -0
- package/dist/FormParts.js +53 -0
- package/dist/HeroBlocks.d.ts +37 -0
- package/dist/HeroBlocks.js +26 -0
- package/dist/Icon.d.ts +29 -0
- package/dist/Icon.js +78 -0
- package/dist/Modal.d.ts +48 -0
- package/dist/Modal.js +57 -0
- package/dist/Pagination.d.ts +33 -0
- package/dist/Pagination.js +25 -0
- package/dist/Popover.d.ts +21 -0
- package/dist/Popover.js +20 -0
- package/dist/Resizable.d.ts +48 -0
- package/dist/Resizable.js +122 -0
- package/dist/ScrollArea.d.ts +29 -0
- package/dist/ScrollArea.js +62 -0
- package/dist/Select.d.ts +26 -0
- package/dist/Select.js +20 -0
- package/dist/SensitiveField.d.ts +26 -0
- package/dist/SensitiveField.js +27 -0
- package/dist/Slider.d.ts +17 -0
- package/dist/Slider.js +10 -0
- package/dist/Switch.d.ts +28 -0
- package/dist/Switch.js +24 -0
- package/dist/Tabs.d.ts +25 -0
- package/dist/Tabs.js +5 -0
- package/dist/TextField.d.ts +44 -0
- package/dist/TextField.js +19 -0
- package/dist/Tooltip.d.ts +29 -0
- package/dist/Tooltip.js +39 -0
- package/dist/Typeahead.d.ts +63 -0
- package/dist/Typeahead.js +90 -0
- package/dist/fieldFrame.d.ts +37 -0
- package/dist/fieldFrame.js +16 -0
- package/dist/forms.css +192 -0
- package/dist/hero.css +23 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +40 -0
- package/dist/internal/ComponentStyleScope.d.ts +1 -0
- package/dist/internal/ComponentStyleScope.js +4 -0
- package/dist/internal/anchor.d.ts +19 -0
- package/dist/internal/anchor.js +52 -0
- package/dist/internal/datePickerModel.d.ts +53 -0
- package/dist/internal/datePickerModel.js +175 -0
- package/dist/internal/popups.d.ts +13 -0
- package/dist/internal/popups.js +16 -0
- package/dist/internal/ruler.d.ts +37 -0
- package/dist/internal/ruler.js +55 -0
- package/dist/masks.d.ts +32 -0
- package/dist/masks.js +62 -0
- package/dist/preferences.css +26 -0
- package/dist/primitives.css +188 -0
- package/dist/standalone.css +413 -0
- package/dist/style.css +19 -0
- package/dist/tokens.css +233 -0
- package/mark.svg +1 -0
- package/package.json +85 -3
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A box you type into and a list that answers, over Base UI's Autocomplete.
|
|
3
|
+
*
|
|
4
|
+
* Three surfaces here are this shape — the record picker, the table picker
|
|
5
|
+
* on the diagram, the reference filter box — and each walked its own list
|
|
6
|
+
* with arrow keys, placed it by hand and closed it on a blur timer. Base UI
|
|
7
|
+
* owns that now: the combobox roles, arrows and Home/End, Enter on the
|
|
8
|
+
* highlighted row, Tab and a click outside to leave, the first row lit as
|
|
9
|
+
* you type. The caller ranks and fetches the items — `filteredItems` says
|
|
10
|
+
* the list is already the answer — draws each row in its own classes, and
|
|
11
|
+
* hears one `onPick`.
|
|
12
|
+
*
|
|
13
|
+
* The text is the caller's: Base UI is told the item's label to fill the
|
|
14
|
+
* box with on a pick, and that fill is ignored here, so a picker that wants
|
|
15
|
+
* the box emptied (a multi-select) or named (a single one) decides itself.
|
|
16
|
+
* Escape closes only this layer, stopped, so the panel underneath does not
|
|
17
|
+
* take the same key. The list is anchored on the zoom-aware ruler, fixed
|
|
18
|
+
* to the viewport so a scrolling drawer cannot clip it, and drawn inside
|
|
19
|
+
* the enclosing dialog when there is one.
|
|
20
|
+
*/
|
|
21
|
+
import { type InputHTMLAttributes, type ReactNode, type Ref, type RefObject } from 'react';
|
|
22
|
+
interface TypeaheadProps<T> {
|
|
23
|
+
text: string;
|
|
24
|
+
onText: (text: string) => void;
|
|
25
|
+
/** Already ranked and bounded; drawn in this order. */
|
|
26
|
+
items: T[];
|
|
27
|
+
itemKey: (item: T) => string;
|
|
28
|
+
itemLabel: (item: T) => string;
|
|
29
|
+
itemClassName: (item: T, highlighted: boolean) => string;
|
|
30
|
+
renderItem: (item: T) => ReactNode;
|
|
31
|
+
onPick: (item: T) => void;
|
|
32
|
+
open: boolean;
|
|
33
|
+
onOpenChange: (open: boolean) => void;
|
|
34
|
+
/** The `<input>`'s own attributes — class, label, placeholder, title, the caller's keys. */
|
|
35
|
+
inputProps: InputHTMLAttributes<HTMLInputElement>;
|
|
36
|
+
inputRef?: Ref<HTMLInputElement>;
|
|
37
|
+
/** The actual popup, including when it is portalled outside the input's parent. */
|
|
38
|
+
popupRef?: Ref<HTMLDivElement>;
|
|
39
|
+
/** The element the list is sized and placed against; the input by default. */
|
|
40
|
+
anchorRef?: RefObject<HTMLElement | null>;
|
|
41
|
+
/** `width` matches the anchor exactly; `min-width` lets a long name widen the list. */
|
|
42
|
+
fit?: 'width' | 'min-width';
|
|
43
|
+
popupClassName: string;
|
|
44
|
+
listClassName: string;
|
|
45
|
+
/** Shown in the list when nothing matches; nothing at all when omitted. */
|
|
46
|
+
empty?: ReactNode;
|
|
47
|
+
emptyClassName?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Where the list is drawn: inside the enclosing dialog (the default), or
|
|
50
|
+
* the body. A popover is a dialog too, but its positioner is placed by a
|
|
51
|
+
* transform, which makes it the containing block of a fixed list inside
|
|
52
|
+
* it — measured there on the unzoomed ruler, the list landed a zoom's
|
|
53
|
+
* worth away from its box. Drawn to the body, Base UI's floating tree
|
|
54
|
+
* still counts the list as the popover's, so a pick does not dismiss it.
|
|
55
|
+
*/
|
|
56
|
+
portal?: 'dialog' | 'body';
|
|
57
|
+
/** Light the first row as the reader types (the default), or nothing until they arrow in: a search box's Enter is its own. */
|
|
58
|
+
autoHighlight?: boolean;
|
|
59
|
+
/** Hears the row lit by the keys or the pointer, and undefined when none is. */
|
|
60
|
+
onHighlight?: (item: T | undefined) => void;
|
|
61
|
+
}
|
|
62
|
+
export declare function Typeahead<T>({ text, onText, items, itemKey, itemLabel, itemClassName, renderItem, onPick, open, onOpenChange, inputProps, inputRef, popupRef, anchorRef, fit, popupClassName, listClassName, empty, emptyClassName, portal, autoHighlight, onHighlight, }: TypeaheadProps<T>): import("react").JSX.Element;
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A box you type into and a list that answers, over Base UI's Autocomplete.
|
|
4
|
+
*
|
|
5
|
+
* Three surfaces here are this shape — the record picker, the table picker
|
|
6
|
+
* on the diagram, the reference filter box — and each walked its own list
|
|
7
|
+
* with arrow keys, placed it by hand and closed it on a blur timer. Base UI
|
|
8
|
+
* owns that now: the combobox roles, arrows and Home/End, Enter on the
|
|
9
|
+
* highlighted row, Tab and a click outside to leave, the first row lit as
|
|
10
|
+
* you type. The caller ranks and fetches the items — `filteredItems` says
|
|
11
|
+
* the list is already the answer — draws each row in its own classes, and
|
|
12
|
+
* hears one `onPick`.
|
|
13
|
+
*
|
|
14
|
+
* The text is the caller's: Base UI is told the item's label to fill the
|
|
15
|
+
* box with on a pick, and that fill is ignored here, so a picker that wants
|
|
16
|
+
* the box emptied (a multi-select) or named (a single one) decides itself.
|
|
17
|
+
* Escape closes only this layer, stopped, so the panel underneath does not
|
|
18
|
+
* take the same key. The list is anchored on the zoom-aware ruler, fixed
|
|
19
|
+
* to the viewport so a scrolling drawer cannot clip it, and drawn inside
|
|
20
|
+
* the enclosing dialog when there is one.
|
|
21
|
+
*/
|
|
22
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
23
|
+
import { Autocomplete } from '@base-ui/react/autocomplete';
|
|
24
|
+
import { viewportBox } from './internal/ruler.js';
|
|
25
|
+
import { layoutViewport, zoomAnchor } from './internal/anchor.js';
|
|
26
|
+
import { popupContainer } from './internal/popups.js';
|
|
27
|
+
export function Typeahead({ text, onText, items, itemKey, itemLabel, itemClassName, renderItem, onPick, open, onOpenChange, inputProps, inputRef, popupRef, anchorRef, fit = 'min-width', popupClassName, listClassName, empty, emptyClassName, portal = 'dialog', autoHighlight = true, onHighlight, }) {
|
|
28
|
+
const input = useRef(null);
|
|
29
|
+
const [container, setContainer] = useState(null);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
setContainer(portal === 'body' ? null : input.current?.closest('dialog, [role="dialog"]'));
|
|
32
|
+
}, [portal]);
|
|
33
|
+
/* A fixed-fit list takes its width from the anchor alone. Base UI's
|
|
34
|
+
--anchor-width is recomputed on every positioner update, snapped against
|
|
35
|
+
the anchor's position, so a list growing as its options arrived flipped
|
|
36
|
+
between two widths inside one ResizeObserver frame ("loop completed with
|
|
37
|
+
undelivered notifications"). Measured here on the house ruler, and again
|
|
38
|
+
only when the anchor itself resizes, the list's own size cannot move it. */
|
|
39
|
+
const [anchorWidth, setAnchorWidth] = useState(null);
|
|
40
|
+
useLayoutEffect(() => {
|
|
41
|
+
const anchor = anchorRef?.current ?? input.current;
|
|
42
|
+
if (!open || fit !== 'width' || !anchor)
|
|
43
|
+
return;
|
|
44
|
+
const measure = () => {
|
|
45
|
+
const width = viewportBox(anchor).width;
|
|
46
|
+
setAnchorWidth(width > 0 ? width : null);
|
|
47
|
+
};
|
|
48
|
+
measure();
|
|
49
|
+
if (typeof ResizeObserver === 'undefined')
|
|
50
|
+
return;
|
|
51
|
+
const observer = new ResizeObserver(measure);
|
|
52
|
+
observer.observe(anchor);
|
|
53
|
+
return () => observer.disconnect();
|
|
54
|
+
}, [open, fit, anchorRef]);
|
|
55
|
+
const setInput = (node) => {
|
|
56
|
+
input.current = node;
|
|
57
|
+
if (typeof inputRef === 'function')
|
|
58
|
+
inputRef(node);
|
|
59
|
+
else if (inputRef)
|
|
60
|
+
inputRef.current = node;
|
|
61
|
+
};
|
|
62
|
+
const { onKeyDown, onFocus, ...rest } = inputProps;
|
|
63
|
+
return (_jsxs(Autocomplete.Root, { items: items, filteredItems: items, itemToStringValue: itemLabel, value: text, onValueChange: (next, details) => {
|
|
64
|
+
// Selection fills the input too; it is not a new search or a cleared key.
|
|
65
|
+
if (details.reason !== 'item-press')
|
|
66
|
+
onText(String(next ?? ''));
|
|
67
|
+
}, open: open, onOpenChange: onOpenChange, openOnInputClick: true, autoHighlight: autoHighlight, onItemHighlighted: onHighlight && ((item) => onHighlight(item)), children: [_jsx(Autocomplete.Input, { ...rest, ref: setInput, onFocus: onFocus, onKeyDown: (e) => {
|
|
68
|
+
onKeyDown?.(e);
|
|
69
|
+
if (e.defaultPrevented)
|
|
70
|
+
return;
|
|
71
|
+
/* Only this layer: a drawer under the list listens for Escape too. */
|
|
72
|
+
if (e.key === 'Escape' && open) {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
e.stopPropagation();
|
|
75
|
+
onOpenChange(false);
|
|
76
|
+
}
|
|
77
|
+
} }), _jsx(Autocomplete.Portal, { container: container ?? popupContainer(), children: _jsx(Autocomplete.Positioner, { anchor: () => {
|
|
78
|
+
const el = anchorRef?.current ?? input.current;
|
|
79
|
+
return el ? zoomAnchor(el) : null;
|
|
80
|
+
}, positionMethod: "fixed", side: "bottom", align: "start", sideOffset: 4, collisionBoundary: layoutViewport(), collisionPadding: 8, style: {
|
|
81
|
+
zIndex: 40,
|
|
82
|
+
// Fixed-fit lists size the observed positioner itself; the child
|
|
83
|
+
// should not determine its intrinsic width while options arrive.
|
|
84
|
+
...(fit === 'width'
|
|
85
|
+
? { width: anchorWidth === null ? 'var(--anchor-width)' : `${anchorWidth}px`, maxWidth: 'var(--available-width)' }
|
|
86
|
+
: {}),
|
|
87
|
+
}, children: _jsx(Autocomplete.Popup, { ref: popupRef, className: popupClassName, style: {
|
|
88
|
+
...(fit === 'width' ? { width: '100%' } : { minWidth: 'var(--anchor-width)', maxWidth: 'var(--available-width)' }),
|
|
89
|
+
}, children: _jsxs(Autocomplete.List, { className: listClassName, children: [empty !== undefined ? _jsx(Autocomplete.Empty, { className: emptyClassName, children: empty }) : null, items.map((item) => (_jsx(Autocomplete.Item, { value: item, className: (s) => itemClassName(item, s.highlighted), onClick: () => onPick(item), children: renderItem(item) }, itemKey(item))))] }) }) }) })] }));
|
|
90
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every form control says about its frame, once: the label, the hint, the problem, the word for an optional
|
|
3
|
+
* field and a help tip. `frameOf` turns it into the Field's props, so a control never joins them by hand.
|
|
4
|
+
*/
|
|
5
|
+
import type { ReactNode } from 'react';
|
|
6
|
+
import { type HelpTipProps } from './FormParts.js';
|
|
7
|
+
export interface FieldFrame {
|
|
8
|
+
label: ReactNode;
|
|
9
|
+
/** A sentence under the control that helps before anything goes wrong. */
|
|
10
|
+
hint?: ReactNode;
|
|
11
|
+
/** What is wrong now, in the reader's words. Its line is kept in the flow, so saying it moves nothing (`reserve`). */
|
|
12
|
+
problem?: ReactNode;
|
|
13
|
+
required?: boolean;
|
|
14
|
+
/** The label is read to assistive technology but not drawn. */
|
|
15
|
+
labelHidden?: boolean;
|
|
16
|
+
/** The word that says a field may be left empty ("(optional)"), in the caller's language; Field draws it where the worn theme asks. */
|
|
17
|
+
optional?: ReactNode;
|
|
18
|
+
/** A help tip beside the label. */
|
|
19
|
+
tip?: HelpTipProps;
|
|
20
|
+
/** Keep the problem's line in the flow while empty. On by default for a form's controls; turn it off for a dense row. */
|
|
21
|
+
reserve?: boolean;
|
|
22
|
+
/** The control's id; one is made when not given. */
|
|
23
|
+
id?: string;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function frameOf(p: FieldFrame): {
|
|
27
|
+
label: ReactNode;
|
|
28
|
+
hint: ReactNode;
|
|
29
|
+
problem: ReactNode;
|
|
30
|
+
required: boolean | undefined;
|
|
31
|
+
labelHidden: boolean | undefined;
|
|
32
|
+
id: string | undefined;
|
|
33
|
+
className: string | undefined;
|
|
34
|
+
optional: ReactNode;
|
|
35
|
+
labelAside: import("react").JSX.Element | undefined;
|
|
36
|
+
reserve: boolean;
|
|
37
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { HelpTip } from './FormParts.js';
|
|
3
|
+
export function frameOf(p) {
|
|
4
|
+
return {
|
|
5
|
+
label: p.label,
|
|
6
|
+
hint: p.hint,
|
|
7
|
+
problem: p.problem,
|
|
8
|
+
required: p.required,
|
|
9
|
+
labelHidden: p.labelHidden,
|
|
10
|
+
id: p.id,
|
|
11
|
+
className: p.className,
|
|
12
|
+
optional: p.optional,
|
|
13
|
+
labelAside: p.tip ? _jsx(HelpTip, { ...p.tip }) : undefined,
|
|
14
|
+
reserve: p.reserve ?? true,
|
|
15
|
+
};
|
|
16
|
+
}
|
package/dist/forms.css
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/* @mistriwork/ui's form controls (TextField, MaskedField, SensitiveField, Select, Choices, Slider, FormParts), apart
|
|
2
|
+
from primitives.css: a page that draws none of them does not pay for their rules at its first paint. style.css
|
|
3
|
+
takes this file with the rest; a shell that layers primitives.css itself (the kit's) loads this one beside the
|
|
4
|
+
first control it draws. */
|
|
5
|
+
|
|
6
|
+
/* The form controls (TextField, MaskedField, SensitiveField, Select, Choices, Slider, FormParts). They wear the
|
|
7
|
+
field's own box above (`field-control`), so a theme that sets --field-fill, --control-h, --control-border-w or
|
|
8
|
+
--field-radius restyles every one of them together, and the label above is the Field's. */
|
|
9
|
+
/* Read, not drawn: a group whose question is the heading above it. */
|
|
10
|
+
.form-field-label-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
11
|
+
/* The problem's line stays in the flow while empty, so a problem that arrives moves nothing below it. */
|
|
12
|
+
.form-field-reserve > .form-field-problem { min-height: calc(var(--text-sm) * 1.45); }
|
|
13
|
+
/* A consent: a paragraph beside its box, in the body's weight. */
|
|
14
|
+
.form-field-consent .form-field-label { font-size: var(--text-sm); font-weight: 400; line-height: 1.45; }
|
|
15
|
+
.field-check { flex: none; width: 18px; height: 18px; margin: 1px 0 0; accent-color: var(--accent); }
|
|
16
|
+
.field-check:focus-visible { outline: var(--focus-width) solid var(--focus); outline-offset: var(--focus-offset); }
|
|
17
|
+
|
|
18
|
+
/* A text box and the mark at its inline end. The room for the mark is the box's from the start (has-mark), so the
|
|
19
|
+
check arriving moves no text. Decorative: validity is aria-invalid's to say. */
|
|
20
|
+
.field-box { position: relative; display: flex; align-items: center; min-width: 0; }
|
|
21
|
+
.field-box.has-mark > .field-control { padding-inline-end: calc(var(--space-3h) + 16px + var(--space-3)); }
|
|
22
|
+
.field-mark { position: absolute; inset-inline-end: var(--space-3h); display: grid; place-items: center; width: 16px; height: 16px; border-radius: 50%; background: var(--status-good); color: var(--surface); pointer-events: none; }
|
|
23
|
+
.field-mark:empty::after { content: ""; width: 4px; height: 8px; margin-top: calc(-1 * var(--space-1)); border: solid currentColor; border-width: 0 2px 2px 0; transform: rotate(45deg); }
|
|
24
|
+
.field-mark > svg { width: 100%; height: 100%; }
|
|
25
|
+
.field-control::placeholder { color: var(--ink-subtle); }
|
|
26
|
+
.field-hidden { letter-spacing: 0.08em; }
|
|
27
|
+
/* Show beside the box, never inside it. */
|
|
28
|
+
.field-with-action { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--space-3); }
|
|
29
|
+
.field-note { display: flex; align-items: center; gap: var(--space-3); margin: 0; font-size: var(--text-sm); line-height: 1.45; color: var(--ink-muted); }
|
|
30
|
+
.field-note > svg { flex: none; width: 14px; height: 14px; }
|
|
31
|
+
|
|
32
|
+
/* The form's listbox is the Dropdown dressed as a field box. */
|
|
33
|
+
.dd-button.field-select { min-height: var(--control-h); border-width: var(--control-border-w); padding: var(--space-2h) var(--space-3h); font: inherit; font-size: var(--text-base); background: var(--field-fill); border-radius: var(--field-radius, var(--radius)); text-align: start; }
|
|
34
|
+
.dd-button.field-select[aria-invalid="true"] { border-color: var(--status-critical); }
|
|
35
|
+
.dd-button.field-select:is([disabled], [aria-disabled="true"]) { background: var(--raised); color: var(--ink-muted); cursor: not-allowed; }
|
|
36
|
+
.dd-value.is-empty { color: var(--ink-muted); }
|
|
37
|
+
.dd-list.field-list .dd-label { font: var(--text-base) var(--sans); }
|
|
38
|
+
.dd-list-multi .dd-option.selected .dd-label { color: var(--ink); font-weight: inherit; }
|
|
39
|
+
.dd-box { position: relative; flex: none; width: 16px; height: 16px; margin-top: var(--space-1); box-sizing: border-box; border: 1px solid var(--line-control); border-radius: 3px; background: var(--surface); }
|
|
40
|
+
.dd-option.selected .dd-box { border-color: var(--accent); background: var(--accent); }
|
|
41
|
+
.dd-option.selected .dd-box::after { content: ""; position: absolute; inset: 1px 0 0; margin: auto; width: 4px; height: 8px; border: solid var(--on-accent); border-width: 0 2px 2px 0; transform: translateY(-1px) rotate(45deg); }
|
|
42
|
+
|
|
43
|
+
/* Choices: one answer is native radios, hidden, with their faces drawn, and the ring shows on the face when the
|
|
44
|
+
keyboard is on the input; several cards are each a face that takes the focus itself (CheckCards). A host may dress
|
|
45
|
+
them through the --choice-* roles — a quiet form draws a card at rest as its well, with an edge only once chosen
|
|
46
|
+
or in focus (formDress.css) — and each falls back to the field's own box here. */
|
|
47
|
+
.choice-input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; pointer-events: none; }
|
|
48
|
+
.choice-pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }
|
|
49
|
+
.choice-pick, .choice-card {
|
|
50
|
+
position: relative; box-sizing: border-box; display: flex; align-items: center; min-height: var(--control-h);
|
|
51
|
+
border: var(--control-border-w) solid var(--choice-edge, var(--line-control)); border-radius: var(--field-radius, var(--radius));
|
|
52
|
+
background: var(--choice-well, var(--field-fill)); color: var(--ink); font-size: var(--text-base); cursor: pointer;
|
|
53
|
+
transition: border-color var(--t-hover) var(--ease-out), background-color var(--t-hover) var(--ease-out), box-shadow var(--t-hover) var(--ease-out);
|
|
54
|
+
}
|
|
55
|
+
.choice-pick { justify-content: center; padding: 0 var(--space-4); }
|
|
56
|
+
.choice-card { gap: var(--space-3h); padding: var(--space-3) var(--space-4); min-height: max(var(--control-h), 48px); }
|
|
57
|
+
@media (hover: hover) {
|
|
58
|
+
.choice-pick:hover, .choice-card:hover { border-color: var(--choice-edge-hover, color-mix(in srgb, var(--line-control) 60%, var(--ink))); background-color: var(--choice-well-hover, var(--field-fill)); }
|
|
59
|
+
}
|
|
60
|
+
/* On is a thicker edge drawn outside the box, not a wider border, so choosing moves nothing. */
|
|
61
|
+
.choice-pick.is-on, .choice-card.is-on { border-color: var(--choice-on, var(--accent)); box-shadow: 0 0 0 1px var(--choice-on, var(--accent)); background: var(--choice-on-well, var(--accent-soft)); }
|
|
62
|
+
.choice-pick:has(.choice-input:focus-visible), .choice-card:has(.choice-input:focus-visible), .choice-card:focus-visible { outline: var(--focus-width) solid var(--focus); outline-offset: var(--focus-offset); }
|
|
63
|
+
.choice-card[aria-disabled="true"] { cursor: not-allowed; opacity: 0.6; }
|
|
64
|
+
.choice-cards { display: flex; flex-direction: column; gap: var(--space-3); }
|
|
65
|
+
.choice-card-icon { display: inline-flex; flex: none; color: var(--accent-ink); }
|
|
66
|
+
.choice-card-icon > svg { width: 22px; height: 22px; }
|
|
67
|
+
.choice-card-label { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
68
|
+
.choice-card-help { display: block; color: var(--ink-muted); font-size: var(--text-sm); }
|
|
69
|
+
.choice-box { position: relative; flex: none; box-sizing: border-box; width: 18px; height: 18px; border: 1.5px solid var(--line-control); border-radius: 3px; background: var(--surface); }
|
|
70
|
+
.choice-card.is-on .choice-box { border-color: var(--choice-check, var(--accent)); background: var(--choice-check, var(--accent)); }
|
|
71
|
+
.choice-card.is-on .choice-box::after { content: ""; position: absolute; inset: 1px 0 0; margin: auto; width: 4px; height: 8px; border: solid var(--choice-on-check, var(--on-accent)); border-width: 0 2px 2px 0; transform: translateY(-1px) rotate(45deg); }
|
|
72
|
+
/* One answer's card says so with a radio's round mark and its dot; several answers' keep the square and its tick, as
|
|
73
|
+
a native radio and checkbox would. Read off the input the card holds, so every host's cards agree. */
|
|
74
|
+
.choice-card:has(> .choice-input[type="radio"]) > .choice-box { border-radius: 50%; }
|
|
75
|
+
.choice-card.is-on:has(> .choice-input[type="radio"]) > .choice-box::after { inset: 0; width: 6px; height: 6px; border: 0; border-radius: 50%; background: var(--choice-on-check, var(--on-accent)); transform: none; }
|
|
76
|
+
|
|
77
|
+
/* The slider: the readout in a field box, the native range under it. */
|
|
78
|
+
.field-readout { display: flex; align-items: center; }
|
|
79
|
+
.field-range { --range-share: 0%; width: 100%; height: 22px; margin: var(--space-3) 0 0; padding: 0; background: transparent; appearance: none; -webkit-appearance: none; cursor: pointer; }
|
|
80
|
+
.field-range::-webkit-slider-runnable-track { height: 4px; border-radius: 2px; background: linear-gradient(to right, var(--accent) 0 var(--range-share), var(--line) var(--range-share) 100%); }
|
|
81
|
+
.field-range::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; margin-top: -7px; /* off-scale: centres the 18px thumb on the 4px track, (4 - 18) / 2 */ border: 0; border-radius: 50%; background: var(--accent); }
|
|
82
|
+
.field-range::-moz-range-track { height: 4px; border-radius: 2px; background: var(--line); }
|
|
83
|
+
.field-range::-moz-range-progress { height: 4px; border-radius: 2px; background: var(--accent); }
|
|
84
|
+
.field-range::-moz-range-thumb { width: 18px; height: 18px; border: 0; border-radius: 50%; background: var(--accent); }
|
|
85
|
+
.field-range:focus-visible { outline: var(--focus-width) solid var(--focus); outline-offset: var(--focus-offset); }
|
|
86
|
+
.field-range:disabled { cursor: not-allowed; opacity: 0.5; }
|
|
87
|
+
.field-range-ends { display: flex; justify-content: space-between; font-size: var(--text-xs); line-height: 1.45; color: var(--ink-muted); }
|
|
88
|
+
|
|
89
|
+
/* The help tip. */
|
|
90
|
+
.help-tip { position: relative; display: inline-flex; }
|
|
91
|
+
.help-tip-button { position: relative; display: inline-grid; place-items: center; width: 16px; height: 16px; padding: 0; border: 1px solid var(--ink-muted); border-radius: 50%; background: transparent; color: var(--ink-muted); font: italic 700 10px/1 var(--sans); cursor: pointer; box-shadow: none; }
|
|
92
|
+
.help-tip-button::after { content: ""; position: absolute; inset: -8px; }
|
|
93
|
+
.help-tip-button > svg { width: 100%; height: 100%; }
|
|
94
|
+
.help-tip-button:focus-visible { outline: var(--focus-width) solid var(--focus); outline-offset: var(--focus-offset); }
|
|
95
|
+
.help-tip-text {
|
|
96
|
+
position: absolute; inset-inline-start: 0; top: calc(100% + var(--space-3)); z-index: 30; width: min(240px, 80vw);
|
|
97
|
+
padding: var(--space-3) var(--space-3h); border: 1px solid var(--line-strong); border-radius: var(--radius-pop);
|
|
98
|
+
background: var(--surface); color: var(--ink); box-shadow: var(--shadow-pop);
|
|
99
|
+
font-size: var(--text-sm); font-weight: 400; line-height: 1.45; text-transform: none; letter-spacing: normal;
|
|
100
|
+
}
|
|
101
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
102
|
+
.help-tip-text { transform-origin: top left; transition: opacity var(--t-popover) var(--ease-out), transform var(--t-popover) var(--ease-out); @starting-style { opacity: 0; transform: scale(0.96) translateY(-4px); } }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* The progress bar: a track and its fill. The fill is not animated: it is information that changes often. */
|
|
106
|
+
.progress-bar { height: 6px; border-radius: 3px; background: var(--raised); overflow: hidden; }
|
|
107
|
+
.progress-bar-fill { display: block; height: 100%; border-radius: 3px; background: var(--accent); }
|
|
108
|
+
|
|
109
|
+
/* A step's heading and lead. */
|
|
110
|
+
.step-header { display: grid; gap: var(--space-3); }
|
|
111
|
+
.step-title { margin: 0; font-size: var(--type-record-title); font-weight: var(--heading-weight); line-height: 1.15; letter-spacing: var(--track-title); color: var(--ink); }
|
|
112
|
+
.step-mark { display: inline-block; margin-inline-end: var(--space-4); }
|
|
113
|
+
.step-lead { margin: 0; font-size: var(--text-md); line-height: 1.45; color: var(--ink-muted); }
|
|
114
|
+
|
|
115
|
+
/* The bar at the foot of its container: sticky, so it stays in the container it belongs to and the page keeps its sidebar. */
|
|
116
|
+
.action-bar { position: sticky; bottom: 0; z-index: 2; display: flex; gap: var(--space-4); padding: var(--space-4) var(--space-5); border-top: 1px solid var(--line); background: var(--surface); }
|
|
117
|
+
.action-bar-back { flex: 0 0 33%; }
|
|
118
|
+
.action-bar-primary { flex: 1; min-width: 0; }
|
|
119
|
+
|
|
120
|
+
/* Fields side by side; a row stacks where its fields no longer fit. */
|
|
121
|
+
.form-columns { display: grid; grid-template-columns: var(--form-columns, repeat(auto-fit, minmax(min(100%, 9rem), 1fr))); gap: var(--space-4); align-items: start; }
|
|
122
|
+
@media (max-width: 22rem) { .form-columns { grid-template-columns: minmax(0, 1fr); } }
|
|
123
|
+
.review-list { display: grid; gap: var(--field-gap); }
|
|
124
|
+
|
|
125
|
+
/* A theme's recipes for a form (themeRecipes.ts: required, checkbox, formStep, actionBar). Their rules are here, not
|
|
126
|
+
in a first stylesheet: they draw nothing a page without a control has. */
|
|
127
|
+
|
|
128
|
+
/* required: the star is primitives.css's; this one says the other word instead. The word stands beside the label
|
|
129
|
+
(Field's optionalWord), so the label's name is the same under either recipe, and it is drawn only here. */
|
|
130
|
+
.form-field-label-row > .form-field-label { min-width: 0; overflow-wrap: anywhere; }
|
|
131
|
+
.form-field-optional-word { display: none; font-size: var(--text-sm); font-weight: 400; line-height: 1.35; color: var(--ink-muted); }
|
|
132
|
+
[data-look-required="optional-word"] .form-field-optional-word { display: inline; }
|
|
133
|
+
[data-look-required="optional-word"] .form-field.is-required > .form-field-label::after { content: none; }
|
|
134
|
+
|
|
135
|
+
/* checkbox: drawn. The native input stays (the keyboard, the name, the ring), with its own face taken off and
|
|
136
|
+
the field's drawn on: its fill, the control's edge, and a corner no rounder than a third of the box, since a
|
|
137
|
+
10px field corner on an 18px box is a circle. On is the accent with a check in its ink. */
|
|
138
|
+
[data-look-checkbox="drawn"] :is(.field-check, .form-field-check-row > input[type="checkbox"]) {
|
|
139
|
+
appearance: none; -webkit-appearance: none; display: inline-grid; place-content: center; box-sizing: border-box;
|
|
140
|
+
background: var(--field-fill); border: var(--control-border-w) solid var(--line-control);
|
|
141
|
+
border-radius: min(var(--field-radius, var(--radius)), 6px); cursor: pointer;
|
|
142
|
+
transition: background-color var(--t-hover) var(--ease-out), border-color var(--t-hover) var(--ease-out);
|
|
143
|
+
}
|
|
144
|
+
[data-look-checkbox="drawn"] :is(.field-check, .form-field-check-row > input[type="checkbox"])::after {
|
|
145
|
+
content: ""; width: 4px; height: 8px; border: solid var(--on-accent); border-width: 0 2px 2px 0;
|
|
146
|
+
transform: translateY(-1px) rotate(45deg); opacity: 0;
|
|
147
|
+
}
|
|
148
|
+
[data-look-checkbox="drawn"] :is(.field-check, .form-field-check-row > input[type="checkbox"]):checked { background: var(--accent); border-color: var(--accent); }
|
|
149
|
+
[data-look-checkbox="drawn"] :is(.field-check, .form-field-check-row > input[type="checkbox"]):checked::after { opacity: 1; }
|
|
150
|
+
[data-look-checkbox="drawn"] :is(.field-check, .form-field-check-row > input[type="checkbox"]):focus-visible { outline: var(--focus-width) solid var(--focus); outline-offset: var(--focus-offset); }
|
|
151
|
+
[data-look-checkbox="drawn"] :is(.field-check, .form-field-check-row > input[type="checkbox"]):disabled { background: var(--raised); border-color: var(--line); cursor: not-allowed; }
|
|
152
|
+
@media (forced-colors: active) {
|
|
153
|
+
[data-look-checkbox="drawn"] :is(.field-check, .form-field-check-row > input[type="checkbox"]) { border: 1px solid ButtonText; }
|
|
154
|
+
[data-look-checkbox="drawn"] :is(.field-check, .form-field-check-row > input[type="checkbox"]):checked { background: Highlight; border-color: Highlight; }
|
|
155
|
+
[data-look-checkbox="drawn"] :is(.field-check, .form-field-check-row > input[type="checkbox"]):checked::after { border-color: HighlightText; }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* formStep: ruled puts a hairline under a step's header. actionBar: raised stands the sticky bar on the surface with a
|
|
159
|
+
soft lift (the theme's own shadow colour, upward since the bar is at the foot) in place of the hairline over it. */
|
|
160
|
+
[data-look-form-step="ruled"] .intake-head { padding-bottom: var(--space-4); border-bottom: 1px solid var(--line); }
|
|
161
|
+
[data-look-action-bar="raised"] :is(.intake-actions, .action-bar) {
|
|
162
|
+
background: var(--surface); border-top-color: transparent;
|
|
163
|
+
box-shadow: 0 -6px 18px color-mix(in srgb, var(--shadow-ink, black) 12%, transparent);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* A control stands level at the theme's height (--control-h): the date field's input and the Button primitive with the
|
|
167
|
+
rest (the field box and the listbox read it above). */
|
|
168
|
+
.date-field .date-field-input { box-sizing: border-box; min-height: var(--control-h); border-width: var(--control-border-w); background: var(--field-fill); border-radius: var(--field-radius, var(--radius)); }
|
|
169
|
+
|
|
170
|
+
/* Mobile. */
|
|
171
|
+
/* No double-tap-to-zoom delay on what is tapped, and no grey flash over it: the browser's highlight is replaced by
|
|
172
|
+
the house press (--t-press, the button gives a little and a link dims), so a tap is still answered. */
|
|
173
|
+
:is(button, a, input, select, textarea, summary, [role="button"], [role="switch"], [role="tab"], [role="option"], [role="menuitem"]) {
|
|
174
|
+
touch-action: manipulation; -webkit-tap-highlight-color: transparent;
|
|
175
|
+
}
|
|
176
|
+
a:active { opacity: 0.7; }
|
|
177
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
178
|
+
.btn { transition: transform var(--t-press) var(--ease-out); }
|
|
179
|
+
.btn:not(:disabled, [aria-disabled="true"]):active, .choice-pick:active, .choice-card:active { transform: scale(0.97); }
|
|
180
|
+
}
|
|
181
|
+
@media (pointer: coarse) {
|
|
182
|
+
/* A finger needs 44px (WCAG 2.5.5) whatever height the theme draws, through the density's zoom (--row-h does the same). */
|
|
183
|
+
/* A form's listbox may be the shell's (.form-field .dd-button) rather than @mistriwork/ui's Select: the same field, so the
|
|
184
|
+
same height as the box beside it in a row. */
|
|
185
|
+
.field-control, .dd-button.field-select, .form-field .dd-button, .choice-pick, .choice-card, .date-field .date-field-input, .btn:not(.btn-compact) {
|
|
186
|
+
min-height: max(var(--control-h), calc(44px / var(--density-zoom, 1)));
|
|
187
|
+
}
|
|
188
|
+
/* iOS Safari zooms the page when a field under 16px is focused, and does not zoom back. The density zoom only
|
|
189
|
+
enlarges (1, 1.12, 1.24), so a flat 16px is at least 16px drawn. Not verified on WebKit: it is not installed
|
|
190
|
+
where this was written, so a real iPhone is the test of this rule. */
|
|
191
|
+
input, select, textarea, .field-control, .dd-button.field-select, .form-field .dd-button, .date-field .date-field-input { font-size: max(16px, var(--text-base)); }
|
|
192
|
+
}
|
package/dist/hero.css
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* @mistriwork/ui's hero blocks (Badge, PhotoTiles, IconRows), apart from primitives.css and forms.css: a page that draws
|
|
2
|
+
none of them does not pay for their rules. style.css takes this file with the rest; a shell that layers its own
|
|
3
|
+
sheets (the kit's landing, mistri's landingHero.css) imports it into the layer it wants. */
|
|
4
|
+
|
|
5
|
+
/* The badge: the author's few words in a chip over the heading (the look's corner), in the accent's
|
|
6
|
+
quiet tint so it reads as a note on the page, never as a button. */
|
|
7
|
+
.front-badge {
|
|
8
|
+
justify-self: start; display: inline-flex; align-items: center; gap: var(--space-2h); margin: 0; padding: var(--space-2) var(--space-4);
|
|
9
|
+
border-radius: var(--radius); background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--ink);
|
|
10
|
+
font: 500 var(--text-sm) var(--sans);
|
|
11
|
+
}
|
|
12
|
+
.front-badge svg { color: var(--accent-ink); flex: none; }
|
|
13
|
+
/* Icon rows: each an icon in the accent and a line in the muted ink, stacked with air between. */
|
|
14
|
+
.front-points { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3); color: var(--ink-muted); font: 400 var(--text-base) var(--sans); }
|
|
15
|
+
.front-points li { display: flex; align-items: flex-start; gap: var(--space-3); }
|
|
16
|
+
.front-points svg { color: var(--accent-ink); flex: none; margin-top: var(--space-1); }
|
|
17
|
+
/* Tiles: the one photo's 4:3 box cut into a mosaic, so it is sized before any photo arrives. Two side by side; three
|
|
18
|
+
with the first tall on the left; four in a square grid. */
|
|
19
|
+
.front-hero-tiles { display: grid; gap: var(--space-2); background: none; border-radius: 0; grid-template-columns: 1fr 1fr; }
|
|
20
|
+
.front-hero-tiles img { border-radius: var(--radius); background: var(--raised); min-height: 0; }
|
|
21
|
+
.front-hero-tiles-3 { grid-template-rows: 1fr 1fr; }
|
|
22
|
+
.front-hero-tiles-3 img:first-child { grid-row: 1 / span 2; }
|
|
23
|
+
.front-hero-tiles-4 { grid-template-rows: 1fr 1fr; }
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mistriwork/ui: the primitives every dialog, menu, listbox, icon and form here is drawn with, over Base UI and dressed in
|
|
3
|
+
* the house classes (`modal`, `dd-*`, `btn`, `form-field`, `date-field`, `tw-icon`), which its own stylesheet styles
|
|
4
|
+
* (`@mistriwork/ui/style.css`). A primitive reads no walk, no address, no transport and no copy catalog: its words, its
|
|
5
|
+
* icons, its answers and its hooks are props (scripts/check-boundary.mjs holds the `ui` layer to importing only
|
|
6
|
+
* itself).
|
|
7
|
+
*/
|
|
8
|
+
export { Button, buttonClass, type ButtonIntent, type ButtonSize } from './Button.js';
|
|
9
|
+
export { Field, type FieldControlProps } from './Field.js';
|
|
10
|
+
export { Modal, type ModalHandle } from './Modal.js';
|
|
11
|
+
export { DropMenu, DropGroup, DropItem, DropSeparator } from './DropMenu.js';
|
|
12
|
+
export { Dropdown, MultiDropdown, type DropdownAction, type DropdownItem, type DropdownProps, type MultiDropdownProps, } from './Dropdown.js';
|
|
13
|
+
export { Typeahead } from './Typeahead.js';
|
|
14
|
+
export { Icon, loadIconsWith, registerIcons, weighIconsWith, type IconGlyph, type IconProps, type IconWeight, type IconWeighting, } from './Icon.js';
|
|
15
|
+
export { DateField, type DateFieldProps, type DateFieldWords } from './DateField.js';
|
|
16
|
+
export type { FieldFrame } from './fieldFrame.js';
|
|
17
|
+
export { TextField, MaskedField, MoneyField, ValidMark, type MaskedFieldProps, type TextFieldProps } from './TextField.js';
|
|
18
|
+
export { SensitiveField, type SensitiveFieldProps } from './SensitiveField.js';
|
|
19
|
+
export { Select, MultiSelect } from './Select.js';
|
|
20
|
+
export { CheckCards, ChoiceCards, Checkbox, Consent, YesNo, type CheckCard, type CheckCardsProps, type CheckboxProps, type ChoiceCardsProps, type ChoiceOption, type YesNoProps, } from './Choices.js';
|
|
21
|
+
export { Slider, type SliderProps } from './Slider.js';
|
|
22
|
+
export { ActionBar, Columns, HelpTip, ProgressBar, ReviewList, StepHeader, type ActionBarProps, type HelpTipProps, type ProgressBarProps, type StepHeaderProps, } from './FormParts.js';
|
|
23
|
+
export { Badge, IconRows, PhotoTiles, type BadgeProps, type IconRowsProps, type PhotoTile, type PhotoTilesProps } from './HeroBlocks.js';
|
|
24
|
+
export { digits, grouped, isoDay, maskNamed, masks, type Mask, type MaskName } from './masks.js';
|
|
25
|
+
export { ComponentStyleScope } from './internal/ComponentStyleScope.js';
|
|
26
|
+
export { layoutViewport, zoomAnchor } from './internal/anchor.js';
|
|
27
|
+
export { popupContainer, drawPopupsIn } from './internal/popups.js';
|
|
28
|
+
export { measureFrom, refreshViewportScale, viewportBox, viewportScale, viewportSize } from './internal/ruler.js';
|
|
29
|
+
export { Switch, type SwitchProps } from './Switch.js';
|
|
30
|
+
export { Tabs, type TabItem, type TabsProps } from './Tabs.js';
|
|
31
|
+
export { Drawer, type DrawerProps, type DrawerSide } from './Drawer.js';
|
|
32
|
+
export { Popover, type PopoverProps } from './Popover.js';
|
|
33
|
+
export { Avatar, initialsOf, type AvatarProps, type AvatarSize } from './Avatar.js';
|
|
34
|
+
export { Pagination, paginationItems, type PaginationProps } from './Pagination.js';
|
|
35
|
+
export { Carousel, type CarouselProps, type CarouselSlide } from './Carousel.js';
|
|
36
|
+
export { Resizable, type ResizablePanel, type ResizableProps } from './Resizable.js';
|
|
37
|
+
export { ScrollArea, type ScrollAreaProps } from './ScrollArea.js';
|
|
38
|
+
export { Tooltip, type TooltipProps } from './Tooltip.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mistriwork/ui: the primitives every dialog, menu, listbox, icon and form here is drawn with, over Base UI and dressed in
|
|
3
|
+
* the house classes (`modal`, `dd-*`, `btn`, `form-field`, `date-field`, `tw-icon`), which its own stylesheet styles
|
|
4
|
+
* (`@mistriwork/ui/style.css`). A primitive reads no walk, no address, no transport and no copy catalog: its words, its
|
|
5
|
+
* icons, its answers and its hooks are props (scripts/check-boundary.mjs holds the `ui` layer to importing only
|
|
6
|
+
* itself).
|
|
7
|
+
*/
|
|
8
|
+
export { Button, buttonClass } from './Button.js';
|
|
9
|
+
export { Field } from './Field.js';
|
|
10
|
+
export { Modal } from './Modal.js';
|
|
11
|
+
export { DropMenu, DropGroup, DropItem, DropSeparator } from './DropMenu.js';
|
|
12
|
+
export { Dropdown, MultiDropdown, } from './Dropdown.js';
|
|
13
|
+
export { Typeahead } from './Typeahead.js';
|
|
14
|
+
export { Icon, loadIconsWith, registerIcons, weighIconsWith, } from './Icon.js';
|
|
15
|
+
export { DateField } from './DateField.js';
|
|
16
|
+
export { TextField, MaskedField, MoneyField, ValidMark } from './TextField.js';
|
|
17
|
+
export { SensitiveField } from './SensitiveField.js';
|
|
18
|
+
export { Select, MultiSelect } from './Select.js';
|
|
19
|
+
export { CheckCards, ChoiceCards, Checkbox, Consent, YesNo, } from './Choices.js';
|
|
20
|
+
export { Slider } from './Slider.js';
|
|
21
|
+
export { ActionBar, Columns, HelpTip, ProgressBar, ReviewList, StepHeader, } from './FormParts.js';
|
|
22
|
+
/* The landing hero's blocks. Preview, like the controls: a badge over a heading, two to four photo tiles, icon rows. */
|
|
23
|
+
export { Badge, IconRows, PhotoTiles } from './HeroBlocks.js';
|
|
24
|
+
export { digits, grouped, isoDay, maskNamed, masks } from './masks.js';
|
|
25
|
+
export { ComponentStyleScope } from './internal/ComponentStyleScope.js';
|
|
26
|
+
export { layoutViewport, zoomAnchor } from './internal/anchor.js';
|
|
27
|
+
export { popupContainer, drawPopupsIn } from './internal/popups.js';
|
|
28
|
+
export { measureFrom, refreshViewportScale, viewportBox, viewportScale, viewportSize } from './internal/ruler.js';
|
|
29
|
+
/* The standalone primitives added for the showcase's missing list: a switch, tabs, a drawer, a popover, an avatar disc and pagination. Preview, like the controls. */
|
|
30
|
+
export { Switch } from './Switch.js';
|
|
31
|
+
export { Tabs } from './Tabs.js';
|
|
32
|
+
export { Drawer } from './Drawer.js';
|
|
33
|
+
export { Popover } from './Popover.js';
|
|
34
|
+
export { Avatar, initialsOf } from './Avatar.js';
|
|
35
|
+
export { Pagination, paginationItems } from './Pagination.js';
|
|
36
|
+
/* The remainder-wave primitives: a carousel, resizable panes, a scroll area and a general tooltip, Preview like the rest. */
|
|
37
|
+
export { Carousel } from './Carousel.js';
|
|
38
|
+
export { Resizable } from './Resizable.js';
|
|
39
|
+
export { ScrollArea } from './ScrollArea.js';
|
|
40
|
+
export { Tooltip } from './Tooltip.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const ComponentStyleScope: import("react").Context<string | undefined>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface VirtualAnchor {
|
|
2
|
+
getBoundingClientRect: () => DOMRect;
|
|
3
|
+
contextElement: Element;
|
|
4
|
+
}
|
|
5
|
+
export declare function zoomAnchor(element: Element): VirtualAnchor;
|
|
6
|
+
/**
|
|
7
|
+
* The viewport, in the same layout pixels the anchor reports, for the
|
|
8
|
+
* positioner's collision boundary — so "is there room below" is answered
|
|
9
|
+
* on the ruler the anchor was measured with. Positioners retain this object
|
|
10
|
+
* across viewport resizes without re-rendering their owning control, so read
|
|
11
|
+
* its dimensions when collision detection runs, not when JSX was rendered.
|
|
12
|
+
*/
|
|
13
|
+
export declare function layoutViewport(): {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
};
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An anchor for Base UI's positioners, measured with the house ruler.
|
|
3
|
+
*
|
|
4
|
+
* The density preference puts a `zoom` on the root, so a box measured with
|
|
5
|
+
* getBoundingClientRect comes back in screen pixels while a `top` set on a
|
|
6
|
+
* positioned element inside the root is scaled on its way back — measured
|
|
7
|
+
* with one ruler, placed with the other, a list landed a room away from
|
|
8
|
+
* its trigger. Floating UI corrects for transforms on an offset parent but
|
|
9
|
+
* not for a zoom on the root, so the anchor it is handed is a virtual
|
|
10
|
+
* element that reports the trigger's box already divided by the zoom
|
|
11
|
+
* (ruler.ts `viewportBox`), and every coordinate it derives is a layout
|
|
12
|
+
* pixel the browser scales back exactly once.
|
|
13
|
+
*/
|
|
14
|
+
import { viewportBox, viewportSize } from './ruler.js';
|
|
15
|
+
export function zoomAnchor(element) {
|
|
16
|
+
return {
|
|
17
|
+
contextElement: element,
|
|
18
|
+
getBoundingClientRect: () => {
|
|
19
|
+
const b = viewportBox(element);
|
|
20
|
+
return {
|
|
21
|
+
x: b.left,
|
|
22
|
+
y: b.top,
|
|
23
|
+
top: b.top,
|
|
24
|
+
left: b.left,
|
|
25
|
+
right: b.right,
|
|
26
|
+
bottom: b.bottom,
|
|
27
|
+
width: b.width,
|
|
28
|
+
height: b.height,
|
|
29
|
+
toJSON: () => b,
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The viewport, in the same layout pixels the anchor reports, for the
|
|
36
|
+
* positioner's collision boundary — so "is there room below" is answered
|
|
37
|
+
* on the ruler the anchor was measured with. Positioners retain this object
|
|
38
|
+
* across viewport resizes without re-rendering their owning control, so read
|
|
39
|
+
* its dimensions when collision detection runs, not when JSX was rendered.
|
|
40
|
+
*/
|
|
41
|
+
export function layoutViewport() {
|
|
42
|
+
return {
|
|
43
|
+
x: 0,
|
|
44
|
+
y: 0,
|
|
45
|
+
get width() {
|
|
46
|
+
return viewportSize().width;
|
|
47
|
+
},
|
|
48
|
+
get height() {
|
|
49
|
+
return viewportSize().height;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|