@flamingo-stack/openframe-frontend-core 0.0.395 → 0.0.396-snapshot.20260709212121
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-NJPMHDFG.js → chunk-2OOZASQE.js} +2 -2
- package/dist/{chunk-PJ2YFSMY.js → chunk-3E4BDAHJ.js} +2 -2
- package/dist/{chunk-M7WTWBCY.cjs → chunk-43C5ECNT.cjs} +37 -37
- package/dist/{chunk-M7WTWBCY.cjs.map → chunk-43C5ECNT.cjs.map} +1 -1
- package/dist/{chunk-OSOKWP5R.cjs → chunk-55SNCMFS.cjs} +31 -31
- package/dist/{chunk-OSOKWP5R.cjs.map → chunk-55SNCMFS.cjs.map} +1 -1
- package/dist/{chunk-BEPZ5YAN.cjs → chunk-BB7TZ6KS.cjs} +59 -59
- package/dist/{chunk-BEPZ5YAN.cjs.map → chunk-BB7TZ6KS.cjs.map} +1 -1
- package/dist/{chunk-RWBTSF2E.js → chunk-CK5UZLIN.js} +5316 -5003
- package/dist/chunk-CK5UZLIN.js.map +1 -0
- package/dist/{chunk-YJE3HL6Q.js → chunk-D4F7ZLJG.js} +4 -4
- package/dist/{chunk-ZCKVP4SU.cjs → chunk-DB6CUWTL.cjs} +26 -26
- package/dist/{chunk-ZCKVP4SU.cjs.map → chunk-DB6CUWTL.cjs.map} +1 -1
- package/dist/{chunk-Y4I4462K.cjs → chunk-HPQKX5IW.cjs} +15 -15
- package/dist/{chunk-Y4I4462K.cjs.map → chunk-HPQKX5IW.cjs.map} +1 -1
- package/dist/{chunk-NN73KMIK.js → chunk-IVXOR2IA.js} +2 -2
- package/dist/{chunk-WHJEU7V7.js → chunk-J6TAK6HE.js} +4 -4
- package/dist/{chunk-45Z6B6CO.js → chunk-JNU3YPLP.js} +4 -4
- package/dist/{chunk-TWLPG5IQ.js → chunk-K5FRWPWF.js} +2 -2
- package/dist/{chunk-RNWLVI3V.cjs → chunk-M6NPDNVB.cjs} +26 -26
- package/dist/{chunk-RNWLVI3V.cjs.map → chunk-M6NPDNVB.cjs.map} +1 -1
- package/dist/{chunk-5U72NXGG.cjs → chunk-OAENYOKC.cjs} +9 -9
- package/dist/{chunk-5U72NXGG.cjs.map → chunk-OAENYOKC.cjs.map} +1 -1
- package/dist/{chunk-T6FEE3R2.cjs → chunk-SBWTMJPQ.cjs} +5 -5
- package/dist/{chunk-T6FEE3R2.cjs.map → chunk-SBWTMJPQ.cjs.map} +1 -1
- package/dist/{chunk-4YVUL4T7.cjs → chunk-T5F54MN4.cjs} +12 -12
- package/dist/{chunk-4YVUL4T7.cjs.map → chunk-T5F54MN4.cjs.map} +1 -1
- package/dist/{chunk-MPN3MV5H.js → chunk-TTHTO5H5.js} +4 -4
- package/dist/{chunk-PFF6Q6N5.js → chunk-VTR4QDL2.js} +2 -2
- package/dist/{chunk-TTEAYVLT.js → chunk-WCDM226S.js} +2 -2
- package/dist/{chunk-J2UDV3FH.cjs → chunk-XNAYZHPK.cjs} +62 -62
- package/dist/{chunk-J2UDV3FH.cjs.map → chunk-XNAYZHPK.cjs.map} +1 -1
- package/dist/{chunk-3VJPMRNT.cjs → chunk-Z6PAD2UN.cjs} +7 -7
- package/dist/{chunk-3VJPMRNT.cjs.map → chunk-Z6PAD2UN.cjs.map} +1 -1
- package/dist/{chunk-ZB5HSWX4.js → chunk-ZFK4HHRX.js} +2 -2
- package/dist/{chunk-PNMX75W4.cjs → chunk-ZUKBC2ZX.cjs} +1114 -801
- package/dist/chunk-ZUKBC2ZX.cjs.map +1 -0
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/entity-cards/blog-card.d.ts +6 -3
- package/dist/components/chat/entity-cards/blog-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/case-study-card.d.ts +7 -3
- package/dist/components/chat/entity-cards/case-study-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/customer-interview-card.d.ts +6 -3
- package/dist/components/chat/entity-cards/customer-interview-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/entity-portrait-card.d.ts +59 -0
- package/dist/components/chat/entity-cards/entity-portrait-card.d.ts.map +1 -0
- package/dist/components/chat/entity-cards/index.d.ts +3 -0
- package/dist/components/chat/entity-cards/index.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/investor-update-card.d.ts +6 -3
- package/dist/components/chat/entity-cards/investor-update-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/onboarding-guide-card.d.ts +4 -2
- package/dist/components/chat/entity-cards/onboarding-guide-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/program-card.d.ts +17 -5
- package/dist/components/chat/entity-cards/program-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/use-cover-image-fallback.d.ts +26 -0
- package/dist/components/chat/entity-cards/use-cover-image-fallback.d.ts.map +1 -0
- package/dist/components/chat/index.cjs +10 -2
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +9 -1
- package/dist/components/chat/types/entities/program-types.d.ts +23 -0
- package/dist/components/chat/types/entities/program-types.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/auth/create-organization-form.d.ts +1 -1
- package/dist/components/features/auth/create-organization-form.d.ts.map +1 -1
- package/dist/components/features/auth/login-form.d.ts +1 -1
- package/dist/components/features/auth/login-form.d.ts.map +1 -1
- package/dist/components/features/auth/sso-providers.d.ts +1 -1
- package/dist/components/features/auth/sso-providers.d.ts.map +1 -1
- package/dist/components/features/cards-strip.d.ts.map +1 -1
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/features/video-bites-strip.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +20 -20
- package/dist/components/help-center-pages/index.js +11 -11
- package/dist/components/index.cjs +128 -120
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +17 -9
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +5 -5
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/index.cjs +10 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +9 -1
- package/dist/index.cjs +10 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +9 -1
- package/package.json +1 -1
- package/src/components/chat/entity-cards/blog-card.tsx +40 -7
- package/src/components/chat/entity-cards/case-study-card.tsx +32 -6
- package/src/components/chat/entity-cards/customer-interview-card.tsx +31 -6
- package/src/components/chat/entity-cards/entity-portrait-card.tsx +201 -0
- package/src/components/chat/entity-cards/index.ts +18 -0
- package/src/components/chat/entity-cards/investor-update-card.tsx +28 -2
- package/src/components/chat/entity-cards/onboarding-guide-card.tsx +30 -1
- package/src/components/chat/entity-cards/program-card.tsx +69 -17
- package/src/components/chat/entity-cards/use-cover-image-fallback.ts +50 -0
- package/src/components/chat/types/entities/program-types.ts +34 -0
- package/src/components/features/auth/create-organization-form.tsx +2 -2
- package/src/components/features/auth/login-form.tsx +2 -2
- package/src/components/features/auth/sso-providers.tsx +2 -2
- package/src/components/features/cards-strip.tsx +122 -8
- package/src/components/features/video-bites-strip.tsx +4 -0
- package/src/stories/auth/CreateOrganization.stories.tsx +0 -1
- package/src/stories/auth/Login.stories.tsx +1 -1
- package/dist/chunk-PNMX75W4.cjs.map +0 -1
- package/dist/chunk-RWBTSF2E.js.map +0 -1
- /package/dist/{chunk-NJPMHDFG.js.map → chunk-2OOZASQE.js.map} +0 -0
- /package/dist/{chunk-PJ2YFSMY.js.map → chunk-3E4BDAHJ.js.map} +0 -0
- /package/dist/{chunk-YJE3HL6Q.js.map → chunk-D4F7ZLJG.js.map} +0 -0
- /package/dist/{chunk-NN73KMIK.js.map → chunk-IVXOR2IA.js.map} +0 -0
- /package/dist/{chunk-WHJEU7V7.js.map → chunk-J6TAK6HE.js.map} +0 -0
- /package/dist/{chunk-45Z6B6CO.js.map → chunk-JNU3YPLP.js.map} +0 -0
- /package/dist/{chunk-TWLPG5IQ.js.map → chunk-K5FRWPWF.js.map} +0 -0
- /package/dist/{chunk-MPN3MV5H.js.map → chunk-TTHTO5H5.js.map} +0 -0
- /package/dist/{chunk-PFF6Q6N5.js.map → chunk-VTR4QDL2.js.map} +0 -0
- /package/dist/{chunk-TTEAYVLT.js.map → chunk-WCDM226S.js.map} +0 -0
- /package/dist/{chunk-ZB5HSWX4.js.map → chunk-ZFK4HHRX.js.map} +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* THE common cover-image fallback chain for entity cards:
|
|
5
|
+
* real cover → branded placeholder → nothing (hide the media element).
|
|
6
|
+
*
|
|
7
|
+
* One hook so no card hand-rolls its own `imageError` state / `displayImage`
|
|
8
|
+
* ternary (they drift: the pre-hook BlogCard chain couldn't recover from a
|
|
9
|
+
* FAILING placeholder, EntityPortraitCard duplicated the same logic with
|
|
10
|
+
* different state). Cards wire `src` + `onError` and render their own
|
|
11
|
+
* empty-state when `src` is null.
|
|
12
|
+
*
|
|
13
|
+
* Implementation note: state is the SET of candidate URLs that failed, and
|
|
14
|
+
* `src` is derived — so a prop change (new cover URL) re-resolves instantly
|
|
15
|
+
* with no reset effect, and a URL that failed once isn't retried in a loop.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useCallback, useState, type SyntheticEvent } from 'react'
|
|
19
|
+
|
|
20
|
+
export interface CoverImageFallback {
|
|
21
|
+
/** Resolved source: first non-failed of [imageUrl, placeholderUrl], else null. */
|
|
22
|
+
src: string | null
|
|
23
|
+
/** Wire to the media element's `onError`. */
|
|
24
|
+
onError: () => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useCoverImageFallback(
|
|
28
|
+
imageUrl?: string | null,
|
|
29
|
+
placeholderUrl?: string | null,
|
|
30
|
+
): CoverImageFallback {
|
|
31
|
+
const [failed, setFailed] = useState<ReadonlySet<string>>(() => new Set())
|
|
32
|
+
|
|
33
|
+
const src =
|
|
34
|
+
imageUrl && !failed.has(imageUrl) ? imageUrl
|
|
35
|
+
: placeholderUrl && !failed.has(placeholderUrl) ? placeholderUrl
|
|
36
|
+
: null
|
|
37
|
+
|
|
38
|
+
const onError = useCallback(() => {
|
|
39
|
+
if (!src) return
|
|
40
|
+
setFailed(prev => new Set(prev).add(src))
|
|
41
|
+
}, [src])
|
|
42
|
+
|
|
43
|
+
return { src, onError }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Fallback for cards with NO placeholder chain (compact/sm image slots):
|
|
47
|
+
* a broken cover simply disappears, leaving the slot's background. */
|
|
48
|
+
export const hideOnError = (e: SyntheticEvent<HTMLImageElement>) => {
|
|
49
|
+
;(e.currentTarget as HTMLImageElement).style.display = 'none'
|
|
50
|
+
}
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import type { ReactNode } from 'react';
|
|
20
|
+
import {
|
|
21
|
+
toStripProfile,
|
|
22
|
+
type VideoBiteStripProfile,
|
|
23
|
+
} from '../../../features/video-bites-shared';
|
|
20
24
|
|
|
21
25
|
// ============================================================================
|
|
22
26
|
// HOST TYPES
|
|
@@ -34,6 +38,36 @@ export interface ProgramHost {
|
|
|
34
38
|
role?: string; // e.g., "Host", "Speaker", "Guest"
|
|
35
39
|
}
|
|
36
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Client-safe author subset hydrated onto program items from the entity's
|
|
43
|
+
* `author_id → profiles` FK join (podcast episodes / webinars). Deliberately
|
|
44
|
+
* excludes `email` — this shape is serialized into public payloads.
|
|
45
|
+
*/
|
|
46
|
+
export interface ProgramAuthorRef {
|
|
47
|
+
id?: string | null;
|
|
48
|
+
full_name?: string | null;
|
|
49
|
+
avatar_url?: string | null;
|
|
50
|
+
job_title?: string | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Video-bites overlay profile for a program item: internal author first
|
|
55
|
+
* (`author_id → profiles`), then the provider-synced primary host (Livestorm
|
|
56
|
+
* speakers / Podbean hosts). Host `role` is a raw provider enum (e.g.
|
|
57
|
+
* `team_member`) — never display copy, so it is not used as the subtitle.
|
|
58
|
+
* Routes through `toStripProfile` (the SSoT — null on missing names).
|
|
59
|
+
*/
|
|
60
|
+
export function programItemToStripProfile(item: {
|
|
61
|
+
author?: ProgramAuthorRef | null;
|
|
62
|
+
hosts?: ProgramHost[] | null;
|
|
63
|
+
}): VideoBiteStripProfile | null {
|
|
64
|
+
const fromAuthor = toStripProfile(item.author);
|
|
65
|
+
if (fromAuthor) return fromAuthor;
|
|
66
|
+
const host = Array.isArray(item.hosts) ? item.hosts[0] : null;
|
|
67
|
+
if (!host?.name) return null;
|
|
68
|
+
return toStripProfile({ full_name: host.name, avatar_url: host.avatar_url ?? null });
|
|
69
|
+
}
|
|
70
|
+
|
|
37
71
|
// ============================================================================
|
|
38
72
|
// DATABASE ROW TYPES
|
|
39
73
|
// ============================================================================
|
|
@@ -51,7 +51,7 @@ export interface CreateOrganizationFormProps {
|
|
|
51
51
|
*/
|
|
52
52
|
ssoProviders?: AuthSsoProvider[]
|
|
53
53
|
onSsoClick?: (provider: AuthSsoProvider) => void
|
|
54
|
-
/** Verb prefix for provider buttons, e.g. "
|
|
54
|
+
/** Verb prefix for provider buttons, e.g. "Continue with". Ignored for "openframe". */
|
|
55
55
|
ssoActionLabel?: string
|
|
56
56
|
className?: string
|
|
57
57
|
}
|
|
@@ -85,7 +85,7 @@ export function CreateOrganizationForm({
|
|
|
85
85
|
domainSlot,
|
|
86
86
|
ssoProviders,
|
|
87
87
|
onSsoClick,
|
|
88
|
-
ssoActionLabel = '
|
|
88
|
+
ssoActionLabel = 'Continue with',
|
|
89
89
|
className,
|
|
90
90
|
}: CreateOrganizationFormProps) {
|
|
91
91
|
const isSsoMode = !!ssoProviders && ssoProviders.length > 0
|
|
@@ -31,7 +31,7 @@ export interface LoginFormProps {
|
|
|
31
31
|
*/
|
|
32
32
|
ssoProviders?: AuthSsoProvider[]
|
|
33
33
|
onSsoClick?: (provider: AuthSsoProvider) => void
|
|
34
|
-
/** Verb prefix for provider buttons, e.g. "
|
|
34
|
+
/** Verb prefix for provider buttons, e.g. "Continue with". Ignored for "openframe". */
|
|
35
35
|
ssoActionLabel?: string
|
|
36
36
|
className?: string
|
|
37
37
|
}
|
|
@@ -55,7 +55,7 @@ export function LoginForm({
|
|
|
55
55
|
errors,
|
|
56
56
|
ssoProviders,
|
|
57
57
|
onSsoClick,
|
|
58
|
-
ssoActionLabel = '
|
|
58
|
+
ssoActionLabel = 'Continue with',
|
|
59
59
|
className,
|
|
60
60
|
}: LoginFormProps) {
|
|
61
61
|
const isSsoMode = !!ssoProviders && ssoProviders.length > 0
|
|
@@ -30,7 +30,7 @@ export const PROVIDER_META: Record<
|
|
|
30
30
|
export interface SsoProviderButtonsProps {
|
|
31
31
|
providers: AuthSsoProvider[]
|
|
32
32
|
onSsoClick?: (provider: AuthSsoProvider) => void
|
|
33
|
-
/** Verb prefix for provider buttons, e.g. "
|
|
33
|
+
/** Verb prefix for provider buttons, e.g. "Continue with". Ignored for "openframe". */
|
|
34
34
|
actionLabel?: string
|
|
35
35
|
disabled?: boolean
|
|
36
36
|
}
|
|
@@ -39,7 +39,7 @@ export interface SsoProviderButtonsProps {
|
|
|
39
39
|
export function SsoProviderButtons({
|
|
40
40
|
providers,
|
|
41
41
|
onSsoClick,
|
|
42
|
-
actionLabel = '
|
|
42
|
+
actionLabel = 'Continue with',
|
|
43
43
|
disabled = false,
|
|
44
44
|
}: SsoProviderButtonsProps) {
|
|
45
45
|
return (
|
|
@@ -284,6 +284,11 @@ export function CardsStrip<T = unknown>(props: CardsStripProps<T>): React.ReactE
|
|
|
284
284
|
// modulo the copy width each frame, so warps are invisible to it.
|
|
285
285
|
const marqueePosRef = useRef(0);
|
|
286
286
|
const glideRemainingRef = useRef(0);
|
|
287
|
+
// Velocity envelope (px/s): the marquee EASES between 0 and autoScrollSpeed
|
|
288
|
+
// (~250ms time constant) instead of binary stop/start — pause decelerates,
|
|
289
|
+
// resume accelerates from the current position (GSAP-marquee behavior;
|
|
290
|
+
// hard cuts read as jank).
|
|
291
|
+
const speedEnvRef = useRef(0);
|
|
287
292
|
useEffect(() => {
|
|
288
293
|
if (!marqueeActive) return;
|
|
289
294
|
const scroller = scrollerRef.current;
|
|
@@ -301,11 +306,21 @@ export function CardsStrip<T = unknown>(props: CardsStripProps<T>): React.ReactE
|
|
|
301
306
|
const tick = (now: number) => {
|
|
302
307
|
const dt = Math.min(now - last, 100) / 1000; // clamp tab-wake jumps
|
|
303
308
|
last = now;
|
|
309
|
+
// Container-level pause (industry-standard marquee behavior): the
|
|
310
|
+
// marquee freezes whenever a hover-capable pointer is ANYWHERE over the
|
|
311
|
+
// cards row — content must never move under a pointing cursor. Per-card
|
|
312
|
+
// activeKey still pauses for keyboard focus (no pointer involved).
|
|
304
313
|
const paused =
|
|
305
|
-
(pauseOnHover && activeKeyRef.current !== null) ||
|
|
314
|
+
(pauseOnHover && (hoverPointerRef.current.inside || activeKeyRef.current !== null)) ||
|
|
306
315
|
!nearViewportRef.current ||
|
|
307
316
|
document.visibilityState === 'hidden' ||
|
|
308
317
|
now < Math.max(chevronSuppressUntilRef.current, userScrollSuppressUntilRef.current);
|
|
318
|
+
// Ease the marquee velocity toward its target (0 when paused) — smooth
|
|
319
|
+
// decel on hover, smooth accel on leave, always from the CURRENT position.
|
|
320
|
+
const targetSpeed = paused ? 0 : autoScrollSpeed;
|
|
321
|
+
speedEnvRef.current += (targetSpeed - speedEnvRef.current) * Math.min(1, dt / 0.25);
|
|
322
|
+
if (targetSpeed === 0 && speedEnvRef.current < 0.5) speedEnvRef.current = 0;
|
|
323
|
+
|
|
309
324
|
const glide = glideRemainingRef.current;
|
|
310
325
|
if (glide !== 0) {
|
|
311
326
|
// Ease-out toward the chevron target; runs even while "paused"
|
|
@@ -318,20 +333,26 @@ export function CardsStrip<T = unknown>(props: CardsStripProps<T>): React.ReactE
|
|
|
318
333
|
glideRemainingRef.current = Math.abs(glide - step) < 0.5 ? 0 : glide - step;
|
|
319
334
|
marqueePosRef.current = wrap(marqueePosRef.current + step);
|
|
320
335
|
scroller.scrollLeft = marqueePosRef.current;
|
|
321
|
-
} else if (
|
|
336
|
+
} else if (speedEnvRef.current > 0) {
|
|
322
337
|
// Resync after external movement (user scroll, seam warp).
|
|
323
338
|
if (Math.abs(scroller.scrollLeft - marqueePosRef.current) > 1.5) {
|
|
324
339
|
marqueePosRef.current = scroller.scrollLeft;
|
|
325
340
|
}
|
|
326
|
-
marqueePosRef.current = wrap(marqueePosRef.current +
|
|
341
|
+
marqueePosRef.current = wrap(marqueePosRef.current + speedEnvRef.current * dt);
|
|
327
342
|
scroller.scrollLeft = marqueePosRef.current;
|
|
328
343
|
} else {
|
|
329
344
|
marqueePosRef.current = scroller.scrollLeft;
|
|
330
345
|
}
|
|
346
|
+
// Track moved under a possibly-stationary pointer → re-resolve which
|
|
347
|
+
// card is hovered (declared below; stable useCallback identity, called
|
|
348
|
+
// lazily per frame so source order is irrelevant — deliberately NOT a
|
|
349
|
+
// dep, same as the activeKeyRef read above).
|
|
350
|
+
syncHoverIfScrolled();
|
|
331
351
|
raf = requestAnimationFrame(tick);
|
|
332
352
|
};
|
|
333
353
|
raf = requestAnimationFrame(tick);
|
|
334
354
|
return () => cancelAnimationFrame(raf);
|
|
355
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- syncHoverIfScrolled is identity-stable
|
|
335
356
|
}, [marqueeActive, autoScrollSpeed, pauseOnHover]);
|
|
336
357
|
|
|
337
358
|
// ---- hover / overlay state -----------------------------------------------------
|
|
@@ -357,6 +378,72 @@ export function CardsStrip<T = unknown>(props: CardsStripProps<T>): React.ReactE
|
|
|
357
378
|
setActiveKey(current => (current === key ? null : current));
|
|
358
379
|
}, []);
|
|
359
380
|
|
|
381
|
+
// ---- container-level hover pause + pointer-tracked activation sync ---------
|
|
382
|
+
// TWO separate concerns, deliberately decoupled (matches how production
|
|
383
|
+
// marquees — react-fast-marquee, GSAP — behave):
|
|
384
|
+
//
|
|
385
|
+
// 1. MARQUEE PAUSE is container-level: `hoverPointerRef.inside` is true
|
|
386
|
+
// whenever a hover-capable pointer is over the SCROLLER (the cards row).
|
|
387
|
+
// The rAF freezes on it, so content NEVER auto-moves under a pointing
|
|
388
|
+
// cursor — which is what made per-card boundary pausing feel flaky
|
|
389
|
+
// (cards sliding under/away from a stationary pointer don't emit
|
|
390
|
+
// pointerenter/leave, gaps caused stutter-steps, seam warps swapped the
|
|
391
|
+
// hovered card's identity mid-playback).
|
|
392
|
+
//
|
|
393
|
+
// 2. CARD ACTIVATION (overlay + video playback) stays per-card: the card/
|
|
394
|
+
// cell pointer handlers are the zero-latency path, and the
|
|
395
|
+
// elementFromPoint sync below re-resolves the card under the pointer
|
|
396
|
+
// whenever the track moves WHILE the pointer is inside (user wheel
|
|
397
|
+
// scroll, chevron glide, seam warp) — scoped to THIS strip's scroller so
|
|
398
|
+
// stacked strips can never activate/pause each other.
|
|
399
|
+
const hoverPointerRef = useRef<{ x: number; y: number; inside: boolean }>({ x: 0, y: 0, inside: false });
|
|
400
|
+
const lastHoverSyncScrollRef = useRef(-1);
|
|
401
|
+
const syncHoverToPointer = useCallback(() => {
|
|
402
|
+
const p = hoverPointerRef.current;
|
|
403
|
+
if (!p.inside) return;
|
|
404
|
+
const el = typeof document !== 'undefined' ? document.elementFromPoint(p.x, p.y) : null;
|
|
405
|
+
const cardEl = el?.closest?.('[data-strip-card-key]') as HTMLElement | null;
|
|
406
|
+
// Strip-scoped: a card from ANOTHER CardsStrip under the tracked point
|
|
407
|
+
// must read as "no card here" (stacked strips share the viewport).
|
|
408
|
+
const scoped = cardEl && scrollerRef.current?.contains(cardEl) ? cardEl : null;
|
|
409
|
+
const key = scoped?.getAttribute('data-strip-card-key') ?? null;
|
|
410
|
+
const current = activeKeyRef.current;
|
|
411
|
+
if (key === current) return;
|
|
412
|
+
if (current !== null) deactivate(current);
|
|
413
|
+
if (key !== null) activate(key);
|
|
414
|
+
}, [activate, deactivate]);
|
|
415
|
+
// Hover-capable pointers only (mouse/pen) — touch drags must not fake hover.
|
|
416
|
+
const onHoverPointerEnter = useCallback((e: React.PointerEvent) => {
|
|
417
|
+
if (e.pointerType === 'touch') return;
|
|
418
|
+
hoverPointerRef.current = { x: e.clientX, y: e.clientY, inside: true };
|
|
419
|
+
}, []);
|
|
420
|
+
const onHoverPointerMove = useCallback((e: React.PointerEvent) => {
|
|
421
|
+
if (e.pointerType === 'touch') return;
|
|
422
|
+
hoverPointerRef.current = { x: e.clientX, y: e.clientY, inside: true };
|
|
423
|
+
}, []);
|
|
424
|
+
const onHoverPointerLeave = useCallback((e: React.PointerEvent) => {
|
|
425
|
+
if (e.pointerType === 'touch') return;
|
|
426
|
+
hoverPointerRef.current.inside = false;
|
|
427
|
+
// The pointer left the whole scroller — no card can be hovered anymore
|
|
428
|
+
// (covers leave events swallowed by DOM churn under the cursor).
|
|
429
|
+
const current = activeKeyRef.current;
|
|
430
|
+
if (current !== null) deactivate(current);
|
|
431
|
+
}, [deactivate]);
|
|
432
|
+
/** Re-sync hover only when the track actually moved under the pointer.
|
|
433
|
+
* KNOWN HOT-PATH COST (intentional): while the marquee runs with the
|
|
434
|
+
* pointer parked over strip whitespace (no card → not paused), scrollLeft
|
|
435
|
+
* changes every frame, so this invokes `document.elementFromPoint` ~60×/s
|
|
436
|
+
* — a synchronous hit-test. It's inherent to detecting a card sliding
|
|
437
|
+
* under a stationary cursor; any throttle trades hover latency for it.
|
|
438
|
+
* Revisit only if profiling flags it on pages with many stacked strips. */
|
|
439
|
+
const syncHoverIfScrolled = useCallback(() => {
|
|
440
|
+
const scroller = scrollerRef.current;
|
|
441
|
+
if (!scroller) return;
|
|
442
|
+
if (scroller.scrollLeft === lastHoverSyncScrollRef.current) return;
|
|
443
|
+
lastHoverSyncScrollRef.current = scroller.scrollLeft;
|
|
444
|
+
syncHoverToPointer();
|
|
445
|
+
}, [syncHoverToPointer]);
|
|
446
|
+
|
|
360
447
|
// ---- shared card-mount gate (by ITEM index, across both copies) -------------
|
|
361
448
|
// A card and its clone show identical content half a copy-width apart. If
|
|
362
449
|
// each card gated its own expensive content (e.g. video players), a seam warp
|
|
@@ -433,6 +520,15 @@ export function CardsStrip<T = unknown>(props: CardsStripProps<T>): React.ReactE
|
|
|
433
520
|
const onUserScrollIntent = useCallback(() => {
|
|
434
521
|
userScrollSuppressUntilRef.current = performance.now() + USER_SCROLL_SUPPRESS_MS;
|
|
435
522
|
}, []);
|
|
523
|
+
// Suppress ONLY on genuine horizontal-scroll intent. Vertical page-scroll
|
|
524
|
+
// wheel events over the strip and plain clicks (pointerdown) used to arm the
|
|
525
|
+
// 3s suppression — the "marquee resumes only after a delay" bug.
|
|
526
|
+
const onWheelIntent = useCallback((e: React.WheelEvent) => {
|
|
527
|
+
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) onUserScrollIntent();
|
|
528
|
+
}, [onUserScrollIntent]);
|
|
529
|
+
const onPointerDownIntent = useCallback((e: React.PointerEvent) => {
|
|
530
|
+
if (e.pointerType === 'touch') onUserScrollIntent();
|
|
531
|
+
}, [onUserScrollIntent]);
|
|
436
532
|
|
|
437
533
|
// Never-ending strip: warp across the clone seam on EVERY scroll (manual
|
|
438
534
|
// wheel/drag/chevron included — the rAF only wraps while the marquee runs,
|
|
@@ -445,7 +541,12 @@ export function CardsStrip<T = unknown>(props: CardsStripProps<T>): React.ReactE
|
|
|
445
541
|
marqueeActiveRef.current = marqueeActive;
|
|
446
542
|
const onSeamWarp = useCallback(() => {
|
|
447
543
|
const scroller = scrollerRef.current;
|
|
448
|
-
if (!scroller
|
|
544
|
+
if (!scroller) return;
|
|
545
|
+
// Every scroll (native wheel/drag, smooth chevron scroll, marquee) moves
|
|
546
|
+
// cards under a possibly-stationary pointer — re-resolve hover regardless
|
|
547
|
+
// of marquee state (pointerenter/leave never fire for track motion).
|
|
548
|
+
syncHoverIfScrolled();
|
|
549
|
+
if (!marqueeActiveRef.current) return;
|
|
449
550
|
const half = singleCopyWidthRef.current;
|
|
450
551
|
if (half <= 0) return;
|
|
451
552
|
let sl = scroller.scrollLeft;
|
|
@@ -459,7 +560,7 @@ export function CardsStrip<T = unknown>(props: CardsStripProps<T>): React.ReactE
|
|
|
459
560
|
marqueePosRef.current = sl;
|
|
460
561
|
}
|
|
461
562
|
lastScrollLeftRef.current = sl;
|
|
462
|
-
}, []);
|
|
563
|
+
}, [syncHoverIfScrolled]);
|
|
463
564
|
|
|
464
565
|
// ---- render ----------------------------------------------------------------
|
|
465
566
|
|
|
@@ -468,6 +569,15 @@ export function CardsStrip<T = unknown>(props: CardsStripProps<T>): React.ReactE
|
|
|
468
569
|
// Seam assertion (see normalization comment above).
|
|
469
570
|
return (props.renderCard as (item: unknown, ctx: CardStripRenderCtx) => React.ReactNode)(item, ctx);
|
|
470
571
|
}
|
|
572
|
+
// Per-child DESKTOP cell-width hint: heterogeneous card designs need
|
|
573
|
+
// heterogeneous cells (wide horizontal cards vs 400px vertical cards —
|
|
574
|
+
// variable widths are native to the engine, exactly like bite ratios).
|
|
575
|
+
// Read from the child ELEMENT's `data-strip-cell-width` prop; mobile keeps
|
|
576
|
+
// the strip-level width (wide cards stack vertically below md anyway).
|
|
577
|
+
const hintedWidth = React.isValidElement(item)
|
|
578
|
+
? (item.props as Record<string, unknown>)['data-strip-cell-width']
|
|
579
|
+
: undefined;
|
|
580
|
+
const desktopWidth = typeof hintedWidth === 'number' ? hintedWidth : cardWidthDesktop;
|
|
471
581
|
// Managed cell — the ONLY place cell width / activation / clone a11y are
|
|
472
582
|
// encoded for children mode.
|
|
473
583
|
return (
|
|
@@ -476,7 +586,8 @@ export function CardsStrip<T = unknown>(props: CardsStripProps<T>): React.ReactE
|
|
|
476
586
|
// clones).
|
|
477
587
|
<div
|
|
478
588
|
className="shrink-0 self-stretch"
|
|
479
|
-
style={{ width: ctx.isMobile ? cardWidthMobile :
|
|
589
|
+
style={{ width: ctx.isMobile ? cardWidthMobile : desktopWidth, maxWidth: STRIP_CELL_MAX_WIDTH }}
|
|
590
|
+
data-strip-card-key={ctx.cardKey}
|
|
480
591
|
onPointerEnter={ctx.isTouch ? undefined : () => ctx.onActivate(ctx.cardKey)}
|
|
481
592
|
onPointerLeave={ctx.isTouch ? undefined : () => ctx.onDeactivate(ctx.cardKey)}
|
|
482
593
|
// Keyboard pause parity (focus bubbles from the card's anchor; WCAG 2.2.2).
|
|
@@ -520,9 +631,12 @@ export function CardsStrip<T = unknown>(props: CardsStripProps<T>): React.ReactE
|
|
|
520
631
|
<div
|
|
521
632
|
ref={scrollerRef}
|
|
522
633
|
className="overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
523
|
-
onWheel={
|
|
634
|
+
onWheel={onWheelIntent}
|
|
524
635
|
onTouchStart={onUserScrollIntent}
|
|
525
|
-
onPointerDown={
|
|
636
|
+
onPointerDown={onPointerDownIntent}
|
|
637
|
+
onPointerEnter={onHoverPointerEnter}
|
|
638
|
+
onPointerMove={onHoverPointerMove}
|
|
639
|
+
onPointerLeave={onHoverPointerLeave}
|
|
526
640
|
onScroll={onSeamWarp}
|
|
527
641
|
>
|
|
528
642
|
<div ref={trackRef} data-copies={copies} className="flex w-max items-stretch gap-4">
|
|
@@ -391,6 +391,10 @@ export function VideoBiteCard({
|
|
|
391
391
|
return rootRef?.(node);
|
|
392
392
|
}}
|
|
393
393
|
aria-hidden={isClone || undefined}
|
|
394
|
+
// Hit-test marker for the strip engine's pointer-tracked hover re-sync
|
|
395
|
+
// (cards move under a stationary pointer — see cards-strip.tsx). The
|
|
396
|
+
// hover area is exactly this card root: media + overlay + toolbar.
|
|
397
|
+
data-strip-card-key={key}
|
|
394
398
|
className={cn(
|
|
395
399
|
'relative rounded-md border border-ods-border bg-ods-card overflow-hidden group/card',
|
|
396
400
|
height !== undefined ? 'shrink-0' : 'w-full',
|
|
@@ -76,5 +76,5 @@ const SSO_PROVIDERS: AuthSsoProvider[] = ['openframe', 'google', 'microsoft']
|
|
|
76
76
|
|
|
77
77
|
/** SSO configured: email filled, submit replaced by provider buttons. */
|
|
78
78
|
export const SSO: Story = {
|
|
79
|
-
render: () => <LoginPage email="roman@mail.com" ssoProviders={SSO_PROVIDERS}
|
|
79
|
+
render: () => <LoginPage email="roman@mail.com" ssoProviders={SSO_PROVIDERS} />,
|
|
80
80
|
}
|