@flamingo-stack/openframe-frontend-core 0.0.455-snapshot.20260719161719 → 0.0.455

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 (122) hide show
  1. package/dist/{chunk-UXKSOJR7.js → chunk-2KUIA2AR.js} +3 -3
  2. package/dist/{chunk-LMBPJAQ2.js → chunk-5YPKZM6Y.js} +6 -6
  3. package/dist/{chunk-YMGOTPY7.cjs → chunk-62AXZKYN.cjs} +866 -981
  4. package/dist/chunk-62AXZKYN.cjs.map +1 -0
  5. package/dist/{chunk-JHIT44TA.js → chunk-6W4ZEVIC.js} +3 -3
  6. package/dist/{chunk-MK2YWJAA.js → chunk-BD3JBEUC.js} +40 -64
  7. package/dist/chunk-BD3JBEUC.js.map +1 -0
  8. package/dist/{chunk-KH5VTGCS.cjs → chunk-BE77G7GW.cjs} +40 -40
  9. package/dist/{chunk-KH5VTGCS.cjs.map → chunk-BE77G7GW.cjs.map} +1 -1
  10. package/dist/{chunk-DYBATZJP.cjs → chunk-D5PBUVGB.cjs} +5 -5
  11. package/dist/{chunk-DYBATZJP.cjs.map → chunk-D5PBUVGB.cjs.map} +1 -1
  12. package/dist/{chunk-2I3GN7WW.js → chunk-DGPKGESY.js} +4 -4
  13. package/dist/{chunk-F3IBBAKV.js → chunk-EGTGRG7N.js} +2 -2
  14. package/dist/{chunk-UHVVNVUE.cjs → chunk-FCVF4YAB.cjs} +40 -40
  15. package/dist/{chunk-UHVVNVUE.cjs.map → chunk-FCVF4YAB.cjs.map} +1 -1
  16. package/dist/{chunk-VZNJ3RYI.cjs → chunk-GNYV6HYO.cjs} +15 -15
  17. package/dist/{chunk-VZNJ3RYI.cjs.map → chunk-GNYV6HYO.cjs.map} +1 -1
  18. package/dist/{chunk-F2CCUPKC.cjs → chunk-IDCPYG7N.cjs} +29 -29
  19. package/dist/{chunk-F2CCUPKC.cjs.map → chunk-IDCPYG7N.cjs.map} +1 -1
  20. package/dist/{chunk-YLTY4Z4B.js → chunk-JUUPMMKI.js} +3 -3
  21. package/dist/{chunk-IOHQJOLR.cjs → chunk-KTGGM5WA.cjs} +12 -36
  22. package/dist/chunk-KTGGM5WA.cjs.map +1 -0
  23. package/dist/{chunk-U27TXTAK.js → chunk-LB3OZE4B.js} +6 -6
  24. package/dist/{chunk-GC63K4VD.cjs → chunk-M5RT26WQ.cjs} +73 -73
  25. package/dist/{chunk-GC63K4VD.cjs.map → chunk-M5RT26WQ.cjs.map} +1 -1
  26. package/dist/{chunk-UEKCD6WV.js → chunk-N4CHA7T7.js} +16 -131
  27. package/dist/chunk-N4CHA7T7.js.map +1 -0
  28. package/dist/{chunk-6K6LAD6H.cjs → chunk-NNDNJCJ3.cjs} +18 -18
  29. package/dist/{chunk-6K6LAD6H.cjs.map → chunk-NNDNJCJ3.cjs.map} +1 -1
  30. package/dist/{chunk-DU6R7XNU.js → chunk-QSIZW6EX.js} +2 -2
  31. package/dist/{chunk-6TI3DCOG.cjs → chunk-RT7DIGTN.cjs} +3 -3
  32. package/dist/{chunk-6TI3DCOG.cjs.map → chunk-RT7DIGTN.cjs.map} +1 -1
  33. package/dist/{chunk-GQZGCMRF.cjs → chunk-RZL7I6YN.cjs} +7 -7
  34. package/dist/{chunk-GQZGCMRF.cjs.map → chunk-RZL7I6YN.cjs.map} +1 -1
  35. package/dist/{chunk-TXS2AMFV.cjs → chunk-UJZ6O2ON.cjs} +32 -32
  36. package/dist/{chunk-TXS2AMFV.cjs.map → chunk-UJZ6O2ON.cjs.map} +1 -1
  37. package/dist/{chunk-ZQHGSR3H.js → chunk-VNE5R4LQ.js} +5 -5
  38. package/dist/{chunk-ILUX3DXS.js → chunk-X62ICFPJ.js} +2 -2
  39. package/dist/{chunk-DAEKUR3J.js → chunk-XAQLBUCP.js} +2 -2
  40. package/dist/{chunk-INH6SIP5.cjs → chunk-XT6S6DYS.cjs} +12 -12
  41. package/dist/{chunk-INH6SIP5.cjs.map → chunk-XT6S6DYS.cjs.map} +1 -1
  42. package/dist/{chunk-A7FTNFBX.cjs → chunk-YSKSBLCG.cjs} +73 -73
  43. package/dist/{chunk-A7FTNFBX.cjs.map → chunk-YSKSBLCG.cjs.map} +1 -1
  44. package/dist/{chunk-LAFCXJVT.js → chunk-YXCYQKAX.js} +5 -5
  45. package/dist/components/case-studies/index.cjs +9 -9
  46. package/dist/components/case-studies/index.js +3 -3
  47. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  48. package/dist/components/chat/guide-welcome.d.ts.map +1 -1
  49. package/dist/components/chat/index.cjs +3 -3
  50. package/dist/components/chat/index.js +2 -2
  51. package/dist/components/chat/mingo-welcome.d.ts.map +1 -1
  52. package/dist/components/chat/quick-action-wall.d.ts +1 -5
  53. package/dist/components/chat/quick-action-wall.d.ts.map +1 -1
  54. package/dist/components/chat/types/component.types.d.ts +0 -17
  55. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  56. package/dist/components/contact/index.cjs +4 -4
  57. package/dist/components/contact/index.js +3 -3
  58. package/dist/components/docs/index.cjs +6 -6
  59. package/dist/components/docs/index.js +5 -5
  60. package/dist/components/embeds/index.cjs +4 -4
  61. package/dist/components/embeds/index.js +3 -3
  62. package/dist/components/faq/index.cjs +5 -5
  63. package/dist/components/faq/index.js +4 -4
  64. package/dist/components/features/cards-strip.d.ts.map +1 -1
  65. package/dist/components/features/index.cjs +3 -3
  66. package/dist/components/features/index.js +2 -2
  67. package/dist/components/help-center-pages/index.cjs +22 -22
  68. package/dist/components/help-center-pages/index.js +13 -13
  69. package/dist/components/index.cjs +135 -135
  70. package/dist/components/index.js +11 -11
  71. package/dist/components/navigation/index.cjs +3 -3
  72. package/dist/components/navigation/index.js +2 -2
  73. package/dist/components/onboarding-guides/index.cjs +7 -7
  74. package/dist/components/onboarding-guides/index.js +6 -6
  75. package/dist/components/related-content/index.cjs +5 -5
  76. package/dist/components/related-content/index.js +4 -4
  77. package/dist/components/tickets/index.cjs +6 -6
  78. package/dist/components/tickets/index.js +5 -5
  79. package/dist/components/ui/index.cjs +3 -3
  80. package/dist/components/ui/index.js +2 -2
  81. package/dist/components/ui/marquee-wall.d.ts +1 -8
  82. package/dist/components/ui/marquee-wall.d.ts.map +1 -1
  83. package/dist/hooks/index.cjs +2 -4
  84. package/dist/hooks/index.cjs.map +1 -1
  85. package/dist/hooks/index.js +1 -3
  86. package/dist/hooks/ui/index.d.ts +0 -1
  87. package/dist/hooks/ui/index.d.ts.map +1 -1
  88. package/dist/hooks/use-near-viewport.d.ts +1 -3
  89. package/dist/hooks/use-near-viewport.d.ts.map +1 -1
  90. package/dist/index.cjs +3 -5
  91. package/dist/index.cjs.map +1 -1
  92. package/dist/index.js +2 -4
  93. package/package.json +1 -1
  94. package/src/components/chat/chat-message-list.tsx +5 -48
  95. package/src/components/chat/embeddable-chat.tsx +3 -31
  96. package/src/components/chat/guide-welcome.tsx +0 -1
  97. package/src/components/chat/mingo-welcome.tsx +0 -1
  98. package/src/components/chat/quick-action-wall.tsx +0 -7
  99. package/src/components/chat/types/component.types.ts +0 -17
  100. package/src/components/features/cards-strip.tsx +0 -16
  101. package/src/components/ui/marquee-wall.tsx +10 -160
  102. package/src/hooks/ui/index.ts +0 -1
  103. package/src/hooks/use-near-viewport.ts +12 -36
  104. package/dist/chunk-IOHQJOLR.cjs.map +0 -1
  105. package/dist/chunk-MK2YWJAA.js.map +0 -1
  106. package/dist/chunk-UEKCD6WV.js.map +0 -1
  107. package/dist/chunk-YMGOTPY7.cjs.map +0 -1
  108. package/dist/hooks/ui/use-reset-on-page-hidden.d.ts +0 -19
  109. package/dist/hooks/ui/use-reset-on-page-hidden.d.ts.map +0 -1
  110. package/src/hooks/ui/use-reset-on-page-hidden.ts +0 -32
  111. /package/dist/{chunk-UXKSOJR7.js.map → chunk-2KUIA2AR.js.map} +0 -0
  112. /package/dist/{chunk-LMBPJAQ2.js.map → chunk-5YPKZM6Y.js.map} +0 -0
  113. /package/dist/{chunk-JHIT44TA.js.map → chunk-6W4ZEVIC.js.map} +0 -0
  114. /package/dist/{chunk-2I3GN7WW.js.map → chunk-DGPKGESY.js.map} +0 -0
  115. /package/dist/{chunk-F3IBBAKV.js.map → chunk-EGTGRG7N.js.map} +0 -0
  116. /package/dist/{chunk-YLTY4Z4B.js.map → chunk-JUUPMMKI.js.map} +0 -0
  117. /package/dist/{chunk-U27TXTAK.js.map → chunk-LB3OZE4B.js.map} +0 -0
  118. /package/dist/{chunk-DU6R7XNU.js.map → chunk-QSIZW6EX.js.map} +0 -0
  119. /package/dist/{chunk-ZQHGSR3H.js.map → chunk-VNE5R4LQ.js.map} +0 -0
  120. /package/dist/{chunk-ILUX3DXS.js.map → chunk-X62ICFPJ.js.map} +0 -0
  121. /package/dist/{chunk-DAEKUR3J.js.map → chunk-XAQLBUCP.js.map} +0 -0
  122. /package/dist/{chunk-LAFCXJVT.js.map → chunk-YXCYQKAX.js.map} +0 -0
