@slithy/base-ui 0.1.0 → 0.2.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # @slithy/base-ui
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Singleton architecture rewrite for Tooltip and Dropdown.
8
+
9
+ - Tooltip and Dropdown rewritten as singleton implementations (one renderer per app)
10
+ - `TooltipStore` / `DropdownStore` + renderer pattern replaces per-instance components
11
+ - `useSafePolygon` hook for hover bridging between trigger and popup
12
+ - `useCloseCleanup` hook for defensive renderer cleanup on unmount
13
+ - Dropdown: `side`, `sideOffset`, `align`, `alignOffset`, `collisionPadding` positioning props
14
+ - Dropdown: `loopFocus`, `highlightItemOnHover`, `orientation`, `modal`, `openOn` props
15
+ - Dropdown: auto-dismiss when trigger scrolls out of view; controlled `open` / `defaultOpen`
16
+ - Dropdown.Trigger: `tooltip` prop (shows tooltip when dropdown is closed)
17
+ - Tooltip: `hoverable` prop with safe polygon bridging; disabled on touch devices
18
+ - Tooltip: controlled `open` / `defaultOpen`
package/README.md CHANGED
@@ -1,47 +1,70 @@
1
1
  # @slithy/base-ui
2
2
 
3
- Compound UI components built on [Base UI](https://base-ui.com/). Provides accessible, unstyled primitives with sensible defaults, deferred rendering for performance, and full compatibility with animation libraries.
3
+ Compound UI components built on [Base UI](https://base-ui.com/). Both Tooltip and Dropdown use a **global singleton architecture**: each trigger is a plain `<button>` with zero Base UI overhead, and a single renderer mounted at the app root owns the positioning and rendering.
4
4
 
5
- ## Components
5
+ ## Setup
6
6
 
7
- ### Tooltip
7
+ Mount both renderers once at the app root:
8
+
9
+ ```tsx
10
+ import { DropdownRenderer, TooltipRenderer } from "@slithy/base-ui";
11
+
12
+ function App() {
13
+ return (
14
+ <>
15
+ <DropdownRenderer />
16
+ <TooltipRenderer />
17
+ {/* rest of your app */}
18
+ </>
19
+ );
20
+ }
21
+ ```
22
+
23
+ ## Tooltip
8
24
 
9
25
  ```tsx
10
26
  import { Tooltip } from "@slithy/base-ui";
11
27
 
12
- <Tooltip.Provider>
13
- <Tooltip.Root>
14
- <Tooltip.Trigger>Hover me</Tooltip.Trigger>
15
- <Tooltip.Portal>
16
- <Tooltip.Positioner sideOffset={8}>
17
- <Tooltip.Popup>
18
- <Tooltip.Arrow />
19
- Tooltip content
20
- </Tooltip.Popup>
21
- </Tooltip.Positioner>
22
- </Tooltip.Portal>
23
- </Tooltip.Root>
24
- </Tooltip.Provider>
28
+ <Tooltip.Root>
29
+ <Tooltip.Trigger>Hover me</Tooltip.Trigger>
30
+ <Tooltip.Portal>
31
+ <Tooltip.Popup>
32
+ <Tooltip.Arrow />
33
+ Tooltip content
34
+ </Tooltip.Popup>
35
+ </Tooltip.Portal>
36
+ </Tooltip.Root>
25
37
  ```
26
38
 
27
- **Parts:** `Provider`, `Root`, `Trigger`, `Portal`, `Positioner`, `Popup`, `Arrow`
39
+ **Parts:** `Root`, `Trigger`, `Portal`, `Popup`, `Arrow`
28
40
 
29
- #### Props
41
+ ### Props
30
42
 
31
- **`Root`** accepts all Base UI Tooltip.Root props, plus:
43
+ **`Root`**
32
44
 
33
45
  | Prop | Type | Default | Description |
34
46
  |------|------|---------|-------------|
47
+ | `open` | `boolean` | — | Controlled open state. |
48
+ | `defaultOpen` | `boolean` | — | Open on first render (uncontrolled). |
49
+ | `disabled` | `boolean` | `false` | Prevent the tooltip from opening. |
35
50
  | `touchDisabled` | `boolean` | `true` | Block activation from touch interactions. Mouse/keyboard still work on hybrid devices. |
36
- | `unmountOnClose` | `boolean` | `false` | Unmount Base UI after close animation completes, returning to the lightweight pre-activation state. Useful for pages with many tooltips where accumulated mounted instances add up. |
37
-
38
- **`Trigger`** accepts all Base UI Tooltip.Trigger props, plus:
51
+ | `hoverable` | `boolean` | `false` | Allow hovering into the popup without closing (hover card behavior). |
52
+ | `delay` | `number` | `600` | Delay before opening in ms. |
53
+ | `closeDelay` | `number` | `300` | Delay before closing in ms. |
54
+ | `warmUpDelay` | `number` | `300` | If a tooltip closed within this window (ms), the next one opens instantly. |
55
+ | `side` | `"top" \| "bottom" \| "left" \| "right"` | `"top"` | Which side of the trigger to place the popup. |
56
+ | `sideOffset` | `number` | `6` | Distance between trigger and popup in pixels. |
57
+ | `align` | `"start" \| "center" \| "end"` | `"center"` | Alignment relative to the trigger. |
58
+ | `alignOffset` | `number` | `0` | Offset along the alignment axis in pixels. |
59
+ | `collisionPadding` | `number \| Partial<Record<Side, number>>` | `5` | Padding from viewport edges for collision detection. |
60
+
61
+ **`Trigger`**
39
62
 
40
63
  | Prop | Type | Default | Description |
41
64
  |------|------|---------|-------------|
42
- | `render` | `ReactElement \| (props, state) => ReactElement` | — | Replace the default `<button>` with a custom element. Works in both pre- and post-activation states. See [Custom trigger element](#custom-trigger-element). |
65
+ | `render` | `ReactElement \| (props) => ReactElement` | — | Replace the default `<button>` with a custom element. See [Custom trigger element](#custom-trigger-element). |
43
66
 
44
- ### Dropdown
67
+ ## Dropdown
45
68
 
46
69
  ```tsx
47
70
  import { Dropdown } from "@slithy/base-ui";
@@ -49,74 +72,88 @@ import { Dropdown } from "@slithy/base-ui";
49
72
  <Dropdown.Root>
50
73
  <Dropdown.Trigger>Open menu</Dropdown.Trigger>
51
74
  <Dropdown.Portal>
52
- <Dropdown.Positioner sideOffset={4}>
53
- <Dropdown.Popup>
54
- <Dropdown.Item onClick={handleEdit}>Edit</Dropdown.Item>
55
- <Dropdown.Item onClick={handleDuplicate}>Duplicate</Dropdown.Item>
56
- <Dropdown.Separator />
57
- <Dropdown.Item onClick={handleDelete}>Delete</Dropdown.Item>
58
- </Dropdown.Popup>
59
- </Dropdown.Positioner>
75
+ <Dropdown.Popup>
76
+ <Dropdown.Item onClick={handleEdit}>Edit</Dropdown.Item>
77
+ <Dropdown.Item onClick={handleDuplicate}>Duplicate</Dropdown.Item>
78
+ <Dropdown.Separator />
79
+ <Dropdown.Item onClick={handleDelete}>Delete</Dropdown.Item>
80
+ </Dropdown.Popup>
60
81
  </Dropdown.Portal>
61
82
  </Dropdown.Root>
62
83
  ```
63
84
 
64
- **Parts:** `Root`, `Trigger`, `Portal`, `Positioner`, `Popup`, `Arrow`, `Item`, `Separator`, `Group`, `GroupLabel`, `CheckboxItem`, `CheckboxItemIndicator`, `RadioGroup`, `RadioItem`, `RadioItemIndicator`
85
+ **Parts:** `Root`, `Trigger`, `Portal`, `Popup`, `Arrow`, `Item`, `Separator`, `Group`, `GroupLabel`, `CheckboxItem`, `CheckboxItemIndicator`, `RadioGroup`, `RadioItem`, `RadioItemIndicator`
65
86
 
66
- #### Disabling the dropdown
87
+ ### Tooltip on trigger
67
88
 
68
- Set `disabled` on `Root` to prevent the dropdown from opening while keeping the trigger interactive. This is useful when you want alternate behavior for the same trigger — for example, opening a modal on mobile instead of a dropdown:
89
+ `Dropdown.Trigger` can show a tooltip on hover/focus that is automatically dismissed when the dropdown opens:
69
90
 
70
91
  ```tsx
71
- const isMobile = useIsMobile();
72
- const [modalOpen, setModalOpen] = useState(false);
92
+ <Dropdown.Trigger tooltip="Edit, duplicate, or delete">
93
+ Actions
94
+ </Dropdown.Trigger>
95
+ ```
96
+
97
+ Requires `TooltipRenderer` to be mounted. Touch interactions do not trigger tooltips.
98
+
99
+ ### Disabling the dropdown
100
+
101
+ Set `disabled` on `Root` to prevent the dropdown from opening while keeping the trigger interactive:
73
102
 
103
+ ```tsx
74
104
  <Dropdown.Root disabled={isMobile}>
75
105
  <Dropdown.Trigger onClick={isMobile ? () => setModalOpen(true) : undefined}>
76
106
  Options
77
107
  </Dropdown.Trigger>
78
108
  <Dropdown.Portal>
79
- <Dropdown.Positioner sideOffset={4}>
80
- <Dropdown.Popup>
81
- <Dropdown.Item onClick={handleEdit}>Edit</Dropdown.Item>
82
- <Dropdown.Item onClick={handleDelete}>Delete</Dropdown.Item>
83
- </Dropdown.Popup>
84
- </Dropdown.Positioner>
109
+ <Dropdown.Popup>
110
+ <Dropdown.Item onClick={handleEdit}>Edit</Dropdown.Item>
111
+ </Dropdown.Popup>
85
112
  </Dropdown.Portal>
86
113
  </Dropdown.Root>
87
114
  ```
88
115
 
89
- Event handlers on `Trigger` (`onClick`, `onPointerDown`, `onKeyDown`) always fire, regardless of the `disabled` state — only the dropdown activation is suppressed.
90
-
91
- #### Props
116
+ ### Props
92
117
 
93
- **`Root`** accepts all Base UI Menu.Root props, plus:
118
+ **`Root`**
94
119
 
95
120
  | Prop | Type | Default | Description |
96
121
  |------|------|---------|-------------|
97
- | `disabled` | `boolean` | `false` | Prevent the dropdown from opening. The trigger button remains interactive so consumers can attach alternate behavior (e.g. opening a modal on mobile) via event handlers on `Trigger`. |
98
- | `unmountOnClose` | `boolean` | `false` | Unmount Base UI after close animation completes, returning to the lightweight pre-activation state. Useful for pages with many dropdowns where accumulated mounted instances add up. |
99
-
100
- **`Trigger`** accepts all Base UI Menu.Trigger props, plus:
122
+ | `open` | `boolean` | | Controlled open state. |
123
+ | `defaultOpen` | `boolean` | | Open on first render (uncontrolled). |
124
+ | `disabled` | `boolean` | `false` | Prevent the dropdown from opening. The trigger remains interactive. |
125
+ | `modal` | `boolean` | `true` | Whether the menu is modal (locks scroll, inerts page). When `false`, the popup auto-dismisses when the trigger scrolls out of view. |
126
+ | `openOn` | `"click" \| "pointerdown"` | `"click"` | Open on full click (mouseup) or on pointerdown (mousedown) for snappier response. |
127
+ | `side` | `"top" \| "bottom" \| "left" \| "right"` | `"bottom"` | Which side of the trigger to place the popup. |
128
+ | `sideOffset` | `number` | `4` | Distance between trigger and popup in pixels. |
129
+ | `align` | `"start" \| "center" \| "end"` | `"center"` | Alignment relative to the trigger. |
130
+ | `alignOffset` | `number` | `0` | Offset along the alignment axis in pixels. |
131
+ | `collisionPadding` | `number \| Partial<Record<Side, number>>` | `5` | Padding from viewport edges for collision detection. |
132
+ | `loopFocus` | `boolean` | `true` | Wrap keyboard focus from last item back to first (and vice versa). |
133
+ | `highlightItemOnHover` | `boolean` | `true` | Highlight items on pointer hover. Set to `false` to differentiate CSS `:hover` from keyboard `data-highlighted`. |
134
+ | `orientation` | `"vertical" \| "horizontal"` | `"vertical"` | Arrow key direction for navigation. |
135
+ | `onOpenChange` | `(open: boolean) => void` | — | Called when the menu opens or closes. |
136
+ | `onOpenChangeComplete` | `(open: boolean) => void` | — | Called after open/close animation completes. |
137
+
138
+ **`Trigger`**
101
139
 
102
140
  | Prop | Type | Default | Description |
103
141
  |------|------|---------|-------------|
104
- | `tooltip` | `ReactNode` | — | Show a tooltip on hover. The tooltip is deferred (not mounted until first hover) and automatically dismissed when the dropdown menu opens. |
105
- | `render` | `ReactElement \| (props, state) => ReactElement` | — | Replace the default `<button>` with a custom element. Works in both pre- and post-activation states. See [Custom trigger element](#custom-trigger-element). |
106
-
107
- ## Deferred rendering
142
+ | `render` | `ReactElement \| (props) => ReactElement` | — | Replace the default `<button>` with a custom element. See [Custom trigger element](#custom-trigger-element). |
143
+ | `tooltip` | `ReactNode` | — | Tooltip content shown on hover/focus. Dismissed when the dropdown opens. Requires `TooltipRenderer`. |
144
+ | `tooltipDelay` | `number` | `600` | Delay before opening the tooltip in ms. |
108
145
 
109
- Both components use a deferred rendering pattern that avoids mounting Base UI's hooks and floating-ui positioning until the user first interacts with the trigger. Before activation, the trigger renders as a plain `<button>` and portal content is not mounted. This keeps initial JS overhead minimal when rendering many tooltips or dropdowns on a page.
146
+ ## Architecture
110
147
 
111
- The activation latch is one-way by default: once triggered, it stays active so that leave animations can play. Controlled `open` or `defaultOpen` bypass the latch entirely.
148
+ Both Tooltip and Dropdown use a **global singleton** pattern. Each trigger is a plain `<button>` with ARIA attributes no Base UI hooks, no floating-ui, no portal. A single renderer (`TooltipRenderer` / `DropdownRenderer`) mounted at the app root subscribes to a global store and renders the active popup anchored to whichever trigger activated it.
112
149
 
113
- Set `unmountOnClose` on Root to reset the latch after the close animation completes. This returns the component to its lightweight pre-activation state, freeing Base UI hooks and floating-ui listeners. Useful for long-lived pages with many tooltips or dropdowns where accumulated mounted instances add up.
150
+ Only one tooltip and one dropdown can be open at a time. 200 triggers on a page have the same overhead as 1.
114
151
 
115
152
  ## Custom trigger element
116
153
 
117
- Both `Tooltip.Trigger` and `Dropdown.Trigger` accept a `render` prop to replace the default `<button>` with a custom element. This works across both pre- and post-activation states — the deferred rendering layer applies the same prop to whichever element is currently rendered.
154
+ Both `Tooltip.Trigger` and `Dropdown.Trigger` accept a `render` prop to replace the default `<button>` with a custom element.
118
155
 
119
- **Element form** — pass a React element. All required event handlers, `ref`, `className`, `style`, and `disabled` are merged in via `cloneElement`. Existing props on the element are preserved for any key that isn't overridden:
156
+ **Element form** — pass a React element:
120
157
 
121
158
  ```tsx
122
159
  <Dropdown.Trigger render={<MyButton variant="ghost" />}>
@@ -124,27 +161,15 @@ Both `Tooltip.Trigger` and `Dropdown.Trigger` accept a `render` prop to replace
124
161
  </Dropdown.Trigger>
125
162
  ```
126
163
 
127
- **Function form** — receive the full props object and spread it onto your element. Use this form when you need to compose your own handlers alongside the trigger's, or when your component needs additional logic:
164
+ **Function form** — receive the full props object:
128
165
 
129
166
  ```tsx
130
- <Tooltip.Trigger
131
- render={(props) => (
132
- <MyButton
133
- {...props}
134
- onFocus={(e) => {
135
- props.onFocus?.(e);
136
- trackFocusEvent();
137
- }}
138
- />
139
- )}
140
- >
141
- Hover me
142
- </Tooltip.Trigger>
167
+ <Dropdown.Trigger render={(props) => <MyButton {...props} />}>
168
+ Open menu
169
+ </Dropdown.Trigger>
143
170
  ```
144
171
 
145
- > **Note:** For the element form, event handlers defined on the render element (e.g. `<MyButton onClick={...} />`) are overridden by the trigger's activation handlers. Use the function form if you need to run your own handlers alongside them.
146
-
147
- Your custom component must forward `ref` to its underlying DOM element so that focus restoration and synthetic event dispatch work correctly after activation.
172
+ Your custom component must forward `ref` to its underlying DOM element.
148
173
 
149
174
  ## Styling
150
175
 
@@ -175,56 +200,6 @@ Each component applies default class names (`slithy-tooltip-*`, `slithy-dropdown
175
200
  --slithy-dropdown-group-label-color
176
201
  ```
177
202
 
178
- ## Positioning
179
-
180
- Both components pass all props through to Base UI's Positioner, which wraps floating-ui internally. Common positioning props:
181
-
182
- ```tsx
183
- <Tooltip.Positioner
184
- side="bottom" // "top" | "bottom" | "left" | "right"
185
- sideOffset={8} // distance from trigger (px)
186
- align="center" // "start" | "center" | "end"
187
- alignOffset={0} // alignment offset (px)
188
- collisionPadding={5} // viewport edge padding (px)
189
- />
190
- ```
191
-
192
203
  ## Custom animations
193
204
 
194
- The `Popup` and `Positioner` components accept a `render` prop (from Base UI) that lets you replace the rendered element. (The `Trigger` `render` prop is documented separately under [Custom trigger element](#custom-trigger-element).) This makes it straightforward to use animation libraries like react-spring:
195
-
196
- ```tsx
197
- import { useTransition, animated } from "@react-spring/web";
198
-
199
- function AnimatedTooltip({ children }: { children: React.ReactNode }) {
200
- return (
201
- <Tooltip.Provider>
202
- <Tooltip.Root>
203
- <Tooltip.Trigger>Hover me</Tooltip.Trigger>
204
- <Tooltip.Portal>
205
- <Tooltip.Positioner sideOffset={8}>
206
- <Tooltip.Popup
207
- render={(props) => (
208
- <animated.div
209
- {...props}
210
- style={{
211
- ...props.style,
212
- opacity: /* your spring value */,
213
- transform: /* your spring value */,
214
- }}
215
- />
216
- )}
217
- >
218
- {children}
219
- </Tooltip.Popup>
220
- </Tooltip.Positioner>
221
- </Tooltip.Portal>
222
- </Tooltip.Root>
223
- </Tooltip.Provider>
224
- );
225
- }
226
- ```
227
-
228
- Base UI exposes `data-open`, `data-starting-style`, and `data-ending-style` attributes on popup elements, and `onOpenChangeComplete` on Root, so you can drive enter/leave animations from open state and signal completion.
229
-
230
- The deferred rendering layer does not interfere with animations -- by the time they run, the activation latch has flipped and Base UI is fully mounted.
205
+ Base UI exposes `data-open`, `data-starting-style`, and `data-ending-style` attributes on popup elements, so you can drive enter/leave animations from open state. The default styles use CSS transitions with these attributes.
package/dist/index.d.ts CHANGED
@@ -1,30 +1,63 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { Menu } from '@base-ui/react/menu';
3
- import * as react from 'react';
4
- import { ComponentPropsWithoutRef, ReactNode } from 'react';
5
- import * as _base_ui_react from '@base-ui/react';
3
+ import { ReactNode, ReactElement, ComponentPropsWithoutRef } from 'react';
6
4
  import { Tooltip as Tooltip$1 } from '@base-ui/react/tooltip';
7
5
 
8
- type RootProps$1 = ComponentPropsWithoutRef<typeof Menu.Root> & {
6
+ type DropdownPositionConfig = {
7
+ side?: "top" | "bottom" | "left" | "right" | "inline-end" | "inline-start";
8
+ sideOffset?: number;
9
+ align?: "start" | "center" | "end";
10
+ alignOffset?: number;
11
+ collisionPadding?: number | Partial<Record<"top" | "right" | "bottom" | "left", number>>;
12
+ };
13
+
14
+ type RootProps$1 = {
9
15
  children?: ReactNode;
10
- /** When `true`, the dropdown will not open. The trigger button remains
11
- * interactive so consumers can attach alternate behavior (e.g. opening
12
- * a modal on mobile) via event handlers on `Dropdown.Trigger`. */
16
+ /** Controlled open state. */
17
+ open?: boolean;
18
+ /** Open on first render (uncontrolled). */
19
+ defaultOpen?: boolean;
13
20
  disabled?: boolean;
14
- /** Unmount Base UI after close animation completes, returning to the
15
- * lightweight pre-activation state. Defaults to `false`. */
16
- unmountOnClose?: boolean;
21
+ /** Whether the menu is modal (locks scroll, inerts page). @default true */
22
+ modal?: boolean;
23
+ /** Fire the open/close toggle on `"click"` (mouseup) or `"pointerdown"` (mousedown). @default "click" */
24
+ openOn?: "click" | "pointerdown";
25
+ /** Wrap keyboard focus from last item back to first (and vice versa). @default true */
26
+ loopFocus?: boolean;
27
+ /** Highlight items on pointer hover (`data-highlighted`). @default true */
28
+ highlightItemOnHover?: boolean;
29
+ /** Arrow key direction: `"vertical"` (up/down) or `"horizontal"` (left/right). @default "vertical" */
30
+ orientation?: "vertical" | "horizontal";
31
+ /** Which side of the trigger to place the popup. @default "bottom" */
32
+ side?: DropdownPositionConfig["side"];
33
+ /** Distance between trigger and popup in pixels. @default 4 */
34
+ sideOffset?: number;
35
+ /** Alignment relative to the trigger. @default "center" */
36
+ align?: DropdownPositionConfig["align"];
37
+ /** Offset along the alignment axis in pixels. @default 0 */
38
+ alignOffset?: number;
39
+ /** Padding from viewport edges for collision detection. @default 5 */
40
+ collisionPadding?: DropdownPositionConfig["collisionPadding"];
41
+ onOpenChange?: (open: boolean) => void;
42
+ onOpenChangeComplete?: (open: boolean) => void;
17
43
  };
18
- declare function Root$1({ children, open, defaultOpen, disabled, unmountOnClose, onOpenChangeComplete, ...props }: RootProps$1): react_jsx_runtime.JSX.Element;
19
- type TriggerProps$1 = ComponentPropsWithoutRef<typeof Menu.Trigger> & {
20
- /** Show a tooltip on hover. Accepts any ReactNode content. */
44
+ declare function Root$1({ children, open, defaultOpen, disabled, modal, openOn, loopFocus, highlightItemOnHover, orientation, side, sideOffset, align, alignOffset, collisionPadding, onOpenChange, onOpenChangeComplete }: RootProps$1): react_jsx_runtime.JSX.Element;
45
+ type TriggerProps$1 = React.ButtonHTMLAttributes<HTMLButtonElement> & {
46
+ children?: ReactNode;
47
+ /** Replace the default `<button>` with a custom element. */
48
+ render?: ReactElement | ((props: Record<string, unknown>) => ReactElement);
49
+ /** Tooltip content shown on hover/focus. Dismissed when the dropdown opens. */
21
50
  tooltip?: ReactNode;
51
+ /** Delay before opening the tooltip in ms. @default 600 */
52
+ tooltipDelay?: number;
53
+ };
54
+ declare function Trigger$1({ children, onClick, onPointerDown, onPointerEnter, onPointerLeave, onFocus, onBlur, onKeyDown, render, tooltip, tooltipDelay, ...props }: TriggerProps$1): react_jsx_runtime.JSX.Element;
55
+ type PortalProps$1 = {
56
+ children?: ReactNode;
22
57
  };
23
- declare function Trigger$1({ children, className, style, disabled, tooltip, ...rest }: TriggerProps$1): react_jsx_runtime.JSX.Element;
24
- type PortalProps$1 = ComponentPropsWithoutRef<typeof Menu.Portal>;
25
- declare function Portal$1(props: PortalProps$1): react_jsx_runtime.JSX.Element | null;
26
- type PositionerProps$1 = ComponentPropsWithoutRef<typeof Menu.Positioner>;
27
- declare function Positioner$1({ className, ...props }: PositionerProps$1): react_jsx_runtime.JSX.Element;
58
+ declare function Portal$1({ children }: PortalProps$1): null;
59
+ type PositionerProps = ComponentPropsWithoutRef<typeof Menu.Positioner>;
60
+ declare function Positioner({ className, ...props }: PositionerProps): react_jsx_runtime.JSX.Element;
28
61
  type PopupProps$1 = ComponentPropsWithoutRef<typeof Menu.Popup>;
29
62
  declare function Popup$1({ className, ...props }: PopupProps$1): react_jsx_runtime.JSX.Element;
30
63
  type ArrowProps$1 = ComponentPropsWithoutRef<typeof Menu.Arrow>;
@@ -51,7 +84,7 @@ declare const Dropdown: {
51
84
  Root: typeof Root$1;
52
85
  Trigger: typeof Trigger$1;
53
86
  Portal: typeof Portal$1;
54
- Positioner: typeof Positioner$1;
87
+ Positioner: typeof Positioner;
55
88
  Popup: typeof Popup$1;
56
89
  Arrow: typeof Arrow$1;
57
90
  Item: typeof Item;
@@ -65,33 +98,83 @@ declare const Dropdown: {
65
98
  RadioItemIndicator: typeof RadioItemIndicator;
66
99
  };
67
100
 
68
- type RootProps = ComponentPropsWithoutRef<typeof Tooltip$1.Root> & {
101
+ /**
102
+ * Singleton renderer — mount once at the app root.
103
+ * Subscribes to the global DropdownStore and renders the active dropdown
104
+ * using Base UI's Menu.Root + Menu.Positioner.
105
+ *
106
+ * Menu.Root stays mounted so CSS transitions can play on both open and close.
107
+ * Content and anchor are swapped when a new dropdown opens.
108
+ */
109
+ declare function DropdownRenderer(): react_jsx_runtime.JSX.Element | null;
110
+
111
+ type TooltipPositionConfig = {
112
+ side?: "top" | "bottom" | "left" | "right" | "inline-end" | "inline-start";
113
+ sideOffset?: number;
114
+ align?: "start" | "center" | "end";
115
+ alignOffset?: number;
116
+ collisionPadding?: number | Partial<Record<"top" | "right" | "bottom" | "left", number>>;
117
+ };
118
+
119
+ type RootProps = {
69
120
  children?: ReactNode;
70
- /** Block activation from touch interactions. Defaults to `true`. */
121
+ /** Controlled open state. */
122
+ open?: boolean;
123
+ /** Open on first render (uncontrolled). */
124
+ defaultOpen?: boolean;
125
+ disabled?: boolean;
126
+ /** Block activation from touch interactions. @default true */
71
127
  touchDisabled?: boolean;
72
- /** Unmount Base UI after close animation completes, returning to the
73
- * lightweight pre-activation state. Defaults to `false`. */
74
- unmountOnClose?: boolean;
128
+ /** Allow hovering into the popup without closing (hover card behavior). @default false */
129
+ hoverable?: boolean;
130
+ /** Delay before opening in ms. @default 600 */
131
+ delay?: number;
132
+ /** Delay before closing in ms. @default 300 */
133
+ closeDelay?: number;
134
+ /** Warm-up window in ms. If a tooltip closed within this window,
135
+ * the next one opens instantly. @default 300 */
136
+ warmUpDelay?: number;
137
+ /** Which side of the trigger to place the popup. @default "top" */
138
+ side?: TooltipPositionConfig["side"];
139
+ /** Distance between trigger and popup in pixels. @default 6 */
140
+ sideOffset?: number;
141
+ /** Alignment relative to the trigger. @default "center" */
142
+ align?: TooltipPositionConfig["align"];
143
+ /** Offset along the alignment axis in pixels. @default 0 */
144
+ alignOffset?: number;
145
+ /** Padding from viewport edges for collision detection. @default 5 */
146
+ collisionPadding?: TooltipPositionConfig["collisionPadding"];
75
147
  };
76
- declare function Root({ children, open, defaultOpen, disabled, touchDisabled, unmountOnClose, onOpenChangeComplete, ...props }: RootProps): react_jsx_runtime.JSX.Element;
77
- type TriggerProps = ComponentPropsWithoutRef<typeof Tooltip$1.Trigger>;
78
- declare function Trigger({ children, className, style, disabled, ...rest }: TriggerProps): react_jsx_runtime.JSX.Element;
79
- type PortalProps = ComponentPropsWithoutRef<typeof Tooltip$1.Portal>;
80
- declare function Portal(props: PortalProps): react_jsx_runtime.JSX.Element | null;
81
- type PositionerProps = ComponentPropsWithoutRef<typeof Tooltip$1.Positioner>;
82
- declare function Positioner({ className, ...props }: PositionerProps): react_jsx_runtime.JSX.Element;
148
+ declare function Root({ children, open, defaultOpen, disabled, touchDisabled, hoverable, delay, closeDelay, warmUpDelay, side, sideOffset, align, alignOffset, collisionPadding, }: RootProps): react_jsx_runtime.JSX.Element;
149
+ type TriggerProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
150
+ children?: ReactNode;
151
+ /** Replace the default `<button>` with a custom element. */
152
+ render?: ReactElement | ((props: Record<string, unknown>) => ReactElement);
153
+ };
154
+ declare function Trigger({ children, render, ...props }: TriggerProps): react_jsx_runtime.JSX.Element;
155
+ type PortalProps = {
156
+ children?: ReactNode;
157
+ };
158
+ declare function Portal({ children }: PortalProps): null;
83
159
  type PopupProps = ComponentPropsWithoutRef<typeof Tooltip$1.Popup>;
84
- declare function Popup({ className, ...props }: PopupProps): react_jsx_runtime.JSX.Element;
160
+ declare function Popup({ className, id, ...props }: PopupProps): react_jsx_runtime.JSX.Element;
85
161
  type ArrowProps = ComponentPropsWithoutRef<typeof Tooltip$1.Arrow>;
86
162
  declare function Arrow({ className, ...props }: ArrowProps): react_jsx_runtime.JSX.Element;
87
163
  declare const Tooltip: {
88
- Provider: react.FC<_base_ui_react.TooltipProviderProps>;
89
164
  Root: typeof Root;
90
165
  Trigger: typeof Trigger;
91
166
  Portal: typeof Portal;
92
- Positioner: typeof Positioner;
93
167
  Popup: typeof Popup;
94
168
  Arrow: typeof Arrow;
95
169
  };
96
170
 
97
- export { Dropdown, Tooltip };
171
+ /**
172
+ * Singleton renderer — mount once at the app root.
173
+ * Subscribes to the global TooltipStore and renders the active tooltip
174
+ * using Base UI's Tooltip.Root + Tooltip.Positioner.
175
+ *
176
+ * Tooltip.Root stays mounted so CSS transitions can play on both open and close.
177
+ */
178
+ declare function TooltipRenderer(): react_jsx_runtime.JSX.Element | null;
179
+
180
+ export { Dropdown, DropdownRenderer, Tooltip, TooltipRenderer };