@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/dist/index.js CHANGED
@@ -1,296 +1,451 @@
1
1
  // src/Dropdown/Dropdown.tsx
2
- import { Menu } from "@base-ui/react/menu";
3
2
  import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";
3
+ import { Menu } from "@base-ui/react/menu";
4
4
  import {
5
5
  cloneElement,
6
6
  createContext,
7
7
  isValidElement,
8
- useCallback,
8
+ useCallback as useCallback3,
9
9
  useContext,
10
10
  useEffect,
11
- useRef,
12
- useState
11
+ useRef as useRef3
13
12
  } from "react";
14
13
 
15
- // #style-inject:#style-inject
16
- function styleInject(css, { insertAt } = {}) {
17
- if (!css || typeof document === "undefined") return;
18
- const head = document.head || document.getElementsByTagName("head")[0];
19
- const style = document.createElement("style");
20
- style.type = "text/css";
21
- if (insertAt === "top") {
22
- if (head.firstChild) {
23
- head.insertBefore(style, head.firstChild);
24
- } else {
25
- head.appendChild(style);
26
- }
27
- } else {
28
- head.appendChild(style);
29
- }
30
- if (style.styleSheet) {
31
- style.styleSheet.cssText = css;
32
- } else {
33
- style.appendChild(document.createTextNode(css));
34
- }
14
+ // src/Dropdown/DropdownStore.ts
15
+ import { useCallback, useRef, useSyncExternalStore } from "react";
16
+ function shallowEqual(a, b) {
17
+ if (Object.is(a, b)) return true;
18
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
19
+ return false;
20
+ const keysA = Object.keys(a);
21
+ const keysB = Object.keys(b);
22
+ if (keysA.length !== keysB.length) return false;
23
+ return keysA.every(
24
+ (k) => Object.is(
25
+ a[k],
26
+ b[k]
27
+ )
28
+ );
35
29
  }
36
-
37
- // src/Dropdown/Dropdown.css
38
- styleInject(".slithy-dropdown-positioner {\n z-index: 1000;\n}\n.slithy-dropdown-popup {\n background: var(--slithy-dropdown-bg, #fff);\n color: var(--slithy-dropdown-color, #222);\n font-size: var(--slithy-dropdown-font-size, 0.875rem);\n line-height: 1.4;\n padding: var(--slithy-dropdown-padding, 4px 0);\n border-radius: var(--slithy-dropdown-radius, 6px);\n min-width: var(--slithy-dropdown-min-width, 160px);\n box-shadow: var( --slithy-dropdown-shadow, 0 4px 16px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.08) );\n border: var(--slithy-dropdown-border, 1px solid rgba(0, 0, 0, 0.08));\n transform-origin: var(--transform-origin);\n transition-property: opacity, transform;\n transition-duration: 150ms;\n transition-timing-function: ease;\n outline: none;\n}\n.slithy-dropdown-popup[data-open] {\n opacity: 1;\n transform: scale(1);\n}\n.slithy-dropdown-popup[data-starting-style],\n.slithy-dropdown-popup[data-ending-style] {\n opacity: 0;\n transform: scale(0.95);\n}\n.slithy-dropdown-popup[data-instant] {\n transition-duration: 0ms;\n}\n.slithy-dropdown-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: var(--slithy-dropdown-item-padding, 6px 12px);\n cursor: default;\n outline: none;\n user-select: none;\n}\n.slithy-dropdown-item[data-highlighted] {\n background: var(--slithy-dropdown-item-highlighted-bg, rgba(0, 0, 0, 0.06));\n}\n.slithy-dropdown-item[data-disabled] {\n opacity: 0.4;\n pointer-events: none;\n}\n.slithy-dropdown-separator {\n height: 1px;\n margin: 4px 0;\n background: var(--slithy-dropdown-separator-color, rgba(0, 0, 0, 0.1));\n}\n.slithy-dropdown-group-label {\n padding: var(--slithy-dropdown-group-label-padding, 6px 12px 2px);\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--slithy-dropdown-group-label-color, rgba(0, 0, 0, 0.5));\n user-select: none;\n}\n.slithy-dropdown-item-indicator {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n}\n.slithy-dropdown-arrow {\n width: 8px;\n height: 8px;\n transform: rotate(45deg);\n background: var(--slithy-dropdown-bg, #fff);\n border: var(--slithy-dropdown-border, 1px solid rgba(0, 0, 0, 0.08));\n}\n.slithy-dropdown-arrow[data-side=top] {\n bottom: -4px;\n border-top: none;\n border-left: none;\n}\n.slithy-dropdown-arrow[data-side=bottom] {\n top: -4px;\n border-bottom: none;\n border-right: none;\n}\n.slithy-dropdown-arrow[data-side=left] {\n right: -4px;\n border-bottom: none;\n border-left: none;\n}\n.slithy-dropdown-arrow[data-side=right] {\n left: -4px;\n border-top: none;\n border-right: none;\n}\n");
39
-
40
- // src/Dropdown/Dropdown.tsx
41
- import { jsx, jsxs } from "react/jsx-runtime";
42
- var DeferredContext = createContext(null);
43
- function InnerRoot({
44
- children,
45
- defaultOpen,
46
- shouldOpenRef,
47
- unmountOnClose,
48
- onDeactivate,
49
- onMenuOpenChange,
50
- onOpenChange,
51
- onOpenChangeComplete,
52
- ...props
53
- }) {
54
- const shouldOpen = useRef(false);
55
- const [deferredOpen, setDeferredOpen] = useState(false);
56
- if (!shouldOpen.current && shouldOpenRef.current) {
57
- shouldOpenRef.current = false;
58
- shouldOpen.current = true;
30
+ function createDropdownStore() {
31
+ const listeners = /* @__PURE__ */ new Set();
32
+ function notify() {
33
+ listeners.forEach((l) => l());
59
34
  }
60
- useEffect(() => {
61
- if (!shouldOpen.current) return;
62
- const frame = requestAnimationFrame(() => {
63
- setDeferredOpen(true);
64
- });
65
- return () => cancelAnimationFrame(frame);
35
+ let state = {
36
+ open: false,
37
+ content: null,
38
+ anchor: null,
39
+ keyboardOpen: false,
40
+ callbacks: {},
41
+ menuConfig: {},
42
+ positionConfig: {},
43
+ openGeneration: 0,
44
+ openDropdown(content, anchor, options) {
45
+ state = {
46
+ ...state,
47
+ open: true,
48
+ content,
49
+ anchor,
50
+ keyboardOpen: options?.keyboard ?? false,
51
+ callbacks: options?.callbacks ?? {},
52
+ menuConfig: options?.menuConfig ?? {},
53
+ positionConfig: options?.positionConfig ?? {},
54
+ openGeneration: state.openGeneration + 1
55
+ };
56
+ notify();
57
+ },
58
+ closeDropdown(generation) {
59
+ if (generation !== void 0 && generation !== state.openGeneration) return;
60
+ state = {
61
+ ...state,
62
+ open: false,
63
+ content: null,
64
+ anchor: null,
65
+ keyboardOpen: false
66
+ };
67
+ notify();
68
+ },
69
+ updateContent(content) {
70
+ if (!state.open || state.content === content) return;
71
+ state = { ...state, content };
72
+ notify();
73
+ }
74
+ };
75
+ return {
76
+ getState: () => state,
77
+ subscribe: (listener) => {
78
+ listeners.add(listener);
79
+ return () => listeners.delete(listener);
80
+ }
81
+ };
82
+ }
83
+ var store = createDropdownStore();
84
+ function useDropdownStore(selector) {
85
+ const selectorRef = useRef(selector);
86
+ selectorRef.current = selector;
87
+ const prevRef = useRef(void 0);
88
+ const getSnapshot = useCallback(() => {
89
+ const result = selectorRef.current(store.getState());
90
+ const prev = prevRef.current;
91
+ if (prev !== void 0 && shallowEqual(prev, result)) {
92
+ return prev;
93
+ }
94
+ prevRef.current = result;
95
+ return result;
66
96
  }, []);
67
- const { open: externalOpen, ...restProps } = props;
68
- const menuProps = {};
69
- if (externalOpen !== void 0) {
70
- menuProps.open = externalOpen;
71
- } else if (shouldOpen.current) {
72
- menuProps.open = deferredOpen;
73
- } else if (defaultOpen !== void 0) {
74
- menuProps.defaultOpen = defaultOpen;
97
+ return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
98
+ }
99
+ useDropdownStore.getState = store.getState;
100
+
101
+ // src/Tooltip/TooltipStore.ts
102
+ import { useCallback as useCallback2, useRef as useRef2, useSyncExternalStore as useSyncExternalStore2 } from "react";
103
+ function shallowEqual2(a, b) {
104
+ if (Object.is(a, b)) return true;
105
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
106
+ return false;
107
+ const keysA = Object.keys(a);
108
+ const keysB = Object.keys(b);
109
+ if (keysA.length !== keysB.length) return false;
110
+ return keysA.every(
111
+ (k) => Object.is(
112
+ a[k],
113
+ b[k]
114
+ )
115
+ );
116
+ }
117
+ function createTooltipStore() {
118
+ const listeners = /* @__PURE__ */ new Set();
119
+ function notify() {
120
+ listeners.forEach((l) => l());
75
121
  }
76
- return /* @__PURE__ */ jsx(
77
- Menu.Root,
78
- {
79
- ...restProps,
80
- ...menuProps,
81
- onOpenChange: (isOpen, event) => {
82
- onOpenChange?.(isOpen, event);
83
- onMenuOpenChange(isOpen);
84
- if (shouldOpen.current) setDeferredOpen(isOpen);
85
- },
86
- onOpenChangeComplete: (isOpen) => {
87
- onOpenChangeComplete?.(isOpen);
88
- if (!isOpen && unmountOnClose) onDeactivate();
89
- },
90
- children
122
+ let state = {
123
+ open: false,
124
+ content: null,
125
+ anchor: null,
126
+ positionConfig: {},
127
+ hoverable: false,
128
+ closeDelay: 300,
129
+ popupId: null,
130
+ lastCloseTime: 0,
131
+ setPopupId(id) {
132
+ if (state.popupId === id) return;
133
+ state = { ...state, popupId: id };
134
+ notify();
135
+ },
136
+ openTooltip(content, anchor, options) {
137
+ state = {
138
+ ...state,
139
+ open: true,
140
+ content,
141
+ anchor,
142
+ positionConfig: options?.positionConfig ?? {},
143
+ hoverable: options?.hoverable ?? false,
144
+ closeDelay: options?.closeDelay ?? 300
145
+ };
146
+ notify();
147
+ },
148
+ closeTooltip(options) {
149
+ if (!state.open) return;
150
+ state = {
151
+ ...state,
152
+ open: false,
153
+ content: null,
154
+ anchor: null,
155
+ lastCloseTime: options?.skipWarmUp ? 0 : Date.now()
156
+ };
157
+ notify();
158
+ },
159
+ updateContent(content) {
160
+ if (!state.open || state.content === content) return;
161
+ state = { ...state, content };
162
+ notify();
91
163
  }
92
- );
164
+ };
165
+ return {
166
+ getState: () => state,
167
+ subscribe: (listener) => {
168
+ listeners.add(listener);
169
+ return () => listeners.delete(listener);
170
+ }
171
+ };
93
172
  }
94
- function Root({
95
- children,
96
- open,
97
- defaultOpen,
98
- disabled,
99
- unmountOnClose = false,
100
- onOpenChangeComplete,
101
- ...props
102
- }) {
103
- const [wasActivated, setWasActivated] = useState(false);
104
- const [menuOpen, setMenuOpen] = useState(false);
105
- const shouldRefocusRef = useRef(false);
106
- const shouldOpenRef = useRef(false);
107
- const activated = wasActivated || !!open || !!defaultOpen;
108
- const activate = useCallback(() => {
109
- if (!disabled) {
110
- if (shouldOpenRef.current) setMenuOpen(true);
111
- setWasActivated(true);
112
- }
113
- }, [disabled]);
114
- const deactivate = useCallback(() => {
115
- setMenuOpen(false);
116
- setWasActivated(false);
173
+ var store2 = createTooltipStore();
174
+ function useTooltipStore(selector) {
175
+ const selectorRef = useRef2(selector);
176
+ selectorRef.current = selector;
177
+ const prevRef = useRef2(void 0);
178
+ const getSnapshot = useCallback2(() => {
179
+ const result = selectorRef.current(store2.getState());
180
+ const prev = prevRef.current;
181
+ if (prev !== void 0 && shallowEqual2(prev, result)) {
182
+ return prev;
183
+ }
184
+ prevRef.current = result;
185
+ return result;
117
186
  }, []);
187
+ return useSyncExternalStore2(store2.subscribe, getSnapshot, getSnapshot);
188
+ }
189
+ useTooltipStore.getState = store2.getState;
190
+
191
+ // src/Dropdown/Dropdown.tsx
192
+ import { jsx } from "react/jsx-runtime";
193
+ var DropdownCtx = createContext(null);
194
+ function Root({ children, open, defaultOpen, disabled = false, modal, openOn = "click", loopFocus, highlightItemOnHover, orientation, side, sideOffset, align, alignOffset, collisionPadding, onOpenChange, onOpenChangeComplete }) {
195
+ const contentRef = useRef3(null);
196
+ const triggerRef = useRef3(null);
197
+ const callbacks = { onOpenChange, onOpenChangeComplete };
198
+ const menuConfig = { modal, loopFocus, highlightItemOnHover, orientation };
199
+ const positionConfig = { side, sideOffset, align, alignOffset, collisionPadding };
200
+ const initializedRef = useRef3(false);
201
+ useEffect(() => {
202
+ if (open === void 0) return;
203
+ const store3 = useDropdownStore.getState();
204
+ if (open && !store3.open) {
205
+ const content = contentRef.current;
206
+ const anchor = triggerRef.current;
207
+ if (content && anchor) {
208
+ store3.openDropdown(content, anchor, { callbacks, menuConfig, positionConfig });
209
+ }
210
+ } else if (!open && store3.open && store3.anchor === triggerRef.current) {
211
+ store3.closeDropdown();
212
+ }
213
+ }, [open]);
214
+ useEffect(() => {
215
+ if (initializedRef.current || defaultOpen === void 0 || !defaultOpen) return;
216
+ initializedRef.current = true;
217
+ requestAnimationFrame(() => {
218
+ const content = contentRef.current;
219
+ const anchor = triggerRef.current;
220
+ if (content && anchor) {
221
+ useDropdownStore.getState().openDropdown(content, anchor, { callbacks, menuConfig, positionConfig });
222
+ }
223
+ });
224
+ }, [defaultOpen]);
118
225
  const ctx = {
119
- activated,
120
- activate,
121
- shouldRefocusRef,
122
- shouldOpenRef,
123
- menuOpen
226
+ contentRef,
227
+ triggerRef,
228
+ disabled,
229
+ openOn,
230
+ callbacks,
231
+ menuConfig,
232
+ positionConfig
124
233
  };
125
- if (!activated) {
126
- return /* @__PURE__ */ jsx(DeferredContext.Provider, { value: ctx, children });
127
- }
128
- return /* @__PURE__ */ jsx(DeferredContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx(
129
- InnerRoot,
130
- {
131
- open,
132
- defaultOpen,
133
- disabled,
134
- shouldOpenRef,
135
- unmountOnClose,
136
- onDeactivate: deactivate,
137
- onMenuOpenChange: setMenuOpen,
138
- onOpenChangeComplete,
139
- ...props,
140
- children
141
- }
142
- ) });
143
- }
144
- var BASE_UI_KEYS = /* @__PURE__ */ new Set([
145
- "handle",
146
- "payload",
147
- "delay",
148
- "closeDelay",
149
- "openOnHover",
150
- "render",
151
- "tooltip"
152
- ]);
153
- function pickHtmlProps(props) {
154
- const html = {};
155
- for (const key in props) {
156
- if (!BASE_UI_KEYS.has(key)) html[key] = props[key];
157
- }
158
- return html;
234
+ return /* @__PURE__ */ jsx(DropdownCtx.Provider, { value: ctx, children });
159
235
  }
160
- function Trigger({
161
- children,
162
- className,
163
- style,
164
- disabled,
165
- tooltip,
166
- ...rest
167
- }) {
168
- const deferred = useContext(DeferredContext);
169
- const triggerElRef = useRef(null);
170
- const [tooltipActivated, setTooltipActivated] = useState(false);
171
- const isHoveringRef = useRef(false);
172
- const pendingRef = useRef(0);
173
- const lastPointerTypeRef = useRef("");
174
- const shouldRefocusRef = deferred?.shouldRefocusRef;
175
- const triggerRef = useCallback(
236
+ function Trigger({ children, onClick, onPointerDown, onPointerEnter, onPointerLeave, onFocus, onBlur, onKeyDown, render, tooltip, tooltipDelay = 600, ...props }) {
237
+ const ctx = useContext(DropdownCtx);
238
+ const triggerRef = useRef3(null);
239
+ const openOn = ctx?.openOn ?? "click";
240
+ const setTriggerRef = useCallback3(
176
241
  (el) => {
177
- triggerElRef.current = el;
178
- if (shouldRefocusRef?.current && el) {
179
- shouldRefocusRef.current = false;
180
- el.focus();
181
- }
242
+ triggerRef.current = el;
243
+ if (ctx) ctx.triggerRef.current = el;
182
244
  },
183
- [shouldRefocusRef]
245
+ [ctx]
184
246
  );
247
+ const storeState = useDropdownStore((s) => ({
248
+ open: s.open,
249
+ anchor: s.anchor
250
+ }));
251
+ const isActive = storeState.open && storeState.anchor === triggerRef.current;
252
+ const tooltipOpenTimerRef = useRef3(0);
253
+ const tooltipCloseTimerRef = useRef3(0);
254
+ const lastPointerTypeRef = useRef3("");
255
+ const tooltipState = useTooltipStore((s) => ({
256
+ open: s.open,
257
+ anchor: s.anchor,
258
+ popupId: s.popupId
259
+ }));
260
+ const isTooltipActive = tooltip != null && tooltipState.open && tooltipState.anchor === triggerRef.current;
261
+ const wasActiveRef = useRef3(false);
262
+ const skipFocusTooltipRef = useRef3(false);
185
263
  useEffect(() => {
186
- if (!tooltipActivated || !isHoveringRef.current) return;
187
- const el = triggerElRef.current;
188
- if (!el) return;
189
- const frame = requestAnimationFrame(() => {
190
- if (!isHoveringRef.current) return;
191
- el.dispatchEvent(
192
- new PointerEvent("pointerenter", {
193
- bubbles: false,
194
- pointerType: "mouse"
195
- })
196
- );
197
- el.dispatchEvent(new MouseEvent("mouseenter", { bubbles: false }));
198
- el.dispatchEvent(new MouseEvent("mousemove", { bubbles: true }));
199
- });
200
- return () => cancelAnimationFrame(frame);
201
- }, [tooltipActivated]);
202
- const tooltipHoverHandlers = tooltip ? {
203
- onMouseEnter: () => {
204
- if (lastPointerTypeRef.current === "touch") return;
205
- isHoveringRef.current = true;
206
- pendingRef.current = requestAnimationFrame(() => {
207
- if (isHoveringRef.current) setTooltipActivated(true);
264
+ if (wasActiveRef.current && !isActive) {
265
+ skipFocusTooltipRef.current = true;
266
+ const id = requestAnimationFrame(() => {
267
+ skipFocusTooltipRef.current = false;
208
268
  });
209
- },
210
- onMouseLeave: () => {
211
- isHoveringRef.current = false;
212
- cancelAnimationFrame(pendingRef.current);
213
- }
214
- } : {};
215
- const menuOpen = deferred?.menuOpen ?? false;
216
- const tooltipPortal = !!tooltip ? /* @__PURE__ */ jsx(BaseTooltip.Portal, { children: /* @__PURE__ */ jsx(BaseTooltip.Positioner, { sideOffset: 8, children: /* @__PURE__ */ jsx(BaseTooltip.Popup, { className: "slithy-tooltip-popup", children: tooltip }) }) }) : null;
217
- if (deferred && !deferred.activated) {
218
- const { render: renderProp } = rest;
219
- const htmlProps = pickHtmlProps(rest);
220
- const callHandler = (handler, event) => {
221
- if (typeof handler === "function") handler(event);
222
- };
223
- const preProps = {
224
- ...htmlProps,
225
- ref: triggerRef,
226
- className: typeof className === "function" ? void 0 : className,
227
- style: typeof style === "function" ? void 0 : style,
228
- disabled,
229
- ...tooltipHoverHandlers,
230
- // Activation strategy: mouse/pen uses onPointerDown for snappy
231
- // desktop UX, matching Base UI's Menu.Trigger (which opens on
232
- // mousedown). Touch skips onPointerDown and falls through to
233
- // onClick instead, because Base UI's Menu.Trigger opens on
234
- // touch-up timing on iOS — activating earlier would make the
235
- // pre-activation and post-activation triggers feel inconsistent.
236
- onPointerDown: (e) => {
237
- callHandler(htmlProps.onPointerDown, e);
238
- lastPointerTypeRef.current = e.pointerType;
239
- if (e.button !== 0 || e.pointerType === "touch") return;
240
- deferred.shouldOpenRef.current = true;
241
- deferred.shouldRefocusRef.current = true;
242
- deferred.activate();
243
- },
244
- onClick: (e) => {
245
- callHandler(htmlProps.onClick, e);
246
- deferred.shouldOpenRef.current = true;
247
- deferred.shouldRefocusRef.current = true;
248
- deferred.activate();
249
- },
250
- onKeyDown: (e) => {
251
- callHandler(htmlProps.onKeyDown, e);
252
- if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown" || e.key === "ArrowUp") {
253
- deferred.shouldOpenRef.current = true;
254
- deferred.shouldRefocusRef.current = true;
255
- deferred.activate();
269
+ return () => cancelAnimationFrame(id);
270
+ }
271
+ wasActiveRef.current = isActive;
272
+ });
273
+ const dismissTooltip = useCallback3(() => {
274
+ clearTimeout(tooltipOpenTimerRef.current);
275
+ clearTimeout(tooltipCloseTimerRef.current);
276
+ const ts = useTooltipStore.getState();
277
+ if (ts.open && ts.anchor === triggerRef.current) {
278
+ ts.closeTooltip({ skipWarmUp: true });
279
+ }
280
+ }, []);
281
+ const scheduleTooltip = useCallback3(() => {
282
+ if (!tooltip || !triggerRef.current) return;
283
+ const ds = useDropdownStore.getState();
284
+ if (ds.open) return;
285
+ clearTimeout(tooltipOpenTimerRef.current);
286
+ clearTimeout(tooltipCloseTimerRef.current);
287
+ const ts = useTooltipStore.getState();
288
+ if (ts.open && ts.anchor === triggerRef.current) return;
289
+ const tooltipContent = /* @__PURE__ */ jsx(BaseTooltip.Popup, { className: "slithy-tooltip-popup", children: tooltip });
290
+ const isSwitch = ts.open && ts.anchor !== triggerRef.current;
291
+ const elapsed = Date.now() - ts.lastCloseTime;
292
+ const isWarmUp = !isSwitch && elapsed < 300;
293
+ const effectiveDelay = isSwitch || isWarmUp ? 0 : tooltipDelay;
294
+ if (effectiveDelay === 0) {
295
+ ts.openTooltip(tooltipContent, triggerRef.current);
296
+ } else {
297
+ tooltipOpenTimerRef.current = window.setTimeout(() => {
298
+ const dds = useDropdownStore.getState();
299
+ if (dds.open) return;
300
+ if (triggerRef.current) {
301
+ const content = /* @__PURE__ */ jsx(BaseTooltip.Popup, { className: "slithy-tooltip-popup", children: tooltip });
302
+ useTooltipStore.getState().openTooltip(content, triggerRef.current);
256
303
  }
304
+ }, effectiveDelay);
305
+ }
306
+ }, [tooltip, tooltipDelay]);
307
+ const scheduleTooltipClose = useCallback3(() => {
308
+ clearTimeout(tooltipOpenTimerRef.current);
309
+ const ts = useTooltipStore.getState();
310
+ if (!ts.open || ts.anchor !== triggerRef.current) return;
311
+ tooltipCloseTimerRef.current = window.setTimeout(() => {
312
+ const s = useTooltipStore.getState();
313
+ if (s.open && s.anchor === triggerRef.current) {
314
+ s.closeTooltip();
257
315
  }
316
+ }, 300);
317
+ }, []);
318
+ useEffect(() => {
319
+ return () => {
320
+ clearTimeout(tooltipOpenTimerRef.current);
321
+ clearTimeout(tooltipCloseTimerRef.current);
258
322
  };
259
- let button;
260
- if (isValidElement(renderProp)) {
261
- button = cloneElement(renderProp, preProps, children);
262
- } else if (typeof renderProp === "function") {
263
- button = renderProp({ ...preProps, children }, {});
264
- } else {
265
- button = /* @__PURE__ */ jsx("button", { ...preProps, children });
266
- }
267
- if (!tooltip || !tooltipActivated) return button;
268
- return /* @__PURE__ */ jsx(BaseTooltip.Provider, { children: /* @__PURE__ */ jsxs(BaseTooltip.Root, { children: [
269
- /* @__PURE__ */ jsx(BaseTooltip.Trigger, { render: button }),
270
- !menuOpen && tooltipPortal
271
- ] }) });
272
- }
273
- const menuTrigger = /* @__PURE__ */ jsx(
274
- Menu.Trigger,
275
- {
276
- ref: triggerRef,
277
- className,
278
- style,
279
- disabled,
280
- ...rest,
281
- children
282
- }
323
+ }, []);
324
+ const toggleDropdown = useCallback3(
325
+ (options) => {
326
+ if (ctx?.disabled) return;
327
+ dismissTooltip();
328
+ const store3 = useDropdownStore.getState();
329
+ if (store3.open && store3.anchor === triggerRef.current) {
330
+ store3.closeDropdown();
331
+ return;
332
+ }
333
+ const content = ctx?.contentRef.current;
334
+ if (content && triggerRef.current) {
335
+ store3.openDropdown(content, triggerRef.current, {
336
+ keyboard: options?.keyboard,
337
+ callbacks: ctx?.callbacks,
338
+ menuConfig: ctx?.menuConfig,
339
+ positionConfig: ctx?.positionConfig
340
+ });
341
+ }
342
+ },
343
+ [ctx, dismissTooltip]
344
+ );
345
+ const handlePointerDown = useCallback3(
346
+ (e) => {
347
+ lastPointerTypeRef.current = e.pointerType;
348
+ onPointerDown?.(e);
349
+ if (e.defaultPrevented || openOn !== "pointerdown") return;
350
+ toggleDropdown();
351
+ },
352
+ [onPointerDown, openOn, toggleDropdown]
353
+ );
354
+ const handleClick = useCallback3(
355
+ (e) => {
356
+ onClick?.(e);
357
+ if (e.defaultPrevented) return;
358
+ if (openOn === "click") {
359
+ toggleDropdown();
360
+ }
361
+ },
362
+ [onClick, openOn, toggleDropdown]
283
363
  );
284
- if (!tooltip) return menuTrigger;
285
- return /* @__PURE__ */ jsx(BaseTooltip.Provider, { children: /* @__PURE__ */ jsxs(BaseTooltip.Root, { children: [
286
- /* @__PURE__ */ jsx(BaseTooltip.Trigger, { render: menuTrigger }),
287
- !menuOpen && tooltipPortal
288
- ] }) });
364
+ const handlePointerEnter = useCallback3(
365
+ (e) => {
366
+ onPointerEnter?.(e);
367
+ if (e.defaultPrevented) return;
368
+ if (lastPointerTypeRef.current === "touch") return;
369
+ scheduleTooltip();
370
+ },
371
+ [onPointerEnter, scheduleTooltip]
372
+ );
373
+ const handlePointerLeave = useCallback3(
374
+ (e) => {
375
+ onPointerLeave?.(e);
376
+ if (e.defaultPrevented) return;
377
+ scheduleTooltipClose();
378
+ },
379
+ [onPointerLeave, scheduleTooltipClose]
380
+ );
381
+ const handleFocus = useCallback3(
382
+ (e) => {
383
+ onFocus?.(e);
384
+ if (e.defaultPrevented) return;
385
+ if (skipFocusTooltipRef.current) {
386
+ skipFocusTooltipRef.current = false;
387
+ return;
388
+ }
389
+ if (lastPointerTypeRef.current === "touch") return;
390
+ scheduleTooltip();
391
+ },
392
+ [onFocus, scheduleTooltip]
393
+ );
394
+ const handleBlur = useCallback3(
395
+ (e) => {
396
+ onBlur?.(e);
397
+ if (e.defaultPrevented) return;
398
+ dismissTooltip();
399
+ },
400
+ [onBlur, dismissTooltip]
401
+ );
402
+ const handleKeyDown = useCallback3(
403
+ (e) => {
404
+ onKeyDown?.(e);
405
+ if (ctx?.disabled || e.defaultPrevented) return;
406
+ if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown" || e.key === "ArrowUp") {
407
+ e.preventDefault();
408
+ toggleDropdown({ keyboard: true });
409
+ }
410
+ },
411
+ [ctx, onKeyDown, toggleDropdown]
412
+ );
413
+ const triggerProps = {
414
+ ref: setTriggerRef,
415
+ type: "button",
416
+ "aria-haspopup": "menu",
417
+ "aria-expanded": isActive,
418
+ "aria-describedby": isTooltipActive && tooltipState.popupId ? tooltipState.popupId : void 0,
419
+ onPointerDown: handlePointerDown,
420
+ onClick: handleClick,
421
+ onPointerEnter: handlePointerEnter,
422
+ onPointerLeave: handlePointerLeave,
423
+ onFocus: handleFocus,
424
+ onBlur: handleBlur,
425
+ onKeyDown: handleKeyDown,
426
+ ...props
427
+ };
428
+ if (isValidElement(render)) {
429
+ return cloneElement(render, triggerProps, children);
430
+ }
431
+ if (typeof render === "function") {
432
+ return render({ ...triggerProps, children });
433
+ }
434
+ return /* @__PURE__ */ jsx("button", { ...triggerProps, children });
289
435
  }
290
- function Portal(props) {
291
- const deferred = useContext(DeferredContext);
292
- if (deferred && !deferred.activated) return null;
293
- return /* @__PURE__ */ jsx(Menu.Portal, { ...props });
436
+ function Portal({ children }) {
437
+ const ctx = useContext(DropdownCtx);
438
+ const storeAnchor = useDropdownStore((s) => s.anchor);
439
+ if (ctx) {
440
+ ctx.contentRef.current = children;
441
+ }
442
+ const isActive = !!(ctx && storeAnchor && storeAnchor === ctx.triggerRef.current);
443
+ useEffect(() => {
444
+ if (isActive) {
445
+ useDropdownStore.getState().updateContent(children);
446
+ }
447
+ });
448
+ return null;
294
449
  }
295
450
  function Positioner({ className, ...props }) {
296
451
  return /* @__PURE__ */ jsx(
@@ -397,199 +552,351 @@ var Dropdown = {
397
552
  RadioItemIndicator
398
553
  };
399
554
 
555
+ // src/Dropdown/DropdownRenderer.tsx
556
+ import { Menu as Menu2 } from "@base-ui/react/menu";
557
+ import { useEffect as useEffect2, useRef as useRef4, useState } from "react";
558
+
559
+ // #style-inject:#style-inject
560
+ function styleInject(css, { insertAt } = {}) {
561
+ if (!css || typeof document === "undefined") return;
562
+ const head = document.head || document.getElementsByTagName("head")[0];
563
+ const style = document.createElement("style");
564
+ style.type = "text/css";
565
+ if (insertAt === "top") {
566
+ if (head.firstChild) {
567
+ head.insertBefore(style, head.firstChild);
568
+ } else {
569
+ head.appendChild(style);
570
+ }
571
+ } else {
572
+ head.appendChild(style);
573
+ }
574
+ if (style.styleSheet) {
575
+ style.styleSheet.cssText = css;
576
+ } else {
577
+ style.appendChild(document.createTextNode(css));
578
+ }
579
+ }
580
+
581
+ // src/Dropdown/Dropdown.css
582
+ styleInject(".slithy-dropdown-positioner {\n z-index: 1000;\n}\n.slithy-dropdown-popup {\n background: var(--slithy-dropdown-bg, #fff);\n color: var(--slithy-dropdown-color, #222);\n font-size: var(--slithy-dropdown-font-size, 0.875rem);\n line-height: 1.4;\n padding: var(--slithy-dropdown-padding, 4px 0);\n border-radius: var(--slithy-dropdown-radius, 6px);\n min-width: var(--slithy-dropdown-min-width, 160px);\n box-shadow: var( --slithy-dropdown-shadow, 0 4px 16px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.08) );\n border: var(--slithy-dropdown-border, 1px solid rgba(0, 0, 0, 0.08));\n transform-origin: var(--transform-origin);\n transition-property: opacity, transform;\n transition-duration: 150ms;\n transition-timing-function: ease;\n outline: none;\n}\n.slithy-dropdown-popup[data-open] {\n opacity: 1;\n transform: scale(1);\n}\n.slithy-dropdown-popup[data-starting-style],\n.slithy-dropdown-popup[data-ending-style] {\n opacity: 0;\n transform: scale(0.95);\n}\n.slithy-dropdown-popup[data-instant] {\n transition-duration: 0ms;\n}\n.slithy-dropdown-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: var(--slithy-dropdown-item-padding, 6px 12px);\n cursor: default;\n outline: none;\n user-select: none;\n}\n.slithy-dropdown-item[data-highlighted] {\n background: var(--slithy-dropdown-item-highlighted-bg, rgba(0, 0, 0, 0.06));\n}\n.slithy-dropdown-item[data-disabled] {\n opacity: 0.4;\n pointer-events: none;\n}\n.slithy-dropdown-separator {\n height: 1px;\n margin: 4px 0;\n background: var(--slithy-dropdown-separator-color, rgba(0, 0, 0, 0.1));\n}\n.slithy-dropdown-group-label {\n padding: var(--slithy-dropdown-group-label-padding, 6px 12px 2px);\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--slithy-dropdown-group-label-color, rgba(0, 0, 0, 0.5));\n user-select: none;\n}\n.slithy-dropdown-item-indicator {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n}\n.slithy-dropdown-arrow {\n width: 8px;\n height: 8px;\n transform: rotate(45deg);\n background: var(--slithy-dropdown-bg, #fff);\n border: var(--slithy-dropdown-border, 1px solid rgba(0, 0, 0, 0.08));\n}\n.slithy-dropdown-arrow[data-side=top] {\n bottom: -4px;\n border-top: none;\n border-left: none;\n}\n.slithy-dropdown-arrow[data-side=bottom] {\n top: -4px;\n border-bottom: none;\n border-right: none;\n}\n.slithy-dropdown-arrow[data-side=left] {\n right: -4px;\n border-bottom: none;\n border-left: none;\n}\n.slithy-dropdown-arrow[data-side=right] {\n left: -4px;\n border-top: none;\n border-right: none;\n}\n");
583
+
584
+ // src/Dropdown/DropdownRenderer.tsx
585
+ import { jsx as jsx2 } from "react/jsx-runtime";
586
+ function DropdownRenderer() {
587
+ const open = useDropdownStore((s) => s.open);
588
+ const content = useDropdownStore((s) => s.content);
589
+ const anchor = useDropdownStore((s) => s.anchor);
590
+ const keyboardOpen = useDropdownStore((s) => s.keyboardOpen);
591
+ const generation = useDropdownStore((s) => s.openGeneration);
592
+ const lastContentRef = useRef4(content);
593
+ const lastAnchorRef = useRef4(anchor);
594
+ if (content) lastContentRef.current = content;
595
+ if (anchor) lastAnchorRef.current = anchor;
596
+ const activeContent = content ?? lastContentRef.current;
597
+ const activeAnchor = anchor ?? lastAnchorRef.current;
598
+ const [hasOpened, setHasOpened] = useState(false);
599
+ const [deferredOpen, setDeferredOpen] = useState(false);
600
+ useEffect2(() => {
601
+ if (open && !hasOpened) {
602
+ setHasOpened(true);
603
+ requestAnimationFrame(() => setDeferredOpen(true));
604
+ } else {
605
+ setDeferredOpen(open);
606
+ }
607
+ }, [open, hasOpened]);
608
+ const prevOpenRef = useRef4(false);
609
+ useEffect2(() => {
610
+ if (prevOpenRef.current && !open && lastAnchorRef.current) {
611
+ const el = lastAnchorRef.current;
612
+ requestAnimationFrame(() => el.focus());
613
+ }
614
+ prevOpenRef.current = open;
615
+ }, [open]);
616
+ const callbacks = useDropdownStore((s) => s.callbacks);
617
+ const menuConfig = useDropdownStore((s) => s.menuConfig);
618
+ const positionConfig = useDropdownStore((s) => s.positionConfig);
619
+ useEffect2(() => {
620
+ if (!open || !anchor || menuConfig.modal !== false) return;
621
+ const observer = new IntersectionObserver(
622
+ ([entry]) => {
623
+ if (!entry.isIntersecting) {
624
+ useDropdownStore.getState().closeDropdown();
625
+ }
626
+ },
627
+ { threshold: 0 }
628
+ );
629
+ observer.observe(anchor);
630
+ return () => observer.disconnect();
631
+ }, [open, anchor, menuConfig.modal]);
632
+ const lastCallbacksRef = useRef4(callbacks);
633
+ if (callbacks.onOpenChange) lastCallbacksRef.current = callbacks;
634
+ const handleOpenChangeComplete = (isOpen) => {
635
+ lastCallbacksRef.current.onOpenChangeComplete?.(isOpen);
636
+ if (!isOpen) {
637
+ lastContentRef.current = null;
638
+ lastAnchorRef.current = null;
639
+ }
640
+ };
641
+ if (!hasOpened) return null;
642
+ return /* @__PURE__ */ jsx2(
643
+ Menu2.Root,
644
+ {
645
+ open: deferredOpen,
646
+ modal: menuConfig.modal,
647
+ loopFocus: menuConfig.loopFocus,
648
+ highlightItemOnHover: menuConfig.highlightItemOnHover,
649
+ orientation: menuConfig.orientation,
650
+ onOpenChange: (isOpen, eventDetails) => {
651
+ if (!isOpen && eventDetails.reason !== "trigger-hover") {
652
+ lastCallbacksRef.current.onOpenChange?.(false);
653
+ useDropdownStore.getState().closeDropdown(generation);
654
+ }
655
+ },
656
+ onOpenChangeComplete: handleOpenChangeComplete,
657
+ children: /* @__PURE__ */ jsx2(Menu2.Portal, { children: /* @__PURE__ */ jsx2(
658
+ Menu2.Positioner,
659
+ {
660
+ anchor: activeAnchor,
661
+ className: "slithy-dropdown-positioner",
662
+ side: positionConfig.side,
663
+ sideOffset: positionConfig.sideOffset ?? 4,
664
+ align: positionConfig.align,
665
+ alignOffset: positionConfig.alignOffset,
666
+ collisionPadding: positionConfig.collisionPadding,
667
+ children: activeContent
668
+ }
669
+ ) })
670
+ }
671
+ );
672
+ }
673
+
400
674
  // src/Tooltip/Tooltip.tsx
401
675
  import { Tooltip as BaseTooltip2 } from "@base-ui/react/tooltip";
402
676
  import {
403
677
  cloneElement as cloneElement2,
404
678
  createContext as createContext2,
405
679
  isValidElement as isValidElement2,
406
- useCallback as useCallback2,
680
+ useCallback as useCallback4,
407
681
  useContext as useContext2,
408
- useEffect as useEffect2,
409
- useRef as useRef2,
410
- useState as useState2
682
+ useEffect as useEffect3,
683
+ useRef as useRef5
411
684
  } from "react";
412
-
413
- // src/Tooltip/Tooltip.css
414
- styleInject(".slithy-tooltip-positioner {\n z-index: 1000;\n}\n.slithy-tooltip-popup {\n background: var(--slithy-tooltip-bg, #222);\n color: var(--slithy-tooltip-color, #fff);\n font-size: var(--slithy-tooltip-font-size, 0.8125rem);\n line-height: 1.4;\n padding: var(--slithy-tooltip-padding, 4px 8px);\n border-radius: var(--slithy-tooltip-radius, 4px);\n max-width: var(--slithy-tooltip-max-width, 300px);\n transform-origin: var(--transform-origin);\n transition-property: opacity, transform;\n transition-duration: 150ms;\n transition-timing-function: ease-out;\n}\n.slithy-tooltip-popup[data-open] {\n opacity: 1;\n transform: scale(1);\n}\n.slithy-tooltip-popup[data-starting-style],\n.slithy-tooltip-popup[data-ending-style] {\n opacity: 0;\n transform: scale(0.95);\n}\n.slithy-tooltip-popup[data-instant] {\n transition-duration: 0ms;\n}\n.slithy-tooltip-arrow {\n width: 8px;\n height: 8px;\n transform: rotate(45deg);\n background: var(--slithy-tooltip-bg, #222);\n}\n.slithy-tooltip-arrow[data-side=top] {\n bottom: -4px;\n}\n.slithy-tooltip-arrow[data-side=bottom] {\n top: -4px;\n}\n.slithy-tooltip-arrow[data-side=left] {\n right: -4px;\n}\n.slithy-tooltip-arrow[data-side=right] {\n left: -4px;\n}\n");
415
-
416
- // src/Tooltip/Tooltip.tsx
417
- import { jsx as jsx2 } from "react/jsx-runtime";
418
- var DeferredContext2 = createContext2(null);
419
- var Provider = BaseTooltip2.Provider;
685
+ import { jsx as jsx3 } from "react/jsx-runtime";
686
+ var TooltipCtx = createContext2(null);
420
687
  function Root2({
421
688
  children,
422
689
  open,
423
690
  defaultOpen,
424
- disabled,
691
+ disabled = false,
425
692
  touchDisabled = true,
426
- unmountOnClose = false,
427
- onOpenChangeComplete,
428
- ...props
693
+ hoverable = false,
694
+ delay = 600,
695
+ closeDelay = 300,
696
+ warmUpDelay = 300,
697
+ side,
698
+ sideOffset,
699
+ align,
700
+ alignOffset,
701
+ collisionPadding
429
702
  }) {
430
- const [wasActivated, setWasActivated] = useState2(false);
431
- const isHoveringRef = useRef2(false);
432
- const shouldRefocusRef = useRef2(false);
433
- const activated = wasActivated || !!open || !!defaultOpen;
434
- const activate = useCallback2(() => {
435
- if (!disabled) setWasActivated(true);
436
- }, [disabled]);
703
+ const contentRef = useRef5(null);
704
+ const triggerRef = useRef5(null);
705
+ const positionConfig = { side, sideOffset, align, alignOffset, collisionPadding };
706
+ const initializedRef = useRef5(false);
707
+ useEffect3(() => {
708
+ if (open === void 0) return;
709
+ const store3 = useTooltipStore.getState();
710
+ if (open && !store3.open) {
711
+ const content = contentRef.current;
712
+ const anchor = triggerRef.current;
713
+ if (content && anchor) {
714
+ store3.openTooltip(content, anchor, { positionConfig, hoverable, closeDelay });
715
+ }
716
+ } else if (!open && store3.open && store3.anchor === triggerRef.current) {
717
+ store3.closeTooltip();
718
+ }
719
+ }, [open]);
720
+ useEffect3(() => {
721
+ if (initializedRef.current || defaultOpen === void 0 || !defaultOpen) return;
722
+ initializedRef.current = true;
723
+ requestAnimationFrame(() => {
724
+ const content = contentRef.current;
725
+ const anchor = triggerRef.current;
726
+ if (content && anchor) {
727
+ useTooltipStore.getState().openTooltip(content, anchor, { positionConfig, hoverable, closeDelay });
728
+ }
729
+ });
730
+ }, [defaultOpen]);
437
731
  const ctx = {
438
- activated,
439
- activate,
440
- isHoveringRef,
441
- shouldRefocusRef,
442
- touchDisabled
732
+ contentRef,
733
+ triggerRef,
734
+ disabled,
735
+ touchDisabled,
736
+ hoverable,
737
+ delay,
738
+ closeDelay,
739
+ warmUpDelay,
740
+ positionConfig
443
741
  };
444
- if (!activated) {
445
- return /* @__PURE__ */ jsx2(DeferredContext2.Provider, { value: ctx, children });
446
- }
447
- return /* @__PURE__ */ jsx2(DeferredContext2.Provider, { value: ctx, children: /* @__PURE__ */ jsx2(
448
- BaseTooltip2.Root,
449
- {
450
- open,
451
- defaultOpen,
452
- disabled,
453
- onOpenChangeComplete: (isOpen) => {
454
- onOpenChangeComplete?.(isOpen);
455
- if (!isOpen && unmountOnClose) setWasActivated(false);
456
- },
457
- ...props,
458
- children
459
- }
460
- ) });
461
- }
462
- var BASE_UI_KEYS2 = /* @__PURE__ */ new Set([
463
- "closeOnClick",
464
- "handle",
465
- "payload",
466
- "delay",
467
- "closeDelay",
468
- "render"
469
- ]);
470
- function pickHtmlProps2(props) {
471
- const html = {};
472
- for (const key in props) {
473
- if (!BASE_UI_KEYS2.has(key)) html[key] = props[key];
474
- }
475
- return html;
742
+ return /* @__PURE__ */ jsx3(TooltipCtx.Provider, { value: ctx, children });
476
743
  }
477
- function Trigger2({
478
- children,
479
- className,
480
- style,
481
- disabled,
482
- ...rest
483
- }) {
484
- const deferred = useContext2(DeferredContext2);
485
- const triggerElRef = useRef2(null);
486
- const pendingRef = useRef2(0);
487
- const lastPointerTypeRef = useRef2("");
488
- const shouldRefocusRef = deferred?.shouldRefocusRef;
489
- const triggerRef = useCallback2(
744
+ function Trigger2({ children, render, ...props }) {
745
+ const ctx = useContext2(TooltipCtx);
746
+ const triggerRef = useRef5(null);
747
+ const openTimerRef = useRef5(0);
748
+ const closeTimerRef = useRef5(0);
749
+ const lastPointerTypeRef = useRef5("");
750
+ const setTriggerRef = useCallback4(
490
751
  (el) => {
491
- triggerElRef.current = el;
492
- if (shouldRefocusRef?.current && el) {
493
- shouldRefocusRef.current = false;
494
- el.focus();
495
- }
752
+ triggerRef.current = el;
753
+ if (ctx) ctx.triggerRef.current = el;
496
754
  },
497
- [shouldRefocusRef]
755
+ [ctx]
498
756
  );
499
- const activated = deferred?.activated ?? true;
500
- useEffect2(() => {
501
- if (!activated || !deferred?.isHoveringRef.current) return;
502
- const el = triggerElRef.current;
503
- if (!el) return;
504
- const frame = requestAnimationFrame(() => {
505
- if (!deferred.isHoveringRef.current) return;
506
- el.dispatchEvent(
507
- new PointerEvent("pointerenter", {
508
- bubbles: false,
509
- pointerType: "mouse"
510
- })
511
- );
512
- el.dispatchEvent(new MouseEvent("mouseenter", { bubbles: false }));
513
- el.dispatchEvent(new MouseEvent("mousemove", { bubbles: true }));
514
- });
515
- return () => cancelAnimationFrame(frame);
516
- }, [activated]);
517
- if (deferred && !deferred.activated) {
518
- const { render: renderProp } = rest;
519
- const htmlProps = pickHtmlProps2(rest);
520
- const preProps = {
521
- ...htmlProps,
522
- ref: triggerRef,
523
- className: typeof className === "function" ? void 0 : className,
524
- style: typeof style === "function" ? void 0 : style,
525
- disabled,
526
- onPointerDown: (e) => {
527
- lastPointerTypeRef.current = e.pointerType;
528
- },
529
- onMouseEnter: () => {
530
- if (deferred.touchDisabled && lastPointerTypeRef.current === "touch")
531
- return;
532
- deferred.isHoveringRef.current = true;
533
- pendingRef.current = requestAnimationFrame(() => {
534
- if (deferred.isHoveringRef.current) deferred.activate();
535
- });
536
- },
537
- onMouseLeave: () => {
538
- deferred.isHoveringRef.current = false;
539
- cancelAnimationFrame(pendingRef.current);
540
- },
541
- onFocus: () => {
542
- if (deferred.touchDisabled && lastPointerTypeRef.current === "touch")
543
- return;
544
- deferred.shouldRefocusRef.current = true;
545
- deferred.activate();
546
- }
757
+ useEffect3(() => {
758
+ return () => {
759
+ clearTimeout(openTimerRef.current);
760
+ clearTimeout(closeTimerRef.current);
547
761
  };
548
- if (isValidElement2(renderProp)) {
549
- return cloneElement2(renderProp, preProps, children);
550
- }
551
- if (typeof renderProp === "function") {
552
- return renderProp({ ...preProps, children }, {});
762
+ }, []);
763
+ const storeState = useTooltipStore((s) => ({
764
+ open: s.open,
765
+ anchor: s.anchor,
766
+ popupId: s.popupId
767
+ }));
768
+ const isActive = storeState.open && storeState.anchor === triggerRef.current;
769
+ const scheduleOpen = useCallback4(() => {
770
+ if (ctx?.disabled || !triggerRef.current) return;
771
+ clearTimeout(closeTimerRef.current);
772
+ const content = ctx?.contentRef.current;
773
+ if (!content) return;
774
+ const store3 = useTooltipStore.getState();
775
+ if (store3.open && store3.anchor === triggerRef.current) return;
776
+ const isSwitch = store3.open && store3.anchor !== triggerRef.current;
777
+ const elapsed = Date.now() - store3.lastCloseTime;
778
+ const isWarmUp = !isSwitch && elapsed < (ctx?.warmUpDelay ?? 300);
779
+ const effectiveDelay = isSwitch || isWarmUp ? 0 : ctx?.delay ?? 600;
780
+ const openOptions = { positionConfig: ctx?.positionConfig, hoverable: ctx?.hoverable, closeDelay: ctx?.closeDelay };
781
+ if (effectiveDelay === 0) {
782
+ store3.openTooltip(content, triggerRef.current, openOptions);
783
+ } else {
784
+ openTimerRef.current = window.setTimeout(() => {
785
+ const currentContent = ctx?.contentRef.current;
786
+ if (currentContent && triggerRef.current) {
787
+ useTooltipStore.getState().openTooltip(currentContent, triggerRef.current, openOptions);
788
+ }
789
+ }, effectiveDelay);
553
790
  }
554
- return /* @__PURE__ */ jsx2("button", { ...preProps, children });
555
- }
556
- return /* @__PURE__ */ jsx2(
557
- BaseTooltip2.Trigger,
558
- {
559
- ref: triggerRef,
560
- className,
561
- style,
562
- disabled,
563
- ...rest,
564
- children
791
+ }, [ctx]);
792
+ const scheduleClose = useCallback4(() => {
793
+ clearTimeout(openTimerRef.current);
794
+ if (ctx?.hoverable) return;
795
+ const store3 = useTooltipStore.getState();
796
+ if (!store3.open || store3.anchor !== triggerRef.current) return;
797
+ const closeDelay = ctx?.closeDelay ?? 300;
798
+ if (closeDelay === 0) {
799
+ store3.closeTooltip();
800
+ } else {
801
+ closeTimerRef.current = window.setTimeout(() => {
802
+ const s = useTooltipStore.getState();
803
+ if (s.open && s.anchor === triggerRef.current) {
804
+ s.closeTooltip();
805
+ }
806
+ }, closeDelay);
565
807
  }
808
+ }, [ctx]);
809
+ const handlePointerDown = useCallback4(
810
+ (e) => {
811
+ lastPointerTypeRef.current = e.pointerType;
812
+ props.onPointerDown?.(e);
813
+ },
814
+ [props.onPointerDown]
566
815
  );
816
+ const handlePointerEnter = useCallback4(
817
+ (e) => {
818
+ props.onPointerEnter?.(e);
819
+ if (e.defaultPrevented) return;
820
+ if (ctx?.touchDisabled && lastPointerTypeRef.current === "touch") return;
821
+ scheduleOpen();
822
+ },
823
+ [ctx, props.onPointerEnter, scheduleOpen]
824
+ );
825
+ const handlePointerLeave = useCallback4(
826
+ (e) => {
827
+ props.onPointerLeave?.(e);
828
+ if (e.defaultPrevented) return;
829
+ scheduleClose();
830
+ },
831
+ [props.onPointerLeave, scheduleClose]
832
+ );
833
+ const handleFocus = useCallback4(
834
+ (e) => {
835
+ props.onFocus?.(e);
836
+ if (e.defaultPrevented) return;
837
+ if (ctx?.touchDisabled && lastPointerTypeRef.current === "touch") return;
838
+ scheduleOpen();
839
+ },
840
+ [ctx, props.onFocus, scheduleOpen]
841
+ );
842
+ const handleBlur = useCallback4(
843
+ (e) => {
844
+ props.onBlur?.(e);
845
+ if (e.defaultPrevented) return;
846
+ clearTimeout(openTimerRef.current);
847
+ const store3 = useTooltipStore.getState();
848
+ if (store3.open && store3.anchor === triggerRef.current) {
849
+ store3.closeTooltip();
850
+ }
851
+ },
852
+ [props.onBlur]
853
+ );
854
+ const triggerProps = {
855
+ ref: setTriggerRef,
856
+ type: "button",
857
+ "aria-describedby": isActive && storeState.popupId ? storeState.popupId : void 0,
858
+ onPointerDown: handlePointerDown,
859
+ onPointerEnter: handlePointerEnter,
860
+ onPointerLeave: handlePointerLeave,
861
+ onFocus: handleFocus,
862
+ onBlur: handleBlur,
863
+ ...props
864
+ };
865
+ if (isValidElement2(render)) {
866
+ return cloneElement2(render, triggerProps, children);
867
+ }
868
+ if (typeof render === "function") {
869
+ return render({ ...triggerProps, children });
870
+ }
871
+ return /* @__PURE__ */ jsx3("button", { ...triggerProps, children });
567
872
  }
568
- function Portal2(props) {
569
- const deferred = useContext2(DeferredContext2);
570
- if (deferred && !deferred.activated) return null;
571
- return /* @__PURE__ */ jsx2(BaseTooltip2.Portal, { ...props });
572
- }
573
- function Positioner2({ className, ...props }) {
574
- return /* @__PURE__ */ jsx2(
575
- BaseTooltip2.Positioner,
576
- {
577
- className: className ?? "slithy-tooltip-positioner",
578
- ...props
873
+ function Portal2({ children }) {
874
+ const ctx = useContext2(TooltipCtx);
875
+ const storeAnchor = useTooltipStore((s) => s.anchor);
876
+ if (ctx) {
877
+ ctx.contentRef.current = children;
878
+ }
879
+ const isActive = !!(ctx && storeAnchor && storeAnchor === ctx.triggerRef.current);
880
+ useEffect3(() => {
881
+ if (isActive) {
882
+ useTooltipStore.getState().updateContent(children);
579
883
  }
580
- );
884
+ });
885
+ return null;
581
886
  }
582
- function Popup2({ className, ...props }) {
583
- return /* @__PURE__ */ jsx2(
887
+ function Popup2({ className, id, ...props }) {
888
+ const popupId = useTooltipStore((s) => s.popupId);
889
+ return /* @__PURE__ */ jsx3(
584
890
  BaseTooltip2.Popup,
585
891
  {
892
+ id: id ?? popupId ?? void 0,
586
893
  className: className ?? "slithy-tooltip-popup",
587
894
  ...props
588
895
  }
589
896
  );
590
897
  }
591
898
  function Arrow2({ className, ...props }) {
592
- return /* @__PURE__ */ jsx2(
899
+ return /* @__PURE__ */ jsx3(
593
900
  BaseTooltip2.Arrow,
594
901
  {
595
902
  className: className ?? "slithy-tooltip-arrow",
@@ -598,15 +905,195 @@ function Arrow2({ className, ...props }) {
598
905
  );
599
906
  }
600
907
  var Tooltip = {
601
- Provider,
602
908
  Root: Root2,
603
909
  Trigger: Trigger2,
604
910
  Portal: Portal2,
605
- Positioner: Positioner2,
606
911
  Popup: Popup2,
607
912
  Arrow: Arrow2
608
913
  };
914
+
915
+ // src/Tooltip/TooltipRenderer.tsx
916
+ import { Tooltip as BaseTooltip3 } from "@base-ui/react/tooltip";
917
+ import { useCallback as useCallback5, useEffect as useEffect5, useId, useRef as useRef7, useState as useState2 } from "react";
918
+
919
+ // src/useSafePolygon.ts
920
+ import { useEffect as useEffect4, useRef as useRef6 } from "react";
921
+ function useSafePolygon({
922
+ enabled,
923
+ anchor,
924
+ popupRef,
925
+ onClose
926
+ }) {
927
+ const exitPointRef = useRef6(null);
928
+ useEffect4(() => {
929
+ if (!enabled || !anchor) return;
930
+ const handlePointerLeave = (e) => {
931
+ exitPointRef.current = { x: e.clientX, y: e.clientY };
932
+ };
933
+ anchor.addEventListener("pointerleave", handlePointerLeave);
934
+ return () => anchor.removeEventListener("pointerleave", handlePointerLeave);
935
+ }, [enabled, anchor]);
936
+ useEffect4(() => {
937
+ if (!enabled || !anchor) return;
938
+ const handlePointerMove = (e) => {
939
+ const exit = exitPointRef.current;
940
+ if (!exit) return;
941
+ const popup = popupRef.current;
942
+ if (!popup) return;
943
+ const popupRect = popup.getBoundingClientRect();
944
+ if (e.clientX >= popupRect.left && e.clientX <= popupRect.right && e.clientY >= popupRect.top && e.clientY <= popupRect.bottom) {
945
+ exitPointRef.current = null;
946
+ return;
947
+ }
948
+ const triangle = getTriangle(exit, popupRect);
949
+ if (!pointInTriangle({ x: e.clientX, y: e.clientY }, triangle)) {
950
+ exitPointRef.current = null;
951
+ onClose();
952
+ }
953
+ };
954
+ document.addEventListener("pointermove", handlePointerMove);
955
+ return () => document.removeEventListener("pointermove", handlePointerMove);
956
+ }, [enabled, anchor, popupRef, onClose]);
957
+ useEffect4(() => {
958
+ if (!enabled) {
959
+ exitPointRef.current = null;
960
+ }
961
+ }, [enabled]);
962
+ }
963
+ function getTriangle(exit, rect) {
964
+ const cx = rect.left + rect.width / 2;
965
+ const cy = rect.top + rect.height / 2;
966
+ const dx = cx - exit.x;
967
+ const dy = cy - exit.y;
968
+ if (Math.abs(dy) >= Math.abs(dx)) {
969
+ if (dy > 0) {
970
+ return [exit, { x: rect.left, y: rect.top }, { x: rect.right, y: rect.top }];
971
+ }
972
+ return [exit, { x: rect.left, y: rect.bottom }, { x: rect.right, y: rect.bottom }];
973
+ }
974
+ if (dx > 0) {
975
+ return [exit, { x: rect.left, y: rect.top }, { x: rect.left, y: rect.bottom }];
976
+ }
977
+ return [exit, { x: rect.right, y: rect.top }, { x: rect.right, y: rect.bottom }];
978
+ }
979
+ function pointInTriangle(p, [a, b, c]) {
980
+ const d1 = sign(p, a, b);
981
+ const d2 = sign(p, b, c);
982
+ const d3 = sign(p, c, a);
983
+ const hasNeg = d1 < 0 || d2 < 0 || d3 < 0;
984
+ const hasPos = d1 > 0 || d2 > 0 || d3 > 0;
985
+ return !(hasNeg && hasPos);
986
+ }
987
+ function sign(p1, p2, p3) {
988
+ return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);
989
+ }
990
+
991
+ // src/Tooltip/Tooltip.css
992
+ styleInject(".slithy-tooltip-positioner {\n z-index: 1000;\n}\n.slithy-tooltip-popup {\n background: var(--slithy-tooltip-bg, #222);\n color: var(--slithy-tooltip-color, #fff);\n font-size: var(--slithy-tooltip-font-size, 0.8125rem);\n line-height: 1.4;\n padding: var(--slithy-tooltip-padding, 4px 8px);\n border-radius: var(--slithy-tooltip-radius, 4px);\n max-width: var(--slithy-tooltip-max-width, 300px);\n transform-origin: var(--transform-origin);\n transition-property: opacity, transform;\n transition-duration: 150ms;\n transition-timing-function: ease-out;\n}\n.slithy-tooltip-popup[data-open] {\n opacity: 1;\n transform: scale(1);\n}\n.slithy-tooltip-popup[data-starting-style],\n.slithy-tooltip-popup[data-ending-style] {\n opacity: 0;\n transform: scale(0.95);\n}\n.slithy-tooltip-popup[data-instant] {\n transition-duration: 0ms;\n}\n.slithy-tooltip-arrow {\n width: 8px;\n height: 8px;\n transform: rotate(45deg);\n background: var(--slithy-tooltip-bg, #222);\n}\n.slithy-tooltip-arrow[data-side=top] {\n bottom: -4px;\n}\n.slithy-tooltip-arrow[data-side=bottom] {\n top: -4px;\n}\n.slithy-tooltip-arrow[data-side=left] {\n right: -4px;\n}\n.slithy-tooltip-arrow[data-side=right] {\n left: -4px;\n}\n");
993
+
994
+ // src/Tooltip/TooltipRenderer.tsx
995
+ import { jsx as jsx4 } from "react/jsx-runtime";
996
+ function TooltipRenderer() {
997
+ const popupId = useId();
998
+ useEffect5(() => {
999
+ useTooltipStore.getState().setPopupId(popupId);
1000
+ }, [popupId]);
1001
+ const open = useTooltipStore((s) => s.open);
1002
+ const content = useTooltipStore((s) => s.content);
1003
+ const anchor = useTooltipStore((s) => s.anchor);
1004
+ const positionConfig = useTooltipStore((s) => s.positionConfig);
1005
+ const lastContentRef = useRef7(content);
1006
+ const lastAnchorRef = useRef7(anchor);
1007
+ if (content) lastContentRef.current = content;
1008
+ if (anchor) lastAnchorRef.current = anchor;
1009
+ const activeContent = content ?? lastContentRef.current;
1010
+ const activeAnchor = anchor ?? lastAnchorRef.current;
1011
+ const [hasOpened, setHasOpened] = useState2(false);
1012
+ const [deferredOpen, setDeferredOpen] = useState2(false);
1013
+ useEffect5(() => {
1014
+ if (open && !hasOpened) {
1015
+ setHasOpened(true);
1016
+ requestAnimationFrame(() => setDeferredOpen(true));
1017
+ } else {
1018
+ setDeferredOpen(open);
1019
+ }
1020
+ }, [open, hasOpened]);
1021
+ useEffect5(() => {
1022
+ if (!open) return;
1023
+ const handleKeyDown = (e) => {
1024
+ if (e.key === "Escape") {
1025
+ useTooltipStore.getState().closeTooltip();
1026
+ }
1027
+ };
1028
+ document.addEventListener("keydown", handleKeyDown);
1029
+ return () => document.removeEventListener("keydown", handleKeyDown);
1030
+ }, [open]);
1031
+ const hoverable = useTooltipStore((s) => s.hoverable);
1032
+ const storeCloseDelay = useTooltipStore((s) => s.closeDelay);
1033
+ const popupRef = useRef7(null);
1034
+ const popupCloseTimerRef = useRef7(0);
1035
+ const handleSafePolygonClose = useCallback5(() => {
1036
+ useTooltipStore.getState().closeTooltip();
1037
+ }, []);
1038
+ useSafePolygon({
1039
+ enabled: open && hoverable,
1040
+ anchor,
1041
+ popupRef,
1042
+ onClose: handleSafePolygonClose
1043
+ });
1044
+ const handlePopupPointerEnter = useCallback5(() => {
1045
+ if (!hoverable) return;
1046
+ clearTimeout(popupCloseTimerRef.current);
1047
+ }, [hoverable]);
1048
+ const handlePopupPointerLeave = useCallback5(() => {
1049
+ if (!hoverable) return;
1050
+ popupCloseTimerRef.current = window.setTimeout(() => {
1051
+ useTooltipStore.getState().closeTooltip();
1052
+ }, storeCloseDelay);
1053
+ }, [hoverable, storeCloseDelay]);
1054
+ useEffect5(() => {
1055
+ if (!open) clearTimeout(popupCloseTimerRef.current);
1056
+ return () => clearTimeout(popupCloseTimerRef.current);
1057
+ }, [open]);
1058
+ const handleOpenChangeComplete = (isOpen) => {
1059
+ if (!isOpen) {
1060
+ lastContentRef.current = null;
1061
+ lastAnchorRef.current = null;
1062
+ }
1063
+ };
1064
+ if (!hasOpened) return null;
1065
+ return /* @__PURE__ */ jsx4(
1066
+ BaseTooltip3.Root,
1067
+ {
1068
+ open: deferredOpen,
1069
+ onOpenChange: (isOpen) => {
1070
+ if (!isOpen) {
1071
+ useTooltipStore.getState().closeTooltip();
1072
+ }
1073
+ },
1074
+ onOpenChangeComplete: handleOpenChangeComplete,
1075
+ children: /* @__PURE__ */ jsx4(BaseTooltip3.Portal, { children: /* @__PURE__ */ jsx4(
1076
+ BaseTooltip3.Positioner,
1077
+ {
1078
+ ref: popupRef,
1079
+ anchor: activeAnchor,
1080
+ className: "slithy-tooltip-positioner",
1081
+ side: positionConfig.side ?? "top",
1082
+ sideOffset: positionConfig.sideOffset ?? 6,
1083
+ align: positionConfig.align,
1084
+ alignOffset: positionConfig.alignOffset,
1085
+ collisionPadding: positionConfig.collisionPadding,
1086
+ onPointerEnter: handlePopupPointerEnter,
1087
+ onPointerLeave: handlePopupPointerLeave,
1088
+ children: activeContent
1089
+ }
1090
+ ) })
1091
+ }
1092
+ );
1093
+ }
609
1094
  export {
610
1095
  Dropdown,
611
- Tooltip
1096
+ DropdownRenderer,
1097
+ Tooltip,
1098
+ TooltipRenderer
612
1099
  };