@rozie-ui/popover-react 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 One Learning Community LTD
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # @rozie-ui/popover-react
2
+
3
+ Idiomatic **react** `Popover` — a headless floating primitive for tooltips and popovers, wrapping [`@floating-ui/dom`](https://floating-ui.com) for collision-aware positioning (offset / flip / shift / arrow) with live `autoUpdate` tracking. You bring the anchor (the `anchor` slot) and the floating content (the default slot); Popover owns placement, the open/close gesture (`trigger`: click / hover / focus), dismissal (Escape + click-outside), the WAI-ARIA wiring (tooltip vs dialog), and a two-way `open` model — compiled from one [Rozie](https://github.com/One-Learning-Community/rozie.js) source. Every visual value is a CSS custom property, so it re-skins to any design system. This package is generated; do not edit `src/` by hand.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i @rozie-ui/popover-react @floating-ui/dom
9
+ ```
10
+
11
+ Peer dependencies: `react + react-dom + @floating-ui/dom`. Install them alongside this package.
12
+
13
+ ## Usage
14
+
15
+ ```tsx
16
+ import { useState } from 'react';
17
+ import { Popover } from '@rozie-ui/popover-react';
18
+ import '@floating-ui/dom'; // peer engine — installed alongside this package
19
+
20
+ export function Demo() {
21
+ const [open, setOpen] = useState(false);
22
+ return (
23
+ <Popover
24
+ open={open}
25
+ onOpenChange={setOpen}
26
+ trigger="click"
27
+ placement="bottom"
28
+ offset={8}
29
+ arrow
30
+ onChange={(next) => console.log('open:', next)}
31
+ anchor={({ toggle }) => <button onClick={toggle}>Menu</button>}
32
+ >
33
+ <div>Floating content</div>
34
+ </Popover>
35
+ );
36
+ }
37
+ ```
38
+
39
+ ## Theming
40
+
41
+ Every visual value is a `--rozie-popover-*` CSS custom property (background, border, radius, shadow, padding, z-index, max-width, arrow size) — override any of them at any ancestor scope to match your design system.
42
+
43
+ ## Props
44
+
45
+ | Name | Type | Default | Two-way (model) | Required | Description |
46
+ | --- | --- | --- | :---: | :---: | --- |
47
+ | `open` | `Boolean` | `false` | ✓ | | Whether the floating content is open. The sole `model: true` prop — two-way bind it (`r-model:open` / `v-model:open` / `bind:open` / `[(open)]`) and Popover writes the new state back whenever the trigger or a dismissal toggles it. Left unbound it falls back to an uncontrolled default. |
48
+ | `placement` | `String` | `"bottom"` | | | Floating UI placement of the content relative to the anchor — one of `top`/`right`/`bottom`/`left`, each optionally suffixed `-start`/`-end` (e.g. `bottom-start`). With `disableFlip` off, the content may flip to the opposite side when it would overflow the viewport. Reconciled at runtime. |
49
+ | `trigger` | `String` | `"click"` | | | How the anchor opens the content: `'click'` toggles on click, `'hover'` opens on pointer-enter and closes on pointer-leave (tooltip-style), `'focus'` opens on focus and closes on blur. Drives both the gesture handlers and the ARIA role (`'hover'`/`'focus'` → tooltip, `'click'` → popover dialog). |
50
+ | `offset` | `Number` | `8` | | | Distance in pixels between the anchor and the floating content (the Floating UI `offset` middleware). Reconciled at runtime. |
51
+ | `disableFlip` | `Boolean` | `false` | | | Disable the Floating UI `flip` middleware. By default the content flips to the opposite side of the anchor when it would overflow the viewport; set this to keep it pinned to `placement` regardless. |
52
+ | `disableShift` | `Boolean` | `false` | | | Disable the Floating UI `shift` middleware. By default the content shifts along its axis to stay within the viewport; set this to keep it strictly aligned to the anchor. |
53
+ | `arrow` | `Boolean` | `false` | | | Opt in to a positioned arrow element. When set, Popover renders an arrow `<div>` and runs the Floating UI `arrow` middleware against it so it points at the anchor. Style it via the `--rozie-popover-*` arrow CSS custom properties. |
54
+ | `disabled` | `Boolean` | `false` | | | Disable the control entirely: the trigger no longer opens the content and any open content is suppressed. |
55
+ | `modal` | `Boolean` | `false` | | | Opt in to modal dialog semantics for a `click` popover. **Off by default:** a click popover is a non-modal, click-outside-dismissable layer, so its panel is rendered role-neutral (the slot content owns its own ARIA role — e.g. a `role="menu"`) and carries NO `aria-modal`. Set `modal` for a genuinely modal dialog popover: the panel then gets `role="dialog"` + `aria-modal="true"`. **Note:** Popover ships no focus trap (it stays a minimal headless primitive); if you set `modal`, provide your own focus containment so the `aria-modal` claim holds. Ignored for `hover`/`focus` triggers (always tooltip-flavored). |
56
+ | `strategy` | `String` | `"absolute"` | | | Floating UI positioning strategy — 'absolute' (default) or 'fixed'. Use 'fixed' to escape a scrollable/overflow-clipping ancestor (e.g. a sticky table header). Reconciled at runtime. |
57
+
58
+ ## Events
59
+
60
+ | Event | Description |
61
+ | --- | --- |
62
+ | `change` | Fired whenever the open state changes — a click/hover/focus trigger gesture, an Escape or click-outside dismissal, or a programmatic `show`/`hide`/`toggle`. Payload is the new `open` boolean. The two-way `open` model is updated alongside it. |
63
+
64
+ ## Imperative handle
65
+
66
+ Beyond props, the component exposes imperative methods (declared once in the Rozie source via `$expose`). Grab a handle with the native ref mechanism and call them directly:
67
+
68
+ | Method | Description |
69
+ | --- | --- |
70
+ | `show` | Open the floating content (no-op when `disabled`). Emits `change` and updates the `open` model. |
71
+ | `hide` | Close the floating content. Emits `change` and updates the `open` model. |
72
+ | `toggle` | Flip the open state (no-op when `disabled`). Emits `change` and updates the `open` model. |
73
+ | `reposition` | Recompute the floating position immediately (the Floating UI `computePosition` pass). Useful after content size changes that `autoUpdate` does not observe. |
74
+
75
+ ```tsx
76
+ import { useRef } from 'react';
77
+ import { Popover, type PopoverHandle } from '@rozie-ui/popover-react';
78
+
79
+ const pop = useRef<PopoverHandle>(null);
80
+ // <Popover ref={pop} ... />
81
+ pop.current?.show();
82
+ pop.current?.hide();
83
+ pop.current?.toggle();
84
+ pop.current?.reposition();
85
+ ```
86
+
87
+ ## Slots
88
+
89
+ | Slot | Params |
90
+ | --- | --- |
91
+ | anchor | open, toggle, show, hide |
92
+ | (default) | |
@@ -0,0 +1,28 @@
1
+ .rozie-popover[data-rozie-s-c6cf02ea] {
2
+ display: contents;
3
+ }
4
+ .rozie-popover-anchor[data-rozie-s-c6cf02ea] {
5
+ display: inline-block;
6
+ }
7
+ .rozie-popover-floating[data-rozie-s-c6cf02ea] {
8
+ position: absolute;
9
+ left: 0;
10
+ top: 0;
11
+ z-index: var(--rozie-popover-z, 1000);
12
+ width: max-content;
13
+ max-width: var(--rozie-popover-max-width, calc(100vw - 16px));
14
+ background: var(--rozie-popover-bg, #fff);
15
+ color: var(--rozie-popover-color, inherit);
16
+ border: var(--rozie-popover-border, 1px solid rgba(0, 0, 0, 0.12));
17
+ border-radius: var(--rozie-popover-radius, 8px);
18
+ box-shadow: var(--rozie-popover-shadow, 0 8px 24px rgba(0, 0, 0, 0.12));
19
+ padding: var(--rozie-popover-padding, 8px 12px);
20
+ }
21
+ .rozie-popover-arrow[data-rozie-s-c6cf02ea] {
22
+ position: absolute;
23
+ width: var(--rozie-popover-arrow-size, 8px);
24
+ height: var(--rozie-popover-arrow-size, 8px);
25
+ background: var(--rozie-popover-bg, #fff);
26
+ border: var(--rozie-popover-border, 1px solid rgba(0, 0, 0, 0.12));
27
+ transform: rotate(45deg);
28
+ }
package/dist/index.cjs ADDED
@@ -0,0 +1,307 @@
1
+ Object.defineProperties(exports, {
2
+ __esModule: { value: true },
3
+ [Symbol.toStringTag]: { value: "Module" }
4
+ });
5
+ let react = require("react");
6
+ let _rozie_runtime_react = require("@rozie/runtime-react");
7
+ require("./Popover.css");
8
+ let _floating_ui_dom = require("@floating-ui/dom");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ //#region src/internal/middleware.ts
11
+ /**
12
+ * Build the ordered middleware array for `computePosition`. Pure — no engine
13
+ * import, no DOM read beyond the passed-in arrow element.
14
+ */
15
+ function buildMiddleware(factories, config) {
16
+ const mw = [factories.offset(config.offset)];
17
+ if (!config.disableFlip) mw.push(factories.flip());
18
+ if (!config.disableShift) mw.push(factories.shift());
19
+ if (config.arrow && config.arrowEl) mw.push(factories.arrow({ element: config.arrowEl }));
20
+ return mw;
21
+ }
22
+ //#endregion
23
+ //#region src/Popover.tsx
24
+ const Popover = (0, react.forwardRef)(function Popover(_props, ref) {
25
+ const props = {
26
+ ..._props,
27
+ placement: _props.placement ?? "bottom",
28
+ trigger: _props.trigger ?? "click",
29
+ offset: _props.offset ?? 8,
30
+ disableFlip: _props.disableFlip ?? false,
31
+ disableShift: _props.disableShift ?? false,
32
+ arrow: _props.arrow ?? false,
33
+ disabled: _props.disabled ?? false,
34
+ modal: _props.modal ?? false,
35
+ strategy: _props.strategy ?? "absolute"
36
+ };
37
+ const attrs = (() => {
38
+ const { open, placement, trigger, offset, disableFlip, disableShift, arrow, disabled, modal, strategy, defaultValue, onOpenChange, defaultOpen, ...rest } = _props;
39
+ return rest;
40
+ })();
41
+ const anchorNode = (0, react.useRef)(null);
42
+ const floatingNode = (0, react.useRef)(null);
43
+ const arrowNode = (0, react.useRef)(null);
44
+ const stopAutoUpdate = (0, react.useRef)(null);
45
+ const lastFocusedEl = (0, react.useRef)(null);
46
+ const [open, setOpen] = (0, _rozie_runtime_react.useControllableState)({
47
+ value: props.open,
48
+ defaultValue: props.defaultOpen ?? false,
49
+ onValueChange: props.onOpenChange
50
+ });
51
+ const _openRef = (0, react.useRef)(open);
52
+ _openRef.current = open;
53
+ const anchorEl = (0, react.useRef)(null);
54
+ const floatingEl = (0, react.useRef)(null);
55
+ const arrowEl = (0, react.useRef)(null);
56
+ const _watch0First = (0, react.useRef)(true);
57
+ const _watch1First = (0, react.useRef)(true);
58
+ const _watch2First = (0, react.useRef)(true);
59
+ const _watch3First = (0, react.useRef)(true);
60
+ const _watch4First = (0, react.useRef)(true);
61
+ const _watch5First = (0, react.useRef)(true);
62
+ function deepActiveElement() {
63
+ let el = document.activeElement;
64
+ while (el && el.shadowRoot && el.shadowRoot.activeElement) el = el.shadowRoot.activeElement;
65
+ return el;
66
+ }
67
+ function requestOpen(next) {
68
+ if (open === next) return;
69
+ if (next && props.trigger === "click") lastFocusedEl.current = deepActiveElement();
70
+ setOpen(next);
71
+ props.onChange && props.onChange(next);
72
+ if (!next && props.trigger === "click" && lastFocusedEl.current && lastFocusedEl.current.isConnected && typeof lastFocusedEl.current.focus === "function") lastFocusedEl.current.focus();
73
+ if (!next) lastFocusedEl.current = null;
74
+ }
75
+ function applyPosition(x, y, middlewareData) {
76
+ if (!floatingNode.current) return;
77
+ floatingNode.current.style.left = x + "px";
78
+ floatingNode.current.style.top = y + "px";
79
+ if (arrowNode.current && middlewareData && middlewareData.arrow) {
80
+ const ax = middlewareData.arrow.x;
81
+ const ay = middlewareData.arrow.y;
82
+ arrowNode.current.style.left = ax == null ? "" : ax + "px";
83
+ arrowNode.current.style.top = ay == null ? "" : ay + "px";
84
+ }
85
+ }
86
+ function position() {
87
+ if (!anchorNode.current || !floatingNode.current) return;
88
+ const middleware = buildMiddleware({
89
+ offset: _floating_ui_dom.offset,
90
+ flip: _floating_ui_dom.flip,
91
+ shift: _floating_ui_dom.shift,
92
+ arrow: _floating_ui_dom.arrow
93
+ }, {
94
+ offset: props.offset,
95
+ disableFlip: props.disableFlip,
96
+ disableShift: props.disableShift,
97
+ arrow: props.arrow,
98
+ arrowEl: arrowNode.current
99
+ });
100
+ if (props.strategy === "fixed") floatingNode.current.style.position = "fixed";
101
+ else floatingNode.current.style.position = "";
102
+ let opts = null;
103
+ opts = {
104
+ placement: props.placement,
105
+ strategy: props.strategy,
106
+ middleware
107
+ };
108
+ (0, _floating_ui_dom.computePosition)(anchorNode.current, floatingNode.current, opts).then((result) => {
109
+ applyPosition(result.x, result.y, result.middlewareData);
110
+ });
111
+ }
112
+ const startTracking = (0, react.useCallback)(() => {
113
+ if (!anchorNode.current || !floatingNode.current) return;
114
+ if (stopAutoUpdate.current) {
115
+ stopAutoUpdate.current();
116
+ stopAutoUpdate.current = null;
117
+ }
118
+ stopAutoUpdate.current = (0, _floating_ui_dom.autoUpdate)(anchorNode.current, floatingNode.current, position);
119
+ }, [position]);
120
+ const stopTracking = (0, react.useCallback)(() => {
121
+ if (stopAutoUpdate.current) {
122
+ stopAutoUpdate.current();
123
+ stopAutoUpdate.current = null;
124
+ }
125
+ }, []);
126
+ const onAnchorClick = (0, react.useCallback)(() => {
127
+ if (props.disabled) return;
128
+ requestOpen(!open);
129
+ }, [
130
+ open,
131
+ props.disabled,
132
+ requestOpen
133
+ ]);
134
+ const onAnchorPointerEnter = (0, react.useCallback)(() => {
135
+ if (props.disabled) return;
136
+ requestOpen(true);
137
+ }, [props.disabled, requestOpen]);
138
+ const onAnchorPointerLeave = (0, react.useCallback)(() => {
139
+ if (props.disabled) return;
140
+ requestOpen(false);
141
+ }, [props.disabled, requestOpen]);
142
+ const onAnchorFocus = (0, react.useCallback)(() => {
143
+ if (props.disabled) return;
144
+ requestOpen(true);
145
+ }, [props.disabled, requestOpen]);
146
+ const onAnchorBlur = (0, react.useCallback)(() => {
147
+ if (props.disabled) return;
148
+ requestOpen(false);
149
+ }, [props.disabled, requestOpen]);
150
+ const dismiss = (0, react.useCallback)(() => {
151
+ requestOpen(false);
152
+ }, [requestOpen]);
153
+ function isTooltip() {
154
+ return props.trigger === "hover" || props.trigger === "focus";
155
+ }
156
+ function floatingRole() {
157
+ return isTooltip() ? "tooltip" : props.modal ? "dialog" : void 0;
158
+ }
159
+ function show() {
160
+ if (!props.disabled) requestOpen(true);
161
+ }
162
+ function hide() {
163
+ requestOpen(false);
164
+ }
165
+ function toggle() {
166
+ if (!props.disabled) requestOpen(!open);
167
+ }
168
+ function reposition() {
169
+ position();
170
+ }
171
+ (0, react.useEffect)(() => {
172
+ anchorNode.current = anchorEl.current;
173
+ if (_openRef.current && !props.disabled) {
174
+ floatingNode.current = floatingEl.current;
175
+ arrowNode.current = arrowEl.current;
176
+ startTracking();
177
+ }
178
+ return () => {
179
+ stopTracking();
180
+ };
181
+ }, []);
182
+ (0, react.useEffect)(() => {
183
+ if (_watch0First.current) {
184
+ _watch0First.current = false;
185
+ return;
186
+ }
187
+ if (open && !props.disabled) queueMicrotask(() => {
188
+ if (!open || props.disabled) return;
189
+ floatingNode.current = floatingEl.current;
190
+ arrowNode.current = arrowEl.current;
191
+ startTracking();
192
+ });
193
+ else stopTracking();
194
+ }, [open]);
195
+ (0, react.useEffect)(() => {
196
+ if (_watch1First.current) {
197
+ _watch1First.current = false;
198
+ return;
199
+ }
200
+ if (open) position();
201
+ }, [props.placement]);
202
+ (0, react.useEffect)(() => {
203
+ if (_watch2First.current) {
204
+ _watch2First.current = false;
205
+ return;
206
+ }
207
+ if (open) position();
208
+ }, [props.offset]);
209
+ (0, react.useEffect)(() => {
210
+ if (_watch3First.current) {
211
+ _watch3First.current = false;
212
+ return;
213
+ }
214
+ if (open) position();
215
+ }, [props.disableFlip]);
216
+ (0, react.useEffect)(() => {
217
+ if (_watch4First.current) {
218
+ _watch4First.current = false;
219
+ return;
220
+ }
221
+ if (open) position();
222
+ }, [props.disableShift]);
223
+ (0, react.useEffect)(() => {
224
+ if (_watch5First.current) {
225
+ _watch5First.current = false;
226
+ return;
227
+ }
228
+ if (open) position();
229
+ }, [props.strategy]);
230
+ (0, react.useEffect)(() => {
231
+ if (!open) return;
232
+ const _rozieHandler = ($event) => {
233
+ if ($event.key !== "Escape") return;
234
+ dismiss($event);
235
+ };
236
+ document.addEventListener("keydown", _rozieHandler);
237
+ return () => document.removeEventListener("keydown", _rozieHandler);
238
+ }, [dismiss, open]);
239
+ (0, _rozie_runtime_react.useOutsideClick)([anchorEl, floatingEl], dismiss, () => !!open);
240
+ const _rozieExposeRef = (0, react.useRef)({
241
+ show,
242
+ hide,
243
+ toggle,
244
+ reposition
245
+ });
246
+ _rozieExposeRef.current = {
247
+ show,
248
+ hide,
249
+ toggle,
250
+ reposition
251
+ };
252
+ (0, react.useImperativeHandle)(ref, () => ({
253
+ show: (...args) => _rozieExposeRef.current.show(...args),
254
+ hide: (...args) => _rozieExposeRef.current.hide(...args),
255
+ toggle: (...args) => _rozieExposeRef.current.toggle(...args),
256
+ reposition: (...args) => _rozieExposeRef.current.reposition(...args)
257
+ }), []);
258
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
259
+ ...attrs,
260
+ className: (0, _rozie_runtime_react.clsx)("rozie-popover", attrs.className),
261
+ "data-rozie-s-c6cf02ea": "",
262
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
263
+ className: "rozie-popover-anchor",
264
+ ref: anchorEl,
265
+ "aria-haspopup": "dialog",
266
+ "aria-expanded": !!open,
267
+ "aria-describedby": (0, _rozie_runtime_react.rozieAttr)(isTooltip() && open ? "rozie-popover-floating" : void 0),
268
+ onClick: ($event) => {
269
+ props.trigger === "click" && onAnchorClick();
270
+ },
271
+ onPointerEnter: ($event) => {
272
+ props.trigger === "hover" && onAnchorPointerEnter();
273
+ },
274
+ onPointerLeave: ($event) => {
275
+ props.trigger === "hover" && onAnchorPointerLeave();
276
+ },
277
+ onFocus: ($event) => {
278
+ props.trigger === "focus" && onAnchorFocus();
279
+ },
280
+ onBlur: ($event) => {
281
+ props.trigger === "focus" && onAnchorBlur();
282
+ },
283
+ "data-rozie-s-c6cf02ea": "",
284
+ children: (props.renderAnchor ?? props.slots?.["anchor"])?.({
285
+ open,
286
+ toggle,
287
+ show,
288
+ hide
289
+ })
290
+ }), !!(open && !props.disabled) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
291
+ className: "rozie-popover-floating",
292
+ ref: floatingEl,
293
+ id: "rozie-popover-floating",
294
+ role: (0, _rozie_runtime_react.rozieAttr)(floatingRole()),
295
+ "aria-modal": !!(floatingRole() === "dialog"),
296
+ "data-rozie-s-c6cf02ea": "",
297
+ children: [!!props.arrow && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
298
+ className: "rozie-popover-arrow",
299
+ ref: arrowEl,
300
+ "data-rozie-s-c6cf02ea": ""
301
+ }), typeof (props.children ?? props.slots?.[""]) === "function" ? (props.children ?? props.slots?.[""])() : props.children ?? props.slots?.[""]]
302
+ })]
303
+ }) });
304
+ });
305
+ //#endregion
306
+ exports.Popover = Popover;
307
+ exports.default = Popover;
@@ -0,0 +1,66 @@
1
+ import * as React from "react";
2
+ import { ReactNode } from "react";
3
+
4
+ //#region src/Popover.d.ts
5
+ interface PopoverProps {
6
+ /**
7
+ * Whether the floating content is open. The sole `model: true` prop — two-way bind it (`r-model:open` / `v-model:open` / `bind:open` / `[(open)]`) and Popover writes the new state back whenever the trigger or a dismissal toggles it. Left unbound it falls back to an uncontrolled default.
8
+ */
9
+ open?: boolean;
10
+ defaultOpen?: boolean;
11
+ onOpenChange?: (next: boolean) => void;
12
+ /**
13
+ * Floating UI placement of the content relative to the anchor — one of `top`/`right`/`bottom`/`left`, each optionally suffixed `-start`/`-end` (e.g. `bottom-start`). With `disableFlip` off, the content may flip to the opposite side when it would overflow the viewport. Reconciled at runtime.
14
+ */
15
+ placement?: string;
16
+ /**
17
+ * How the anchor opens the content: `'click'` toggles on click, `'hover'` opens on pointer-enter and closes on pointer-leave (tooltip-style), `'focus'` opens on focus and closes on blur. Drives both the gesture handlers and the ARIA role (`'hover'`/`'focus'` → tooltip, `'click'` → popover dialog).
18
+ */
19
+ trigger?: string;
20
+ /**
21
+ * Distance in pixels between the anchor and the floating content (the Floating UI `offset` middleware). Reconciled at runtime.
22
+ */
23
+ offset?: number;
24
+ /**
25
+ * Disable the Floating UI `flip` middleware. By default the content flips to the opposite side of the anchor when it would overflow the viewport; set this to keep it pinned to `placement` regardless.
26
+ */
27
+ disableFlip?: boolean;
28
+ /**
29
+ * Disable the Floating UI `shift` middleware. By default the content shifts along its axis to stay within the viewport; set this to keep it strictly aligned to the anchor.
30
+ */
31
+ disableShift?: boolean;
32
+ /**
33
+ * Opt in to a positioned arrow element. When set, Popover renders an arrow `<div>` and runs the Floating UI `arrow` middleware against it so it points at the anchor. Style it via the `--rozie-popover-*` arrow CSS custom properties.
34
+ */
35
+ arrow?: boolean;
36
+ /**
37
+ * Disable the control entirely: the trigger no longer opens the content and any open content is suppressed.
38
+ */
39
+ disabled?: boolean;
40
+ /**
41
+ * Opt in to modal dialog semantics for a `click` popover. **Off by default:** a click popover is a non-modal, click-outside-dismissable layer, so its panel is rendered role-neutral (the slot content owns its own ARIA role — e.g. a `role="menu"`) and carries NO `aria-modal`. Set `modal` for a genuinely modal dialog popover: the panel then gets `role="dialog"` + `aria-modal="true"`. **Note:** Popover ships no focus trap (it stays a minimal headless primitive); if you set `modal`, provide your own focus containment so the `aria-modal` claim holds. Ignored for `hover`/`focus` triggers (always tooltip-flavored).
42
+ */
43
+ modal?: boolean;
44
+ /**
45
+ * Floating UI positioning strategy — 'absolute' (default) or 'fixed'. Use 'fixed' to escape a scrollable/overflow-clipping ancestor (e.g. a sticky table header). Reconciled at runtime.
46
+ */
47
+ strategy?: string;
48
+ onChange?: (...args: unknown[]) => void;
49
+ renderAnchor?: (params: {
50
+ open: boolean;
51
+ toggle: () => void;
52
+ show: () => void;
53
+ hide: () => void;
54
+ }) => ReactNode;
55
+ children?: ReactNode;
56
+ slots?: Record<string, () => ReactNode>;
57
+ }
58
+ interface PopoverHandle {
59
+ show: (...args: any[]) => any;
60
+ hide: (...args: any[]) => any;
61
+ toggle: (...args: any[]) => any;
62
+ reposition: (...args: any[]) => any;
63
+ }
64
+ declare const Popover: React.ForwardRefExoticComponent<PopoverProps & React.RefAttributes<PopoverHandle>>;
65
+ //#endregion
66
+ export { Popover, Popover as default, type PopoverHandle };
@@ -0,0 +1,66 @@
1
+ import * as _$react from "react";
2
+ import { ReactNode } from "react";
3
+ //#region src/Popover.d.ts
4
+ interface AnchorCtx {
5
+ open: any;
6
+ toggle: any;
7
+ show: any;
8
+ hide: any;
9
+ }
10
+ interface PopoverProps {
11
+ /**
12
+ * Whether the floating content is open. The sole `model: true` prop — two-way bind it (`r-model:open` / `v-model:open` / `bind:open` / `[(open)]`) and Popover writes the new state back whenever the trigger or a dismissal toggles it. Left unbound it falls back to an uncontrolled default.
13
+ */
14
+ open?: boolean;
15
+ defaultOpen?: boolean;
16
+ onOpenChange?: (open: boolean) => void;
17
+ /**
18
+ * Floating UI placement of the content relative to the anchor — one of `top`/`right`/`bottom`/`left`, each optionally suffixed `-start`/`-end` (e.g. `bottom-start`). With `disableFlip` off, the content may flip to the opposite side when it would overflow the viewport. Reconciled at runtime.
19
+ */
20
+ placement?: string;
21
+ /**
22
+ * How the anchor opens the content: `'click'` toggles on click, `'hover'` opens on pointer-enter and closes on pointer-leave (tooltip-style), `'focus'` opens on focus and closes on blur. Drives both the gesture handlers and the ARIA role (`'hover'`/`'focus'` → tooltip, `'click'` → popover dialog).
23
+ */
24
+ trigger?: string;
25
+ /**
26
+ * Distance in pixels between the anchor and the floating content (the Floating UI `offset` middleware). Reconciled at runtime.
27
+ */
28
+ offset?: number;
29
+ /**
30
+ * Disable the Floating UI `flip` middleware. By default the content flips to the opposite side of the anchor when it would overflow the viewport; set this to keep it pinned to `placement` regardless.
31
+ */
32
+ disableFlip?: boolean;
33
+ /**
34
+ * Disable the Floating UI `shift` middleware. By default the content shifts along its axis to stay within the viewport; set this to keep it strictly aligned to the anchor.
35
+ */
36
+ disableShift?: boolean;
37
+ /**
38
+ * Opt in to a positioned arrow element. When set, Popover renders an arrow `<div>` and runs the Floating UI `arrow` middleware against it so it points at the anchor. Style it via the `--rozie-popover-*` arrow CSS custom properties.
39
+ */
40
+ arrow?: boolean;
41
+ /**
42
+ * Disable the control entirely: the trigger no longer opens the content and any open content is suppressed.
43
+ */
44
+ disabled?: boolean;
45
+ /**
46
+ * Opt in to modal dialog semantics for a `click` popover. **Off by default:** a click popover is a non-modal, click-outside-dismissable layer, so its panel is rendered role-neutral (the slot content owns its own ARIA role — e.g. a `role="menu"`) and carries NO `aria-modal`. Set `modal` for a genuinely modal dialog popover: the panel then gets `role="dialog"` + `aria-modal="true"`. **Note:** Popover ships no focus trap (it stays a minimal headless primitive); if you set `modal`, provide your own focus containment so the `aria-modal` claim holds. Ignored for `hover`/`focus` triggers (always tooltip-flavored).
47
+ */
48
+ modal?: boolean;
49
+ /**
50
+ * Floating UI positioning strategy — 'absolute' (default) or 'fixed'. Use 'fixed' to escape a scrollable/overflow-clipping ancestor (e.g. a sticky table header). Reconciled at runtime.
51
+ */
52
+ strategy?: string;
53
+ onChange?: (...args: any[]) => void;
54
+ renderAnchor?: (ctx: AnchorCtx) => ReactNode;
55
+ children?: ReactNode;
56
+ slots?: Record<string, () => _$react.ReactNode>;
57
+ }
58
+ interface PopoverHandle {
59
+ show: (...args: any[]) => any;
60
+ hide: (...args: any[]) => any;
61
+ toggle: (...args: any[]) => any;
62
+ reposition: (...args: any[]) => any;
63
+ }
64
+ declare const Popover: _$react.ForwardRefExoticComponent<PopoverProps & _$react.RefAttributes<PopoverHandle>>;
65
+ //#endregion
66
+ export { Popover, Popover as default, type PopoverHandle };