@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.
Files changed (131) hide show
  1. package/dist/{chunk-NJPMHDFG.js → chunk-2OOZASQE.js} +2 -2
  2. package/dist/{chunk-PJ2YFSMY.js → chunk-3E4BDAHJ.js} +2 -2
  3. package/dist/{chunk-M7WTWBCY.cjs → chunk-43C5ECNT.cjs} +37 -37
  4. package/dist/{chunk-M7WTWBCY.cjs.map → chunk-43C5ECNT.cjs.map} +1 -1
  5. package/dist/{chunk-OSOKWP5R.cjs → chunk-55SNCMFS.cjs} +31 -31
  6. package/dist/{chunk-OSOKWP5R.cjs.map → chunk-55SNCMFS.cjs.map} +1 -1
  7. package/dist/{chunk-BEPZ5YAN.cjs → chunk-BB7TZ6KS.cjs} +59 -59
  8. package/dist/{chunk-BEPZ5YAN.cjs.map → chunk-BB7TZ6KS.cjs.map} +1 -1
  9. package/dist/{chunk-RWBTSF2E.js → chunk-CK5UZLIN.js} +5316 -5003
  10. package/dist/chunk-CK5UZLIN.js.map +1 -0
  11. package/dist/{chunk-YJE3HL6Q.js → chunk-D4F7ZLJG.js} +4 -4
  12. package/dist/{chunk-ZCKVP4SU.cjs → chunk-DB6CUWTL.cjs} +26 -26
  13. package/dist/{chunk-ZCKVP4SU.cjs.map → chunk-DB6CUWTL.cjs.map} +1 -1
  14. package/dist/{chunk-Y4I4462K.cjs → chunk-HPQKX5IW.cjs} +15 -15
  15. package/dist/{chunk-Y4I4462K.cjs.map → chunk-HPQKX5IW.cjs.map} +1 -1
  16. package/dist/{chunk-NN73KMIK.js → chunk-IVXOR2IA.js} +2 -2
  17. package/dist/{chunk-WHJEU7V7.js → chunk-J6TAK6HE.js} +4 -4
  18. package/dist/{chunk-45Z6B6CO.js → chunk-JNU3YPLP.js} +4 -4
  19. package/dist/{chunk-TWLPG5IQ.js → chunk-K5FRWPWF.js} +2 -2
  20. package/dist/{chunk-RNWLVI3V.cjs → chunk-M6NPDNVB.cjs} +26 -26
  21. package/dist/{chunk-RNWLVI3V.cjs.map → chunk-M6NPDNVB.cjs.map} +1 -1
  22. package/dist/{chunk-5U72NXGG.cjs → chunk-OAENYOKC.cjs} +9 -9
  23. package/dist/{chunk-5U72NXGG.cjs.map → chunk-OAENYOKC.cjs.map} +1 -1
  24. package/dist/{chunk-T6FEE3R2.cjs → chunk-SBWTMJPQ.cjs} +5 -5
  25. package/dist/{chunk-T6FEE3R2.cjs.map → chunk-SBWTMJPQ.cjs.map} +1 -1
  26. package/dist/{chunk-4YVUL4T7.cjs → chunk-T5F54MN4.cjs} +12 -12
  27. package/dist/{chunk-4YVUL4T7.cjs.map → chunk-T5F54MN4.cjs.map} +1 -1
  28. package/dist/{chunk-MPN3MV5H.js → chunk-TTHTO5H5.js} +4 -4
  29. package/dist/{chunk-PFF6Q6N5.js → chunk-VTR4QDL2.js} +2 -2
  30. package/dist/{chunk-TTEAYVLT.js → chunk-WCDM226S.js} +2 -2
  31. package/dist/{chunk-J2UDV3FH.cjs → chunk-XNAYZHPK.cjs} +62 -62
  32. package/dist/{chunk-J2UDV3FH.cjs.map → chunk-XNAYZHPK.cjs.map} +1 -1
  33. package/dist/{chunk-3VJPMRNT.cjs → chunk-Z6PAD2UN.cjs} +7 -7
  34. package/dist/{chunk-3VJPMRNT.cjs.map → chunk-Z6PAD2UN.cjs.map} +1 -1
  35. package/dist/{chunk-ZB5HSWX4.js → chunk-ZFK4HHRX.js} +2 -2
  36. package/dist/{chunk-PNMX75W4.cjs → chunk-ZUKBC2ZX.cjs} +1114 -801
  37. package/dist/chunk-ZUKBC2ZX.cjs.map +1 -0
  38. package/dist/components/case-studies/index.cjs +8 -8
  39. package/dist/components/case-studies/index.js +2 -2
  40. package/dist/components/chat/entity-cards/blog-card.d.ts +6 -3
  41. package/dist/components/chat/entity-cards/blog-card.d.ts.map +1 -1
  42. package/dist/components/chat/entity-cards/case-study-card.d.ts +7 -3
  43. package/dist/components/chat/entity-cards/case-study-card.d.ts.map +1 -1
  44. package/dist/components/chat/entity-cards/customer-interview-card.d.ts +6 -3
  45. package/dist/components/chat/entity-cards/customer-interview-card.d.ts.map +1 -1
  46. package/dist/components/chat/entity-cards/entity-portrait-card.d.ts +59 -0
  47. package/dist/components/chat/entity-cards/entity-portrait-card.d.ts.map +1 -0
  48. package/dist/components/chat/entity-cards/index.d.ts +3 -0
  49. package/dist/components/chat/entity-cards/index.d.ts.map +1 -1
  50. package/dist/components/chat/entity-cards/investor-update-card.d.ts +6 -3
  51. package/dist/components/chat/entity-cards/investor-update-card.d.ts.map +1 -1
  52. package/dist/components/chat/entity-cards/onboarding-guide-card.d.ts +4 -2
  53. package/dist/components/chat/entity-cards/onboarding-guide-card.d.ts.map +1 -1
  54. package/dist/components/chat/entity-cards/program-card.d.ts +17 -5
  55. package/dist/components/chat/entity-cards/program-card.d.ts.map +1 -1
  56. package/dist/components/chat/entity-cards/use-cover-image-fallback.d.ts +26 -0
  57. package/dist/components/chat/entity-cards/use-cover-image-fallback.d.ts.map +1 -0
  58. package/dist/components/chat/index.cjs +10 -2
  59. package/dist/components/chat/index.cjs.map +1 -1
  60. package/dist/components/chat/index.js +9 -1
  61. package/dist/components/chat/types/entities/program-types.d.ts +23 -0
  62. package/dist/components/chat/types/entities/program-types.d.ts.map +1 -1
  63. package/dist/components/contact/index.cjs +3 -3
  64. package/dist/components/contact/index.js +2 -2
  65. package/dist/components/docs/index.cjs +5 -5
  66. package/dist/components/docs/index.js +4 -4
  67. package/dist/components/embeds/index.cjs +3 -3
  68. package/dist/components/embeds/index.js +2 -2
  69. package/dist/components/faq/index.cjs +3 -3
  70. package/dist/components/faq/index.js +2 -2
  71. package/dist/components/features/auth/create-organization-form.d.ts +1 -1
  72. package/dist/components/features/auth/create-organization-form.d.ts.map +1 -1
  73. package/dist/components/features/auth/login-form.d.ts +1 -1
  74. package/dist/components/features/auth/login-form.d.ts.map +1 -1
  75. package/dist/components/features/auth/sso-providers.d.ts +1 -1
  76. package/dist/components/features/auth/sso-providers.d.ts.map +1 -1
  77. package/dist/components/features/cards-strip.d.ts.map +1 -1
  78. package/dist/components/features/index.cjs +2 -2
  79. package/dist/components/features/index.js +1 -1
  80. package/dist/components/features/video-bites-strip.d.ts.map +1 -1
  81. package/dist/components/help-center-pages/index.cjs +20 -20
  82. package/dist/components/help-center-pages/index.js +11 -11
  83. package/dist/components/index.cjs +128 -120
  84. package/dist/components/index.cjs.map +1 -1
  85. package/dist/components/index.js +17 -9
  86. package/dist/components/index.js.map +1 -1
  87. package/dist/components/navigation/index.cjs +2 -2
  88. package/dist/components/navigation/index.js +1 -1
  89. package/dist/components/onboarding-guides/index.cjs +5 -5
  90. package/dist/components/onboarding-guides/index.js +4 -4
  91. package/dist/components/related-content/index.cjs +3 -3
  92. package/dist/components/related-content/index.js +2 -2
  93. package/dist/components/tickets/index.cjs +5 -5
  94. package/dist/components/tickets/index.js +4 -4
  95. package/dist/components/ui/index.cjs +10 -2
  96. package/dist/components/ui/index.cjs.map +1 -1
  97. package/dist/components/ui/index.js +9 -1
  98. package/dist/index.cjs +10 -2
  99. package/dist/index.cjs.map +1 -1
  100. package/dist/index.js +9 -1
  101. package/package.json +1 -1
  102. package/src/components/chat/entity-cards/blog-card.tsx +40 -7
  103. package/src/components/chat/entity-cards/case-study-card.tsx +32 -6
  104. package/src/components/chat/entity-cards/customer-interview-card.tsx +31 -6
  105. package/src/components/chat/entity-cards/entity-portrait-card.tsx +201 -0
  106. package/src/components/chat/entity-cards/index.ts +18 -0
  107. package/src/components/chat/entity-cards/investor-update-card.tsx +28 -2
  108. package/src/components/chat/entity-cards/onboarding-guide-card.tsx +30 -1
  109. package/src/components/chat/entity-cards/program-card.tsx +69 -17
  110. package/src/components/chat/entity-cards/use-cover-image-fallback.ts +50 -0
  111. package/src/components/chat/types/entities/program-types.ts +34 -0
  112. package/src/components/features/auth/create-organization-form.tsx +2 -2
  113. package/src/components/features/auth/login-form.tsx +2 -2
  114. package/src/components/features/auth/sso-providers.tsx +2 -2
  115. package/src/components/features/cards-strip.tsx +122 -8
  116. package/src/components/features/video-bites-strip.tsx +4 -0
  117. package/src/stories/auth/CreateOrganization.stories.tsx +0 -1
  118. package/src/stories/auth/Login.stories.tsx +1 -1
  119. package/dist/chunk-PNMX75W4.cjs.map +0 -1
  120. package/dist/chunk-RWBTSF2E.js.map +0 -1
  121. /package/dist/{chunk-NJPMHDFG.js.map → chunk-2OOZASQE.js.map} +0 -0
  122. /package/dist/{chunk-PJ2YFSMY.js.map → chunk-3E4BDAHJ.js.map} +0 -0
  123. /package/dist/{chunk-YJE3HL6Q.js.map → chunk-D4F7ZLJG.js.map} +0 -0
  124. /package/dist/{chunk-NN73KMIK.js.map → chunk-IVXOR2IA.js.map} +0 -0
  125. /package/dist/{chunk-WHJEU7V7.js.map → chunk-J6TAK6HE.js.map} +0 -0
  126. /package/dist/{chunk-45Z6B6CO.js.map → chunk-JNU3YPLP.js.map} +0 -0
  127. /package/dist/{chunk-TWLPG5IQ.js.map → chunk-K5FRWPWF.js.map} +0 -0
  128. /package/dist/{chunk-MPN3MV5H.js.map → chunk-TTHTO5H5.js.map} +0 -0
  129. /package/dist/{chunk-PFF6Q6N5.js.map → chunk-VTR4QDL2.js.map} +0 -0
  130. /package/dist/{chunk-TTEAYVLT.js.map → chunk-WCDM226S.js.map} +0 -0
  131. /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. "Sign Up with". Ignored for "openframe". */
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 = 'Sign Up with',
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. "Sign Up with". Ignored for "openframe". */
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 = 'Sign Up with',
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. "Sign Up with". Ignored for "openframe". */
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 = 'Sign Up with',
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 (!paused) {
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 + autoScrollSpeed * dt);
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 || !marqueeActiveRef.current) return;
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 : cardWidthDesktop, maxWidth: STRIP_CELL_MAX_WIDTH }}
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={onUserScrollIntent}
634
+ onWheel={onWheelIntent}
524
635
  onTouchStart={onUserScrollIntent}
525
- onPointerDown={onUserScrollIntent}
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',
@@ -108,7 +108,6 @@ export const SSO: Story = {
108
108
  domain="rgroup"
109
109
  domainSuffix=".openframe.ai"
110
110
  ssoProviders={SSO_PROVIDERS}
111
- ssoActionLabel="Sign Up with"
112
111
  termsUrl="#terms"
113
112
  privacyPolicyUrl="#privacy"
114
113
  />
@@ -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} ssoActionLabel="Sign Up with" />,
79
+ render: () => <LoginPage email="roman@mail.com" ssoProviders={SSO_PROVIDERS} />,
80
80
  }