@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.
- package/LICENSE +21 -0
- package/README.md +50 -2
- 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 +84 -4
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Resizable panels: two or more panes side by side (or stacked), divided
|
|
4
|
+
* by a handle the reader drags or walks with the keyboard. Plain React —
|
|
5
|
+
* Base UI has no resizable, and a flex row with a draggable divider is all
|
|
6
|
+
* one is. The panes are the caller's array; each names its own `minSize`
|
|
7
|
+
* in percent (10 by default) and its starting `defaultSize`, and sizes
|
|
8
|
+
* that do not add to 100 are scaled to it, so a caller who writes 1 and 2
|
|
9
|
+
* gets a third and two thirds. Controlled with `sizes`/`onSizesChange`,
|
|
10
|
+
* uncontrolled with the panels' `defaultSize`s.
|
|
11
|
+
*
|
|
12
|
+
* A handle is a separator, not a button: `role="separator"`,
|
|
13
|
+
* `aria-orientation`, `aria-valuenow`/`min`/`max` in percent, and the two
|
|
14
|
+
* panes it divides named by `aria-controls`. Arrow keys move it 5 points
|
|
15
|
+
* (10 with Shift), Home and End take it to its ends, and a pointer drag
|
|
16
|
+
* moves it by the pointer's share of the container — the same handle for
|
|
17
|
+
* mouse, pen and touch, so no reader needs a mouse to resize. The panes
|
|
18
|
+
* themselves never shrink below their minimum, and a drag that would push
|
|
19
|
+
* a neighbour below its minimum stops at that neighbour's edge.
|
|
20
|
+
*/
|
|
21
|
+
import { Fragment, useId, useRef, useState } from 'react';
|
|
22
|
+
const ENGLISH = {
|
|
23
|
+
resize: (before, after) => `Resize ${before} and ${after}`,
|
|
24
|
+
panel: (number) => `panel ${number}`,
|
|
25
|
+
};
|
|
26
|
+
function initialSizes(panels) {
|
|
27
|
+
const given = panels.map((panel) => panel.defaultSize ?? 0);
|
|
28
|
+
const total = given.reduce((sum, size) => sum + size, 0);
|
|
29
|
+
if (total > 0)
|
|
30
|
+
return given.map((size) => (size / total) * 100);
|
|
31
|
+
return panels.map(() => 100 / panels.length);
|
|
32
|
+
}
|
|
33
|
+
function clampSizes(panels, sizes) {
|
|
34
|
+
const mins = panels.map((panel) => panel.minSize ?? 10);
|
|
35
|
+
const clamped = sizes.map((size, i) => Math.min(Math.max(size, mins[i]), 100 - mins.reduce((sum, min, j) => (j === i ? sum : sum + min), 0)));
|
|
36
|
+
const total = clamped.reduce((sum, size) => sum + size, 0);
|
|
37
|
+
return total ? clamped.map((size) => (size / total) * 100) : clamped;
|
|
38
|
+
}
|
|
39
|
+
export function Resizable({ panels, direction = 'horizontal', sizes, onSizesChange, ariaLabel, words: given, className }) {
|
|
40
|
+
const words = { ...ENGLISH, ...given };
|
|
41
|
+
const [uncontrolled, setUncontrolled] = useState(() => initialSizes(panels));
|
|
42
|
+
const containerRef = useRef(null);
|
|
43
|
+
const dragRef = useRef(null);
|
|
44
|
+
const baseId = useId();
|
|
45
|
+
if (panels.length < 2)
|
|
46
|
+
return null;
|
|
47
|
+
const current = clampSizes(panels, sizes ?? uncontrolled);
|
|
48
|
+
const mins = panels.map((panel) => panel.minSize ?? 10);
|
|
49
|
+
const setSizes = (next) => {
|
|
50
|
+
const clamped = clampSizes(panels, next);
|
|
51
|
+
if (sizes === undefined)
|
|
52
|
+
setUncontrolled(clamped);
|
|
53
|
+
onSizesChange?.(clamped);
|
|
54
|
+
};
|
|
55
|
+
/** Move handle `i` so pane `i` grows by `delta` points and pane `i + 1` shrinks by the same. */
|
|
56
|
+
const move = (i, delta) => {
|
|
57
|
+
const next = [...current];
|
|
58
|
+
const low = mins[i] - current[i];
|
|
59
|
+
const high = current[i + 1] - mins[i + 1];
|
|
60
|
+
const applied = Math.min(Math.max(delta, low), high);
|
|
61
|
+
next[i] = current[i] + applied;
|
|
62
|
+
next[i + 1] = current[i + 1] - applied;
|
|
63
|
+
setSizes(next);
|
|
64
|
+
};
|
|
65
|
+
const onHandleKeyDown = (i) => (event) => {
|
|
66
|
+
const horizontal = direction === 'horizontal';
|
|
67
|
+
const step = event.shiftKey ? 10 : 5;
|
|
68
|
+
if (event.key === (horizontal ? 'ArrowRight' : 'ArrowDown')) {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
move(i, step);
|
|
71
|
+
}
|
|
72
|
+
else if (event.key === (horizontal ? 'ArrowLeft' : 'ArrowUp')) {
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
move(i, -step);
|
|
75
|
+
}
|
|
76
|
+
else if (event.key === 'Home') {
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
move(i, -(current[i] - mins[i]));
|
|
79
|
+
}
|
|
80
|
+
else if (event.key === 'End') {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
move(i, current[i + 1] - mins[i + 1]);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const onPointerDown = (i) => (event) => {
|
|
86
|
+
const container = containerRef.current;
|
|
87
|
+
if (!container)
|
|
88
|
+
return;
|
|
89
|
+
const rect = container.getBoundingClientRect();
|
|
90
|
+
const horizontal = direction === 'horizontal';
|
|
91
|
+
const pos = horizontal ? event.clientX - rect.left : event.clientY - rect.top;
|
|
92
|
+
const span = horizontal ? rect.width : rect.height;
|
|
93
|
+
dragRef.current = { handle: i, start: span ? (pos / span) * 100 : 0, sizes: current };
|
|
94
|
+
event.target.setPointerCapture?.(event.pointerId);
|
|
95
|
+
};
|
|
96
|
+
const onPointerMove = (event) => {
|
|
97
|
+
const drag = dragRef.current;
|
|
98
|
+
const container = containerRef.current;
|
|
99
|
+
if (!drag || !container)
|
|
100
|
+
return;
|
|
101
|
+
const rect = container.getBoundingClientRect();
|
|
102
|
+
const horizontal = direction === 'horizontal';
|
|
103
|
+
const pos = horizontal ? event.clientX - rect.left : event.clientY - rect.top;
|
|
104
|
+
const span = horizontal ? rect.width : rect.height;
|
|
105
|
+
if (!span)
|
|
106
|
+
return;
|
|
107
|
+
const delta = (pos / span) * 100 - drag.start;
|
|
108
|
+
const next = [...drag.sizes];
|
|
109
|
+
const low = mins[drag.handle] - drag.sizes[drag.handle];
|
|
110
|
+
const high = drag.sizes[drag.handle + 1] - mins[drag.handle + 1];
|
|
111
|
+
const applied = Math.min(Math.max(delta, low), high);
|
|
112
|
+
next[drag.handle] = drag.sizes[drag.handle] + applied;
|
|
113
|
+
next[drag.handle + 1] = drag.sizes[drag.handle + 1] - applied;
|
|
114
|
+
setSizes(next);
|
|
115
|
+
};
|
|
116
|
+
const onPointerUp = () => {
|
|
117
|
+
dragRef.current = null;
|
|
118
|
+
};
|
|
119
|
+
return (_jsx("div", { ref: containerRef, className: ['ui-resizable', direction === 'vertical' ? 'ui-resizable-vertical' : 'ui-resizable-horizontal', className]
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.join(' '), role: "group", "aria-label": ariaLabel, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerUp, children: panels.map((panel, i) => (_jsxs(Fragment, { children: [_jsx("div", { id: `${baseId}-panel-${i}`, className: "ui-resizable-panel", style: { flex: `${current[i]} 1 0` }, role: "group", "aria-label": panel.label, "data-scrolls": "", children: panel.children }), i < panels.length - 1 ? (_jsx("div", { className: "ui-resizable-handle", role: "separator", "aria-orientation": direction === 'horizontal' ? 'vertical' : 'horizontal', "aria-valuenow": Math.round(current[i]), "aria-valuemin": mins[i], "aria-valuemax": 100 - mins[i + 1], "aria-controls": `${baseId}-panel-${i} ${baseId}-panel-${i + 1}`, "aria-label": words.resize(panels[i]?.label ?? words.panel(i + 1), panels[i + 1]?.label ?? words.panel(i + 2)), tabIndex: 0, onKeyDown: onHandleKeyDown(i), onPointerDown: onPointerDown(i) })) : null] }, i))) }));
|
|
122
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A scroll area: a region that scrolls inside itself, with its own
|
|
3
|
+
* thumb, instead of growing the page around it. Over Base UI's ScrollArea,
|
|
4
|
+
* which gives it the viewport, the thumb that tracks it and the corner
|
|
5
|
+
* where two thumbs meet. Where its content overflows, the viewport is a
|
|
6
|
+
* tab stop with the region's name, so a keyboard reader can reach it and
|
|
7
|
+
* scroll with the arrows without a mouse; a region whose content never
|
|
8
|
+
* overflows draws no thumb and is no tab stop to nowhere. `orientation`
|
|
9
|
+
* says which thumbs exist (vertical by default); `maxHeight` caps the
|
|
10
|
+
* region where the caller wants one, as a CSS length, and is otherwise
|
|
11
|
+
* the caller's own height.
|
|
12
|
+
* Where the scrolled content is a list, the list's own semantics stay on
|
|
13
|
+
* the content — this region adds scrolling, never a second list. The
|
|
14
|
+
* viewport is a tab stop only while its content overflows: a region
|
|
15
|
+
* whose content fits draws no thumb and must be no tab stop to nowhere.
|
|
16
|
+
* Overflow is measured, not assumed — on mount, on content change, and
|
|
17
|
+
* whenever the box resizes.
|
|
18
|
+
*/
|
|
19
|
+
import { type ReactNode } from 'react';
|
|
20
|
+
export interface ScrollAreaProps {
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
/** The scrollable region's accessible name ("Recent activity"). */
|
|
23
|
+
ariaLabel?: string;
|
|
24
|
+
orientation?: 'vertical' | 'horizontal' | 'both';
|
|
25
|
+
/** A cap on the region's height, as a CSS length (for example "320px"). */
|
|
26
|
+
maxHeight?: string;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function ScrollArea({ children, ariaLabel, orientation, maxHeight, className }: ScrollAreaProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A scroll area: a region that scrolls inside itself, with its own
|
|
4
|
+
* thumb, instead of growing the page around it. Over Base UI's ScrollArea,
|
|
5
|
+
* which gives it the viewport, the thumb that tracks it and the corner
|
|
6
|
+
* where two thumbs meet. Where its content overflows, the viewport is a
|
|
7
|
+
* tab stop with the region's name, so a keyboard reader can reach it and
|
|
8
|
+
* scroll with the arrows without a mouse; a region whose content never
|
|
9
|
+
* overflows draws no thumb and is no tab stop to nowhere. `orientation`
|
|
10
|
+
* says which thumbs exist (vertical by default); `maxHeight` caps the
|
|
11
|
+
* region where the caller wants one, as a CSS length, and is otherwise
|
|
12
|
+
* the caller's own height.
|
|
13
|
+
* Where the scrolled content is a list, the list's own semantics stay on
|
|
14
|
+
* the content — this region adds scrolling, never a second list. The
|
|
15
|
+
* viewport is a tab stop only while its content overflows: a region
|
|
16
|
+
* whose content fits draws no thumb and must be no tab stop to nowhere.
|
|
17
|
+
* Overflow is measured, not assumed — on mount, on content change, and
|
|
18
|
+
* whenever the box resizes.
|
|
19
|
+
*/
|
|
20
|
+
import { useEffect, useRef, useState } from 'react';
|
|
21
|
+
import { ScrollArea as BaseScrollArea } from '@base-ui/react/scroll-area';
|
|
22
|
+
export function ScrollArea({ children, ariaLabel, orientation = 'vertical', maxHeight, className }) {
|
|
23
|
+
const vertical = orientation === 'vertical' || orientation === 'both';
|
|
24
|
+
const horizontal = orientation === 'horizontal' || orientation === 'both';
|
|
25
|
+
const viewportRef = useRef(null);
|
|
26
|
+
const contentRef = useRef(null);
|
|
27
|
+
const [overflows, setOverflows] = useState(false);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const viewport = viewportRef.current;
|
|
30
|
+
if (!viewport)
|
|
31
|
+
return;
|
|
32
|
+
const measure = () => {
|
|
33
|
+
const at = viewportRef.current;
|
|
34
|
+
if (!at)
|
|
35
|
+
return;
|
|
36
|
+
const tall = vertical && at.scrollHeight - at.clientHeight > 1;
|
|
37
|
+
const wide = horizontal && at.scrollWidth - at.clientWidth > 1;
|
|
38
|
+
setOverflows(tall || wide);
|
|
39
|
+
};
|
|
40
|
+
measure();
|
|
41
|
+
/* Content growth changes scroll size without resizing the viewport,
|
|
42
|
+
so the content is watched too; where ResizeObserver is absent
|
|
43
|
+
(a test renderer) the mount-time measure above is all there is. */
|
|
44
|
+
let observer;
|
|
45
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
46
|
+
observer = new ResizeObserver(measure);
|
|
47
|
+
observer.observe(viewport);
|
|
48
|
+
const content = contentRef.current;
|
|
49
|
+
if (content)
|
|
50
|
+
observer.observe(content);
|
|
51
|
+
}
|
|
52
|
+
window.addEventListener('resize', measure);
|
|
53
|
+
return () => {
|
|
54
|
+
observer?.disconnect();
|
|
55
|
+
window.removeEventListener('resize', measure);
|
|
56
|
+
};
|
|
57
|
+
}, [vertical, horizontal, children]);
|
|
58
|
+
return (_jsxs(BaseScrollArea.Root, { className: ['ui-scroll-area', className].filter(Boolean).join(' '), children: [_jsx(BaseScrollArea.Viewport, { ref: viewportRef, className: "ui-scroll-area-viewport", tabIndex: overflows ? 0 : -1,
|
|
59
|
+
/* Base UI renders its viewport as presentation; a named scroll
|
|
60
|
+
region needs the region role, or the label is dropped with it. */
|
|
61
|
+
role: "region", "aria-label": ariaLabel, "data-scrolls": "", style: maxHeight ? { maxHeight } : undefined, children: _jsx(BaseScrollArea.Content, { ref: contentRef, className: "ui-scroll-area-content", children: children }) }), vertical ? (_jsx(BaseScrollArea.Scrollbar, { className: "ui-scroll-area-scrollbar ui-scroll-area-scrollbar-vertical", orientation: "vertical", children: _jsx(BaseScrollArea.Thumb, { className: "ui-scroll-area-thumb" }) })) : null, horizontal ? (_jsx(BaseScrollArea.Scrollbar, { className: "ui-scroll-area-scrollbar ui-scroll-area-scrollbar-horizontal", orientation: "horizontal", children: _jsx(BaseScrollArea.Thumb, { className: "ui-scroll-area-thumb" }) })) : null, vertical && horizontal ? _jsx(BaseScrollArea.Corner, { className: "ui-scroll-area-corner" }) : null] }));
|
|
62
|
+
}
|
package/dist/Select.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The listbox of a form: one choice, or several, in a field. It is the Dropdown (Base UI's Select) dressed as a field
|
|
3
|
+
* box, so the keyboard is the one it has: ArrowDown opens it, the arrows move, Enter and Space choose, Escape closes and
|
|
4
|
+
* returns focus to the box. Props only. Preview: @mistriwork/ui is Preview at 1.0.
|
|
5
|
+
*/
|
|
6
|
+
import { type ReactNode, type Ref } from 'react';
|
|
7
|
+
import { type DropdownItem } from './Dropdown.js';
|
|
8
|
+
import { type FieldFrame } from './fieldFrame.js';
|
|
9
|
+
interface ListFrame extends FieldFrame {
|
|
10
|
+
options: DropdownItem[];
|
|
11
|
+
/** What the box says while nothing is chosen ("Choose"). */
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
buttonRef?: Ref<HTMLButtonElement>;
|
|
15
|
+
}
|
|
16
|
+
/** One choice from a list, under a field box. */
|
|
17
|
+
export declare function Select(props: ListFrame & {
|
|
18
|
+
value: string;
|
|
19
|
+
onChange: (value: string) => void;
|
|
20
|
+
}): ReactNode;
|
|
21
|
+
/** Several choices, each with a checkbox; the closed box says them as "Acura, Alfa Romeo". */
|
|
22
|
+
export declare function MultiSelect(props: ListFrame & {
|
|
23
|
+
value: string[];
|
|
24
|
+
onChange: (value: string[]) => void;
|
|
25
|
+
}): ReactNode;
|
|
26
|
+
export {};
|
package/dist/Select.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The listbox of a form: one choice, or several, in a field. It is the Dropdown (Base UI's Select) dressed as a field
|
|
4
|
+
* box, so the keyboard is the one it has: ArrowDown opens it, the arrows move, Enter and Space choose, Escape closes and
|
|
5
|
+
* returns focus to the box. Props only. Preview: @mistriwork/ui is Preview at 1.0.
|
|
6
|
+
*/
|
|
7
|
+
import { useId } from 'react';
|
|
8
|
+
import { Dropdown, MultiDropdown } from './Dropdown.js';
|
|
9
|
+
import { Field } from './Field.js';
|
|
10
|
+
import { frameOf } from './fieldFrame.js';
|
|
11
|
+
/** One choice from a list, under a field box. */
|
|
12
|
+
export function Select(props) {
|
|
13
|
+
const valueId = useId();
|
|
14
|
+
return (_jsx(Field, { ...frameOf(props), group: true, children: (control) => (_jsx(Dropdown, { items: props.options, value: props.value, onChange: props.onChange, id: control.id, ariaLabelledBy: `${control['aria-labelledby']} ${valueId}`, ariaDescribedBy: control['aria-describedby'], ariaInvalid: control['aria-invalid'], valueId: valueId, buttonClassName: "field-control field-select", listClassName: "field-list", buttonRef: props.buttonRef, placeholder: props.placeholder, disabled: props.disabled })) }));
|
|
15
|
+
}
|
|
16
|
+
/** Several choices, each with a checkbox; the closed box says them as "Acura, Alfa Romeo". */
|
|
17
|
+
export function MultiSelect(props) {
|
|
18
|
+
const valueId = useId();
|
|
19
|
+
return (_jsx(Field, { ...frameOf(props), group: true, children: (control) => (_jsx(MultiDropdown, { items: props.options, value: props.value, onChange: props.onChange, id: control.id, ariaLabelledBy: `${control['aria-labelledby']} ${valueId}`, ariaDescribedBy: control['aria-describedby'], ariaInvalid: control['aria-invalid'], valueId: valueId, buttonClassName: "field-control field-select", listClassName: "field-list", buttonRef: props.buttonRef, placeholder: props.placeholder, disabled: props.disabled })) }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A number kept out of sight (an SSN, say): dots until Show is pressed, typed at the end. Show is a button of its own,
|
|
3
|
+
* after the box: two controls do not share a box (docs/design.md). The intake style draws it over the box's inline end,
|
|
4
|
+
* in room the box keeps for it, so it reads as part of the field and is still its own Tab stop. The value lives in the caller's state alone:
|
|
5
|
+
* this never logs it, stores it or writes it anywhere, the box turns off autofill and spellcheck, and while hidden the
|
|
6
|
+
* dots are all the page holds. Preview: @mistriwork/ui is Preview at 1.0.
|
|
7
|
+
*/
|
|
8
|
+
import { type ReactNode } from 'react';
|
|
9
|
+
import { type FieldFrame } from './fieldFrame.js';
|
|
10
|
+
import { type Mask } from './masks.js';
|
|
11
|
+
export interface SensitiveFieldProps extends FieldFrame {
|
|
12
|
+
/** The raw value. */
|
|
13
|
+
value: string;
|
|
14
|
+
onChange: (raw: string) => void;
|
|
15
|
+
mask: Mask;
|
|
16
|
+
valid?: boolean;
|
|
17
|
+
validIcon?: ReactNode;
|
|
18
|
+
onBlur?: () => void;
|
|
19
|
+
/** The line under the box. */
|
|
20
|
+
note?: ReactNode;
|
|
21
|
+
noteIcon?: ReactNode;
|
|
22
|
+
/** The words of the button beside the box. */
|
|
23
|
+
showLabel: string;
|
|
24
|
+
hideLabel: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function SensitiveField(props: SensitiveFieldProps): ReactNode;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A number kept out of sight (an SSN, say): dots until Show is pressed, typed at the end. Show is a button of its own,
|
|
4
|
+
* after the box: two controls do not share a box (docs/design.md). The intake style draws it over the box's inline end,
|
|
5
|
+
* in room the box keeps for it, so it reads as part of the field and is still its own Tab stop. The value lives in the caller's state alone:
|
|
6
|
+
* this never logs it, stores it or writes it anywhere, the box turns off autofill and spellcheck, and while hidden the
|
|
7
|
+
* dots are all the page holds. Preview: @mistriwork/ui is Preview at 1.0.
|
|
8
|
+
*/
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
import { Button } from './Button.js';
|
|
11
|
+
import { Field } from './Field.js';
|
|
12
|
+
import { frameOf } from './fieldFrame.js';
|
|
13
|
+
import { digits } from './masks.js';
|
|
14
|
+
import { ValidMark } from './TextField.js';
|
|
15
|
+
export function SensitiveField(props) {
|
|
16
|
+
const [shown, setShown] = useState(false);
|
|
17
|
+
const formatted = props.mask.format(props.value);
|
|
18
|
+
const display = shown ? formatted : '•'.repeat(formatted.length);
|
|
19
|
+
const change = (typed) => {
|
|
20
|
+
if (shown)
|
|
21
|
+
return props.onChange(props.mask.parse(typed));
|
|
22
|
+
// Hidden: what is left of the dots keeps that many characters of the number; a digit typed is added at the end.
|
|
23
|
+
const kept = formatted.slice(0, typed.replace(/[^•]/g, '').length);
|
|
24
|
+
props.onChange(props.mask.parse(digits(kept) + digits(typed)));
|
|
25
|
+
};
|
|
26
|
+
return (_jsx(Field, { ...frameOf(props), children: (control) => (_jsxs(_Fragment, { children: [_jsxs("div", { className: "field-with-action", children: [_jsxs("div", { className: `field-box${props.valid === undefined ? '' : ' has-mark'}`, children: [_jsx("input", { ...control, className: `field-control${shown ? '' : ' field-hidden'}`, value: display, inputMode: props.mask.inputMode ?? 'numeric', autoComplete: "off", autoCorrect: "off", autoCapitalize: "off", spellCheck: false, "data-1p-ignore": true, "data-lpignore": "true", onChange: (e) => change(e.target.value), onBlur: props.onBlur }), props.valid ? _jsx(ValidMark, { icon: props.validIcon }) : null] }), _jsx(Button, { intent: "quiet", onClick: () => setShown(!shown), children: shown ? props.hideLabel : props.showLabel })] }), props.note ? (_jsxs("p", { className: "field-note", children: [props.noteIcon, props.note] })) : null] })) }));
|
|
27
|
+
}
|
package/dist/Slider.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An amount set by a slider and read out in a box above it, with the range's ends labelled. The slider is the native
|
|
3
|
+
* range input, so the arrows, Home, End and Page keys are the browser's. Props only. Preview: @mistriwork/ui is Preview at 1.0.
|
|
4
|
+
*/
|
|
5
|
+
import type { ReactNode } from 'react';
|
|
6
|
+
import { type FieldFrame } from './fieldFrame.js';
|
|
7
|
+
export interface SliderProps extends FieldFrame {
|
|
8
|
+
value: number;
|
|
9
|
+
onChange: (value: number) => void;
|
|
10
|
+
min: number;
|
|
11
|
+
max: number;
|
|
12
|
+
step?: number;
|
|
13
|
+
/** How a number is said, in the box, at the ends and to assistive technology. Whole numbers with separators by default. */
|
|
14
|
+
format?: (n: number) => string;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function Slider(props: SliderProps): ReactNode;
|
package/dist/Slider.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Field } from './Field.js';
|
|
3
|
+
import { frameOf } from './fieldFrame.js';
|
|
4
|
+
import { grouped } from './masks.js';
|
|
5
|
+
export function Slider(props) {
|
|
6
|
+
const say = props.format ?? grouped;
|
|
7
|
+
const span = props.max - props.min;
|
|
8
|
+
const share = span > 0 ? ((props.value - props.min) / span) * 100 : 0;
|
|
9
|
+
return (_jsx(Field, { ...frameOf(props), children: (control) => (_jsxs(_Fragment, { children: [_jsx("div", { className: "field-control field-readout", "aria-hidden": "true", children: say(props.value) }), _jsx("input", { ...control, type: "range", className: "field-range", min: props.min, max: props.max, step: props.step, value: props.value, disabled: props.disabled, "aria-valuetext": say(props.value), style: { '--range-share': `${share}%` }, onChange: (e) => props.onChange(Number(e.target.value)) }), _jsxs("div", { className: "field-range-ends", "aria-hidden": "true", children: [_jsx("span", { children: say(props.min) }), _jsx("span", { children: say(props.max) })] })] })) }));
|
|
10
|
+
}
|
package/dist/Switch.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A switch: one on/off choice, said as a switch, not a checkbox. Over Base
|
|
3
|
+
* UI's Switch, which gives it the role, the keyboard (Space toggles, as a
|
|
4
|
+
* checkbox does) and the checked state; the label beside it is a click
|
|
5
|
+
* target too, and a description under it never is. Controlled with
|
|
6
|
+
* `checked`/`onCheckedChange`, uncontrolled with `defaultChecked`.
|
|
7
|
+
* Busy is not disabled here either (Button.tsx): a disabled switch drops
|
|
8
|
+
* the focus it holds, so a switch that is working says `aria-disabled`
|
|
9
|
+
* and refuses the press itself.
|
|
10
|
+
*/
|
|
11
|
+
import { type ReactNode, type Ref } from 'react';
|
|
12
|
+
export interface SwitchProps {
|
|
13
|
+
/** The label beside the switch; its click toggles, as the switch's own does. */
|
|
14
|
+
label?: ReactNode;
|
|
15
|
+
/** A line under the label, in the muted ink, never a click target. */
|
|
16
|
+
description?: ReactNode;
|
|
17
|
+
checked?: boolean;
|
|
18
|
+
defaultChecked?: boolean;
|
|
19
|
+
/** Base UI reports the reason second; the caller's claim is the state, so the reason is untyped here. */
|
|
20
|
+
onCheckedChange?: (checked: boolean, event: unknown) => void;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
/** Working: pressed once already. Says so and refuses another press, keeping the focus. */
|
|
23
|
+
busy?: boolean;
|
|
24
|
+
id?: string;
|
|
25
|
+
className?: string;
|
|
26
|
+
ref?: Ref<HTMLButtonElement>;
|
|
27
|
+
}
|
|
28
|
+
export declare function Switch({ label, description, checked, defaultChecked, onCheckedChange, disabled, busy, id, className, ref, }: SwitchProps): import("react").JSX.Element;
|
package/dist/Switch.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A switch: one on/off choice, said as a switch, not a checkbox. Over Base
|
|
4
|
+
* UI's Switch, which gives it the role, the keyboard (Space toggles, as a
|
|
5
|
+
* checkbox does) and the checked state; the label beside it is a click
|
|
6
|
+
* target too, and a description under it never is. Controlled with
|
|
7
|
+
* `checked`/`onCheckedChange`, uncontrolled with `defaultChecked`.
|
|
8
|
+
* Busy is not disabled here either (Button.tsx): a disabled switch drops
|
|
9
|
+
* the focus it holds, so a switch that is working says `aria-disabled`
|
|
10
|
+
* and refuses the press itself.
|
|
11
|
+
*/
|
|
12
|
+
import { useId } from 'react';
|
|
13
|
+
import { Switch as BaseSwitch } from '@base-ui/react/switch';
|
|
14
|
+
export function Switch({ label, description, checked, defaultChecked, onCheckedChange, disabled = false, busy = false, id, className, ref, }) {
|
|
15
|
+
/* The label names the switch by its id; a caller who gives none gets
|
|
16
|
+
a generated one, never Base UI's own, which the label cannot know. */
|
|
17
|
+
const generatedId = useId();
|
|
18
|
+
const switchId = id ?? generatedId;
|
|
19
|
+
return (_jsxs("span", { className: ['ui-switch-row', className].filter(Boolean).join(' '), children: [_jsx(BaseSwitch.Root, { id: switchId, ref: ref, className: "ui-switch", checked: checked, defaultChecked: defaultChecked, disabled: disabled, "aria-disabled": busy || undefined, onCheckedChange: (next, event) => {
|
|
20
|
+
if (busy)
|
|
21
|
+
return;
|
|
22
|
+
onCheckedChange?.(next, event);
|
|
23
|
+
}, children: _jsx(BaseSwitch.Thumb, { className: "ui-switch-thumb" }) }), label !== undefined || description !== undefined ? (_jsxs("span", { className: "ui-switch-copy", children: [label !== undefined ? (_jsx("label", { htmlFor: switchId, className: "ui-switch-label", children: label })) : null, description !== undefined ? _jsx("span", { className: "ui-switch-description", children: description }) : null] })) : null] }));
|
|
24
|
+
}
|
package/dist/Tabs.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tabs: one panel at a time, chosen from a row. Over Base UI's Tabs, which
|
|
3
|
+
* gives it the tablist, the arrow-key walk between tabs and the panel that
|
|
4
|
+
* belongs to the chosen one; a disabled tab is skipped by that walk, never
|
|
5
|
+
* a dead end. Controlled with `value`/`onValueChange`, uncontrolled with
|
|
6
|
+
* `defaultValue`. Panels are the tab's own children, not a second render
|
|
7
|
+
* the caller keeps in step.
|
|
8
|
+
*/
|
|
9
|
+
import type { ReactNode } from 'react';
|
|
10
|
+
export interface TabItem {
|
|
11
|
+
value: string;
|
|
12
|
+
label: ReactNode;
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface TabsProps {
|
|
17
|
+
tabs: readonly TabItem[];
|
|
18
|
+
value?: string;
|
|
19
|
+
defaultValue?: string;
|
|
20
|
+
onValueChange?: (value: string) => void;
|
|
21
|
+
className?: string;
|
|
22
|
+
/** The tablist's accessible name, where the tabs need one ("Settings sections"). */
|
|
23
|
+
ariaLabel?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function Tabs({ tabs, value, defaultValue, onValueChange, className, ariaLabel }: TabsProps): import("react").JSX.Element;
|
package/dist/Tabs.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Tabs as BaseTabs } from '@base-ui/react/tabs';
|
|
3
|
+
export function Tabs({ tabs, value, defaultValue, onValueChange, className, ariaLabel }) {
|
|
4
|
+
return (_jsxs(BaseTabs.Root, { className: ['ui-tabs', className].filter(Boolean).join(' '), value: value, defaultValue: defaultValue ?? tabs.find((tab) => !tab.disabled)?.value, onValueChange: onValueChange, children: [_jsx(BaseTabs.List, { className: "ui-tabs-list", "aria-label": ariaLabel, children: tabs.map((tab) => (_jsx(BaseTabs.Tab, { className: "ui-tab", value: tab.value, disabled: tab.disabled, children: tab.label }, tab.value))) }), tabs.map((tab) => (_jsx(BaseTabs.Panel, { className: "ui-tab-panel", value: tab.value, children: tab.children }, tab.value)))] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text boxes of a form: a plain box, a box that draws its value through a mask, and whole money. A Field's label,
|
|
3
|
+
* hint and problem are wired into the box; a valid mark stands at its inline end in space that is always reserved, so
|
|
4
|
+
* it arrives without moving the text. Props only. Preview: @mistriwork/ui is Preview at 1.0.
|
|
5
|
+
*/
|
|
6
|
+
import type { ReactNode, Ref } from 'react';
|
|
7
|
+
import { type FieldFrame } from './fieldFrame.js';
|
|
8
|
+
import { type Mask } from './masks.js';
|
|
9
|
+
/** The mark at a box's inline end: drawn in CSS unless a picture is lent. Decorative: validity is `aria-invalid`'s to say. */
|
|
10
|
+
export declare function ValidMark({ icon }: {
|
|
11
|
+
icon?: ReactNode;
|
|
12
|
+
}): ReactNode;
|
|
13
|
+
export interface TextFieldProps extends FieldFrame {
|
|
14
|
+
value: string;
|
|
15
|
+
onChange: (value: string) => void;
|
|
16
|
+
onBlur?: () => void;
|
|
17
|
+
/** Present, the box reserves room for the mark; true draws it. Leave it out for a box that never shows one. */
|
|
18
|
+
valid?: boolean;
|
|
19
|
+
/** A picture for the mark in place of the drawn check. */
|
|
20
|
+
validIcon?: ReactNode;
|
|
21
|
+
type?: 'text' | 'email' | 'tel' | 'url';
|
|
22
|
+
inputMode?: 'text' | 'email' | 'tel' | 'numeric' | 'url';
|
|
23
|
+
autoComplete?: string;
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
name?: string;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
readOnly?: boolean;
|
|
28
|
+
inputRef?: Ref<HTMLInputElement>;
|
|
29
|
+
/** Something drawn under the box: a computed line, say. */
|
|
30
|
+
after?: ReactNode;
|
|
31
|
+
}
|
|
32
|
+
export declare function TextField(props: TextFieldProps): ReactNode;
|
|
33
|
+
export interface MaskedFieldProps extends Omit<TextFieldProps, 'value' | 'onChange'> {
|
|
34
|
+
mask: Mask;
|
|
35
|
+
/** The raw value: the digits, usually. */
|
|
36
|
+
value: string;
|
|
37
|
+
onChange: (raw: string) => void;
|
|
38
|
+
}
|
|
39
|
+
/** A box that shows its value through a mask and keeps the raw one: `masks.phone`, `masks.date` (typed MM/DD/YYYY), `masks.ssn`… */
|
|
40
|
+
export declare function MaskedField({ mask, value, onChange, ...rest }: MaskedFieldProps): ReactNode;
|
|
41
|
+
/** Whole money, drawn `$2,500` (or `25,000` with `symbol=""`). The value is the digits. */
|
|
42
|
+
export declare function MoneyField({ symbol, ...rest }: Omit<MaskedFieldProps, 'mask'> & {
|
|
43
|
+
symbol?: string;
|
|
44
|
+
}): ReactNode;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Field } from './Field.js';
|
|
3
|
+
import { frameOf } from './fieldFrame.js';
|
|
4
|
+
import { masks } from './masks.js';
|
|
5
|
+
/** The mark at a box's inline end: drawn in CSS unless a picture is lent. Decorative: validity is `aria-invalid`'s to say. */
|
|
6
|
+
export function ValidMark({ icon }) {
|
|
7
|
+
return (_jsx("span", { className: "field-mark", "aria-hidden": "true", children: icon }));
|
|
8
|
+
}
|
|
9
|
+
export function TextField(props) {
|
|
10
|
+
return (_jsx(Field, { ...frameOf(props), children: (control) => (_jsxs(_Fragment, { children: [_jsxs("div", { className: `field-box${props.valid === undefined ? '' : ' has-mark'}`, children: [_jsx("input", { ...control, ref: props.inputRef, className: "field-control", type: props.type ?? 'text', name: props.name, value: props.value, inputMode: props.inputMode, autoComplete: props.autoComplete ?? 'off', placeholder: props.placeholder, disabled: props.disabled, readOnly: props.readOnly, onChange: (e) => props.onChange(e.target.value), onBlur: props.onBlur }), props.valid ? _jsx(ValidMark, { icon: props.validIcon }) : null] }), props.after] })) }));
|
|
11
|
+
}
|
|
12
|
+
/** A box that shows its value through a mask and keeps the raw one: `masks.phone`, `masks.date` (typed MM/DD/YYYY), `masks.ssn`… */
|
|
13
|
+
export function MaskedField({ mask, value, onChange, ...rest }) {
|
|
14
|
+
return (_jsx(TextField, { ...rest, value: mask.format(value), onChange: (typed) => onChange(mask.parse(typed)), inputMode: rest.inputMode ?? mask.inputMode, placeholder: rest.placeholder ?? mask.placeholder }));
|
|
15
|
+
}
|
|
16
|
+
/** Whole money, drawn `$2,500` (or `25,000` with `symbol=""`). The value is the digits. */
|
|
17
|
+
export function MoneyField({ symbol = '$', ...rest }) {
|
|
18
|
+
return _jsx(MaskedField, { ...rest, mask: masks.money(symbol) });
|
|
19
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tooltip: a short note on an element, said on hover and on focus alike,
|
|
3
|
+
* never on hover alone. Over Base UI's Tooltip, which gives it the
|
|
4
|
+
* anchoring and the Escape to dismiss. Base UI does not name the note
|
|
5
|
+
* itself in this version — no `role="tooltip"`, no `aria-describedby` on
|
|
6
|
+
* the trigger — so this wrapper owns both: the popup carries the role
|
|
7
|
+
* and an id, and the trigger carries that id as its description while
|
|
8
|
+
* the note is open. The trigger is the caller's element (a button, a
|
|
9
|
+
* word); the note is the popup's children. The render prop merges onto
|
|
10
|
+
* the caller's element (Popover.tsx does the same), so a button passed as
|
|
11
|
+
* the trigger is never nested inside Base UI's own button.
|
|
12
|
+
*
|
|
13
|
+
* A tooltip says something extra, never something essential: a reader who
|
|
14
|
+
* never hovers must still understand the trigger from its own label, and
|
|
15
|
+
* a note that carries an action belongs in a Popover instead, where it
|
|
16
|
+
* can be reached and used.
|
|
17
|
+
*/
|
|
18
|
+
import { type ReactNode } from 'react';
|
|
19
|
+
export interface TooltipProps {
|
|
20
|
+
/** The element the note belongs to; a button, a word, an icon. */
|
|
21
|
+
trigger: ReactNode;
|
|
22
|
+
/** The note itself: short, supplementary, never an action. */
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
open?: boolean;
|
|
25
|
+
defaultOpen?: boolean;
|
|
26
|
+
onOpenChange?: (open: boolean) => void;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function Tooltip({ trigger, children, open, defaultOpen, onOpenChange, className }: TooltipProps): import("react").JSX.Element;
|
package/dist/Tooltip.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A tooltip: a short note on an element, said on hover and on focus alike,
|
|
4
|
+
* never on hover alone. Over Base UI's Tooltip, which gives it the
|
|
5
|
+
* anchoring and the Escape to dismiss. Base UI does not name the note
|
|
6
|
+
* itself in this version — no `role="tooltip"`, no `aria-describedby` on
|
|
7
|
+
* the trigger — so this wrapper owns both: the popup carries the role
|
|
8
|
+
* and an id, and the trigger carries that id as its description while
|
|
9
|
+
* the note is open. The trigger is the caller's element (a button, a
|
|
10
|
+
* word); the note is the popup's children. The render prop merges onto
|
|
11
|
+
* the caller's element (Popover.tsx does the same), so a button passed as
|
|
12
|
+
* the trigger is never nested inside Base UI's own button.
|
|
13
|
+
*
|
|
14
|
+
* A tooltip says something extra, never something essential: a reader who
|
|
15
|
+
* never hovers must still understand the trigger from its own label, and
|
|
16
|
+
* a note that carries an action belongs in a Popover instead, where it
|
|
17
|
+
* can be reached and used.
|
|
18
|
+
*/
|
|
19
|
+
import { cloneElement, isValidElement, useId, useState } from 'react';
|
|
20
|
+
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';
|
|
21
|
+
export function Tooltip({ trigger, children, open, defaultOpen, onOpenChange, className }) {
|
|
22
|
+
const tooltipId = useId();
|
|
23
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen ?? false);
|
|
24
|
+
const isOpen = open ?? uncontrolledOpen;
|
|
25
|
+
const base = isValidElement(trigger) ? trigger : _jsx("button", { type: "button", children: trigger });
|
|
26
|
+
/* The description wire exists only while the note is open, so a closed
|
|
27
|
+
tooltip never points at an id that is not in the tree. It joins the
|
|
28
|
+
trigger's own description (a field's hint) rather than replacing it. */
|
|
29
|
+
const existing = base.props['aria-describedby'];
|
|
30
|
+
const triggerElement = isOpen
|
|
31
|
+
? cloneElement(base, { 'aria-describedby': [existing, tooltipId].filter(Boolean).join(' ') })
|
|
32
|
+
: base;
|
|
33
|
+
return (_jsxs(BaseTooltip.Root, { open: open, defaultOpen: defaultOpen, onOpenChange: (next, details) => {
|
|
34
|
+
if (open === undefined)
|
|
35
|
+
setUncontrolledOpen(next);
|
|
36
|
+
onOpenChange?.(next);
|
|
37
|
+
void details;
|
|
38
|
+
}, children: [_jsx(BaseTooltip.Trigger, { className: "ui-tooltip-trigger", render: triggerElement }), _jsx(BaseTooltip.Portal, { children: _jsx(BaseTooltip.Positioner, { sideOffset: 8, children: _jsx(BaseTooltip.Popup, { id: tooltipId, role: "tooltip", className: ['ui-tooltip', className].filter(Boolean).join(' '), children: children }) }) })] }));
|
|
39
|
+
}
|