@graphprotocol/gds-react 0.2.0 → 0.2.1

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.
Files changed (129) hide show
  1. package/dist/GDSContext.d.ts +13 -0
  2. package/dist/GDSContext.d.ts.map +1 -0
  3. package/dist/GDSContext.js +4 -0
  4. package/dist/GDSContext.js.map +1 -0
  5. package/dist/GDSProvider.d.ts +1 -9
  6. package/dist/GDSProvider.d.ts.map +1 -1
  7. package/dist/GDSProvider.js +4 -3
  8. package/dist/GDSProvider.js.map +1 -1
  9. package/dist/components/Avatar.d.ts.map +1 -1
  10. package/dist/components/Avatar.js +2 -2
  11. package/dist/components/Avatar.js.map +1 -1
  12. package/dist/components/Breadcrumbs.parts.js +1 -1
  13. package/dist/components/Breadcrumbs.parts.js.map +1 -1
  14. package/dist/components/Button.d.ts.map +1 -1
  15. package/dist/components/Button.js +69 -69
  16. package/dist/components/Button.js.map +1 -1
  17. package/dist/components/Card.js +2 -2
  18. package/dist/components/Card.js.map +1 -1
  19. package/dist/components/CodeBlock.d.ts +1 -1
  20. package/dist/components/CodeBlock.parts.d.ts +1 -1
  21. package/dist/components/CopyButton.d.ts +1 -1
  22. package/dist/components/CopyButton.d.ts.map +1 -1
  23. package/dist/components/CopyButton.js +46 -19
  24. package/dist/components/CopyButton.js.map +1 -1
  25. package/dist/components/Input.js +2 -2
  26. package/dist/components/Input.js.map +1 -1
  27. package/dist/components/Link.js +2 -2
  28. package/dist/components/Link.js.map +1 -1
  29. package/dist/components/Menu.parts.d.ts +4 -5
  30. package/dist/components/Menu.parts.d.ts.map +1 -1
  31. package/dist/components/Menu.parts.js +49 -44
  32. package/dist/components/Menu.parts.js.map +1 -1
  33. package/dist/components/Modal.parts.d.ts.map +1 -1
  34. package/dist/components/Modal.parts.js +17 -21
  35. package/dist/components/Modal.parts.js.map +1 -1
  36. package/dist/components/Pane.d.ts +9 -0
  37. package/dist/components/Pane.d.ts.map +1 -0
  38. package/dist/components/Pane.js +8 -0
  39. package/dist/components/Pane.js.map +1 -0
  40. package/dist/components/Pane.meta.d.ts +20 -0
  41. package/dist/components/Pane.meta.d.ts.map +1 -0
  42. package/dist/components/Pane.meta.js +30 -0
  43. package/dist/components/Pane.meta.js.map +1 -0
  44. package/dist/components/Pane.parts.d.ts +77 -0
  45. package/dist/components/Pane.parts.d.ts.map +1 -0
  46. package/dist/components/Pane.parts.js +412 -0
  47. package/dist/components/Pane.parts.js.map +1 -0
  48. package/dist/components/Search.js +1 -1
  49. package/dist/components/Tooltip.parts.d.ts +13 -4
  50. package/dist/components/Tooltip.parts.d.ts.map +1 -1
  51. package/dist/components/Tooltip.parts.js +51 -63
  52. package/dist/components/Tooltip.parts.js.map +1 -1
  53. package/dist/components/base/ButtonOrLink.d.ts +1 -1
  54. package/dist/components/base/ButtonOrLink.d.ts.map +1 -1
  55. package/dist/components/base/ButtonOrLink.parts.d.ts +10 -3
  56. package/dist/components/base/ButtonOrLink.parts.d.ts.map +1 -1
  57. package/dist/components/base/ButtonOrLink.parts.js +27 -35
  58. package/dist/components/base/ButtonOrLink.parts.js.map +1 -1
  59. package/dist/components/base/MaybeButtonOrLink.d.ts +19 -2
  60. package/dist/components/base/MaybeButtonOrLink.d.ts.map +1 -1
  61. package/dist/components/base/MaybeButtonOrLink.js +5 -3
  62. package/dist/components/base/MaybeButtonOrLink.js.map +1 -1
  63. package/dist/components/base/Presence.d.ts +157 -0
  64. package/dist/components/base/Presence.d.ts.map +1 -0
  65. package/dist/components/base/Presence.js +808 -0
  66. package/dist/components/base/Presence.js.map +1 -0
  67. package/dist/components/base/index.d.ts +1 -0
  68. package/dist/components/base/index.d.ts.map +1 -1
  69. package/dist/components/base/index.js +1 -0
  70. package/dist/components/base/index.js.map +1 -1
  71. package/dist/components/index.d.ts +2 -0
  72. package/dist/components/index.d.ts.map +1 -1
  73. package/dist/components/index.js +2 -0
  74. package/dist/components/index.js.map +1 -1
  75. package/dist/hooks/useCSSProp.js +1 -1
  76. package/dist/hooks/useCSSProp.js.map +1 -1
  77. package/dist/hooks/useControlled.d.ts.map +1 -1
  78. package/dist/hooks/useControlled.js +6 -4
  79. package/dist/hooks/useControlled.js.map +1 -1
  80. package/dist/hooks/useGDS.js +1 -1
  81. package/dist/hooks/useGDS.js.map +1 -1
  82. package/dist/hooks/useStyleObserver.js +1 -1
  83. package/dist/hooks/useStyleObserver.js.map +1 -1
  84. package/dist/tailwind-plugin.d.ts.map +1 -1
  85. package/dist/tailwind-plugin.js +3 -0
  86. package/dist/tailwind-plugin.js.map +1 -1
  87. package/dist/utils/InlineCounter.d.ts +3 -0
  88. package/dist/utils/InlineCounter.d.ts.map +1 -0
  89. package/dist/utils/InlineCounter.js +7 -0
  90. package/dist/utils/InlineCounter.js.map +1 -0
  91. package/dist/utils/RenderCount.d.ts +3 -0
  92. package/dist/utils/RenderCount.d.ts.map +1 -0
  93. package/dist/utils/RenderCount.js +7 -0
  94. package/dist/utils/RenderCount.js.map +1 -0
  95. package/dist/utils/index.d.ts +2 -0
  96. package/dist/utils/index.d.ts.map +1 -1
  97. package/dist/utils/index.js +2 -0
  98. package/dist/utils/index.js.map +1 -1
  99. package/package.json +14 -14
  100. package/src/GDSContext.ts +16 -0
  101. package/src/GDSProvider.tsx +20 -31
  102. package/src/components/Avatar.tsx +3 -2
  103. package/src/components/Breadcrumbs.parts.tsx +1 -1
  104. package/src/components/Button.tsx +113 -107
  105. package/src/components/Card.tsx +2 -2
  106. package/src/components/CopyButton.tsx +49 -25
  107. package/src/components/Input.tsx +1 -1
  108. package/src/components/Link.tsx +2 -2
  109. package/src/components/Menu.parts.tsx +75 -72
  110. package/src/components/Modal.parts.tsx +26 -31
  111. package/src/components/Pane.meta.ts +31 -0
  112. package/src/components/Pane.parts.tsx +713 -0
  113. package/src/components/Pane.tsx +17 -0
  114. package/src/components/Search.tsx +1 -1
  115. package/src/components/Tooltip.parts.tsx +95 -80
  116. package/src/components/base/ButtonOrLink.parts.tsx +71 -51
  117. package/src/components/base/ButtonOrLink.tsx +1 -0
  118. package/src/components/base/MaybeButtonOrLink.tsx +26 -5
  119. package/src/components/base/Presence.tsx +1375 -0
  120. package/src/components/base/index.ts +1 -0
  121. package/src/components/index.ts +10 -0
  122. package/src/hooks/useCSSProp.ts +1 -1
  123. package/src/hooks/useControlled.ts +16 -8
  124. package/src/hooks/useGDS.ts +1 -1
  125. package/src/hooks/useStyleObserver.ts +1 -1
  126. package/src/tailwind-plugin.ts +3 -0
  127. package/src/utils/InlineCounter.tsx +17 -0
  128. package/src/utils/RenderCount.tsx +7 -0
  129. package/src/utils/index.ts +2 -0
