@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.
@@ -1 +1 @@
1
- {"version":3,"file":"Select.d.ts","sourceRoot":"","sources":["../../../src/components/core/Select.jsx"],"names":[],"mappings":";AAiDA,gFA6kBE;kBAxnBK,OAAO"}
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
- setPlacement("bottom");
3557
- setMenuMaxHeight(Math.min(desiredMenuHeight, Math.max(80, spaceBelow)));
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
- setPlacement("top");
3560
- setMenuMaxHeight(Math.min(desiredMenuHeight, Math.max(80, spaceAbove)));
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) return;
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
- // update on resize/scroll (throttle by requestAnimationFrame)
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
- if (selectRef.current && !selectRef.current.contains(e.target)) {
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("absolute z-50 w-full rounded-md border border-gray-300 bg-white shadow-lg", {
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
- // set inline style for maxHeight based on available space (converted to px)
3913
- style: menuMaxHeight ? {
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
- setPlacement("bottom");
3577
- setMenuMaxHeight(Math.min(desiredMenuHeight, Math.max(80, spaceBelow)));
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
- setPlacement("top");
3580
- setMenuMaxHeight(Math.min(desiredMenuHeight, Math.max(80, spaceAbove)));
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) return;
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
- // update on resize/scroll (throttle by requestAnimationFrame)
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
- if (selectRef.current && !selectRef.current.contains(e.target)) {
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("absolute z-50 w-full rounded-md border border-gray-300 bg-white shadow-lg", {
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
- // set inline style for maxHeight based on available space (converted to px)
3933
- style: menuMaxHeight ? {
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreamtree-org/twreact-ui",
3
- "version": "1.1.64",
3
+ "version": "1.1.65",
4
4
  "description": "A comprehensive React + Tailwind components library for building modern web apps",
5
5
  "author": {
6
6
  "name": "Partha Preetham Krishna",