@flamingo-stack/openframe-frontend-core 0.0.733 → 0.0.735

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 (89) hide show
  1. package/dist/build/export-map.json +1 -1
  2. package/dist/{chunk-3KGICRH7.cjs → chunk-36CCUJOE.cjs} +406 -353
  3. package/dist/{chunk-TIMCHP4X.cjs → chunk-3FZZPR76.cjs} +42 -42
  4. package/dist/{chunk-CJVAA7KL.cjs → chunk-3JP2OVH3.cjs} +30 -30
  5. package/dist/{chunk-HSWHOCZ3.cjs → chunk-3X4N3IT7.cjs} +18 -18
  6. package/dist/{chunk-ATMBJBOI.cjs → chunk-4F7V2MAC.cjs} +12 -12
  7. package/dist/{chunk-U4T5NLZD.cjs → chunk-6TTAILSV.cjs} +3 -3
  8. package/dist/{chunk-XDWY6HAW.cjs → chunk-7HT3VHLJ.cjs} +17 -17
  9. package/dist/{chunk-AV5533DH.cjs → chunk-EQ7OOK4B.cjs} +11 -11
  10. package/dist/{chunk-XY632BCH.cjs → chunk-EQH7BUNS.cjs} +4 -4
  11. package/dist/{chunk-3UTPZW36.cjs → chunk-F7STMHT7.cjs} +65 -65
  12. package/dist/{chunk-EMG3T55N.cjs → chunk-HLBFTDUL.cjs} +2 -2
  13. package/dist/{chunk-YIXKH2JO.cjs → chunk-JDXQ6T5P.cjs} +128 -112
  14. package/dist/{chunk-GB3CHAW7.cjs → chunk-MRVZ56GT.cjs} +79 -79
  15. package/dist/{chunk-FNRMDDVN.cjs → chunk-NVCSVGNI.cjs} +33 -33
  16. package/dist/{chunk-P3R23TOE.cjs → chunk-RH3AKULV.cjs} +21 -11
  17. package/dist/{chunk-J7JKPTGO.cjs → chunk-SHQZN5LF.cjs} +6 -6
  18. package/dist/{chunk-ZBNA6TCW.cjs → chunk-SZRZBCWC.cjs} +67 -67
  19. package/dist/{chunk-E26I4XH6.cjs → chunk-T323E2Y6.cjs} +4 -4
  20. package/dist/{chunk-IC4FT3A6.cjs → chunk-TUMUT7CV.cjs} +15 -15
  21. package/dist/{chunk-BP6R5WDW.cjs → chunk-VA7OMDZO.cjs} +41 -23
  22. package/dist/{chunk-EQORT7NK.cjs → chunk-W6E7XCBI.cjs} +38 -38
  23. package/dist/{cloud-tenants-3GOGR3EN.cjs → cloud-tenants-H5D57P4U.cjs} +7 -7
  24. package/dist/components/case-studies/index.cjs +19 -19
  25. package/dist/components/chat/entity-cards/program-card.d.ts.map +1 -1
  26. package/dist/components/chat/entity-cards/program-card.js +6 -6
  27. package/dist/components/chat/index.cjs +5 -5
  28. package/dist/components/contact/index.cjs +8 -8
  29. package/dist/components/docs/index.cjs +11 -11
  30. package/dist/components/embeds/index.cjs +8 -8
  31. package/dist/components/faq/index.cjs +8 -8
  32. package/dist/components/features/cards-strip.d.ts.map +1 -1
  33. package/dist/components/features/cards-strip.js +6 -2
  34. package/dist/components/features/index.cjs +7 -7
  35. package/dist/components/features/video-bites-strip.d.ts +6 -1
  36. package/dist/components/features/video-bites-strip.d.ts.map +1 -1
  37. package/dist/components/features/video-bites-strip.js +15 -3
  38. package/dist/components/help-center-pages/index.cjs +106 -106
  39. package/dist/components/index.cjs +61 -55
  40. package/dist/components/layout/page-heading.d.ts +9 -1
  41. package/dist/components/layout/page-heading.d.ts.map +1 -1
  42. package/dist/components/layout/page-heading.js +15 -5
  43. package/dist/components/meeting-scheduler/index.cjs +29 -29
  44. package/dist/components/navigation/index.cjs +10 -10
  45. package/dist/components/onboarding-guides/index.cjs +10 -10
  46. package/dist/components/product-demo/index.cjs +6 -6
  47. package/dist/components/related-content/index.cjs +4 -4
  48. package/dist/components/tickets/index.cjs +11 -11
  49. package/dist/components/ui/fade-preview.d.ts +5 -1
  50. package/dist/components/ui/fade-preview.d.ts.map +1 -1
  51. package/dist/components/ui/fade-preview.js +18 -1
  52. package/dist/components/ui/index.cjs +13 -7
  53. package/dist/components/ui/index.d.ts +1 -0
  54. package/dist/components/ui/index.d.ts.map +1 -1
  55. package/dist/components/ui/index.js +1 -0
  56. package/dist/components/ui/layout-hold.d.ts +31 -0
  57. package/dist/components/ui/layout-hold.d.ts.map +1 -0
  58. package/dist/components/ui/layout-hold.js +25 -0
  59. package/dist/components/ui/snap-carousel.d.ts.map +1 -1
  60. package/dist/components/ui/snap-carousel.js +28 -15
  61. package/dist/{devices-POZXTP7X.cjs → devices-SQTEXY2H.cjs} +8 -8
  62. package/dist/{embeddable-chat-I3Y5GCCR.cjs → embeddable-chat-S5ODWE3L.cjs} +71 -71
  63. package/dist/hooks/index.cjs +4 -2
  64. package/dist/hooks/ui/index.d.ts +1 -0
  65. package/dist/hooks/ui/index.d.ts.map +1 -1
  66. package/dist/hooks/ui/index.js +1 -0
  67. package/dist/hooks/ui/use-resumable-timeout.d.ts +22 -0
  68. package/dist/hooks/ui/use-resumable-timeout.d.ts.map +1 -0
  69. package/dist/hooks/ui/use-resumable-timeout.js +26 -0
  70. package/dist/hooks/use-scenario-player.d.ts +4 -3
  71. package/dist/hooks/use-scenario-player.d.ts.map +1 -1
  72. package/dist/hooks/use-scenario-player.js +17 -23
  73. package/dist/index.cjs +19 -11
  74. package/dist/{knowledge-AP34IMXN.cjs → knowledge-FO3TPLFO.cjs} +8 -8
  75. package/dist/{logs-CEVMIOPW.cjs → logs-Y4WQB2YR.cjs} +15 -15
  76. package/dist/{policies-AMBMRCGB.cjs → policies-D6VY2ODF.cjs} +11 -11
  77. package/dist/{software-update-Z3MWMSHI.cjs → software-update-7XN6U7HC.cjs} +10 -10
  78. package/package.json +1 -1
  79. package/src/components/chat/entity-cards/program-card.tsx +9 -8
  80. package/src/components/features/cards-strip.tsx +11 -2
  81. package/src/components/features/video-bites-strip.tsx +22 -3
  82. package/src/components/layout/page-heading.tsx +37 -7
  83. package/src/components/ui/fade-preview.tsx +30 -1
  84. package/src/components/ui/index.ts +1 -0
  85. package/src/components/ui/layout-hold.tsx +55 -0
  86. package/src/components/ui/snap-carousel.tsx +36 -20
  87. package/src/hooks/ui/index.ts +1 -0
  88. package/src/hooks/ui/use-resumable-timeout.ts +55 -0
  89. package/src/hooks/use-scenario-player.ts +28 -32
