@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.
- package/dist/{chunk-UXKSOJR7.js → chunk-2KUIA2AR.js} +3 -3
- package/dist/{chunk-LMBPJAQ2.js → chunk-5YPKZM6Y.js} +6 -6
- package/dist/{chunk-YMGOTPY7.cjs → chunk-62AXZKYN.cjs} +866 -981
- package/dist/chunk-62AXZKYN.cjs.map +1 -0
- package/dist/{chunk-JHIT44TA.js → chunk-6W4ZEVIC.js} +3 -3
- package/dist/{chunk-MK2YWJAA.js → chunk-BD3JBEUC.js} +40 -64
- package/dist/chunk-BD3JBEUC.js.map +1 -0
- package/dist/{chunk-KH5VTGCS.cjs → chunk-BE77G7GW.cjs} +40 -40
- package/dist/{chunk-KH5VTGCS.cjs.map → chunk-BE77G7GW.cjs.map} +1 -1
- package/dist/{chunk-DYBATZJP.cjs → chunk-D5PBUVGB.cjs} +5 -5
- package/dist/{chunk-DYBATZJP.cjs.map → chunk-D5PBUVGB.cjs.map} +1 -1
- package/dist/{chunk-2I3GN7WW.js → chunk-DGPKGESY.js} +4 -4
- package/dist/{chunk-F3IBBAKV.js → chunk-EGTGRG7N.js} +2 -2
- package/dist/{chunk-UHVVNVUE.cjs → chunk-FCVF4YAB.cjs} +40 -40
- package/dist/{chunk-UHVVNVUE.cjs.map → chunk-FCVF4YAB.cjs.map} +1 -1
- package/dist/{chunk-VZNJ3RYI.cjs → chunk-GNYV6HYO.cjs} +15 -15
- package/dist/{chunk-VZNJ3RYI.cjs.map → chunk-GNYV6HYO.cjs.map} +1 -1
- package/dist/{chunk-F2CCUPKC.cjs → chunk-IDCPYG7N.cjs} +29 -29
- package/dist/{chunk-F2CCUPKC.cjs.map → chunk-IDCPYG7N.cjs.map} +1 -1
- package/dist/{chunk-YLTY4Z4B.js → chunk-JUUPMMKI.js} +3 -3
- package/dist/{chunk-IOHQJOLR.cjs → chunk-KTGGM5WA.cjs} +12 -36
- package/dist/chunk-KTGGM5WA.cjs.map +1 -0
- package/dist/{chunk-U27TXTAK.js → chunk-LB3OZE4B.js} +6 -6
- package/dist/{chunk-GC63K4VD.cjs → chunk-M5RT26WQ.cjs} +73 -73
- package/dist/{chunk-GC63K4VD.cjs.map → chunk-M5RT26WQ.cjs.map} +1 -1
- package/dist/{chunk-UEKCD6WV.js → chunk-N4CHA7T7.js} +16 -131
- package/dist/chunk-N4CHA7T7.js.map +1 -0
- package/dist/{chunk-6K6LAD6H.cjs → chunk-NNDNJCJ3.cjs} +18 -18
- package/dist/{chunk-6K6LAD6H.cjs.map → chunk-NNDNJCJ3.cjs.map} +1 -1
- package/dist/{chunk-DU6R7XNU.js → chunk-QSIZW6EX.js} +2 -2
- package/dist/{chunk-6TI3DCOG.cjs → chunk-RT7DIGTN.cjs} +3 -3
- package/dist/{chunk-6TI3DCOG.cjs.map → chunk-RT7DIGTN.cjs.map} +1 -1
- package/dist/{chunk-GQZGCMRF.cjs → chunk-RZL7I6YN.cjs} +7 -7
- package/dist/{chunk-GQZGCMRF.cjs.map → chunk-RZL7I6YN.cjs.map} +1 -1
- package/dist/{chunk-TXS2AMFV.cjs → chunk-UJZ6O2ON.cjs} +32 -32
- package/dist/{chunk-TXS2AMFV.cjs.map → chunk-UJZ6O2ON.cjs.map} +1 -1
- package/dist/{chunk-ZQHGSR3H.js → chunk-VNE5R4LQ.js} +5 -5
- package/dist/{chunk-ILUX3DXS.js → chunk-X62ICFPJ.js} +2 -2
- package/dist/{chunk-DAEKUR3J.js → chunk-XAQLBUCP.js} +2 -2
- package/dist/{chunk-INH6SIP5.cjs → chunk-XT6S6DYS.cjs} +12 -12
- package/dist/{chunk-INH6SIP5.cjs.map → chunk-XT6S6DYS.cjs.map} +1 -1
- package/dist/{chunk-A7FTNFBX.cjs → chunk-YSKSBLCG.cjs} +73 -73
- package/dist/{chunk-A7FTNFBX.cjs.map → chunk-YSKSBLCG.cjs.map} +1 -1
- package/dist/{chunk-LAFCXJVT.js → chunk-YXCYQKAX.js} +5 -5
- package/dist/components/case-studies/index.cjs +9 -9
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/guide-welcome.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +3 -3
- package/dist/components/chat/index.js +2 -2
- package/dist/components/chat/mingo-welcome.d.ts.map +1 -1
- package/dist/components/chat/quick-action-wall.d.ts +1 -5
- package/dist/components/chat/quick-action-wall.d.ts.map +1 -1
- package/dist/components/chat/types/component.types.d.ts +0 -17
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/docs/index.cjs +6 -6
- package/dist/components/docs/index.js +5 -5
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +5 -5
- package/dist/components/faq/index.js +4 -4
- package/dist/components/features/cards-strip.d.ts.map +1 -1
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/help-center-pages/index.cjs +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +135 -135
- package/dist/components/index.js +11 -11
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/onboarding-guides/index.cjs +7 -7
- package/dist/components/onboarding-guides/index.js +6 -6
- package/dist/components/related-content/index.cjs +5 -5
- package/dist/components/related-content/index.js +4 -4
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/components/ui/marquee-wall.d.ts +1 -8
- package/dist/components/ui/marquee-wall.d.ts.map +1 -1
- package/dist/hooks/index.cjs +2 -4
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +1 -3
- package/dist/hooks/ui/index.d.ts +0 -1
- package/dist/hooks/ui/index.d.ts.map +1 -1
- package/dist/hooks/use-near-viewport.d.ts +1 -3
- package/dist/hooks/use-near-viewport.d.ts.map +1 -1
- package/dist/index.cjs +3 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -4
- package/package.json +1 -1
- package/src/components/chat/chat-message-list.tsx +5 -48
- package/src/components/chat/embeddable-chat.tsx +3 -31
- package/src/components/chat/guide-welcome.tsx +0 -1
- package/src/components/chat/mingo-welcome.tsx +0 -1
- package/src/components/chat/quick-action-wall.tsx +0 -7
- package/src/components/chat/types/component.types.ts +0 -17
- package/src/components/features/cards-strip.tsx +0 -16
- package/src/components/ui/marquee-wall.tsx +10 -160
- package/src/hooks/ui/index.ts +0 -1
- package/src/hooks/use-near-viewport.ts +12 -36
- package/dist/chunk-IOHQJOLR.cjs.map +0 -1
- package/dist/chunk-MK2YWJAA.js.map +0 -1
- package/dist/chunk-UEKCD6WV.js.map +0 -1
- package/dist/chunk-YMGOTPY7.cjs.map +0 -1
- package/dist/hooks/ui/use-reset-on-page-hidden.d.ts +0 -19
- package/dist/hooks/ui/use-reset-on-page-hidden.d.ts.map +0 -1
- package/src/hooks/ui/use-reset-on-page-hidden.ts +0 -32
- /package/dist/{chunk-UXKSOJR7.js.map → chunk-2KUIA2AR.js.map} +0 -0
- /package/dist/{chunk-LMBPJAQ2.js.map → chunk-5YPKZM6Y.js.map} +0 -0
- /package/dist/{chunk-JHIT44TA.js.map → chunk-6W4ZEVIC.js.map} +0 -0
- /package/dist/{chunk-2I3GN7WW.js.map → chunk-DGPKGESY.js.map} +0 -0
- /package/dist/{chunk-F3IBBAKV.js.map → chunk-EGTGRG7N.js.map} +0 -0
- /package/dist/{chunk-YLTY4Z4B.js.map → chunk-JUUPMMKI.js.map} +0 -0
- /package/dist/{chunk-U27TXTAK.js.map → chunk-LB3OZE4B.js.map} +0 -0
- /package/dist/{chunk-DU6R7XNU.js.map → chunk-QSIZW6EX.js.map} +0 -0
- /package/dist/{chunk-ZQHGSR3H.js.map → chunk-VNE5R4LQ.js.map} +0 -0
- /package/dist/{chunk-ILUX3DXS.js.map → chunk-X62ICFPJ.js.map} +0 -0
- /package/dist/{chunk-DAEKUR3J.js.map → chunk-XAQLBUCP.js.map} +0 -0
- /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
|
-
|
|
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
|
-
//
|
|
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
|
|
@@ -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
|
|
460
|
+
const { posRef } = useMarqueeEngine({
|
|
508
461
|
active: marqueeActive && nearViewport && isDriver,
|
|
509
462
|
speed,
|
|
510
|
-
isPaused:
|
|
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:
|
|
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}
|
package/src/hooks/ui/index.ts
CHANGED
|
@@ -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-
|
|
33
|
-
//
|
|
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
|
-
|
|
38
|
-
|
|
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
|
|
55
|
+
{ rootMargin }
|
|
76
56
|
);
|
|
77
|
-
observers.set(
|
|
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(
|
|
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
|
|
108
|
+
getObserverFor(rootMargin).observe(node);
|
|
133
109
|
},
|
|
134
|
-
[
|
|
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(
|
|
120
|
+
observers.get(rootMargin)?.unobserve(el);
|
|
145
121
|
}
|
|
146
122
|
};
|
|
147
|
-
}, [
|
|
123
|
+
}, [rootMargin]);
|
|
148
124
|
|
|
149
125
|
return { ref, isNear };
|
|
150
126
|
}
|