@@ -2264,19 +2264,7 @@ function EmbeddableChatInner({
2264
2264
  <ChatMessageList
2265
2265
  messages={messages}
2266
2266
  isTyping={chatLoading}
2267
- // Real drawer: the library's smart follow. Passive in-page
2268
- // demo (previewMode): deterministic hard pin instead — a
2269
- // scripted assistant-only stream from a cold mount never
2270
- // satisfies the library's "at bottom" gate, so the reply
2271
- // landed below the fold. `pinBottom` snaps to bottom on
2272
- // every frame; identical mechanism to the Fae demo box, so
2273
- // both surfaces behave 1:1.
2274
- autoScroll={!previewMode}
2275
- pinBottom={previewMode}
2276
- // Passive in-page demos (previewMode) let the surrounding
2277
- // page scroll over the thread; the real drawer keeps
2278
- // containment (deck slide-scroll fix).
2279
- overscrollContain={!previewMode}
2267
+ autoScroll={true}
2280
2268
  assistantType="mingo"
2281
2269
  assistantIcon={mingoAssistantIcon}
2282
2270
  renderEntityCard={renderEntityCard}
@@ -2284,27 +2272,11 @@ function EmbeddableChatInner({
2284
2272
  renderContextItem={renderContextItem}
2285
2273
  renderMention={renderMention}
2286
2274
  NavLinkAnchor={NavLinkAnchorViaRuntime}
2287
- // Real Mingo drawer: hide the message-list scrollbar
2275
+ // Hide the message-list scrollbar for the Mingo panel
2288
2276
  // (scroll stays functional). Scoped here via `className`
2289
2277
  // instead of `ChatMessageList` itself, so other list
2290
2278
  // consumers (host chat, tickets) keep their thin bar.
2291
- //
2292
- // `previewMode` (the passive in-page demo) instead:
2293
- // • KEEP the default thin scrollbar — the "elevator" —
2294
- // so the Mingo demo's scroll controls match the Fae
2295
- // demo box (which renders the default ChatMessageList
2296
- // scrollbar) 1:1.
2297
- // • Re-enable pointer events on the scroller: previewMode
2298
- // puts `pointer-events-none` on the whole panel (line
2299
- // ~1981) so the demo doesn't trap clicks, but that also
2300
- // killed wheel/touch scroll on the thread (the Mingo
2301
- // embed couldn't scroll while the Fae box could). Only
2302
- // the scroller re-enables; composer + chrome stay inert.
2303
- className={
2304
- previewMode
2305
- ? 'flex-1 pointer-events-auto'
2306
- : 'flex-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'
2307
- }
2279
+ className="flex-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
2308
2280
  // No inner `px`/`pb`: the panel wrapper already pads with
2309
2281
  // `p-[var(--spacing-system-m)]`. The default content class
2310
2282
  // adds `px-[var(--spacing-system-m)]` + `pb-…xs`, which
@@ -207,7 +207,6 @@ export function GuideWelcome({
207
207
  chips={chipItems}
208
208
  rows={4}
209
209
  pauseOnHover
210
- dragScroll
211
210
  fade={['left', 'right']}
212
211
  fadeSize={{ left: 32 }}
213
212
  fadeColor="var(--color-bg)"
@@ -339,7 +339,6 @@ export function MingoWelcome({
339
339
  }))}
340
340
  rows={4}
341
341
  pauseOnHover
342
- dragScroll
343
342
  fade={['left', 'right']}
344
343
  fadeSize={{ left: 32 }}
345
344
  fadeColor="var(--color-bg)"
@@ -71,10 +71,6 @@ export interface QuickActionWallProps {
71
71
  /** Marquee on/off (auto-off when content fits / reduced motion / loading). */
72
72
  animate?: boolean
73
73
  pauseOnHover?: boolean
74
- /** Opt-in manual drag + wheel scroll (forwarded to {@link MarqueeWall}) — the
75
- * auto-marquee pauses while the user drags/wheels and resumes from there.
76
- * Enabled on the embeddable-chat quick-action walls. */
77
- dragScroll?: boolean
78
74
  fade?: MarqueeWallFadeEdge | ReadonlyArray<MarqueeWallFadeEdge>
79
75
  /** Color behind the wall — the fades blend into it. */
80
76
  fadeColor?: string
@@ -143,7 +139,6 @@ export function QuickActionWall({
143
139
  speed,
144
140
  animate = true,
145
141
  pauseOnHover,
146
- dragScroll = false,
147
142
  fade,
148
143
  fadeColor,
149
144
  fadeSize,
@@ -211,7 +206,6 @@ export function QuickActionWall({
211
206
  speed={speed}
212
207
  animate={animate && !loading}
213
208
  pauseOnHover={pauseOnHover}
214
- dragScroll={dragScroll}
215
209
  copyGap={gap}
216
210
  contentClassName={cn('flex items-center', contentClassName)}
217
211
  contentStyle={{ gap }}
@@ -235,7 +229,6 @@ export function QuickActionWall({
235
229
  // Skeleton walls stay put — the marquee starts when real chips land.
236
230
  animate={animate && !loading}
237
231
  pauseOnHover={pauseOnHover}
238
- dragScroll={dragScroll}
239
232
  fade={fade}
240
233
  fadeColor={fadeColor}
241
234
  fadeSize={fadeSize}
@@ -222,23 +222,6 @@ export interface ChatMessageListProps extends HTMLAttributes<HTMLDivElement> {
222
222
  typingMessage?: string
223
223
  smoothScroll?: boolean
224
224
  autoScroll?: boolean
225
- /** `overscroll-behavior: contain` on the scroller (default true) — stops
226
- * wheel/touch scroll from chaining to the page at the thread's edges (the
227
- * deck / drawer need this). Passive in-page demo chats set `false` so the
228
- * page keeps scrolling normally over the non-interactive thread. */
229
- overscrollContain?: boolean
230
- /** Deterministic stick-to-bottom for PASSIVE scripted replays (the in-page
231
- * Fae/Mingo demos). Whenever the scroll HEIGHT changes — a streamed token
232
- * grows the thread, or a new conversation replaces it — the scroller is
233
- * hard-snapped to its full height, so the newest streamed line is always
234
- * visible even though no human is at the keyboard. Unlike `autoScroll` (the
235
- * library's smart follow, which only tracks while the viewer is "at bottom"
236
- * and is unreliable for an assistant-only stream from a cold, non-bottom
237
- * mount), this pins on real growth without that gate. A same-height idle
238
- * re-render does NOT re-pin, so once the stream settles the viewer can freely
239
- * scroll up. Pair with `autoScroll={false}` so the library's spring doesn't
240
- * run in parallel. */
241
- pinBottom?: boolean
242
225
  showAvatars?: boolean
243
226
  /** Same `fullWidth` semantics as `ChatHeaderProps.fullWidth` — drops
244
227
  * the inner content wrapper's `max-w-ods-content-narrow` so messages
@@ -45,7 +45,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
45
45
  import { cn } from '../../utils/cn';
46
46
  import { NEAR_VIEWPORT_ROOT_MARGIN } from '../../hooks/use-near-viewport';
47
47
  import { useMarqueeEngine } from '../../hooks/ui/use-marquee-engine';
48
- import { useResetOnPageHidden } from '../../hooks/ui/use-reset-on-page-hidden';
49
48
  import { useSuppressCloneFocus } from '../../hooks/ui/use-suppress-clone-focus';
50
49
  import { Button } from '../ui/button';
51
50
  import { SECTION_HEADING_CLASS } from '../layout/page-heading';
@@ -425,20 +424,6 @@ export function CardsStrip<T = unknown>(props: CardsStripProps<T>): React.ReactE
425
424
  const current = activeKeyRef.current;
426
425
  if (current !== null) deactivate(current);
427
426
  }, [deactivate]);
428
- // Unconditional hover-clear (any pointer type) — the self-heal for a MISSED
429
- // `pointerleave`. A leave can be swallowed by a tab blur, an interrupted
430
- // pointer (pointercancel), or an overlay mounting under the cursor, leaving
431
- // `inside` stuck true so the marquee freezes FOREVER ("gets stuck every once
432
- // in a while"). Reset it on pointercancel + whenever the tab hides / the
433
- // window loses focus; the next real hover re-arms it. Can only un-stick.
434
- const clearHover = useCallback(() => {
435
- hoverPointerRef.current.inside = false;
436
- const current = activeKeyRef.current;
437
- if (current !== null) deactivate(current);
438
- }, [deactivate]);
439
- // Shared SSOT self-heal: un-stick the hover flag whenever the tab hides / the
440
- // window loses focus (a `pointerleave`/`pointercancel` can be swallowed).
441
- useResetOnPageHidden(clearHover);
442
427
  /** Re-sync hover only when the track actually moved under the pointer.
443
428
  * KNOWN HOT-PATH COST (intentional): while the marquee runs with the
444
429
  * pointer parked over strip whitespace (no card → not paused), scrollLeft
@@ -669,7 +654,6 @@ export function CardsStrip<T = unknown>(props: CardsStripProps<T>): React.ReactE
669
654
  onPointerEnter={onHoverPointerEnter}
670
655
  onPointerMove={onHoverPointerMove}
671
656
  onPointerLeave={onHoverPointerLeave}
672
- onPointerCancel={clearHover}
673
657
  onScroll={onSeamWarp}
674
658
  >
675
659
  <div ref={trackRef} data-copies={copies} className="flex w-max items-stretch gap-4">
@@ -4,7 +4,6 @@ import * as React from 'react'
4
4
  import { cn } from '../../utils/cn'
5
5
  import { useMarqueeEngine } from '../../hooks/ui/use-marquee-engine'
6
6
  import { useMediaQuery } from '../../hooks/ui/use-media-query'
7
- import { useResetOnPageHidden } from '../../hooks/ui/use-reset-on-page-hidden'
8
7
  import { useSuppressCloneFocus } from '../../hooks/ui/use-suppress-clone-focus'
9
8
  import { NEAR_VIEWPORT_ROOT_MARGIN } from '../../hooks/use-near-viewport'
10
9
 
@@ -154,13 +153,6 @@ export interface MarqueeWallProps {
154
153
  * must never move under a pointing cursor (required for interactive chip
155
154
  * walls). Default true. */
156
155
  pauseOnHover?: boolean
157
- /** Opt-in MANUAL scroll: pointer-drag + horizontal wheel move the wall along
158
- * its axis (transform-based — no inner scroller is created, so it stays
159
- * deck-safe). The auto-marquee pauses during the interaction and briefly
160
- * after, then resumes drifting from wherever the user left it. Off by
161
- * default (decorative walls stay auto-only); enabled on the embeddable-chat
162
- * quick-action walls so users can browse the actions by hand. */
163
- dragScroll?: boolean
164
156
  /** Edge fade(s) — the "there's more" affordance. ONE spelling of the
165
157
  * clip-and-fade across every wall surface. */
166
158
  fade?: MarqueeWallFadeEdge | ReadonlyArray<MarqueeWallFadeEdge>
@@ -303,7 +295,6 @@ export function MarqueeWall({
303
295
  speed = 40,
304
296
  animate = true,
305
297
  pauseOnHover = true,
306
- dragScroll = false,
307
298
  fade,
308
299
  fadeColor = 'var(--color-bg)',
309
300
  fadeSize,
@@ -395,9 +386,6 @@ export function MarqueeWall({
395
386
 
396
387
  // ---- pause-reason set ------------------------------------------------------
397
388
  const pointerInsideRef = React.useRef(false)
398
- const clearPointerInside = React.useCallback(() => {
399
- pointerInsideRef.current = false
400
- }, [])
401
389
  const onPointerEnter = React.useCallback((e: React.PointerEvent) => {
402
390
  if (e.pointerType === 'touch') return
403
391
  pointerInsideRef.current = true
@@ -406,21 +394,6 @@ export function MarqueeWall({
406
394
  if (e.pointerType === 'touch') return
407
395
  pointerInsideRef.current = false
408
396
  }, [])
409
- // Stale-flag self-heal (shared SSOT): if a `pointerleave` is MISSED (tab blur,
410
- // overlay under the cursor, interrupted pointer), `inside` stays stuck true and
411
- // the wall freezes forever — so un-stick it whenever the tab hides / the window
412
- // loses focus. The next real pointer/enter re-arms it.
413
- useResetOnPageHidden(clearPointerInside)
414
-
415
- // ---- manual drag / wheel scroll (opt-in via `dragScroll`) ------------------
416
- // Transform-based (no inner scroller → deck-safe): drag/wheel move the
417
- // engine's float position directly; `draggingRef` + a short suppress window
418
- // pause the auto-marquee during and just after the interaction so it resumes
419
- // drifting from where the user left it. Refs declared here (before the engine)
420
- // so `isPaused` can read them; the handlers (which need the engine's `wrap`)
421
- // are defined after the engine call.
422
- const draggingRef = React.useRef(false)
423
- const dragSuppressUntilRef = React.useRef(0)
424
397
 
425
398
  // ---- transform driver ------------------------------------------------------
426
399
  const applyTransform = React.useCallback(
@@ -441,26 +414,6 @@ export function MarqueeWall({
441
414
  },
442
415
  [axis, reverse, trackId],
443
416
  )
444
- // Sync-aware position writer — THE single entry point for every position
445
- // update (the engine's rAF apply AND the manual drag/wheel handlers). With a
446
- // `sync` controller it publishes to the shared position and drives every peer
447
- // wall's transform; without one it just writes our own. Routing drag/wheel
448
- // through here (instead of raw `applyTransform`) keeps a wall that is both
449
- // `sync`-ed and `dragScroll`-ed in lockstep with its peers — a direct
450
- // `applyTransform` would move only this wall and leave `sync.position` + peers
451
- // stale. Members' own `apply` stays the raw `applyTransform` (no re-publish),
452
- // so the `forEach` fan-out can't recurse.
453
- const applyPos = React.useCallback(
454
- (pos: number) => {
455
- if (sync) {
456
- sync.position = pos
457
- sync.members.forEach(m => m.apply(pos))
458
- } else {
459
- applyTransform(pos)
460
- }
461
- },
462
- [sync, applyTransform],
463
- )
464
417
  // Clear any residual offset only when the marquee turns off STRUCTURALLY
465
418
  // (content shrank so it fits, reduced-motion flipped) — the static wall must
466
419
  // sit at its natural origin. A mere run-gate pause (`animate` false) does NOT
@@ -504,16 +457,18 @@ export function MarqueeWall({
504
457
  }
505
458
  }, [sync, applyTransform])
506
459
 
507
- const { posRef, wrap } = useMarqueeEngine({
460
+ const { posRef } = useMarqueeEngine({
508
461
  active: marqueeActive && nearViewport && isDriver,
509
462
  speed,
510
- isPaused: now =>
511
- (pauseOnHover && pointerInsideRef.current) ||
512
- draggingRef.current ||
513
- now < dragSuppressUntilRef.current ||
514
- document.visibilityState === 'hidden',
463
+ isPaused: () =>
464
+ (pauseOnHover && pointerInsideRef.current) || document.visibilityState === 'hidden',
515
465
  getWrapSize: () => wrapSizeRef.current,
516
- apply: applyPos,
466
+ apply: sync
467
+ ? pos => {
468
+ sync.position = pos
469
+ sync.members.forEach(m => m.apply(pos))
470
+ }
471
+ : applyTransform,
517
472
  // Synced pairs: a re-elected driver resumes from the pair's live position
518
473
  // (engine seeds from readBack on activation) instead of restarting at 0.
519
474
  readBack: sync ? () => sync.position : undefined,
@@ -535,122 +490,17 @@ export function MarqueeWall({
535
490
  }
536
491
  }, [marqueeMounted, reverse, posRef])
537
492
 
538
- // ---- manual drag / wheel handlers (only wired when `dragScroll`) -----------
539
- const DRAG_RESUME_MS = 1200
540
- const dragStartRef = React.useRef({ coord: 0, pos: 0 })
541
- const dragMovedRef = React.useRef(false)
542
- const suppressClickRef = React.useRef(false)
543
-
544
- const onDragPointerDown = React.useCallback((e: React.PointerEvent) => {
545
- if (!marqueeMounted) return
546
- draggingRef.current = true
547
- dragMovedRef.current = false
548
- // Clear any click-suppress left armed by a prior drag that ended WITHOUT a
549
- // synthetic click on this container (pointercancel, or the pointer released
550
- // off the wall) — otherwise `onDragClickCapture` would swallow this fresh
551
- // tap's action. A new press is unambiguously a new intent.
552
- suppressClickRef.current = false
553
- dragStartRef.current = { coord: axis === 'y' ? e.clientY : e.clientX, pos: posRef.current }
554
- try { containerRef.current?.setPointerCapture(e.pointerId) } catch { /* capture is best-effort */ }
555
- }, [marqueeMounted, axis, posRef])
556
-
557
- const onDragPointerMove = React.useCallback((e: React.PointerEvent) => {
558
- if (!draggingRef.current) return
559
- const delta = (axis === 'y' ? e.clientY : e.clientX) - dragStartRef.current.coord
560
- if (Math.abs(delta) > 3) dragMovedRef.current = true
561
- // Track follows the pointer. `applyPos` maps pos→translate as `-pos`
562
- // (forward) / `pos-wrap` (reverse), so a rightward/downward drag (delta > 0)
563
- // must DECREASE pos on a forward wall and INCREASE it on a reverse one.
564
- // Via `applyPos` (not raw `applyTransform`) so synced peer walls follow.
565
- posRef.current = wrap(dragStartRef.current.pos + (reverse ? delta : -delta))
566
- applyPos(posRef.current)
567
- dragSuppressUntilRef.current = performance.now() + DRAG_RESUME_MS
568
- }, [axis, reverse, wrap, posRef, applyPos])
569
-
570
- const onDragPointerUp = React.useCallback((e: React.PointerEvent) => {
571
- if (!draggingRef.current) return
572
- draggingRef.current = false
573
- // A real drag ends over a chip → swallow the synthetic click so the drag
574
- // doesn't fire the chip's action.
575
- if (dragMovedRef.current) suppressClickRef.current = true
576
- dragSuppressUntilRef.current = performance.now() + DRAG_RESUME_MS
577
- try { containerRef.current?.releasePointerCapture(e.pointerId) } catch { /* best-effort */ }
578
- }, [])
579
-
580
- const onDragClickCapture = React.useCallback((e: React.MouseEvent) => {
581
- if (suppressClickRef.current) {
582
- suppressClickRef.current = false
583
- e.preventDefault()
584
- e.stopPropagation()
585
- }
586
- }, [])
587
-
588
- // Along-axis wheel → move the wall. Wired as a NATIVE non-passive listener
589
- // (not React's `onWheel`, which React attaches passively so `preventDefault`
590
- // is a no-op): once WE consume an along-axis wheel we must `preventDefault` to
591
- // suppress the browser's horizontal history-swipe (two-finger trackpad) on an
592
- // x-wall. An x-wall ignores vertical page-scroll wheels (and vice-versa) so the
593
- // surrounding page still scrolls normally.
594
- React.useEffect(() => {
595
- const el = containerRef.current
596
- if (!el || !dragScroll || !marqueeMounted) return
597
- const onWheel = (e: WheelEvent) => {
598
- // Only claim the DOMINANT-axis wheel (per axis), so a cross-axis wheel
599
- // still scrolls the page: an x-wall ignores vertical wheels, a y-wall
600
- // ignores horizontal ones.
601
- const primary =
602
- axis === 'y'
603
- ? Math.abs(e.deltaY) >= Math.abs(e.deltaX)
604
- ? e.deltaY
605
- : 0
606
- : Math.abs(e.deltaX) >= Math.abs(e.deltaY)
607
- ? e.deltaX
608
- : 0
609
- if (primary === 0) return
610
- e.preventDefault()
611
- // Via `applyPos` (not raw `applyTransform`) so synced peer walls follow.
612
- posRef.current = wrap(posRef.current + (reverse ? -primary : primary))
613
- applyPos(posRef.current)
614
- dragSuppressUntilRef.current = performance.now() + DRAG_RESUME_MS
615
- }
616
- el.addEventListener('wheel', onWheel, { passive: false })
617
- return () => el.removeEventListener('wheel', onWheel)
618
- }, [dragScroll, marqueeMounted, axis, reverse, wrap, posRef, applyPos])
619
-
620
493
  // ---- render ----------------------------------------------------------------
621
494
  const renderContent = (isClone: boolean) =>
622
495
  typeof children === 'function' ? children({ isClone }) : children
623
496
 
624
- // Reduced-motion static fallback: when the marquee does NOT mount (chiefly
625
- // prefers-reduced-motion) but the content OVERFLOWS, the drag/transform path
626
- // is inert — so on a `dragScroll` wall fall back to NATIVE
627
- // overflow scroll along the axis. Otherwise the overflowing chips would be
628
- // clipped by `overflow-hidden` with no way to reach them (a11y regression vs
629
- // the old wrapping row). The visible thin scrollbar (global `*` rule) is the
630
- // right affordance for reduced-motion users. No engine needed — the browser
631
- // scrolls the single static track.
632
- const staticScrollable = dragScroll && overflows && !marqueeMounted
633
-
634
497
  return (
635
498
  <div
636
499
  ref={containerRef}
637
- className={cn(
638
- 'relative',
639
- staticScrollable ? (axis === 'y' ? 'overflow-y-auto' : 'overflow-x-auto') : 'overflow-hidden',
640
- dragScroll && marqueeMounted && 'cursor-grab active:cursor-grabbing select-none',
641
- className,
642
- )}
643
- // x-walls let vertical page-scroll pass (`pan-y`) and own horizontal
644
- // drags; y-walls the reverse. Only when drag is actually live.
645
- style={dragScroll && marqueeMounted ? { touchAction: axis === 'y' ? 'pan-x' : 'pan-y' } : undefined}
500
+ className={cn('relative overflow-hidden', className)}
646
501
  data-marquee-track={trackId}
647
502
  onPointerEnter={onPointerEnter}
648
503
  onPointerLeave={onPointerLeave}
649
- onPointerDown={dragScroll ? onDragPointerDown : undefined}
650
- onPointerMove={dragScroll ? onDragPointerMove : undefined}
651
- onPointerUp={dragScroll ? onDragPointerUp : undefined}
652
- onPointerCancel={dragScroll ? onDragPointerUp : undefined}
653
- onClickCapture={dragScroll ? onDragClickCapture : undefined}
654
504
  >
655
505
  <div
656
506
  ref={trackRef}
@@ -12,7 +12,6 @@ export * from './use-media-query'
12
12
  export * from './use-memoized-callback'
13
13
  export * from './use-notification-permission'
14
14
  export * from './use-onboarding-state'
15
- export * from './use-reset-on-page-hidden'
16
15
  export * from './use-search'
17
16
  export * from './use-suppress-clone-focus'
18
17
  export * from './use-table-pagination'
@@ -29,39 +29,19 @@
29
29
 
30
30
  import { useEffect, useRef, useState, useCallback } from 'react';
31
31
 
32
- // Per-(rootMargin, threshold) IO map. Multiple call sites with different
33
- // margins/thresholds each get their own singleton observer.
32
+ // Per-rootMargin IO map. Multiple call sites with different margins each
33
+ // get their own singleton observer.
34
34
  const observers = new Map<string, IntersectionObserver>();
35
35
  const subscribers = new WeakMap<Element, () => void>();
36
36
 
37
- // Slack for the explicit ratio check: an element sized to exactly meet the
38
- // threshold can report a ratio a hair under it (sub-pixel / float rounding), so
39
- // accept `ratio >= threshold - EPSILON`. With `threshold: 0` this is a no-op
40
- // (ratio is always ≥ 0 > -EPSILON) — the `isIntersecting` check alone gates.
41
- const THRESHOLD_EPSILON = 0.01;
42
-
43
- /** Stable map key for an observer config. */
44
- function observerKey(rootMargin: string, threshold: number): string {
45
- return `${rootMargin}|${threshold}`;
46
- }
47
-
48
- function getObserverFor(rootMargin: string, threshold: number): IntersectionObserver {
49
- const key = observerKey(rootMargin, threshold);
50
- const existing = observers.get(key);
37
+ function getObserverFor(rootMargin: string): IntersectionObserver {
38
+ const existing = observers.get(rootMargin);
51
39
  if (existing) return existing;
52
40
 
53
41
  const io = new IntersectionObserver(
54
42
  (entries) => {
55
43
  entries.forEach((entry) => {
56
- // Enforce the threshold EXPLICITLY. `isIntersecting` is spec-defined as
57
- // `intersectionRatio > 0` (any overlap), so the initial `observe()`
58
- // firing can report `true` at, say, 20% visible even with
59
- // `threshold: 0.5` — which would prematurely pass the gate. Require both
60
- // an intersection AND the ratio to actually meet the threshold (minus a
61
- // small epsilon for the float rounding where the ratio lands a hair
62
- // under the exact value).
63
44
  if (!entry.isIntersecting) return;
64
- if (entry.intersectionRatio < threshold - THRESHOLD_EPSILON) return;
65
45
  // Race-safe: re-read the callback at fire time. A late IO firing
66
46
  // after cleanup must not invoke a stale callback.
67
47
  const cb = subscribers.get(entry.target);
@@ -72,9 +52,9 @@ function getObserverFor(rootMargin: string, threshold: number): IntersectionObse
72
52
  }
73
53
  });
74
54
  },
75
- { rootMargin, threshold }
55
+ { rootMargin }
76
56
  );
77
- observers.set(key, io);
57
+ observers.set(rootMargin, io);
78
58
  return io;
79
59
  }
80
60
 
@@ -98,16 +78,12 @@ export interface UseNearViewportResult<T extends Element = HTMLElement> {
98
78
  * '500px' = element starts mounting 500px before scroll-in.
99
79
  * '1000px' = a full viewport's worth of lookahead.
100
80
  * '0px' = strict on-screen detection.
101
- * @param threshold Fraction of the element that must be visible before firing
102
- * (0 = any pixel — the default; 0.5 = at least half on-screen).
103
81
  */
104
82
  export function useNearViewport<T extends Element = HTMLElement>(
105
- rootMargin: string = NEAR_VIEWPORT_ROOT_MARGIN,
106
- threshold: number = 0
83
+ rootMargin: string = NEAR_VIEWPORT_ROOT_MARGIN
107
84
  ): UseNearViewportResult<T> {
108
85
  const [isNear, setIsNear] = useState(false);
109
86
  const elRef = useRef<T | null>(null);
110
- const key = observerKey(rootMargin, threshold);
111
87
 
112
88
  // Subscribe/unsubscribe on element change.
113
89
  const ref = useCallback(
@@ -120,7 +96,7 @@ export function useNearViewport<T extends Element = HTMLElement>(
120
96
  const stillOurs = subscribers.get(prev);
121
97
  if (stillOurs) {
122
98
  subscribers.delete(prev);
123
- observers.get(key)?.unobserve(prev);
99
+ observers.get(rootMargin)?.unobserve(prev);
124
100
  }
125
101
  }
126
102
 
@@ -129,9 +105,9 @@ export function useNearViewport<T extends Element = HTMLElement>(
129
105
 
130
106
  const cb = () => setIsNear(true);
131
107
  subscribers.set(node, cb);
132
- getObserverFor(rootMargin, threshold).observe(node);
108
+ getObserverFor(rootMargin).observe(node);
133
109
  },
134
- [key, rootMargin, threshold]
110
+ [rootMargin]
135
111
  );
136
112
 
137
113
  // Unsubscribe on unmount. Identity check guards the StrictMode race.
@@ -141,10 +117,10 @@ export function useNearViewport<T extends Element = HTMLElement>(
141
117
  if (!el) return;
142
118
  if (subscribers.get(el)) {
143
119
  subscribers.delete(el);
144
- observers.get(key)?.unobserve(el);
120
+ observers.get(rootMargin)?.unobserve(el);
145
121
  }
146
122
  };
147
- }, [key]);
123
+ }, [rootMargin]);
148
124
 
149
125
  return { ref, isNear };
150
126
  }