@@ -0,0 +1,808 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Children, createContext, isValidElement, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
4
+ import { flushSync } from 'react-dom';
5
+ import { Render } from "./Render.js";
6
+ const PresenceContext = createContext(null);
7
+ /**
8
+ * Hook to access presence information from the nearest `Presence` item ancestor.
9
+ *
10
+ * @returns An object with `status` and `present`, or `null` if not inside a `Presence` component.
11
+ */
12
+ export function usePresence() {
13
+ return useContext(PresenceContext);
14
+ }
15
+ /**
16
+ * Presence.
17
+ *
18
+ * - If you pass no `visibleKey` → unmounting mode.
19
+ * - If you pass `visibleKey` → persistent mode.
20
+ */
21
+ export function Presence({ visibleKey, enterMs = 0, exitMs = 0, initial = false, render, renderChild = _jsx("div", {}), getChildKey, onBeforeTransitionStart, onTransitionStart, onBeforeTransitionEnd, onTransitionEnd, children, }) {
22
+ if (visibleKey !== undefined) {
23
+ return (_jsx(PersistentPresence, { visibleKey: visibleKey, enterMs: enterMs, exitMs: exitMs, initial: initial, render: render, renderChild: renderChild, getChildKey: getChildKey, onBeforeTransitionStart: onBeforeTransitionStart, onTransitionStart: onTransitionStart, onBeforeTransitionEnd: onBeforeTransitionEnd, onTransitionEnd: onTransitionEnd, children: children }));
24
+ }
25
+ return (_jsx(UnmountingPresence, { enterMs: enterMs, exitMs: exitMs, initial: initial, render: render, renderChild: renderChild, getChildKey: getChildKey, onBeforeTransitionStart: onBeforeTransitionStart, onTransitionStart: onTransitionStart, onBeforeTransitionEnd: onBeforeTransitionEnd, onTransitionEnd: onTransitionEnd, children: children }));
26
+ }
27
+ /**
28
+ * UnmountingPresence.
29
+ *
30
+ * Mental model:
31
+ *
32
+ * - Build a map of all items (previous items + current children).
33
+ * - Update their statuses ("entering" → "visible" when added, "visible" → "exiting" when unmounted,
34
+ * "exiting" → "entering" when re-added).
35
+ * - Sort: streaming merge that walks through previous items, using survivors as anchors. For each
36
+ * survivor, flush all unprocessed current children up to and including it. Exiting items are
37
+ * inserted at their previous position.
38
+ */
39
+ function UnmountingPresence({ enterMs, exitMs, initial, render, renderChild, getChildKey, onBeforeTransitionStart, onTransitionStart, onBeforeTransitionEnd, onTransitionEnd, children, }) {
40
+ const getChildKeyRef = useRef(getChildKey);
41
+ getChildKeyRef.current = getChildKey;
42
+ const { setElementRef, fireBeforeTransitionEnd, fireCallbacks } = usePresenceCallbacks(onBeforeTransitionStart, onTransitionStart, onBeforeTransitionEnd, onTransitionEnd);
43
+ const [items, setItems] = useState(() => {
44
+ const { array } = buildChildMap(children, getChildKey);
45
+ const isEntering = initial && enterMs > 0;
46
+ return array.map((child, index) => ({
47
+ key: getStableKey(child, index, getChildKey),
48
+ element: child,
49
+ status: isEntering ? 'entering' : 'visible',
50
+ previousStatus: null,
51
+ initial: true,
52
+ }));
53
+ });
54
+ const { timeoutsRef, addEntered, addExited, flushCallbackRef } = useTransitionTimeouts();
55
+ // Memoize to avoid re-running `useLayoutEffect` on every render (all callbacks are stable)
56
+ const callbacks = useMemo(() => ({ setElementRef, fireBeforeTransitionEnd, fireCallbacks }),
57
+ // oxlint-disable-next-line react-hooks/exhaustive-deps -- all callbacks are stable (useCallback with [])
58
+ []);
59
+ // Set up the flush callback to handle batched transition ends
60
+ flushCallbackRef.current = (entered, exited) => {
61
+ callbacks.fireBeforeTransitionEnd(entered, exited);
62
+ setItems((current) => current
63
+ .filter((item) => !exited.has(item.key))
64
+ .map((item) => entered.has(item.key)
65
+ ? { ...item, status: 'visible', previousStatus: item.status }
66
+ : item));
67
+ };
68
+ useInitialTransitions(initial, enterMs, () => {
69
+ const keys = new Set();
70
+ items.forEach((item) => {
71
+ if (item.status === 'entering' && !timeoutsRef.current.has(item.key)) {
72
+ keys.add(item.key);
73
+ }
74
+ });
75
+ return keys;
76
+ }, timeoutsRef, callbacks, (key) => setItems((curr) => curr.map((i) => i.key === key ? { ...i, status: 'visible', previousStatus: i.status } : i)));
77
+ useLayoutEffect(() => {
78
+ const { map: presentByKey, keys: childKeys } = buildChildMap(children, getChildKeyRef.current);
79
+ const nextKeySet = new Set(childKeys);
80
+ setItems((prevItems) => {
81
+ const keysToScheduleExitEnd = new Set();
82
+ const keysToScheduleEnterEnd = new Set();
83
+ const keysEntering = new Set();
84
+ const keysExiting = new Set();
85
+ // Build map of items that are currently transitioning (before this update)
86
+ const previouslyTransitioning = new Map();
87
+ const prevByKey = new Map();
88
+ prevItems.forEach((item) => {
89
+ prevByKey.set(item.key, item);
90
+ if (item.status === 'entering' || item.status === 'exiting') {
91
+ previouslyTransitioning.set(item.key, item.status);
92
+ }
93
+ });
94
+ const nextItems = [];
95
+ // Process current children
96
+ childKeys.forEach((key) => {
97
+ const prev = prevByKey.get(key);
98
+ const present = presentByKey.get(key);
99
+ if (prev?.status === 'exiting') {
100
+ // Item coming back from exiting
101
+ cancelTimeout(key, timeoutsRef);
102
+ const status = enterMs > 0 ? 'entering' : 'visible';
103
+ nextItems.push({
104
+ key,
105
+ element: present.element,
106
+ status,
107
+ previousStatus: prev.status,
108
+ initial: false,
109
+ });
110
+ keysEntering.add(key);
111
+ if (status === 'entering') {
112
+ keysToScheduleEnterEnd.add(key);
113
+ }
114
+ }
115
+ else if (prev) {
116
+ // Item already exists, keep its status (but instant-transition "entering" → "visible" if `enterMs` changed to 0)
117
+ const status = prev.status === 'entering' && enterMs <= 0 ? 'visible' : prev.status;
118
+ const statusChanged = status !== prev.status;
119
+ nextItems.push({
120
+ key,
121
+ element: present.element,
122
+ status,
123
+ previousStatus: statusChanged ? prev.status : prev.previousStatus,
124
+ initial: prev.initial,
125
+ });
126
+ }
127
+ else {
128
+ // New item
129
+ const status = enterMs > 0 ? 'entering' : 'visible';
130
+ nextItems.push({
131
+ key,
132
+ element: present.element,
133
+ status,
134
+ previousStatus: 'hidden',
135
+ initial: false,
136
+ });
137
+ keysEntering.add(key);
138
+ if (status === 'entering') {
139
+ keysToScheduleEnterEnd.add(key);
140
+ }
141
+ }
142
+ });
143
+ // Process items being removed
144
+ prevItems.forEach((item) => {
145
+ if (!nextKeySet.has(item.key)) {
146
+ const wasAlreadyExiting = item.status === 'exiting';
147
+ if (!wasAlreadyExiting) {
148
+ cancelTimeout(item.key, timeoutsRef);
149
+ keysExiting.add(item.key);
150
+ if (exitMs > 0) {
151
+ keysToScheduleExitEnd.add(item.key);
152
+ }
153
+ }
154
+ // Keep exiting items if:
155
+ // - `exitMs` > 0: allows exit transition
156
+ // - already exiting: let it finish (e.g., children changed while item was mid-exit)
157
+ if (exitMs > 0 || wasAlreadyExiting) {
158
+ nextItems.push({
159
+ ...item,
160
+ status: 'exiting',
161
+ previousStatus: wasAlreadyExiting ? item.previousStatus : item.status,
162
+ });
163
+ }
164
+ }
165
+ });
166
+ // Fire callbacks (handles both timed and instant transitions)
167
+ callbacks.fireCallbacks(children, enterMs, exitMs, keysEntering, keysExiting, previouslyTransitioning);
168
+ // Sort: preserve order by merging current children with previous items
169
+ const itemsByKey = new Map(nextItems.map((item) => [item.key, item]));
170
+ const childIndexByKey = new Map(childKeys.map((key, index) => [key, index]));
171
+ const result = [];
172
+ const added = new Set();
173
+ let childIndex = 0;
174
+ const addChild = (key) => {
175
+ if (!added.has(key)) {
176
+ const item = itemsByKey.get(key);
177
+ if (item) {
178
+ result.push(item);
179
+ added.add(key);
180
+ }
181
+ }
182
+ };
183
+ // Walk through previous items, inserting current children at their positions
184
+ prevItems.forEach((prevItem) => {
185
+ const item = itemsByKey.get(prevItem.key);
186
+ if (!item)
187
+ return;
188
+ if (item.status === 'exiting') {
189
+ // Exiting items stay at their previous position
190
+ result.push(item);
191
+ added.add(item.key);
192
+ }
193
+ else {
194
+ // Current child - add all unprocessed children up to and including it
195
+ const indexInChildren = childIndexByKey.get(prevItem.key);
196
+ if (indexInChildren !== undefined) {
197
+ while (childIndex <= indexInChildren) {
198
+ addChild(childKeys[childIndex]);
199
+ childIndex++;
200
+ }
201
+ }
202
+ }
203
+ });
204
+ // Add any remaining new children
205
+ while (childIndex < childKeys.length) {
206
+ addChild(childKeys[childIndex]);
207
+ childIndex++;
208
+ }
209
+ // Schedule async transitions (after state update completes)
210
+ scheduleTransitionTimeouts(enterMs, exitMs, keysToScheduleEnterEnd, keysToScheduleExitEnd, timeoutsRef, addEntered, addExited);
211
+ return result;
212
+ });
213
+ }, [children, enterMs, exitMs, timeoutsRef, addEntered, addExited, callbacks]);
214
+ // Clear `previousStatus` after the browser has painted the new styles
215
+ useAfterPaint(items.some((item) => item.previousStatus !== null), () => {
216
+ flushSync(() => {
217
+ setItems((current) => current.map((item) => item.previousStatus !== null ? { ...item, previousStatus: null } : item));
218
+ });
219
+ });
220
+ return (_jsx(PresenceWrapper, { render: render, items: items, children: items.map((item) => (_jsx(PresenceItemProvider, { status: item.status, children: _jsx(Render, { ref: (el) => callbacks.setElementRef(item.key, el), render: renderChild, inert: item.status === 'exiting', "data-key": item.key, "data-status": item.status, "data-starting-style": item.previousStatus !== null || undefined, "data-initial": item.initial || undefined, state: {
221
+ key: item.key,
222
+ status: item.status,
223
+ previousStatus: item.previousStatus,
224
+ initial: item.initial,
225
+ }, children: item.element }) }, item.key))) }));
226
+ }
227
+ function normalizeVisibleKey(input) {
228
+ if (input === null) {
229
+ return new Set();
230
+ }
231
+ if (Array.isArray(input)) {
232
+ return new Set(input);
233
+ }
234
+ // Single key - TypeScript can't narrow the union type here
235
+ return new Set([input]);
236
+ }
237
+ /**
238
+ * PersistentPresence.
239
+ *
240
+ * - Tracks children by key in a `Map`.
241
+ * - Items are never unmounted _unless_ you stop rendering them at all.
242
+ * - When a key enters `visibleKey`:
243
+ *
244
+ * - status goes "hidden" → "entering" for `enterMs`
245
+ * - then "visible"
246
+ * - When a key leaves `visibleKey`:
247
+ *
248
+ * - status goes "visible" → "exiting" for `exitMs`
249
+ * - then "hidden" (still mounted, but `hidden` attribute on wrapper)
250
+ */
251
+ function PersistentPresence({ visibleKey, enterMs, exitMs, initial, render, renderChild, getChildKey, onBeforeTransitionStart, onTransitionStart, onBeforeTransitionEnd, onTransitionEnd, children, }) {
252
+ const getChildKeyRef = useRef(getChildKey);
253
+ getChildKeyRef.current = getChildKey;
254
+ const { setElementRef, fireBeforeTransitionEnd, fireCallbacks } = usePresenceCallbacks(onBeforeTransitionStart, onTransitionStart, onBeforeTransitionEnd, onTransitionEnd);
255
+ const [itemsMap, setItemsMap] = useState(() => {
256
+ const { array } = buildChildMap(children, getChildKey);
257
+ const visibleSet = normalizeVisibleKey(visibleKey);
258
+ const map = new Map();
259
+ array.forEach((child, index) => {
260
+ const key = getStableKey(child, index, getChildKey);
261
+ const status = getInitialStatus(visibleSet.has(key), initial, enterMs);
262
+ map.set(key, {
263
+ key,
264
+ element: child,
265
+ status,
266
+ previousStatus: null,
267
+ initial: true,
268
+ order: index,
269
+ });
270
+ });
271
+ return map;
272
+ });
273
+ const { timeoutsRef, addEntered, addExited, flushCallbackRef } = useTransitionTimeouts();
274
+ // Memoize to avoid re-running `useLayoutEffect` on every render (all callbacks are stable)
275
+ const callbacks = useMemo(() => ({ setElementRef, fireBeforeTransitionEnd, fireCallbacks }),
276
+ // oxlint-disable-next-line react-hooks/exhaustive-deps -- all callbacks are stable (useCallback with [])
277
+ []);
278
+ // Set up the flush callback to handle batched transition ends
279
+ flushCallbackRef.current = (entered, exited) => {
280
+ callbacks.fireBeforeTransitionEnd(entered, exited);
281
+ setItemsMap((current) => {
282
+ const copy = new Map(current);
283
+ exited.forEach((key) => {
284
+ const item = copy.get(key);
285
+ if (item)
286
+ copy.set(key, { ...item, status: 'hidden', previousStatus: item.status });
287
+ });
288
+ entered.forEach((key) => {
289
+ const item = copy.get(key);
290
+ if (item)
291
+ copy.set(key, { ...item, status: 'visible', previousStatus: item.status });
292
+ });
293
+ return copy;
294
+ });
295
+ };
296
+ useInitialTransitions(initial, enterMs, () => {
297
+ const keys = new Set();
298
+ itemsMap.forEach((item) => {
299
+ if (item.status === 'entering' && !timeoutsRef.current.has(item.key)) {
300
+ keys.add(item.key);
301
+ }
302
+ });
303
+ return keys;
304
+ }, timeoutsRef, callbacks, (key) => setItemsMap((curr) => {
305
+ const copy = new Map(curr);
306
+ const item = copy.get(key);
307
+ if (item?.status === 'entering') {
308
+ copy.set(key, { ...item, status: 'visible', previousStatus: item.status });
309
+ }
310
+ return copy;
311
+ }));
312
+ // Sync items with children and handle visibility transitions
313
+ useLayoutEffect(() => {
314
+ const { map: presentByKey } = buildChildMap(children, getChildKeyRef.current);
315
+ const visibleSet = normalizeVisibleKey(visibleKey);
316
+ setItemsMap((prevMap) => {
317
+ const nextMap = new Map(prevMap);
318
+ const keysToScheduleExitEnd = new Set();
319
+ const keysToScheduleEnterEnd = new Set();
320
+ const keysEntering = new Set();
321
+ const keysExiting = new Set();
322
+ // Build map of items that are currently transitioning (before this update)
323
+ const previouslyTransitioning = new Map();
324
+ prevMap.forEach((item) => {
325
+ if (item.status === 'entering' || item.status === 'exiting') {
326
+ previouslyTransitioning.set(item.key, item.status);
327
+ }
328
+ });
329
+ // 1) Update / add all present keys
330
+ presentByKey.forEach(({ element, index }, key) => {
331
+ const existing = nextMap.get(key);
332
+ if (existing) {
333
+ nextMap.set(key, { ...existing, element, order: index });
334
+ }
335
+ else {
336
+ const isVisible = visibleSet.has(key);
337
+ const status = getInitialStatus(isVisible, false, enterMs);
338
+ nextMap.set(key, {
339
+ key,
340
+ element,
341
+ status,
342
+ previousStatus: null,
343
+ initial: false,
344
+ order: index,
345
+ });
346
+ // Track new entering items (for callbacks, even if instant)
347
+ if (isVisible) {
348
+ keysEntering.add(key);
349
+ if (status === 'entering') {
350
+ keysToScheduleEnterEnd.add(key);
351
+ }
352
+ }
353
+ }
354
+ });
355
+ // 2) Remove keys that no longer appear in children
356
+ nextMap.forEach((_item, key) => {
357
+ if (!presentByKey.has(key))
358
+ nextMap.delete(key);
359
+ });
360
+ // 3) Handle visibility transitions for existing items
361
+ nextMap.forEach((item, key) => {
362
+ // Skip newly added items (already handled above)
363
+ if (!prevMap.has(key))
364
+ return;
365
+ const shouldBeVisible = visibleSet.has(key);
366
+ if (shouldBeVisible) {
367
+ if (item.status === 'exiting' || item.status === 'hidden') {
368
+ cancelTimeout(key, timeoutsRef);
369
+ const status = enterMs > 0 ? 'entering' : 'visible';
370
+ nextMap.set(key, { ...item, status, previousStatus: item.status, initial: false });
371
+ keysEntering.add(key);
372
+ if (status === 'entering') {
373
+ keysToScheduleEnterEnd.add(key);
374
+ }
375
+ }
376
+ else if (item.status === 'entering' && enterMs <= 0) {
377
+ nextMap.set(key, { ...item, status: 'visible', previousStatus: item.status });
378
+ }
379
+ }
380
+ else if (item.status === 'visible' || item.status === 'entering') {
381
+ cancelTimeout(key, timeoutsRef);
382
+ const status = exitMs > 0 ? 'exiting' : 'hidden';
383
+ nextMap.set(key, { ...item, status, previousStatus: item.status });
384
+ keysExiting.add(key);
385
+ if (status === 'exiting') {
386
+ keysToScheduleExitEnd.add(key);
387
+ }
388
+ }
389
+ else if (item.status === 'exiting' && exitMs <= 0) {
390
+ nextMap.set(key, { ...item, status: 'hidden', previousStatus: item.status });
391
+ }
392
+ });
393
+ // Fire callbacks (handles both timed and instant transitions)
394
+ callbacks.fireCallbacks(visibleKey, enterMs, exitMs, keysEntering, keysExiting, previouslyTransitioning);
395
+ // Schedule async transitions (after state update completes)
396
+ scheduleTransitionTimeouts(enterMs, exitMs, keysToScheduleEnterEnd, keysToScheduleExitEnd, timeoutsRef, addEntered, addExited);
397
+ return nextMap;
398
+ });
399
+ }, [children, visibleKey, enterMs, exitMs, timeoutsRef, addEntered, addExited, callbacks]);
400
+ const items = Array.from(itemsMap.values()).sort((a, b) => a.order - b.order);
401
+ // Clear `previousStatus` after the browser has painted the new styles
402
+ useAfterPaint(items.some((item) => item.previousStatus !== null), () => {
403
+ flushSync(() => {
404
+ setItemsMap((current) => {
405
+ const copy = new Map(current);
406
+ copy.forEach((item, key) => {
407
+ if (item.previousStatus !== null) {
408
+ copy.set(key, { ...item, previousStatus: null });
409
+ }
410
+ });
411
+ return copy;
412
+ });
413
+ });
414
+ });
415
+ return (_jsx(PresenceWrapper, { render: render, items: items, children: items.map((item) => (_jsx(PresenceItemProvider, { status: item.status, children: _jsx(Render, { ref: (el) => callbacks.setElementRef(item.key, el), render: renderChild, hidden: item.status === 'hidden', inert: item.status === 'exiting', "data-key": item.key, "data-status": item.status, "data-starting-style": item.previousStatus !== null || undefined, "data-initial": item.initial || undefined, state: {
416
+ key: item.key,
417
+ status: item.status,
418
+ previousStatus: item.previousStatus,
419
+ initial: item.initial,
420
+ }, children: item.element }) }, item.key))) }));
421
+ }
422
+ /* -------------------------------------------------------------------------- */
423
+ /* Helper components and utilities */
424
+ /* -------------------------------------------------------------------------- */
425
+ /** Renders a wrapper with computed state if the `render` prop is passed. */
426
+ function PresenceWrapper({ render, items, children, }) {
427
+ if (!render) {
428
+ return children;
429
+ }
430
+ return (_jsx(Render, { render: render, state: {
431
+ transitioning: items.some((item) => item.status === 'entering' || item.status === 'exiting'),
432
+ hasPresent: items.some((item) => item.status === 'entering' || item.status === 'visible'),
433
+ items,
434
+ }, children: children }));
435
+ }
436
+ /**
437
+ * Provider component that wraps each `Presence` item to provide presence context to descendants.
438
+ * Computes `present` by checking if this item and all ancestor items are present.
439
+ */
440
+ function PresenceItemProvider({ status, children }) {
441
+ const parentContext = useContext(PresenceContext);
442
+ // Current item is present if it's entering or visible
443
+ const isPresent = status === 'entering' || status === 'visible';
444
+ // Overall present state is true only if this item AND all ancestors are present
445
+ const present = isPresent && (parentContext?.present ?? true);
446
+ return _jsx(PresenceContext, { value: { status, present }, children: children });
447
+ }
448
+ /**
449
+ * Get a stable key for our internal arrays.
450
+ *
451
+ * - If a custom `getChildKey` function is provided, use it.
452
+ * - Otherwise, use the element's `key` prop (with React's internal prefixes stripped).
453
+ * - Fall back to the index if no key is available.
454
+ */
455
+ function getStableKey(child, index, getChildKey) {
456
+ // Use custom key function if provided and if it returns a value for this child
457
+ if (getChildKey) {
458
+ const customKey = getChildKey(child, index);
459
+ if (customKey !== undefined) {
460
+ return customKey;
461
+ }
462
+ }
463
+ // For valid React elements, use their `key` prop
464
+ if (isValidElement(child)) {
465
+ if (child.key !== null) {
466
+ // Undo prefix added by `Children.toArray` for explicit keys: ".$foo" → "foo"
467
+ if (typeof child.key === 'string' && child.key.startsWith('.$')) {
468
+ const slicedKey = child.key.slice(2);
469
+ if (/^-?\d+(\.\d+)?$/.test(slicedKey)) {
470
+ return Number(slicedKey);
471
+ }
472
+ return slicedKey;
473
+ }
474
+ return child.key;
475
+ }
476
+ }
477
+ // Fallback to index for anything else
478
+ return index;
479
+ }
480
+ /** Build a map of child keys to their elements and indices. */
481
+ function buildChildMap(children, getChildKey) {
482
+ const array = Children.toArray(children);
483
+ const map = new Map();
484
+ const keys = [];
485
+ array.forEach((child, index) => {
486
+ const key = getStableKey(child, index, getChildKey);
487
+ keys.push(key);
488
+ map.set(key, { element: child, index });
489
+ });
490
+ return { array, map, keys };
491
+ }
492
+ /** Cancel a pending timeout and remove it from the timeouts map. */
493
+ function cancelTimeout(key, timeoutsRef) {
494
+ const timeoutId = timeoutsRef.current.get(key);
495
+ if (timeoutId !== undefined) {
496
+ window.clearTimeout(timeoutId);
497
+ timeoutsRef.current.delete(key);
498
+ }
499
+ }
500
+ /**
501
+ * Hook that runs a callback after the browser has painted. Uses double `requestAnimationFrame`: the
502
+ * first rAF schedules before the next paint, the second runs after that paint completes.
503
+ */
504
+ function useAfterPaint(shouldRun, callback) {
505
+ useLayoutEffect(() => {
506
+ if (!shouldRun)
507
+ return;
508
+ let outerFrameId;
509
+ let innerFrameId;
510
+ outerFrameId = requestAnimationFrame(() => {
511
+ innerFrameId = requestAnimationFrame(() => {
512
+ callback();
513
+ });
514
+ });
515
+ return () => {
516
+ cancelAnimationFrame(outerFrameId);
517
+ cancelAnimationFrame(innerFrameId);
518
+ };
519
+ });
520
+ }
521
+ /** Determine initial status for an item based on visibility and transition settings. */
522
+ function getInitialStatus(isVisible, initial, enterMs) {
523
+ if (!isVisible)
524
+ return 'hidden';
525
+ return initial && enterMs > 0 ? 'entering' : 'visible';
526
+ }
527
+ /**
528
+ * Hook to manage transition timeouts with batching. Each key gets its own timeout (so cancelling
529
+ * one doesn't affect others), but callbacks are batched together when timeouts fire in the same
530
+ * event loop tick.
531
+ */
532
+ function useTransitionTimeouts() {
533
+ const timeoutsRef = useRef(new Map());
534
+ const pendingEnteredRef = useRef(new Set());
535
+ const pendingExitedRef = useRef(new Set());
536
+ const flushScheduledRef = useRef(false);
537
+ const flushCallbackRef = useRef(null);
538
+ useEffect(() => {
539
+ const timeouts = timeoutsRef.current;
540
+ return () => {
541
+ for (const id of timeouts.values()) {
542
+ window.clearTimeout(id);
543
+ }
544
+ timeouts.clear();
545
+ };
546
+ }, []);
547
+ const flush = useCallback(() => {
548
+ flushScheduledRef.current = false;
549
+ const pendingEntered = pendingEnteredRef.current;
550
+ const pendingExited = pendingExitedRef.current;
551
+ if (pendingEntered.size === 0 && pendingExited.size === 0)
552
+ return;
553
+ const entered = new Set(pendingEntered);
554
+ const exited = new Set(pendingExited);
555
+ pendingEntered.clear();
556
+ pendingExited.clear();
557
+ flushCallbackRef.current?.(entered, exited);
558
+ }, []);
559
+ const scheduleFlush = useCallback(() => {
560
+ if (flushScheduledRef.current)
561
+ return;
562
+ flushScheduledRef.current = true;
563
+ // Use setTimeout(0) instead of queueMicrotask because each setTimeout callback is its own
564
+ // macrotask. Microtasks run at the end of the CURRENT macrotask, so queueMicrotask would flush
565
+ // before other setTimeout callbacks with the same delay have a chance to run.
566
+ window.setTimeout(flush, 0);
567
+ }, [flush]);
568
+ const addEntered = useCallback((key) => {
569
+ timeoutsRef.current.delete(key);
570
+ pendingEnteredRef.current.add(key);
571
+ scheduleFlush();
572
+ }, [scheduleFlush]);
573
+ const addExited = useCallback((key) => {
574
+ timeoutsRef.current.delete(key);
575
+ pendingExitedRef.current.add(key);
576
+ scheduleFlush();
577
+ }, [scheduleFlush]);
578
+ return { timeoutsRef, addEntered, addExited, flushCallbackRef };
579
+ }
580
+ /**
581
+ * Schedule timeouts for entering/exiting transitions. Each key gets its own timeout to ensure
582
+ * cancelling one key's transition doesn't affect others. Callbacks are batched when timeouts fire
583
+ * in the same event loop tick.
584
+ */
585
+ function scheduleTransitionTimeouts(enterMs, exitMs, keysToScheduleEnterEnd, keysToScheduleExitEnd, timeoutsRef, addEntered, addExited) {
586
+ if (exitMs > 0) {
587
+ keysToScheduleExitEnd.forEach((key) => {
588
+ if (timeoutsRef.current.has(key))
589
+ return;
590
+ const timeoutId = window.setTimeout(() => addExited(key), exitMs);
591
+ timeoutsRef.current.set(key, timeoutId);
592
+ });
593
+ }
594
+ if (enterMs > 0) {
595
+ keysToScheduleEnterEnd.forEach((key) => {
596
+ if (timeoutsRef.current.has(key))
597
+ return;
598
+ const timeoutId = window.setTimeout(() => addEntered(key), enterMs);
599
+ timeoutsRef.current.set(key, timeoutId);
600
+ });
601
+ }
602
+ }
603
+ /** Hook to handle initial "entering" → "visible" transitions on mount (when `initial={true}`). */
604
+ function useInitialTransitions(initial, enterMs, getEnteringKeys, timeoutsRef, callbacks, updateItemStatus) {
605
+ const initialCallbacksFiredRef = useRef(false);
606
+ // Fire `onBeforeTransitionStart` and `onTransitionStart` for initial transitions
607
+ useLayoutEffect(() => {
608
+ if (!initial || enterMs <= 0 || initialCallbacksFiredRef.current)
609
+ return;
610
+ const enteringKeys = getEnteringKeys();
611
+ if (enteringKeys.size === 0)
612
+ return;
613
+ initialCallbacksFiredRef.current = true;
614
+ // For initial transitions, elements already exist, so we can fire callbacks directly
615
+ // Use `enterMs > 0` to ensure `fireCallbacks` treats this as a timed transition
616
+ // No previously transitioning items on initial mount
617
+ callbacks.fireCallbacks({}, enterMs, 0, enteringKeys, new Set(), new Map());
618
+ // oxlint-disable-next-line react-hooks/exhaustive-deps -- only run on mount
619
+ }, []);
620
+ // Schedule timeouts for "entering" → "visible" transitions
621
+ // This must be a separate effect without a guard because React Strict Mode clears timeouts
622
+ // on cleanup, and they need to be re-scheduled on the second run.
623
+ useEffect(() => {
624
+ if (!initial || enterMs <= 0)
625
+ return;
626
+ const enteringKeys = getEnteringKeys();
627
+ if (enteringKeys.size === 0)
628
+ return;
629
+ enteringKeys.forEach((key) => {
630
+ const timeoutId = window.setTimeout(() => {
631
+ callbacks.fireBeforeTransitionEnd(new Set([key]), new Set());
632
+ updateItemStatus(key);
633
+ timeoutsRef.current.delete(key);
634
+ }, enterMs);
635
+ timeoutsRef.current.set(key, timeoutId);
636
+ });
637
+ // oxlint-disable-next-line react-hooks/exhaustive-deps -- only run on mount
638
+ }, []);
639
+ }
640
+ function createEmptyPending() {
641
+ return {
642
+ transitionStartEntering: new Set(),
643
+ transitionStartExiting: new Set(),
644
+ transitionStartPreviously: new Map(),
645
+ beforeEndEntered: new Set(),
646
+ transitionEndEntered: new Set(),
647
+ transitionEndExited: new Set(),
648
+ };
649
+ }
650
+ /** Hook to manage callback-related refs and helpers for `Presence` components. */
651
+ function usePresenceCallbacks(onBeforeTransitionStart, onTransitionStart, onBeforeTransitionEnd, onTransitionEnd) {
652
+ // Store callbacks in refs to avoid re-creating `useCallback` functions
653
+ const callbackRefs = useRef({
654
+ onBeforeTransitionStart,
655
+ onTransitionStart,
656
+ onBeforeTransitionEnd,
657
+ onTransitionEnd,
658
+ });
659
+ callbackRefs.current = {
660
+ onBeforeTransitionStart,
661
+ onTransitionStart,
662
+ onBeforeTransitionEnd,
663
+ onTransitionEnd,
664
+ };
665
+ const elementRefsRef = useRef(new Map());
666
+ const pendingRef = useRef(createEmptyPending());
667
+ // Guard to prevent double-firing in React Strict Mode
668
+ // Initialize with a unique symbol to avoid collision with any valid `children` value (including `null`)
669
+ const guardRef = useRef(Symbol('unset'));
670
+ const setElementRef = useCallback((key, element) => {
671
+ if (element) {
672
+ elementRefsRef.current.set(key, element);
673
+ }
674
+ else {
675
+ elementRefsRef.current.delete(key);
676
+ }
677
+ }, []);
678
+ /** Resolves keys to elements from refs. */
679
+ const resolveElements = useCallback((keys) => {
680
+ const map = new Map();
681
+ keys.forEach((key) => {
682
+ const element = elementRefsRef.current.get(key);
683
+ if (element)
684
+ map.set(key, element);
685
+ });
686
+ return map;
687
+ }, []);
688
+ /**
689
+ * Fires `onBeforeTransitionEnd` callback and tracks keys for `onTransitionEnd`. Called from:
690
+ *
691
+ * - Render phase for instant exits (`exitMs <= 0`)
692
+ * - `useLayoutEffect` for instant enters (`enterMs <= 0`)
693
+ * - `flushCallbackRef.current` for timed transitions (via `useTransitionTimeouts` batching)
694
+ */
695
+ const fireBeforeTransitionEnd = useCallback((entered, exited) => {
696
+ if (entered.size === 0 && exited.size === 0)
697
+ return;
698
+ // Track for the `onTransitionEnd` callback (will fire in `useLayoutEffect`)
699
+ entered.forEach((key) => pendingRef.current.transitionEndEntered.add(key));
700
+ exited.forEach((key) => pendingRef.current.transitionEndExited.add(key));
701
+ // Fire the `onBeforeTransitionEnd` callback synchronously
702
+ callbackRefs.current.onBeforeTransitionEnd?.({
703
+ entered: resolveElements(entered),
704
+ exited: resolveElements(exited),
705
+ });
706
+ }, [resolveElements]);
707
+ /**
708
+ * Main entry point for firing callbacks during state updates. Handles both timed and instant
709
+ * transitions:
710
+ *
711
+ * - `onBeforeTransitionStart`: Always fires synchronously (entering keys + exiting elements)
712
+ * - `onTransitionStart` for timed: Tracks for `useLayoutEffect`.
713
+ * - `onTransitionStart` for instant exits: Fires synchronously (element exists, will be unmounted)
714
+ * - `onTransitionStart` for instant enters: Tracks for `useLayoutEffect` (element doesn't exist
715
+ * yet)
716
+ * - `onBeforeTransitionEnd` for instant exits: Fires synchronously.
717
+ * - `onBeforeTransitionEnd` for instant enters: Tracks for `useLayoutEffect`.
718
+ */
719
+ const fireCallbacks = useCallback((guardValue, enterMs, exitMs, entering, exiting, previouslyTransitioning) => {
720
+ // Guard against React Strict Mode double-firing
721
+ if (guardRef.current === guardValue)
722
+ return;
723
+ if (entering.size === 0 && exiting.size === 0)
724
+ return;
725
+ guardRef.current = guardValue;
726
+ const pending = pendingRef.current;
727
+ const hasInstantExits = exitMs <= 0 && exiting.size > 0;
728
+ const hasInstantEnters = enterMs <= 0 && entering.size > 0;
729
+ // 1. Fire `onBeforeTransitionStart` synchronously (always)
730
+ callbackRefs.current.onBeforeTransitionStart?.({
731
+ entering,
732
+ exiting: resolveElements(exiting),
733
+ previouslyTransitioning,
734
+ });
735
+ // 2. Handle `onTransitionStart`
736
+ // - All enters go to `useLayoutEffect` (element may not exist yet)
737
+ // - Timed exits go to `useLayoutEffect`
738
+ // - Instant exits fire synchronously (element exists, will be unmounted)
739
+ entering.forEach((key) => pending.transitionStartEntering.add(key));
740
+ if (exitMs > 0) {
741
+ exiting.forEach((key) => pending.transitionStartExiting.add(key));
742
+ }
743
+ previouslyTransitioning.forEach((status, key) => {
744
+ if (!pending.transitionStartPreviously.has(key)) {
745
+ pending.transitionStartPreviously.set(key, status);
746
+ }
747
+ });
748
+ if (hasInstantExits) {
749
+ // Fire `onTransitionStart` synchronously for instant exits only
750
+ callbackRefs.current.onTransitionStart?.({
751
+ entering: new Map(),
752
+ exiting: resolveElements(exiting),
753
+ previouslyTransitioning,
754
+ });
755
+ }
756
+ // 3. Handle `onBeforeTransitionEnd`
757
+ // - Instant exits: fire synchronously (element exists)
758
+ // - Instant enters: track for `useLayoutEffect` (element doesn't exist yet)
759
+ if (hasInstantExits) {
760
+ fireBeforeTransitionEnd(new Set(), exiting);
761
+ }
762
+ if (hasInstantEnters) {
763
+ entering.forEach((key) => pending.beforeEndEntered.add(key));
764
+ }
765
+ }, [resolveElements, fireBeforeTransitionEnd]);
766
+ // Fire pending callbacks in `useLayoutEffect` (after DOM commit, before paint)
767
+ useLayoutEffect(() => {
768
+ const pending = pendingRef.current;
769
+ const hasStart = pending.transitionStartEntering.size > 0 || pending.transitionStartExiting.size > 0;
770
+ const hasBeforeEnd = pending.beforeEndEntered.size > 0;
771
+ const hasEnd = pending.transitionEndEntered.size > 0 || pending.transitionEndExited.size > 0;
772
+ if (!hasStart && !hasBeforeEnd && !hasEnd)
773
+ return;
774
+ // Capture pending state before resetting
775
+ const { transitionStartEntering, transitionStartExiting, transitionStartPreviously, beforeEndEntered, transitionEndEntered, transitionEndExited, } = pending;
776
+ pendingRef.current = createEmptyPending();
777
+ // Fire `onTransitionStart`
778
+ if (hasStart) {
779
+ callbackRefs.current.onTransitionStart?.({
780
+ entering: resolveElements(transitionStartEntering),
781
+ exiting: resolveElements(transitionStartExiting),
782
+ previouslyTransitioning: transitionStartPreviously,
783
+ });
784
+ }
785
+ // Fire `onBeforeTransitionEnd` for instant enters (this also tracks for `onTransitionEnd`)
786
+ if (hasBeforeEnd) {
787
+ fireBeforeTransitionEnd(beforeEndEntered, new Set());
788
+ }
789
+ // Merge any new entries added by `fireBeforeTransitionEnd` above
790
+ pendingRef.current.transitionEndEntered.forEach((key) => transitionEndEntered.add(key));
791
+ pendingRef.current.transitionEndExited.forEach((key) => transitionEndExited.add(key));
792
+ pendingRef.current.transitionEndEntered = new Set();
793
+ pendingRef.current.transitionEndExited = new Set();
794
+ // Fire `onTransitionEnd` (includes keys from both synchronous and `useLayoutEffect` paths)
795
+ if (transitionEndEntered.size > 0 || transitionEndExited.size > 0) {
796
+ callbackRefs.current.onTransitionEnd?.({
797
+ entered: resolveElements(transitionEndEntered),
798
+ exited: transitionEndExited,
799
+ });
800
+ }
801
+ });
802
+ return {
803
+ setElementRef,
804
+ fireBeforeTransitionEnd,
805
+ fireCallbacks,
806
+ };
807
+ }
808
+ //# sourceMappingURL=Presence.js.map