@dreamtree-org/twreact-ui 1.1.64 → 1.1.65
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/dist/components/core/Select.d.ts.map +1 -1
- package/dist/index.esm.js +71 -19
- package/dist/index.js +71 -19
- package/mcp/catalog.snapshot.json +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Select.d.ts","sourceRoot":"","sources":["../../../src/components/core/Select.jsx"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"Select.d.ts","sourceRoot":"","sources":["../../../src/components/core/Select.jsx"],"names":[],"mappings":";AAmDA,gFA+nBE;kBA3qBK,OAAO"}
|
package/dist/index.esm.js
CHANGED
|
@@ -3490,6 +3490,11 @@ var Select = /*#__PURE__*/React__default.forwardRef(function (_ref, forwardedRef
|
|
|
3490
3490
|
_useState10 = _slicedToArray(_useState1, 2),
|
|
3491
3491
|
menuMaxHeight = _useState10[0],
|
|
3492
3492
|
setMenuMaxHeight = _useState10[1]; // px or null
|
|
3493
|
+
// Fixed-position coordinates for the portaled menu (anchored to the trigger).
|
|
3494
|
+
var _useState11 = useState(null),
|
|
3495
|
+
_useState12 = _slicedToArray(_useState11, 2),
|
|
3496
|
+
menuCoords = _useState12[0],
|
|
3497
|
+
setMenuCoords = _useState12[1]; // { left, top, width } in viewport px
|
|
3493
3498
|
useImperativeHandle(forwardedRef, function () {
|
|
3494
3499
|
return selectRef.current;
|
|
3495
3500
|
}, []);
|
|
@@ -3552,16 +3557,47 @@ var Select = /*#__PURE__*/React__default.forwardRef(function (_ref, forwardedRef
|
|
|
3552
3557
|
desiredMenuHeight = Math.min(menuRef.current.scrollHeight, DEFAULT_MENU_MAX);
|
|
3553
3558
|
}
|
|
3554
3559
|
// decide placement: prefer bottom unless not enough space and top has more space
|
|
3560
|
+
var nextPlacement;
|
|
3561
|
+
var nextMaxHeight;
|
|
3555
3562
|
if (spaceBelow >= desiredMenuHeight || spaceBelow >= spaceAbove) {
|
|
3556
|
-
|
|
3557
|
-
|
|
3563
|
+
nextPlacement = "bottom";
|
|
3564
|
+
nextMaxHeight = Math.min(desiredMenuHeight, Math.max(80, spaceBelow));
|
|
3565
|
+
} else {
|
|
3566
|
+
nextPlacement = "top";
|
|
3567
|
+
nextMaxHeight = Math.min(desiredMenuHeight, Math.max(80, spaceAbove));
|
|
3568
|
+
}
|
|
3569
|
+
setPlacement(nextPlacement);
|
|
3570
|
+
setMenuMaxHeight(nextMaxHeight);
|
|
3571
|
+
// The menu is portaled to document.body with position:fixed, so it floats
|
|
3572
|
+
// above any ancestor with overflow:hidden|auto|scroll. Anchor it to the
|
|
3573
|
+
// trigger via the viewport-relative rect (no scroll offset for fixed).
|
|
3574
|
+
// maxHeight here mirrors the inline style applied to the menu (+10 fudge).
|
|
3575
|
+
var renderedMaxHeight = nextMaxHeight + 10;
|
|
3576
|
+
var next = {
|
|
3577
|
+
left: rect.left,
|
|
3578
|
+
width: rect.width
|
|
3579
|
+
};
|
|
3580
|
+
if (nextPlacement === "bottom") {
|
|
3581
|
+
next.top = rect.bottom + 4; // ~mt-1
|
|
3558
3582
|
} else {
|
|
3559
|
-
|
|
3560
|
-
|
|
3583
|
+
// place above the trigger; bottom of menu sits just above the trigger top
|
|
3584
|
+
next.top = Math.max(MARGIN, rect.top - 4 - renderedMaxHeight);
|
|
3561
3585
|
}
|
|
3586
|
+
setMenuCoords(next);
|
|
3562
3587
|
};
|
|
3588
|
+
// Anchor the portaled menu BEFORE the browser paints, so it never flashes
|
|
3589
|
+
// at (0,0). The menu is already in the DOM (createPortal runs during render),
|
|
3590
|
+
// we just need its fixed coords set synchronously.
|
|
3591
|
+
useLayoutEffect(function () {
|
|
3592
|
+
if (isOpen) calculatePlacement();
|
|
3593
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3594
|
+
}, [isOpen, filtered.length]);
|
|
3563
3595
|
useEffect(function () {
|
|
3564
|
-
if (!isOpen)
|
|
3596
|
+
if (!isOpen) {
|
|
3597
|
+
// drop stale coords so the next open re-anchors from scratch
|
|
3598
|
+
setMenuCoords(null);
|
|
3599
|
+
return;
|
|
3600
|
+
}
|
|
3565
3601
|
// initial calc on open; wait a tick to allow DOM to render
|
|
3566
3602
|
var raf = requestAnimationFrame(function () {
|
|
3567
3603
|
var _searchRef$current;
|
|
@@ -3569,7 +3605,10 @@ var Select = /*#__PURE__*/React__default.forwardRef(function (_ref, forwardedRef
|
|
|
3569
3605
|
// focus search input if available
|
|
3570
3606
|
if (searchable) (_searchRef$current = searchRef.current) === null || _searchRef$current === void 0 || _searchRef$current.focus();
|
|
3571
3607
|
});
|
|
3572
|
-
//
|
|
3608
|
+
// Reposition on resize/scroll while open (throttled by rAF). Use
|
|
3609
|
+
// capture-phase scroll so we also catch any scrolling ancestor — the
|
|
3610
|
+
// menu is portaled with position:fixed, so its coords must track the
|
|
3611
|
+
// trigger as the page (or a scroll container) moves.
|
|
3573
3612
|
var rafId = null;
|
|
3574
3613
|
var onWindowChange = function onWindowChange() {
|
|
3575
3614
|
if (rafId) cancelAnimationFrame(rafId);
|
|
@@ -3581,13 +3620,16 @@ var Select = /*#__PURE__*/React__default.forwardRef(function (_ref, forwardedRef
|
|
|
3581
3620
|
passive: true
|
|
3582
3621
|
});
|
|
3583
3622
|
window.addEventListener("scroll", onWindowChange, {
|
|
3584
|
-
passive: true
|
|
3623
|
+
passive: true,
|
|
3624
|
+
capture: true
|
|
3585
3625
|
});
|
|
3586
3626
|
return function () {
|
|
3587
3627
|
cancelAnimationFrame(raf);
|
|
3588
3628
|
if (rafId) cancelAnimationFrame(rafId);
|
|
3589
3629
|
window.removeEventListener("resize", onWindowChange);
|
|
3590
|
-
window.removeEventListener("scroll", onWindowChange
|
|
3630
|
+
window.removeEventListener("scroll", onWindowChange, {
|
|
3631
|
+
capture: true
|
|
3632
|
+
});
|
|
3591
3633
|
};
|
|
3592
3634
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3593
3635
|
}, [isOpen, searchable, filtered.length]);
|
|
@@ -3677,7 +3719,13 @@ var Select = /*#__PURE__*/React__default.forwardRef(function (_ref, forwardedRef
|
|
|
3677
3719
|
}
|
|
3678
3720
|
};
|
|
3679
3721
|
var handleClickOutside = function handleClickOutside(e) {
|
|
3680
|
-
|
|
3722
|
+
// The menu is portaled to document.body, so it lives OUTSIDE selectRef's
|
|
3723
|
+
// DOM subtree. Treat clicks inside the portaled menu as "inside" too —
|
|
3724
|
+
// otherwise selecting an option would close the menu before the option's
|
|
3725
|
+
// own click handler runs.
|
|
3726
|
+
var inTrigger = selectRef.current && selectRef.current.contains(e.target);
|
|
3727
|
+
var inMenu = menuRef.current && menuRef.current.contains(e.target);
|
|
3728
|
+
if (!inTrigger && !inMenu) {
|
|
3681
3729
|
setIsOpen(false);
|
|
3682
3730
|
setFocusedIndex(-1);
|
|
3683
3731
|
}
|
|
@@ -3901,19 +3949,23 @@ var Select = /*#__PURE__*/React__default.forwardRef(function (_ref, forwardedRef
|
|
|
3901
3949
|
type: "hidden",
|
|
3902
3950
|
name: name,
|
|
3903
3951
|
value: selectedValues !== null && selectedValues !== void 0 ? selectedValues : ""
|
|
3904
|
-
})), isOpen && jsxs("div", {
|
|
3952
|
+
})), isOpen && typeof document !== "undefined" && /*#__PURE__*/createPortal(jsxs("div", {
|
|
3905
3953
|
ref: menuRef,
|
|
3906
|
-
className: cn$1("
|
|
3907
|
-
// if placement is bottom, place it below with margin-top; if top, place it above with bottom full and margin-bottom
|
|
3908
|
-
"mt-1 top-full": placement === "bottom",
|
|
3909
|
-
"mb-1 bottom-full": placement === "top"
|
|
3910
|
-
}),
|
|
3954
|
+
className: cn$1("fixed z-50 rounded-md border border-gray-300 bg-white shadow-lg"),
|
|
3911
3955
|
role: "dialog",
|
|
3912
|
-
|
|
3913
|
-
|
|
3956
|
+
"data-placement": placement,
|
|
3957
|
+
// Portaled to document.body with position:fixed so the menu
|
|
3958
|
+
// escapes any ancestor overflow:hidden|auto|scroll. Coordinates
|
|
3959
|
+
// are anchored to the trigger's viewport rect; width matches the
|
|
3960
|
+
// trigger; maxHeight is driven by available space.
|
|
3961
|
+
style: _objectSpread$C({
|
|
3962
|
+
left: menuCoords ? "".concat(menuCoords.left, "px") : undefined,
|
|
3963
|
+
top: menuCoords ? "".concat(menuCoords.top, "px") : undefined,
|
|
3964
|
+
width: menuCoords ? "".concat(menuCoords.width, "px") : undefined
|
|
3965
|
+
}, menuMaxHeight ? {
|
|
3914
3966
|
maxHeight: "".concat(menuMaxHeight + 10, "px"),
|
|
3915
3967
|
overflow: "auto"
|
|
3916
|
-
} : {},
|
|
3968
|
+
} : {}),
|
|
3917
3969
|
children: [searchable && jsx("div", {
|
|
3918
3970
|
className: "border-b border-gray-200 p-2",
|
|
3919
3971
|
children: jsxs("div", {
|
|
@@ -3956,7 +4008,7 @@ var Select = /*#__PURE__*/React__default.forwardRef(function (_ref, forwardedRef
|
|
|
3956
4008
|
"aria-multiselectable": multiSelect,
|
|
3957
4009
|
children: grouped ? renderGroupedOptions() : renderFlatOptions()
|
|
3958
4010
|
})]
|
|
3959
|
-
})]
|
|
4011
|
+
}), document.body)]
|
|
3960
4012
|
}), error && jsx("p", {
|
|
3961
4013
|
className: "mt-1 text-sm text-error-500",
|
|
3962
4014
|
children: error
|
package/dist/index.js
CHANGED
|
@@ -3510,6 +3510,11 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
|
|
|
3510
3510
|
_useState10 = _slicedToArray(_useState1, 2),
|
|
3511
3511
|
menuMaxHeight = _useState10[0],
|
|
3512
3512
|
setMenuMaxHeight = _useState10[1]; // px or null
|
|
3513
|
+
// Fixed-position coordinates for the portaled menu (anchored to the trigger).
|
|
3514
|
+
var _useState11 = React.useState(null),
|
|
3515
|
+
_useState12 = _slicedToArray(_useState11, 2),
|
|
3516
|
+
menuCoords = _useState12[0],
|
|
3517
|
+
setMenuCoords = _useState12[1]; // { left, top, width } in viewport px
|
|
3513
3518
|
React.useImperativeHandle(forwardedRef, function () {
|
|
3514
3519
|
return selectRef.current;
|
|
3515
3520
|
}, []);
|
|
@@ -3572,16 +3577,47 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
|
|
|
3572
3577
|
desiredMenuHeight = Math.min(menuRef.current.scrollHeight, DEFAULT_MENU_MAX);
|
|
3573
3578
|
}
|
|
3574
3579
|
// decide placement: prefer bottom unless not enough space and top has more space
|
|
3580
|
+
var nextPlacement;
|
|
3581
|
+
var nextMaxHeight;
|
|
3575
3582
|
if (spaceBelow >= desiredMenuHeight || spaceBelow >= spaceAbove) {
|
|
3576
|
-
|
|
3577
|
-
|
|
3583
|
+
nextPlacement = "bottom";
|
|
3584
|
+
nextMaxHeight = Math.min(desiredMenuHeight, Math.max(80, spaceBelow));
|
|
3585
|
+
} else {
|
|
3586
|
+
nextPlacement = "top";
|
|
3587
|
+
nextMaxHeight = Math.min(desiredMenuHeight, Math.max(80, spaceAbove));
|
|
3588
|
+
}
|
|
3589
|
+
setPlacement(nextPlacement);
|
|
3590
|
+
setMenuMaxHeight(nextMaxHeight);
|
|
3591
|
+
// The menu is portaled to document.body with position:fixed, so it floats
|
|
3592
|
+
// above any ancestor with overflow:hidden|auto|scroll. Anchor it to the
|
|
3593
|
+
// trigger via the viewport-relative rect (no scroll offset for fixed).
|
|
3594
|
+
// maxHeight here mirrors the inline style applied to the menu (+10 fudge).
|
|
3595
|
+
var renderedMaxHeight = nextMaxHeight + 10;
|
|
3596
|
+
var next = {
|
|
3597
|
+
left: rect.left,
|
|
3598
|
+
width: rect.width
|
|
3599
|
+
};
|
|
3600
|
+
if (nextPlacement === "bottom") {
|
|
3601
|
+
next.top = rect.bottom + 4; // ~mt-1
|
|
3578
3602
|
} else {
|
|
3579
|
-
|
|
3580
|
-
|
|
3603
|
+
// place above the trigger; bottom of menu sits just above the trigger top
|
|
3604
|
+
next.top = Math.max(MARGIN, rect.top - 4 - renderedMaxHeight);
|
|
3581
3605
|
}
|
|
3606
|
+
setMenuCoords(next);
|
|
3582
3607
|
};
|
|
3608
|
+
// Anchor the portaled menu BEFORE the browser paints, so it never flashes
|
|
3609
|
+
// at (0,0). The menu is already in the DOM (createPortal runs during render),
|
|
3610
|
+
// we just need its fixed coords set synchronously.
|
|
3611
|
+
React.useLayoutEffect(function () {
|
|
3612
|
+
if (isOpen) calculatePlacement();
|
|
3613
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3614
|
+
}, [isOpen, filtered.length]);
|
|
3583
3615
|
React.useEffect(function () {
|
|
3584
|
-
if (!isOpen)
|
|
3616
|
+
if (!isOpen) {
|
|
3617
|
+
// drop stale coords so the next open re-anchors from scratch
|
|
3618
|
+
setMenuCoords(null);
|
|
3619
|
+
return;
|
|
3620
|
+
}
|
|
3585
3621
|
// initial calc on open; wait a tick to allow DOM to render
|
|
3586
3622
|
var raf = requestAnimationFrame(function () {
|
|
3587
3623
|
var _searchRef$current;
|
|
@@ -3589,7 +3625,10 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
|
|
|
3589
3625
|
// focus search input if available
|
|
3590
3626
|
if (searchable) (_searchRef$current = searchRef.current) === null || _searchRef$current === void 0 || _searchRef$current.focus();
|
|
3591
3627
|
});
|
|
3592
|
-
//
|
|
3628
|
+
// Reposition on resize/scroll while open (throttled by rAF). Use
|
|
3629
|
+
// capture-phase scroll so we also catch any scrolling ancestor — the
|
|
3630
|
+
// menu is portaled with position:fixed, so its coords must track the
|
|
3631
|
+
// trigger as the page (or a scroll container) moves.
|
|
3593
3632
|
var rafId = null;
|
|
3594
3633
|
var onWindowChange = function onWindowChange() {
|
|
3595
3634
|
if (rafId) cancelAnimationFrame(rafId);
|
|
@@ -3601,13 +3640,16 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
|
|
|
3601
3640
|
passive: true
|
|
3602
3641
|
});
|
|
3603
3642
|
window.addEventListener("scroll", onWindowChange, {
|
|
3604
|
-
passive: true
|
|
3643
|
+
passive: true,
|
|
3644
|
+
capture: true
|
|
3605
3645
|
});
|
|
3606
3646
|
return function () {
|
|
3607
3647
|
cancelAnimationFrame(raf);
|
|
3608
3648
|
if (rafId) cancelAnimationFrame(rafId);
|
|
3609
3649
|
window.removeEventListener("resize", onWindowChange);
|
|
3610
|
-
window.removeEventListener("scroll", onWindowChange
|
|
3650
|
+
window.removeEventListener("scroll", onWindowChange, {
|
|
3651
|
+
capture: true
|
|
3652
|
+
});
|
|
3611
3653
|
};
|
|
3612
3654
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3613
3655
|
}, [isOpen, searchable, filtered.length]);
|
|
@@ -3697,7 +3739,13 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
|
|
|
3697
3739
|
}
|
|
3698
3740
|
};
|
|
3699
3741
|
var handleClickOutside = function handleClickOutside(e) {
|
|
3700
|
-
|
|
3742
|
+
// The menu is portaled to document.body, so it lives OUTSIDE selectRef's
|
|
3743
|
+
// DOM subtree. Treat clicks inside the portaled menu as "inside" too —
|
|
3744
|
+
// otherwise selecting an option would close the menu before the option's
|
|
3745
|
+
// own click handler runs.
|
|
3746
|
+
var inTrigger = selectRef.current && selectRef.current.contains(e.target);
|
|
3747
|
+
var inMenu = menuRef.current && menuRef.current.contains(e.target);
|
|
3748
|
+
if (!inTrigger && !inMenu) {
|
|
3701
3749
|
setIsOpen(false);
|
|
3702
3750
|
setFocusedIndex(-1);
|
|
3703
3751
|
}
|
|
@@ -3921,19 +3969,23 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
|
|
|
3921
3969
|
type: "hidden",
|
|
3922
3970
|
name: name,
|
|
3923
3971
|
value: selectedValues !== null && selectedValues !== void 0 ? selectedValues : ""
|
|
3924
|
-
})), isOpen && jsxRuntime.jsxs("div", {
|
|
3972
|
+
})), isOpen && typeof document !== "undefined" && /*#__PURE__*/ReactDOM.createPortal(jsxRuntime.jsxs("div", {
|
|
3925
3973
|
ref: menuRef,
|
|
3926
|
-
className: cn$1("
|
|
3927
|
-
// if placement is bottom, place it below with margin-top; if top, place it above with bottom full and margin-bottom
|
|
3928
|
-
"mt-1 top-full": placement === "bottom",
|
|
3929
|
-
"mb-1 bottom-full": placement === "top"
|
|
3930
|
-
}),
|
|
3974
|
+
className: cn$1("fixed z-50 rounded-md border border-gray-300 bg-white shadow-lg"),
|
|
3931
3975
|
role: "dialog",
|
|
3932
|
-
|
|
3933
|
-
|
|
3976
|
+
"data-placement": placement,
|
|
3977
|
+
// Portaled to document.body with position:fixed so the menu
|
|
3978
|
+
// escapes any ancestor overflow:hidden|auto|scroll. Coordinates
|
|
3979
|
+
// are anchored to the trigger's viewport rect; width matches the
|
|
3980
|
+
// trigger; maxHeight is driven by available space.
|
|
3981
|
+
style: _objectSpread$C({
|
|
3982
|
+
left: menuCoords ? "".concat(menuCoords.left, "px") : undefined,
|
|
3983
|
+
top: menuCoords ? "".concat(menuCoords.top, "px") : undefined,
|
|
3984
|
+
width: menuCoords ? "".concat(menuCoords.width, "px") : undefined
|
|
3985
|
+
}, menuMaxHeight ? {
|
|
3934
3986
|
maxHeight: "".concat(menuMaxHeight + 10, "px"),
|
|
3935
3987
|
overflow: "auto"
|
|
3936
|
-
} : {},
|
|
3988
|
+
} : {}),
|
|
3937
3989
|
children: [searchable && jsxRuntime.jsx("div", {
|
|
3938
3990
|
className: "border-b border-gray-200 p-2",
|
|
3939
3991
|
children: jsxRuntime.jsxs("div", {
|
|
@@ -3976,7 +4028,7 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
|
|
|
3976
4028
|
"aria-multiselectable": multiSelect,
|
|
3977
4029
|
children: grouped ? renderGroupedOptions() : renderFlatOptions()
|
|
3978
4030
|
})]
|
|
3979
|
-
})]
|
|
4031
|
+
}), document.body)]
|
|
3980
4032
|
}), error && jsxRuntime.jsx("p", {
|
|
3981
4033
|
className: "mt-1 text-sm text-error-500",
|
|
3982
4034
|
children: error
|
|
@@ -3155,7 +3155,7 @@
|
|
|
3155
3155
|
"Radio": "# Radio\n\nSingle radio button; use multiple with the same `name` for a group. Controlled via `checked` or uncontrolled via `defaultChecked`.\n\n## Import\n\n```jsx\nimport { Radio } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `id` | `string` | auto | Input id. |\n| `name` | `string` | — | Group name (required for grouping). |\n| `value` | `any` | — | Value when selected. |\n| `checked` | `boolean` | — | Controlled checked. |\n| `defaultChecked` | `boolean` | — | Uncontrolled initial. |\n| `onChange` | `(e: Event) => void` | — | Change handler. |\n| `disabled` | `boolean` | `false` | Disable. |\n| `required` | `boolean` | `false` | Required. |\n| `label` | `string` | — | Label text. |\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `\"md\"` | Size. |\n| `labelPosition` | `\"left\"` \\| `\"right\"` | `\"right\"` | Label side. |\n| `radioColor` | `string` | `\"blue\"` | Accent color (Tailwind name). |\n| `className` | `string` | `\"\"` | Label wrapper classes. |\n| `ariaLabel` | `string` | — | Accessibility label. |\n\n## Example\n\n```jsx\nconst [choice, setChoice] = useState('a');\n<>\n <Radio name=\"choice\" value=\"a\" checked={choice === 'a'} onChange={() => setChoice('a')} label=\"Option A\" />\n <Radio name=\"choice\" value=\"b\" checked={choice === 'b'} onChange={() => setChoice('b')} label=\"Option B\" />\n</>\n```\n\n[← Component overview](README.md)\n",
|
|
3156
3156
|
"Rate": "# Rate\n\nStar (or custom icon) rating input/display. Controlled via `value` or uncontrolled via `defaultValue`. Supports hover preview and optional read-only.\n\n## Import\n\n```jsx\nimport { Rate } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `count` | `number` | `5` | Number of stars. |\n| `value` | `number` | — | Controlled value (0 to count). |\n| `defaultValue` | `number` | `0` | Uncontrolled initial value. |\n| `readOnly` | `boolean` | `false` | Read-only (no onChange). |\n| `icon` | `ReactNode` | `<StarOutline />` | Icon for inactive star. |\n| `toggledIcon` | `ReactNode` | `<StarFilled />` | Icon for active star. |\n| `activeColor` | `string` | `\"#f6b026\"` | Color for active stars. |\n| `inactiveColor` | `string` | `\"#e5e7eb\"` | Color for inactive stars. |\n| `size` | `number` | `20` | Icon size in px. |\n| `onChange` | `(value: number) => void` | — | Called when value changes. |\n| `onClick` | `(value: number) => void` | — | Called on star click. |\n| `text` | `ReactNode` | — | Optional text (e.g. label) next to stars. |\n| `className` | `string` | `\"\"` | Wrapper CSS classes. |\n| `id` | `string` | — | Wrapper id. |\n| `name` | `string` | — | Form field name. |\n\n## Example\n\n```jsx\nconst [rating, setRating] = useState(0);\n<Rate value={rating} onChange={setRating} count={5} />\n<Rate defaultValue={3} readOnly />\n```\n\n[← Component overview](README.md)\n",
|
|
3157
3157
|
"RoundedTag": "# RoundedTag\n\nTag/chip with optional avatar (image or initials) and optional remove button. Used for labels, selected items, or filters.\n\n## Import\n\n```jsx\nimport { RoundedTag } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `label` | `string` | — | Main label text. |\n| `avatarSrc` | `string` | `null` | Image URL for avatar. |\n| `avatarAlt` | `string` | `\"avatar\"` | Alt for avatar image. |\n| `initials` | `string` | `\"\"` | Initials when no image (e.g. \"AB\"). |\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `\"md\"` | Size. |\n| `onRemove` | `(e?) => void` | `null` | If provided, shows remove button and calls on click. |\n| `onClick` | `(e?) => void` | `null` | Click handler for the tag. |\n| `className` | `string` | `\"\"` | Wrapper CSS classes. |\n| `closeClass` | `string` | `\"\"` | Remove button CSS classes. |\n| `ariaLabel` | `string` | `\"tag\"` | Accessibility label. |\n| `avatarPosition` | `\"left\"` \\| `\"right\"` | `\"left\"` | Avatar position. |\n| `avatarClassname` | `string` | `\"\"` | Avatar wrapper classes. |\n| `initialClassname` | `string` | `\"\"` | Initials span classes. |\n\n## Example\n\n```jsx\n<RoundedTag label=\"React\" />\n<RoundedTag label=\"John\" initials=\"JD\" onRemove={() => {}} />\n<RoundedTag label=\"With image\" avatarSrc=\"/user.jpg\" size=\"lg\" />\n```\n\n[← Component overview](README.md)\n",
|
|
3158
|
-
"Select": "# Select\n\nSingle or multi-select dropdown with search, grouped options, creatable options, and optional select-all.\n\n## Import\n\n```jsx\nimport { Select } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `options` | `Array` | `[]` | `[{ value, label, disabled }]` or grouped `[{ label, options: [...] }]` |\n| `value` | `any` \\| `any[]` | — | Selected value(s); array when `multiSelect` |\n| `onChange` | `function` | — | `(value)` or `(values[])` |\n| `placeholder` | `string` | `\"Select an option...\"` | Placeholder text |\n| `label` | `string` | — | Label |\n| `error` | `string` | — | Error message |\n| `disabled` | `boolean` | — | Disable select |\n| `required` | `boolean` | — | Required |\n| `multiSelect` | `boolean` | `false` | Allow multiple selection |\n| `searchable` | `boolean` | `false` | Show search input |\n| `grouped` | `boolean` | `false` | Options are grouped |\n| `allowClear` | `boolean` | `true` | Show clear button |\n| `creatable` | `boolean` | `false` | Allow creating new option when no match |\n| `onCreateOption` | `function` | — | Called when creating option |\n| `onSearch` | `function` | — | Search term callback |\n| `loading` | `boolean` | `false` | Loading state |\n| `selectAllOption` | `boolean` | `true` | Show select all (multi) |\n| `closeOnSelect` | `boolean` | `false` | Close dropdown on select (single) |\n| `maxTagCount` | `number` | `3` | Max tags shown in multi (rest as \"+N\") |\n| `onMenuItemRender` | `function` | — | Custom option render |\n| `renderGroupLabel` | `function` | — | Custom group label |\n| `name` | `string` | — | Form field name |\n| `className` | `string` | — | Extra CSS classes |\n\n## Examples\n\n### Basic single select\n\n```jsx\nconst options = [\n { value: 'a', label: 'Option A' },\n { value: 'b', label: 'Option B' },\n];\n<Select\n options={options}\n value={value}\n onChange={setValue}\n placeholder=\"Choose one\"\n/>\n```\n\n### Multi select with search\n\n```jsx\n<Select\n options={options}\n value={selected}\n onChange={setSelected}\n multiSelect\n searchable\n placeholder=\"Choose multiple\"\n/>\n```\n\n### Grouped options\n\n```jsx\nconst grouped = [\n { label: 'Fruits', options: [{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }] },\n { label: 'Veggies', options: [{ value: 'carrot', label: 'Carrot' }] },\n];\n<Select options={grouped} grouped value={value} onChange={setValue} />\n```\n\n[← Component overview](README.md)\n",
|
|
3158
|
+
"Select": "# Select\n\nSingle or multi-select dropdown with search, grouped options, creatable options, and optional select-all.\n\n> **The open menu portals to `document.body`.** The dropdown is rendered through\n> a React portal with `position: fixed`, anchored to the trigger via its viewport\n> rect. This means the menu **escapes any ancestor with `overflow: hidden | auto |\n> scroll`** instead of being clipped by it, and it repositions on scroll/resize\n> while open. There is no new prop and no opt-out — portaling is the default\n> behavior. Behaviorally everything else is unchanged (keyboard nav,\n> click-outside-to-close, placement flip, trigger-width matching, ARIA wiring).\n\n## Import\n\n```jsx\nimport { Select } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `options` | `Array` | `[]` | `[{ value, label, disabled }]` or grouped `[{ label, options: [...] }]` |\n| `value` | `any` \\| `any[]` | — | Selected value(s); array when `multiSelect` |\n| `onChange` | `function` | — | `(value)` or `(values[])` |\n| `placeholder` | `string` | `\"Select an option...\"` | Placeholder text |\n| `label` | `string` | — | Label |\n| `error` | `string` | — | Error message |\n| `disabled` | `boolean` | — | Disable select |\n| `required` | `boolean` | — | Required |\n| `multiSelect` | `boolean` | `false` | Allow multiple selection |\n| `searchable` | `boolean` | `false` | Show search input |\n| `grouped` | `boolean` | `false` | Options are grouped |\n| `allowClear` | `boolean` | `true` | Show clear button |\n| `creatable` | `boolean` | `false` | Allow creating new option when no match |\n| `onCreateOption` | `function` | — | Called when creating option |\n| `onSearch` | `function` | — | Search term callback |\n| `loading` | `boolean` | `false` | Loading state |\n| `selectAllOption` | `boolean` | `true` | Show select all (multi) |\n| `closeOnSelect` | `boolean` | `false` | Close dropdown on select (single) |\n| `maxTagCount` | `number` | `3` | Max tags shown in multi (rest as \"+N\") |\n| `onMenuItemRender` | `function` | — | Custom option render |\n| `renderGroupLabel` | `function` | — | Custom group label |\n| `name` | `string` | — | Form field name |\n| `className` | `string` | — | Extra CSS classes |\n\n## Examples\n\n### Basic single select\n\n```jsx\nconst options = [\n { value: 'a', label: 'Option A' },\n { value: 'b', label: 'Option B' },\n];\n<Select\n options={options}\n value={value}\n onChange={setValue}\n placeholder=\"Choose one\"\n/>\n```\n\n### Multi select with search\n\n```jsx\n<Select\n options={options}\n value={selected}\n onChange={setSelected}\n multiSelect\n searchable\n placeholder=\"Choose multiple\"\n/>\n```\n\n### Grouped options\n\n```jsx\nconst grouped = [\n { label: 'Fruits', options: [{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }] },\n { label: 'Veggies', options: [{ value: 'carrot', label: 'Carrot' }] },\n];\n<Select options={grouped} grouped value={value} onChange={setValue} />\n```\n\n[← Component overview](README.md)\n",
|
|
3159
3159
|
"Sidebar": "# Sidebar\n\nCollapsible side navigation with optional nested items, logo, and user block. Supports desktop collapsed state and mobile drawer. Tooltips show when collapsed; optional `drawerPosition` for mobile.\n\n## Import\n\n```jsx\nimport { Sidebar } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `items` | `Array` | `[]` | Nav items: `{ id, label, icon, children?, onClick?, active? }`. `children` = nested items. |\n| `collapsed` | `boolean` | `false` | Desktop collapsed state (icon-only). |\n| `onToggle` | `() => void` | — | Toggle collapse (e.g. chevron click). |\n| `className` | `string` | — | Wrapper CSS classes. |\n| `logo` | `ReactNode` | — | Logo at top. |\n| `user` | `object` \\| `ReactNode` | — | User block at bottom. |\n| `onUserClick` | `function` | — | User block click. |\n| `drawerPosition` | `\"left\"` \\| `\"right\"` | `\"left\"` | Mobile drawer side. |\n| `isMobileOpen` | `boolean` | — | Controlled mobile open. |\n| `setIsMobileOpen` | `(open: boolean) => void` | — | Set mobile open (when controlled). |\n| `showCollapsedTooltips` | `boolean` | `true` | Show tooltips when collapsed. |\n| `tooltipOptions` | `object` | — | Options for internal Tooltip. |\n\n## Item shape\n\n- `id`: string (required for expand/active).\n- `label`: string.\n- `icon`: ReactNode.\n- `children`: optional array of same shape (nested items).\n- `onClick`: optional handler.\n- `active`: optional boolean for current page.\n\n## Example\n\n```jsx\nconst [collapsed, setCollapsed] = useState(false);\n<Sidebar\n items={[\n { id: 'dash', label: 'Dashboard', icon: <Home /> },\n { id: 'settings', label: 'Settings', icon: <Settings />, children: [\n { id: 'profile', label: 'Profile' },\n { id: 'security', label: 'Security' },\n ]},\n ]}\n collapsed={collapsed}\n onToggle={() => setCollapsed(!collapsed)}\n logo={<Logo />}\n user={{ name: 'Jane', avatar: '/jane.jpg' }}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
3160
3160
|
"Skeleton": "# Skeleton\n\nPlaceholder blocks for loading states. Renders shimmer bars (or circles) when `active` is true; otherwise renders `children`.\n\n## Import\n\n```jsx\nimport { Skeleton } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `count` | `number` | `1` | Number of skeleton items. |\n| `circle` | `boolean` | `false` | Render items as circles. |\n| `height` | `number` \\| `string` | `16` | Height in px or CSS unit (e.g. `\"2rem\"`). |\n| `width` | `number` \\| `string` | — | Width; default 100% (column) or height (inline/circle). |\n| `rounded` | `boolean` | `true` | Rounded corners (when not circle). |\n| `animated` | `boolean` | `true` | Shimmer animation. |\n| `active` | `boolean` | `true` | If true show skeletons; if false render children. |\n| `gap` | `string` | `\"8px\"` | Spacing between items. |\n| `inline` | `boolean` | `false` | Layout items in a row. |\n| `className` | `string` | `\"\"` | Wrapper CSS classes. |\n| `style` | `object` | `{}` | Wrapper inline style. |\n| `children` | `ReactNode` | `null` | Rendered when `active` is false. |\n\n## Examples\n\n```jsx\n<Skeleton />\n<Skeleton count={3} height={20} gap=\"12px\" />\n<Skeleton circle height={40} />\n<Skeleton active={loading} count={2}>\n <RealContent />\n</Skeleton>\n```\n\n[← Component overview](README.md)\n",
|
|
3161
3161
|
"SpeechToText": "# SpeechToText\n\nVoice-to-text using the Web Speech API. Headless by default — render a custom\ncontrol via `renderButton`, or drive it imperatively through the ref. The\nbuilt-in fallback button is `className`-extensible and forwards arbitrary props.\n\n## Import\n\n```jsx\nimport { SpeechToText } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `lang` | `string` | `\"en-US\"` | Recognition language |\n| `continuous` | `boolean` | `true` | Keep listening until stopped (forced `false` on mobile) |\n| `interimResults` | `boolean` | `true` | Return interim results |\n| `onSpeechComplete` | `function` | — | Final transcript chunk callback `(text) => void` |\n| `onSpeaking` | `function` | — | Interim transcript callback `(text) => void` |\n| `onError` | `function` | — | Fatal error callback `(Error) => void`. Routine events (`no-speech`, `aborted`) are **not** surfaced |\n| `onStart` | `function` | — | Start callback |\n| `onStop` | `function` | — | Stop callback |\n| `renderButton` | `function` | — | Custom control render-prop (see signature below) |\n| `autoStart` | `boolean` | `false` | Start on mount (once, after support is detected) |\n| `disabled` | `boolean` | `false` | Disable |\n| `resetOnStart` | `boolean` | `false` | Clear the accumulated transcript when a new session starts |\n| `className` | `string` | — | Merged (via `cn`) onto the default button |\n| `...rest` | — | — | Forwarded to the default `<button>` |\n\nThe default button is keyboard-accessible (`aria-pressed`, `aria-label`) and\ntoken-driven. When `renderButton` is supplied, `className`/`...rest` are not\napplied (you own the rendered control).\n\n## `renderButton` signature\n\n```js\nrenderButton({ isListening, isSupported, error, start, stop, toggle, disabled })\n```\n\n## Ref methods\n\n`start()`, `stop()`, `toggle()`, `isListening()`, `isSupported()`,\n`getTranscript()`, `clearTranscript()`, `getError()`.\n\n## Example\n\n```jsx\nconst [transcript, setTranscript] = useState('');\n<SpeechToText\n onSpeechComplete={(chunk) => setTranscript((t) => `${t} ${chunk}`.trim())}\n onError={(e) => console.error(e)}\n/>\n<div>Transcript: {transcript}</div>\n```\n\n[← Component overview](README.md)\n",
|