@arun-dev/headless 4.11.0 → 4.13.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.
@@ -0,0 +1,262 @@
1
+ import {
2
+ __export,
3
+ useRender
4
+ } from "../chunk-OQTWWZ4B.js";
5
+
6
+ // src/toast/index.parts.ts
7
+ var index_parts_exports = {};
8
+ __export(index_parts_exports, {
9
+ Action: () => ToastAction,
10
+ Close: () => ToastClose,
11
+ Description: () => ToastDescription,
12
+ Provider: () => ToastProvider,
13
+ Root: () => ToastRoot,
14
+ Title: () => ToastTitle,
15
+ Viewport: () => ToastViewport
16
+ });
17
+
18
+ // src/toast/ToastProvider.tsx
19
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
20
+
21
+ // src/toast/ToastContext.ts
22
+ import { createContext, useContext } from "react";
23
+ var ToastContext = createContext(null);
24
+ function useToastContext(part) {
25
+ const context = useContext(ToastContext);
26
+ if (context === null) {
27
+ throw new Error(`${part} must be used inside <Toast.Provider>.`);
28
+ }
29
+ return context;
30
+ }
31
+ function useToastManager() {
32
+ const { toasts, add, update, close } = useToastContext("useToastManager()");
33
+ return { toasts, add, update, close };
34
+ }
35
+ var ToastRootContext = createContext(null);
36
+ function useToastRootContext(part) {
37
+ const toast = useContext(ToastRootContext);
38
+ if (toast === null) {
39
+ throw new Error(`<Toast.${part}> must be rendered inside <Toast.Root>.`);
40
+ }
41
+ return toast;
42
+ }
43
+
44
+ // src/toast/ToastProvider.tsx
45
+ import { jsx } from "react/jsx-runtime";
46
+ function ToastProvider({ limit = 3, timeout = 5e3, children }) {
47
+ const [all, setAll] = useState([]);
48
+ const prefix = useId();
49
+ const counter = useRef(0);
50
+ const closedRef = useRef(/* @__PURE__ */ new Set());
51
+ const add = useCallback(
52
+ (options) => {
53
+ const id = options.id ?? `${prefix}-${++counter.current}`;
54
+ const toast = {
55
+ ...options,
56
+ id,
57
+ timeout: options.timeout ?? timeout,
58
+ priority: options.priority ?? "low"
59
+ };
60
+ closedRef.current.delete(id);
61
+ setAll((current) => [...current.filter((t) => t.id !== id), toast]);
62
+ return id;
63
+ },
64
+ [prefix, timeout]
65
+ );
66
+ const update = useCallback((id, patch) => {
67
+ setAll(
68
+ (current) => current.map((t) => t.id === id ? { ...t, ...patch, id } : t)
69
+ );
70
+ }, []);
71
+ const close = useCallback((id) => {
72
+ setAll((current) => {
73
+ const toast = current.find((t) => t.id === id);
74
+ if (toast && !closedRef.current.has(id)) {
75
+ closedRef.current.add(id);
76
+ queueMicrotask(() => toast.onClose?.());
77
+ }
78
+ return current.filter((t) => t.id !== id);
79
+ });
80
+ }, []);
81
+ const [pointerInside, setPointerInside] = useState(false);
82
+ const [focusInside, setFocusInside] = useState(false);
83
+ const [pageHidden, setPageHidden] = useState(false);
84
+ useEffect(() => {
85
+ const onVisibility = () => setPageHidden(document.visibilityState === "hidden");
86
+ onVisibility();
87
+ document.addEventListener("visibilitychange", onVisibility);
88
+ return () => document.removeEventListener("visibilitychange", onVisibility);
89
+ }, []);
90
+ const viewportRef = useRef(null);
91
+ const toasts = useMemo(() => all.slice(-limit), [all, limit]);
92
+ const context = useMemo(
93
+ () => ({
94
+ toasts,
95
+ add,
96
+ update,
97
+ close,
98
+ paused: pointerInside || focusInside || pageHidden,
99
+ setPointerInside,
100
+ setFocusInside,
101
+ viewportRef
102
+ }),
103
+ [toasts, add, update, close, pointerInside, focusInside, pageHidden]
104
+ );
105
+ return /* @__PURE__ */ jsx(ToastContext.Provider, { value: context, children });
106
+ }
107
+
108
+ // src/toast/ToastViewport.tsx
109
+ import { useLayoutEffect, useRef as useRef2 } from "react";
110
+ function ToastViewport({ className, children, render, ...rest }) {
111
+ const { toasts, viewportRef, setPointerInside, setFocusInside } = useToastContext("<Toast.Viewport>");
112
+ const elementRef = useRef2(null);
113
+ const newest = toasts.at(-1)?.id;
114
+ useLayoutEffect(() => {
115
+ const element = elementRef.current;
116
+ viewportRef.current = element;
117
+ if (!element || typeof element.showPopover !== "function") return;
118
+ if (element.matches(":popover-open")) element.hidePopover();
119
+ element.showPopover();
120
+ }, [newest, viewportRef]);
121
+ return useRender({
122
+ render,
123
+ defaultTagName: "section",
124
+ props: {
125
+ popover: "manual",
126
+ "aria-label": "Notifications",
127
+ "aria-live": "polite",
128
+ tabIndex: -1,
129
+ className,
130
+ ref: elementRef,
131
+ children: typeof children === "function" ? children(toasts) : children,
132
+ onPointerEnter() {
133
+ setPointerInside(true);
134
+ },
135
+ onPointerLeave() {
136
+ setPointerInside(false);
137
+ },
138
+ onFocus() {
139
+ setFocusInside(true);
140
+ },
141
+ onBlur(event) {
142
+ const next = event.relatedTarget;
143
+ if (!next || !event.currentTarget.contains(next)) setFocusInside(false);
144
+ }
145
+ },
146
+ consumerProps: rest
147
+ });
148
+ }
149
+
150
+ // src/toast/ToastRoot.tsx
151
+ import { useEffect as useEffect2, useRef as useRef3 } from "react";
152
+ import { jsx as jsx2 } from "react/jsx-runtime";
153
+ function ToastRoot({ toast, className, children, render, ...rest }) {
154
+ const { close, paused, viewportRef } = useToastContext("<Toast.Root>");
155
+ const elementRef = useRef3(null);
156
+ const { id, timeout } = toast;
157
+ const remaining = useRef3(timeout);
158
+ const lastTimeout = useRef3(timeout);
159
+ if (lastTimeout.current !== timeout) {
160
+ lastTimeout.current = timeout;
161
+ remaining.current = timeout;
162
+ }
163
+ useEffect2(() => {
164
+ if (paused || timeout <= 0) return;
165
+ const started = Date.now();
166
+ const timer = setTimeout(() => close(id), remaining.current);
167
+ return () => {
168
+ clearTimeout(timer);
169
+ remaining.current = Math.max(0, remaining.current - (Date.now() - started));
170
+ };
171
+ }, [paused, timeout, id, close]);
172
+ const focusInside = useRef3(false);
173
+ useEffect2(
174
+ () => () => {
175
+ if (focusInside.current) viewportRef.current?.focus();
176
+ },
177
+ [viewportRef]
178
+ );
179
+ const element = useRender({
180
+ render,
181
+ defaultTagName: "div",
182
+ props: {
183
+ role: toast.priority === "high" ? "alert" : void 0,
184
+ "aria-atomic": true,
185
+ "data-type": toast.type,
186
+ className,
187
+ children,
188
+ ref: elementRef,
189
+ onFocus() {
190
+ focusInside.current = true;
191
+ },
192
+ onBlur(event) {
193
+ const next = event.relatedTarget;
194
+ if (!next || !event.currentTarget.contains(next)) focusInside.current = false;
195
+ },
196
+ onKeyDown(event) {
197
+ if (event.key === "Escape") {
198
+ event.preventDefault();
199
+ close(id);
200
+ }
201
+ }
202
+ },
203
+ consumerProps: rest
204
+ });
205
+ return /* @__PURE__ */ jsx2(ToastRootContext.Provider, { value: toast, children: element });
206
+ }
207
+
208
+ // src/toast/ToastTitle.tsx
209
+ function ToastTitle({ className, children, render, ...rest }) {
210
+ const toast = useToastRootContext("Title");
211
+ return useRender({
212
+ render,
213
+ defaultTagName: "p",
214
+ props: { className, children: children ?? toast.title },
215
+ consumerProps: rest
216
+ });
217
+ }
218
+
219
+ // src/toast/ToastDescription.tsx
220
+ function ToastDescription({ className, children, render, ...rest }) {
221
+ const toast = useToastRootContext("Description");
222
+ return useRender({
223
+ render,
224
+ defaultTagName: "div",
225
+ props: { className, children: children ?? toast.description },
226
+ consumerProps: rest
227
+ });
228
+ }
229
+
230
+ // src/toast/ToastAction.tsx
231
+ function ToastAction({ className, children, render, ...rest }) {
232
+ useToastRootContext("Action");
233
+ return useRender({
234
+ render,
235
+ defaultTagName: "button",
236
+ props: { type: "button", className, children },
237
+ consumerProps: rest
238
+ });
239
+ }
240
+
241
+ // src/toast/ToastClose.tsx
242
+ function ToastClose({ className, children, render, ...rest }) {
243
+ const { close } = useToastContext("<Toast.Close>");
244
+ const { id } = useToastRootContext("Close");
245
+ return useRender({
246
+ render,
247
+ defaultTagName: "button",
248
+ props: {
249
+ type: "button",
250
+ className,
251
+ children,
252
+ onClick() {
253
+ close(id);
254
+ }
255
+ },
256
+ consumerProps: rest
257
+ });
258
+ }
259
+ export {
260
+ index_parts_exports as Toast,
261
+ useToastManager
262
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arun-dev/headless",
3
- "version": "4.11.0",
3
+ "version": "4.13.0",
4
4
  "description": "Unstyled React behaviour primitives — render engine, controlled/uncontrolled state, and data-* state projection. Ships no CSS and no class names.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -104,6 +104,26 @@
104
104
  "default": "./dist/radio-group/index.cjs"
105
105
  }
106
106
  },
107
+ "./field": {
108
+ "import": {
109
+ "types": "./dist/field/index.d.ts",
110
+ "default": "./dist/field/index.js"
111
+ },
112
+ "require": {
113
+ "types": "./dist/field/index.d.cts",
114
+ "default": "./dist/field/index.cjs"
115
+ }
116
+ },
117
+ "./toast": {
118
+ "import": {
119
+ "types": "./dist/toast/index.d.ts",
120
+ "default": "./dist/toast/index.js"
121
+ },
122
+ "require": {
123
+ "types": "./dist/toast/index.d.cts",
124
+ "default": "./dist/toast/index.cjs"
125
+ }
126
+ },
107
127
  "./switch": {
108
128
  "import": {
109
129
  "types": "./dist/switch/index.d.ts",