@@ -3,6 +3,7 @@
3
3
  import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react';
4
4
  import { useAutoplay } from '../../hooks/ui/use-autoplay';
5
5
  import { useInView } from '../../hooks/ui/use-in-view';
6
+ import { useResumableTimeout } from '../../hooks/ui/use-resumable-timeout';
6
7
  import { cn } from '../../utils/cn';
7
8
  import { Chevron02LeftIcon } from '../icons-v2-generated/arrows/chevron-02-left-icon';
8
9
  import { Chevron02RightIcon } from '../icons-v2-generated/arrows/chevron-02-right-icon';
@@ -39,20 +40,31 @@ export interface SnapCarouselProps<T> {
39
40
  const GAP_PX = 12;
40
41
 
41
42
  /**
42
- * The active dot filling up while the carousel waits to advance. A transition,
43
- * not a keyframe animation: it mounts empty and is told to be full one frame
44
- * later, taking the whole interval to get there.
43
+ * The active dot's fill: how much of the slide's wait has been run. It starts
44
+ * from where the wait stands (`ranMs`, nothing for a new slide) and runs to
45
+ * full over what is LEFT of it. While the carousel is not advancing it stands
46
+ * still at that point; it never empties because the carousel left the screen.
45
47
  */
46
- function DotFill({ durationMs }: { durationMs: number }) {
47
- const [full, setFull] = useState(false);
48
+ function DotFill({ durationMs, ranMs, running }: { durationMs: number; ranMs: number; running: boolean }) {
49
+ // One frame at the starting point, so the browser has it to run from.
50
+ const [going, setGoing] = useState(false);
48
51
  useEffect(() => {
49
- const frame = requestAnimationFrame(() => setFull(true));
50
- return () => cancelAnimationFrame(frame);
51
- }, []);
52
+ if (!running) return undefined;
53
+ const frame = requestAnimationFrame(() => setGoing(true));
54
+ return () => {
55
+ cancelAnimationFrame(frame);
56
+ setGoing(false);
57
+ };
58
+ }, [running]);
59
+ const moving = running && going;
52
60
  return (
53
61
  <span
54
- className="absolute inset-0 origin-left bg-ods-text-primary transition-transform ease-linear"
55
- style={{ transform: full ? 'scaleX(1)' : 'scaleX(0)', transitionDuration: `${durationMs}ms` }}
62
+ className="absolute inset-0 origin-left bg-ods-text-primary ease-linear"
63
+ style={{
64
+ transform: `scaleX(${moving ? 1 : durationMs > 0 ? ranMs / durationMs : 0})`,
65
+ transitionProperty: moving ? 'transform' : 'none',
66
+ transitionDuration: `${Math.max(0, durationMs - ranMs)}ms`,
67
+ }}
56
68
  />
57
69
  );
58
70
  }
@@ -122,12 +134,15 @@ export function SnapCarousel<T>({
122
134
  const canAutoAdvance = autoAdvanceMs > 0 && count > 1;
123
135
  const playing = auto.playing && canAutoAdvance;
124
136
 
125
- // Restarts on every slide change, so a swipe or an arrow is followed by a full interval.
126
- useEffect(() => {
127
- if (!playing) return undefined;
128
- const timer = setTimeout(() => goTo(index >= count - 1 ? 0 : index + 1), autoAdvanceMs);
129
- return () => clearTimeout(timer);
130
- }, [playing, index, count, autoAdvanceMs, goTo]);
137
+ // A new slide starts a whole interval (a swipe or an arrow is followed by a
138
+ // full one); an interruption (off screen, a hold, another tab) only pauses it.
139
+ const ranMs = useResumableTimeout({
140
+ running: playing,
141
+ durationMs: autoAdvanceMs,
142
+ resetKey: index,
143
+ onElapsed: () => goTo(index >= count - 1 ? 0 : index + 1),
144
+ });
145
+ const showsWait = canAutoAdvance && !auto.reducedMotion;
131
146
 
132
147
  const setRefs = useCallback(
133
148
  (node: HTMLDivElement | null) => {
@@ -172,12 +187,13 @@ export function SnapCarousel<T>({
172
187
  className={cn(
173
188
  'relative h-1.5 overflow-hidden rounded-full transition-[width] duration-200 motion-reduce:transition-none',
174
189
  dotIndex === index ? 'w-6' : 'w-1.5',
175
- dotIndex === index && !playing ? 'bg-ods-text-primary' : 'bg-ods-border',
190
+ // A carousel that never advances by itself marks its slide with a whole dot.
191
+ dotIndex === index && !showsWait ? 'bg-ods-text-primary' : 'bg-ods-border',
176
192
  )}
177
193
  >
178
- {dotIndex === index && playing && (
179
- // Re-keyed per slide so the fill restarts from empty.
180
- <DotFill key={index} durationMs={autoAdvanceMs} />
194
+ {dotIndex === index && showsWait && (
195
+ // Re-keyed per slide so a NEW slide starts from empty; a pause keeps its place.
196
+ <DotFill key={index} durationMs={autoAdvanceMs} ranMs={ranMs} running={playing} />
181
197
  )}
182
198
  </span>
183
199
  ))}
@@ -35,3 +35,4 @@ export * from './use-autoplay';
35
35
  export * from './use-active-section';
36
36
  export * from './use-nav-menus';
37
37
  export * from './use-visitor-os';
38
+ export * from './use-resumable-timeout';
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+
5
+ export interface UseResumableTimeoutOptions {
6
+ /** The wait counts down only while this is true. */
7
+ running: boolean;
8
+ /** How long the whole wait is. */
9
+ durationMs: number;
10
+ /** A new value starts the wait over from nothing (the slide changed). */
11
+ resetKey: string | number;
12
+ /** Called once the whole wait has been run. */
13
+ onElapsed: () => void;
14
+ }
15
+
16
+ /**
17
+ * A wait that PAUSES instead of starting over: `durationMs` of time spent
18
+ * `running`, however many times it is interrupted. A carousel that scrolls off
19
+ * screen half-way through a slide comes back with half a slide left, not a
20
+ * whole one, and the bar that shows the wait picks up where it stopped.
21
+ *
22
+ * Returns how much of the wait had been run at the last stop (0 after a
23
+ * reset), which is what a progress bar draws while it is not running and
24
+ * where it resumes from.
25
+ */
26
+ export function useResumableTimeout({ running, durationMs, resetKey, onElapsed }: UseResumableTimeoutOptions): number {
27
+ // What has been run so far, for the key it was run under. The ref is the
28
+ // clock's own memory (read when a stretch starts); the state is the same
29
+ // figure for whoever draws it.
30
+ const banked = useRef({ key: resetKey, ms: 0 });
31
+ const [shown, setShown] = useState({ key: resetKey, ms: 0 });
32
+
33
+ const onElapsedRef = useRef(onElapsed);
34
+ useEffect(() => {
35
+ onElapsedRef.current = onElapsed;
36
+ }, [onElapsed]);
37
+
38
+ useEffect(() => {
39
+ // A new key: the wait starts over.
40
+ if (banked.current.key !== resetKey) banked.current = { key: resetKey, ms: 0 };
41
+ if (!running) return undefined;
42
+ const startedAt = Date.now();
43
+ const timer = setTimeout(() => onElapsedRef.current(), Math.max(0, durationMs - banked.current.ms));
44
+ return () => {
45
+ clearTimeout(timer);
46
+ // Bank what this stretch ran.
47
+ const ms = Math.min(durationMs, banked.current.ms + (Date.now() - startedAt));
48
+ banked.current = { key: resetKey, ms };
49
+ setShown({ key: resetKey, ms });
50
+ };
51
+ }, [running, durationMs, resetKey]);
52
+
53
+ // Under another key (the slide changed) nothing has been run yet.
54
+ return shown.key === resetKey ? shown.ms : 0;
55
+ }
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
 
3
- import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { useCallback, useState } from 'react';
4
4
  import { type AutoplayHoldProps, useAutoplay } from './ui/use-autoplay';
5
+ import { useResumableTimeout } from './ui/use-resumable-timeout';
5
6
 
6
7
  /**
7
8
  * How soon the first step follows the moment the demo becomes playable (it
@@ -78,9 +79,10 @@ export interface UseScenarioPlayerResult {
78
79
  * not held by the visitor, motion allowed. Under reduced motion it parks on the last step
79
80
  * of the current scenario, and every scenario can still be read by choosing it.
80
81
  *
81
- * The moment it becomes playable, and at the top of every scenario (the
82
- * rotation moved on, or a tab was chosen), its first step follows within
83
- * `START_MS`, not a whole step later; from there every step takes `stepMs`.
82
+ * A step is `stepMs` of time spent playing: an interruption pauses it and it
83
+ * resumes with what was left (`useResumableTimeout`, the one clock every timed
84
+ * thing here runs on). At the top of a scenario whose first step is the empty
85
+ * stage, that step is left within `START_MS`.
84
86
  *
85
87
  * A visitor's own choice (`go`, `setStep`) moves the clock there and lets go of
86
88
  * any hold; the rotation carries on from that point.
@@ -110,34 +112,28 @@ export function useScenarioPlayer({
110
112
  const [started, setStarted] = useState(false);
111
113
  if (playing && !started) setStarted(true);
112
114
 
113
- // True from the moment it becomes playable until its first tick has been scheduled.
114
- const starting = useRef(false);
115
- const wasPlaying = useRef(false);
116
- useEffect(() => {
117
- if (playing && !wasPlaying.current) starting.current = true;
118
- wasPlaying.current = playing;
119
- if (!playing) return undefined;
120
- const atEnd = rawStep >= lastStep;
121
- // The first step comes quickly, both after it becomes playable and at the
122
- // top of every scenario whose step 0 is the empty stage (the rotation
123
- // moved on, or the visitor chose a tab): a stage that sits empty for a
124
- // whole step after a tap reads as not responding. A finished scenario still
125
- // holds its full time: it is there to be read.
126
- const quickStart = emptyFirstStep && (starting.current || rawStep === 0) && !atEnd;
127
- starting.current = false;
128
- const timer = setTimeout(
129
- () => {
130
- if (atEnd) {
131
- setScenario(current => (current + 1) % scenarioCount);
132
- setRawStep(0);
133
- } else {
134
- setRawStep(current => Math.min(current, lastStep) + 1);
135
- }
136
- },
137
- atEnd ? holdMs : quickStart ? Math.min(START_MS, stepMs) : stepMs,
138
- );
139
- return () => clearTimeout(timer);
140
- }, [playing, rawStep, lastStep, stepMs, holdMs, scenario, scenarioCount, emptyFirstStep]);
115
+ // ONE clock for everything that waits and moves on (`useResumableTimeout`,
116
+ // the carousel's too): a step is `stepMs` of time spent PLAYING. Interrupted
117
+ // (off screen, held, another tab), it comes back with what was left of the
118
+ // step, never a whole new one and never a skipped one.
119
+ const atEnd = rawStep >= lastStep;
120
+ // Step 0 of a scenario whose first step is the empty stage is left at once:
121
+ // a stage that sits empty for a whole step after a tap reads as not
122
+ // responding. A finished scenario holds its full time: it is there to be read.
123
+ const leavesAtOnce = emptyFirstStep && rawStep === 0 && !atEnd;
124
+ useResumableTimeout({
125
+ running: playing,
126
+ durationMs: atEnd ? holdMs : leavesAtOnce ? Math.min(START_MS, stepMs) : stepMs,
127
+ resetKey: `${scenario}:${Math.min(rawStep, lastStep)}`,
128
+ onElapsed: () => {
129
+ if (atEnd) {
130
+ setScenario(current => (current + 1) % scenarioCount);
131
+ setRawStep(0);
132
+ } else {
133
+ setRawStep(current => Math.min(current, lastStep) + 1);
134
+ }
135
+ },
136
+ });
141
137
 
142
138
  // A visitor's choice is an instruction to play from there: it lets go of any hold.
143
139
  const setStep = useCallback(