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

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 (149) hide show
  1. package/dist/build/export-map.json +1 -1
  2. package/dist/{chunk-RAHRDJBP.cjs → chunk-3HPB4H4C.cjs} +4 -4
  3. package/dist/{chunk-2UNQJVYN.cjs → chunk-3KGICRH7.cjs} +1457 -1187
  4. package/dist/{chunk-A4HNF4MP.cjs → chunk-3UTPZW36.cjs} +89 -89
  5. package/dist/{chunk-GID62YEN.cjs → chunk-AAFW4STC.cjs} +3 -3
  6. package/dist/{chunk-XDBHGWFH.cjs → chunk-ATMBJBOI.cjs} +16 -16
  7. package/dist/{chunk-YDGBRI3Y.cjs → chunk-AV5533DH.cjs} +15 -15
  8. package/dist/{chunk-BFHLGEJK.cjs → chunk-BP6R5WDW.cjs} +63 -60
  9. package/dist/{chunk-CLILWI6O.cjs → chunk-CJVAA7KL.cjs} +32 -32
  10. package/dist/{chunk-YJFN2W7A.cjs → chunk-DUVOQHQI.cjs} +7 -7
  11. package/dist/{chunk-E3N24573.cjs → chunk-DUXLZPMJ.cjs} +9 -9
  12. package/dist/{chunk-TH4N726A.cjs → chunk-E26I4XH6.cjs} +17 -17
  13. package/dist/{chunk-Y4QRK3CB.cjs → chunk-EMG3T55N.cjs} +7 -7
  14. package/dist/{chunk-JK75ZZP3.cjs → chunk-EQORT7NK.cjs} +59 -59
  15. package/dist/chunk-FCGLSWKR.cjs +417 -0
  16. package/dist/{chunk-MANNIJ42.cjs → chunk-FICJS5OA.cjs} +6 -0
  17. package/dist/{chunk-QP6QS3YU.cjs → chunk-FNRMDDVN.cjs} +48 -50
  18. package/dist/{chunk-KLXHCNDC.cjs → chunk-FTTLBMHU.cjs} +3 -3
  19. package/dist/{chunk-JPZ4Y6X3.cjs → chunk-GB3CHAW7.cjs} +93 -93
  20. package/dist/{chunk-7DUXR4HL.cjs → chunk-GVTDVS5U.cjs} +3 -3
  21. package/dist/{chunk-ZV6473Z4.cjs → chunk-HSWHOCZ3.cjs} +20 -20
  22. package/dist/{chunk-53XZ6MJ5.cjs → chunk-IC4FT3A6.cjs} +28 -28
  23. package/dist/{chunk-6V4GEE55.cjs → chunk-J7JKPTGO.cjs} +12 -12
  24. package/dist/{chunk-FKKUQK4C.cjs → chunk-L4ZNR5V7.cjs} +43 -39
  25. package/dist/{chunk-NYY25425.cjs → chunk-MYBTNOS6.cjs} +2 -2
  26. package/dist/chunk-NTJE6SM6.cjs +28 -0
  27. package/dist/{chunk-RBOIGJYF.cjs → chunk-P3R23TOE.cjs} +35 -35
  28. package/dist/chunk-PX7FY655.cjs +114 -0
  29. package/dist/{chunk-YUXRIR45.cjs → chunk-SA7LDEVJ.cjs} +98 -50
  30. package/dist/{chunk-32ENYVFA.cjs → chunk-TIMCHP4X.cjs} +42 -42
  31. package/dist/{chunk-3SWLL4IM.cjs → chunk-U4T5NLZD.cjs} +8 -8
  32. package/dist/{chunk-HI7EFAFQ.cjs → chunk-XDWY6HAW.cjs} +28 -28
  33. package/dist/{chunk-25H2SZH7.cjs → chunk-XY632BCH.cjs} +4 -4
  34. package/dist/{chunk-HY2KTQSL.cjs → chunk-YIXKH2JO.cjs} +236 -236
  35. package/dist/{chunk-4YPV4OI4.cjs → chunk-ZBNA6TCW.cjs} +82 -82
  36. package/dist/{chunk-HLIJ3U2T.cjs → chunk-ZJNYWNCB.cjs} +23 -21
  37. package/dist/{chunk-HEOD765R.cjs → chunk-ZRYKDN2J.cjs} +32 -379
  38. package/dist/{cloud-tenants-SC4FNAC2.cjs → cloud-tenants-3GOGR3EN.cjs} +22 -22
  39. package/dist/components/agent-identity.d.ts +8 -0
  40. package/dist/components/agent-identity.d.ts.map +1 -1
  41. package/dist/components/agent-identity.js +13 -1
  42. package/dist/components/case-studies/index.cjs +35 -35
  43. package/dist/components/chat/embeddable-chat.d.ts +16 -0
  44. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  45. package/dist/components/chat/embeddable-chat.js +5 -2
  46. package/dist/components/chat/index.cjs +16 -16
  47. package/dist/components/chat/utils/icon-library.d.ts.map +1 -1
  48. package/dist/components/chat/utils/icon-library.js +6 -0
  49. package/dist/components/contact/index.cjs +23 -23
  50. package/dist/components/docs/index.cjs +26 -26
  51. package/dist/components/embeds/index.cjs +23 -23
  52. package/dist/components/faq/index.cjs +23 -23
  53. package/dist/components/features/index.cjs +22 -22
  54. package/dist/components/help-center-pages/index.cjs +138 -138
  55. package/dist/components/index.cjs +89 -82
  56. package/dist/components/meeting-scheduler/index.cjs +67 -67
  57. package/dist/components/navigation/index.cjs +25 -25
  58. package/dist/components/onboarding-guides/index.cjs +25 -25
  59. package/dist/components/product-demo/index.cjs +8 -8
  60. package/dist/components/product-demo/screens/remote-desktop-picture.d.ts.map +1 -1
  61. package/dist/components/product-demo/screens/remote-desktop-picture.js +2 -1
  62. package/dist/components/related-content/index.cjs +10 -10
  63. package/dist/components/tickets/index.cjs +26 -26
  64. package/dist/components/ui/device-wallpaper.d.ts +7 -0
  65. package/dist/components/ui/device-wallpaper.d.ts.map +1 -0
  66. package/dist/components/ui/device-wallpaper.js +5 -0
  67. package/dist/components/ui/feature-list.d.ts +9 -4
  68. package/dist/components/ui/feature-list.d.ts.map +1 -1
  69. package/dist/components/ui/feature-list.js +26 -35
  70. package/dist/components/ui/index.cjs +28 -22
  71. package/dist/components/ui/index.d.ts +3 -1
  72. package/dist/components/ui/index.d.ts.map +1 -1
  73. package/dist/components/ui/index.js +3 -1
  74. package/dist/components/ui/notification-stack.d.ts +33 -0
  75. package/dist/components/ui/notification-stack.d.ts.map +1 -0
  76. package/dist/components/ui/notification-stack.js +78 -0
  77. package/dist/components/ui/numbered-step.d.ts +29 -0
  78. package/dist/components/ui/numbered-step.d.ts.map +1 -0
  79. package/dist/components/ui/numbered-step.js +57 -0
  80. package/dist/components/ui/phone-frame.d.ts +14 -9
  81. package/dist/components/ui/phone-frame.d.ts.map +1 -1
  82. package/dist/components/ui/phone-frame.js +50 -6
  83. package/dist/components/ui/phone-glyphs/lock-camera-glyph.d.ts +5 -0
  84. package/dist/components/ui/phone-glyphs/lock-camera-glyph.d.ts.map +1 -0
  85. package/dist/components/ui/phone-glyphs/lock-camera-glyph.js +14 -0
  86. package/dist/components/ui/phone-glyphs/lock-flashlight-glyph.d.ts +5 -0
  87. package/dist/components/ui/phone-glyphs/lock-flashlight-glyph.d.ts.map +1 -0
  88. package/dist/components/ui/phone-glyphs/lock-flashlight-glyph.js +14 -0
  89. package/dist/components/ui/phone-glyphs/status-bar-glyphs.d.ts +5 -0
  90. package/dist/components/ui/phone-glyphs/status-bar-glyphs.d.ts.map +1 -0
  91. package/dist/components/ui/phone-glyphs/status-bar-glyphs.js +68 -0
  92. package/dist/components/ui/searchable-select.d.ts +29 -3
  93. package/dist/components/ui/searchable-select.d.ts.map +1 -1
  94. package/dist/components/ui/searchable-select.js +101 -45
  95. package/dist/components/ui/snap-carousel.d.ts +5 -8
  96. package/dist/components/ui/snap-carousel.d.ts.map +1 -1
  97. package/dist/components/ui/snap-carousel.js +2 -27
  98. package/dist/components/ui/tab-navigation.d.ts +8 -1
  99. package/dist/components/ui/tab-navigation.d.ts.map +1 -1
  100. package/dist/components/ui/tab-navigation.js +30 -20
  101. package/dist/{devices-7MHSJNXS.cjs → devices-POZXTP7X.cjs} +23 -23
  102. package/dist/{embeddable-chat-VPIG3F7E.cjs → embeddable-chat-I3Y5GCCR.cjs} +96 -93
  103. package/dist/hooks/index.cjs +9 -9
  104. package/dist/hooks/ui/use-autoplay.d.ts +50 -13
  105. package/dist/hooks/ui/use-autoplay.d.ts.map +1 -1
  106. package/dist/hooks/ui/use-autoplay.js +76 -13
  107. package/dist/hooks/ui/use-in-view.d.ts +8 -0
  108. package/dist/hooks/ui/use-in-view.d.ts.map +1 -1
  109. package/dist/hooks/ui/use-in-view.js +21 -36
  110. package/dist/hooks/use-agent-identities.d.ts +6 -0
  111. package/dist/hooks/use-agent-identities.d.ts.map +1 -1
  112. package/dist/hooks/use-scenario-player.d.ts +26 -9
  113. package/dist/hooks/use-scenario-player.d.ts.map +1 -1
  114. package/dist/hooks/use-scenario-player.js +12 -9
  115. package/dist/index.cjs +58 -51
  116. package/dist/{knowledge-4DREGSOZ.cjs → knowledge-AP34IMXN.cjs} +23 -23
  117. package/dist/{logs-BWXNT2KP.cjs → logs-CEVMIOPW.cjs} +30 -30
  118. package/dist/{policies-MGFUI23M.cjs → policies-AMBMRCGB.cjs} +26 -26
  119. package/dist/{remote-session-O6QGXV2N.cjs → remote-session-Z4YZFG5E.cjs} +10 -9
  120. package/dist/{software-update-7LF7XEYC.cjs → software-update-Z3MWMSHI.cjs} +26 -26
  121. package/dist/{tickets-board-KNXK747O.cjs → tickets-board-JOSYLOS7.cjs} +13 -13
  122. package/package.json +1 -1
  123. package/src/components/agent-identity.tsx +20 -1
  124. package/src/components/chat/embeddable-chat.tsx +29 -2
  125. package/src/components/chat/utils/icon-library.tsx +7 -0
  126. package/src/components/product-demo/screens/remote-desktop-picture.tsx +2 -1
  127. package/src/components/ui/device-wallpaper.ts +7 -0
  128. package/src/components/ui/feature-list.tsx +24 -30
  129. package/src/components/ui/index.ts +3 -1
  130. package/src/components/ui/notification-stack.tsx +133 -0
  131. package/src/components/ui/numbered-step.tsx +81 -0
  132. package/src/components/ui/phone-frame.tsx +83 -19
  133. package/src/components/ui/phone-glyphs/lock-camera-glyph.tsx +14 -0
  134. package/src/components/ui/phone-glyphs/lock-flashlight-glyph.tsx +14 -0
  135. package/src/components/ui/phone-glyphs/status-bar-glyphs.tsx +45 -0
  136. package/src/components/ui/searchable-select.tsx +124 -32
  137. package/src/components/ui/snap-carousel.tsx +6 -38
  138. package/src/components/ui/tab-navigation.tsx +49 -21
  139. package/src/hooks/ui/use-autoplay.ts +144 -27
  140. package/src/hooks/ui/use-in-view.ts +31 -38
  141. package/src/hooks/use-agent-identities.ts +6 -0
  142. package/src/hooks/use-scenario-player.ts +44 -20
  143. package/dist/chunk-2WXLQ5R5.cjs +0 -99
  144. package/dist/chunk-BE2WAMG5.cjs +0 -54
  145. package/dist/chunk-LCTDHLDL.cjs +0 -43
  146. package/dist/components/ui/playback-toggle.d.ts +0 -24
  147. package/dist/components/ui/playback-toggle.d.ts.map +0 -1
  148. package/dist/components/ui/playback-toggle.js +0 -32
  149. package/src/components/ui/playback-toggle.tsx +0 -53
@@ -1,59 +1,176 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useState } from 'react';
3
+ import { useCallback, useMemo, useRef, useState, useSyncExternalStore } from 'react';
4
+ import type { FocusEvent, PointerEvent } from 'react';
4
5
  import { usePrefersReducedMotionState } from './use-prefers-reduced-motion';
5
6
 
7
+ /** The handlers that hold a moving thing still while a visitor is on it. Spread them on its outer element. */
8
+ export interface AutoplayHoldProps {
9
+ onPointerDown: (event: PointerEvent<HTMLElement>) => void;
10
+ onFocus: (event: FocusEvent<HTMLElement>) => void;
11
+ }
12
+
6
13
  export interface UseAutoplayResult {
7
14
  /** True while the thing may move by itself. */
8
15
  playing: boolean;
9
- /** The visitor stopped it with the pause control. */
10
- paused: boolean;
11
- setPaused: (paused: boolean) => void;
16
+ /** The visitor is holding it still: a finger or the mouse button is down on it, or the keyboard is inside it. */
17
+ held: boolean;
18
+ /** Spread on the element that moves: it is how a visitor stops it, with no control to find. */
19
+ holdProps: AutoplayHoldProps;
20
+ /**
21
+ * Lets go of every hold at once. Called when the visitor CHOOSES something on
22
+ * the thing itself (a tab, a step, an arrow): a choice is an instruction to
23
+ * play from there, and it must never depend on the browser reporting the end
24
+ * of the press or the focus that made it.
25
+ */
26
+ release: () => void;
12
27
  /** True once the system preference is known to be reduced motion. */
13
28
  reducedMotion: boolean;
14
29
  }
15
30
 
31
+ /** Subscribes to `events` on `target` for as long as React keeps the subscription; one signal removes them all. */
32
+ function listen(target: EventTarget, events: readonly string[], onChange: () => void): () => void {
33
+ const controller = new AbortController();
34
+ for (const event of events) target.addEventListener(event, onChange, { signal: controller.signal });
35
+ return () => controller.abort();
36
+ }
37
+
38
+ // The tab: hidden or not is the document's own state, read where it lives (never mirrored into React state).
39
+ const subscribeToTab = (onChange: () => void) => {
40
+ const stopDocument = listen(document, ['visibilitychange'], onChange);
41
+ // `pageshow`: a page restored from the back/forward cache.
42
+ const stopWindow = listen(window, ['pageshow'], onChange);
43
+ return () => {
44
+ stopDocument();
45
+ stopWindow();
46
+ };
47
+ };
48
+ const tabIsHidden = () => document.visibilityState === 'hidden';
49
+
50
+ const never = () => false;
51
+
52
+ /** Every way a press can end. A drag the browser takes over (an image, selected text) ends in `dragend`, never `pointerup`. */
53
+ const RELEASE_EVENTS = ['pointerup', 'pointercancel', 'mouseup', 'touchend', 'dragend', 'blur'] as const;
54
+
16
55
  /**
17
56
  * THE rule for anything that moves by itself (a looping demo, an
18
57
  * auto-advancing carousel): it plays while four plain facts hold, and nothing
19
58
  * else decides it.
20
59
  *
21
60
  * 1. it is on screen (`onScreen`, the caller's `useInView`);
22
- * 2. the browser tab is visible (`visibilitychange`, and `pageshow` for a
23
- * page restored from the back/forward cache);
24
- * 3. the visitor has not pressed pause (`setPaused`: the stop control
61
+ * 2. the browser tab is visible;
62
+ * 3. the visitor is not holding it (`holdProps`: the way to stop it that
25
63
  * WCAG 2.2.2 asks of anything that moves for more than five seconds);
26
64
  * 4. motion is allowed (never under reduced motion, and not before the
27
65
  * preference is known).
28
66
  *
29
- * Every one of those has a way back that needs nothing particular from the
30
- * visitor: scroll back, return to the tab, press play. It deliberately does
31
- * NOT stop on hover, on keyboard focus, on a wheel event or on a touch.
32
- * Each was tried: a browser re-reports focus when its window is re-activated,
33
- * and a wheel or a touch is what scrolling the PAGE past the thing produces,
34
- * so those rules stopped it for reasons the visitor could not see, with no
35
- * visible way to start it again.
67
+ * There is NO pause button. A visitor holds it by PRESSING on it (a finger or
68
+ * the mouse button down, as a story is held) or by having KEYBOARD focus inside
69
+ * it. Each ends by itself (the press is released, wherever that happens; focus
70
+ * moves on), so it can never be left stopped with no visible way to start it again.
71
+ *
72
+ * HOW it knows, which is what keeps it from sticking. Each hold has more than
73
+ * one way to end, and every one of them is checked:
74
+ * - a PRESS ends on its release, listened for on the WINDOW (a drag that
75
+ * leaves the element still ends it), by ANY of the events a release can
76
+ * arrive as (`RELEASE_EVENTS`: a native drag ends in `dragend`, not
77
+ * `pointerup`), and, as the net under those, by the first pointer movement
78
+ * that reports no button down;
79
+ * - the KEYBOARD holds it only when focus ARRIVED by keyboard (the browser
80
+ * drew a ring for it as it came in), and only until focus leaves the
81
+ * element or the visitor reaches for the pointer. "Is `:focus-visible`
82
+ * inside" alone is not the test: a browser turns the ring on for a control
83
+ * that was CLICKED as soon as any key is pressed, which held the demo for a
84
+ * visitor who clicked a tab and then scrolled with the space bar;
85
+ * - a CHOICE made on the thing itself (`release`, called by whoever handles
86
+ * the tab, the step or the arrow) lets go of everything: it plays from
87
+ * there whatever the browser did or did not report;
88
+ * - the tab is read from the document (`useSyncExternalStore`), never copied
89
+ * into state.
90
+ *
91
+ * What does NOT hold it, each tried and removed:
92
+ * - HOVER: a visitor watching a demo rests the mouse on it, so it stopped the
93
+ * moment it had their attention and read as stuck;
94
+ * - a WHEEL event or a passing touch: that is what scrolling the PAGE past
95
+ * the thing produces (the browser cancels a press the moment it becomes a scroll);
96
+ * - focus the browser shows no ring for: a browser re-reports focus when its
97
+ * window is re-activated, and a mouse click on a control is not the keyboard.
36
98
  */
37
99
  export function useAutoplay(onScreen: boolean): UseAutoplayResult {
38
100
  const reducedState = usePrefersReducedMotionState();
39
- const [paused, setPaused] = useState(false);
40
- const [tabHidden, setTabHidden] = useState(false);
41
-
42
- useEffect(() => {
43
- const sync = () => setTabHidden(document.visibilityState === 'hidden');
44
- sync();
45
- document.addEventListener('visibilitychange', sync);
46
- window.addEventListener('pageshow', sync);
101
+ const tabHidden = useSyncExternalStore(subscribeToTab, tabIsHidden, never);
102
+
103
+ // The element the handlers are on: known from the first event it receives, so no ref has to be threaded to it.
104
+ const host = useRef<HTMLElement | null>(null);
105
+ // True from the moment focus comes in by keyboard until it leaves or the pointer is used.
106
+ const keyboardCameIn = useRef(false);
107
+ const subscribeToKeyboard = useCallback((onChange: () => void) => {
108
+ const usedPointer = () => {
109
+ keyboardCameIn.current = false;
110
+ onChange();
111
+ };
112
+ const stopFocus = listen(document, ['focusin', 'focusout', 'visibilitychange'], onChange);
113
+ const stopPointer = listen(document, ['pointerdown', 'pointermove'], usedPointer);
114
+ const stopWindow = listen(window, ['focus', 'blur'], onChange);
47
115
  return () => {
48
- document.removeEventListener('visibilitychange', sync);
49
- window.removeEventListener('pageshow', sync);
116
+ stopFocus();
117
+ stopPointer();
118
+ stopWindow();
50
119
  };
51
120
  }, []);
121
+ const keyboardIsInside = useCallback(() => {
122
+ if (!keyboardCameIn.current) return false;
123
+ const inside = !!host.current?.isConnected && host.current.contains(document.activeElement);
124
+ if (!inside) keyboardCameIn.current = false;
125
+ return inside;
126
+ }, []);
127
+ const focused = useSyncExternalStore(subscribeToKeyboard, keyboardIsInside, never);
128
+
129
+ const [pressed, setPressed] = useState(false);
130
+ const holdProps = useMemo<AutoplayHoldProps>(
131
+ () => ({
132
+ onPointerDown: event => {
133
+ host.current = event.currentTarget;
134
+ setPressed(true);
135
+ // One release ends it, wherever and however it happens; the same signal removes every listener.
136
+ const released = new AbortController();
137
+ const release = () => {
138
+ setPressed(false);
139
+ released.abort();
140
+ };
141
+ for (const type of RELEASE_EVENTS) window.addEventListener(type, release, { signal: released.signal });
142
+ // The net: a pointer that moves with no button down has been released, whatever was or was not reported.
143
+ window.addEventListener(
144
+ 'pointermove',
145
+ (move: globalThis.PointerEvent) => {
146
+ if (move.buttons === 0) release();
147
+ },
148
+ { signal: released.signal },
149
+ );
150
+ },
151
+ onFocus: event => {
152
+ host.current = event.currentTarget;
153
+ // Only focus that arrives by keyboard holds it (the browser draws its ring as it comes in).
154
+ if (event.target.matches(':focus-visible')) keyboardCameIn.current = true;
155
+ },
156
+ }),
157
+ [],
158
+ );
159
+
160
+ // Bumped by `release`, so the keyboard store re-reads after it lets go.
161
+ const [, setReleases] = useState(0);
162
+ const release = useCallback(() => {
163
+ keyboardCameIn.current = false;
164
+ setPressed(false);
165
+ setReleases(count => count + 1);
166
+ }, []);
52
167
 
168
+ const held = pressed || focused;
53
169
  return {
54
- playing: onScreen && !paused && !tabHidden && reducedState === false,
55
- paused,
56
- setPaused,
170
+ playing: onScreen && !held && !tabHidden && reducedState === false,
171
+ held,
172
+ holdProps,
173
+ release,
57
174
  reducedMotion: reducedState === true,
58
175
  };
59
176
  }
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { useEffect, useState } from 'react';
4
4
 
5
5
  export interface UseInViewOptions {
6
6
  /**
@@ -43,49 +43,42 @@ const STEPS = Array.from({ length: 21 }, (_, i) => i / 20);
43
43
  * `useNearViewport` is the fire-once sibling (mount a heavy thing when it gets
44
44
  * close, never unmount). This one is for things that should run only WHILE
45
45
  * visible, like an auto-advancing carousel or a looping demo.
46
+ *
47
+ * The observer LIVES IN AN EFFECT keyed on the element, never in the ref
48
+ * callback. An observer made in the callback and torn down by a separate
49
+ * effect is torn down every time React re-runs effects without re-attaching
50
+ * refs (a Fast Refresh, an Activity that hides and shows): nothing observed any
51
+ * more, and `inView` froze at its last value, so a demo that happened to be off
52
+ * screen at that moment never played again. An effect that owns it makes it
53
+ * again whenever it is torn down.
46
54
  */
47
55
  export function useInView<T extends Element = HTMLElement>({
48
56
  threshold = 0,
49
57
  rootMargin = '0px',
50
58
  initial = false,
51
59
  }: UseInViewOptions = {}): UseInViewResult<T> {
52
- const [inView, setInView] = useState(initial);
53
- const observerRef = useRef<IntersectionObserver | null>(null);
54
-
55
- const disconnect = useCallback(() => {
56
- observerRef.current?.disconnect();
57
- observerRef.current = null;
58
- }, []);
59
-
60
- const ref = useCallback(
61
- (node: T | null) => {
62
- disconnect();
63
- if (!node) {
64
- setInView(false);
65
- return;
66
- }
67
- if (typeof IntersectionObserver === 'undefined') {
68
- setInView(initial);
69
- return;
70
- }
71
- const observer = new IntersectionObserver(
72
- entries => {
73
- const entry = entries[entries.length - 1];
74
- if (!entry) return;
75
- const ofElement = entry.intersectionRatio >= threshold;
76
- const viewportHeight = entry.rootBounds?.height ?? 0;
77
- const ofViewport = viewportHeight > 0 && (entry.intersectionRect?.height ?? 0) >= viewportHeight * threshold;
78
- setInView(entry.isIntersecting && (ofElement || ofViewport));
79
- },
80
- { threshold: STEPS, rootMargin },
81
- );
82
- observer.observe(node);
83
- observerRef.current = observer;
84
- },
85
- [disconnect, threshold, rootMargin, initial],
86
- );
60
+ // The element, as state: the effect below is keyed on it. `setNode` is the ref (stable).
61
+ const [node, setNode] = useState<T | null>(null);
62
+ const [seen, setSeen] = useState<{ node: T; inView: boolean } | null>(null);
87
63
 
88
- useEffect(() => disconnect, [disconnect]);
64
+ useEffect(() => {
65
+ if (!node || typeof IntersectionObserver === 'undefined') return undefined;
66
+ const observer = new IntersectionObserver(
67
+ entries => {
68
+ const entry = entries[entries.length - 1];
69
+ if (!entry) return;
70
+ const ofElement = entry.intersectionRatio >= threshold;
71
+ const viewportHeight = entry.rootBounds?.height ?? 0;
72
+ const ofViewport = viewportHeight > 0 && (entry.intersectionRect?.height ?? 0) >= viewportHeight * threshold;
73
+ setSeen({ node, inView: entry.isIntersecting && (ofElement || ofViewport) });
74
+ },
75
+ { threshold: STEPS, rootMargin },
76
+ );
77
+ observer.observe(node);
78
+ return () => observer.disconnect();
79
+ }, [node, threshold, rootMargin]);
89
80
 
90
- return { ref, inView };
81
+ // No element: not in view. An element the observer has not reported on yet (or cannot be observed): `initial`.
82
+ const inView = !node ? false : seen?.node === node ? seen.inView : initial;
83
+ return { ref: setNode, inView };
91
84
  }
@@ -8,6 +8,12 @@ export interface AgentIdentityIcon {
8
8
  name?: string | null;
9
9
  url?: string | null;
10
10
  props?: Record<string, unknown> | null;
11
+ /**
12
+ * The identity's accent, travelling with its icon: a hex colour (`#5efaf0`),
13
+ * as the server stores it. What is drawn in the agent's colour (its tab, the
14
+ * step it is on) reads it through `useAgentAccents`; unset, that is the platform's accent.
15
+ */
16
+ accent?: string | null;
11
17
  }
12
18
 
13
19
  /** Each agent's identity icon, keyed by the agent's slug. */
@@ -1,11 +1,11 @@
1
1
  'use client';
2
2
 
3
3
  import { useCallback, useEffect, useRef, useState } from 'react';
4
- import { useAutoplay } from './ui/use-autoplay';
4
+ import { type AutoplayHoldProps, useAutoplay } from './ui/use-autoplay';
5
5
 
6
6
  /**
7
7
  * How soon the first step follows the moment the demo becomes playable (it
8
- * scrolled into view, the tab came back, play was pressed). A demo that sits
8
+ * scrolled into view, the tab came back, the visitor let go of it). A demo that sits
9
9
  * still for a whole step after it appears reads as broken; one that moves
10
10
  * within a third of a second reads as responding to you.
11
11
  */
@@ -32,6 +32,14 @@ export interface UseScenarioPlayerOptions {
32
32
  * never gets to play (it is never scrolled into view) still reads complete.
33
33
  */
34
34
  startAtEnd?: boolean;
35
+ /**
36
+ * Whether step 0 of every scenario is an EMPTY stage, to be left at once
37
+ * (default true: a stage that sits empty for a whole step after a tap reads
38
+ * as not responding). False when step 0 already shows something to read:
39
+ * every step then lasts a whole `stepMs`, however the scenario was started
40
+ * (a page load, the rotation and a chosen tab all pace it the same).
41
+ */
42
+ emptyFirstStep?: boolean;
35
43
  }
36
44
 
37
45
  export interface UseScenarioPlayerResult {
@@ -39,9 +47,16 @@ export interface UseScenarioPlayerResult {
39
47
  scenario: number;
40
48
  /** Current step, 0..lastStep. */
41
49
  step: number;
42
- /** The visitor stopped it with the pause control. */
43
- paused: boolean;
44
- setPaused: (paused: boolean) => void;
50
+ /**
51
+ * False until it has been playable once (it has not been on screen yet).
52
+ * A progress line reads 0 until then, so it starts from empty when the
53
+ * visitor first reaches it, never part-way through a step nobody saw begin.
54
+ */
55
+ started: boolean;
56
+ /** The visitor is holding it still (pressing on it, or the keyboard is inside it). */
57
+ held: boolean;
58
+ /** Spread on the stage's outer element: how a visitor stops it (`useAutoplay`). */
59
+ holdProps: AutoplayHoldProps;
45
60
  /** Jump to a step in the current scenario (a visitor's tap, a stepper button). */
46
61
  setStep: (step: number) => void;
47
62
  /** Switch scenario and restart it from step 0. */
@@ -60,14 +75,15 @@ export interface UseScenarioPlayerResult {
60
75
  * line and the stage itself agree by construction.
61
76
  *
62
77
  * WHEN IT MOVES is `useAutoplay`'s rule: on screen (`enabled`), tab visible,
63
- * not paused, motion allowed. Under reduced motion it parks on the last step
78
+ * not held by the visitor, motion allowed. Under reduced motion it parks on the last step
64
79
  * of the current scenario, and every scenario can still be read by choosing it.
65
80
  *
66
- * The moment it becomes playable its first step follows within `START_MS`,
67
- * not a whole step later; from there every step takes `stepMs`.
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`.
68
84
  *
69
- * A visitor's own choice (`go`, `setStep`) moves the clock there and un-pauses
70
- * it; the rotation carries on from that point.
85
+ * A visitor's own choice (`go`, `setStep`) moves the clock there and lets go of
86
+ * any hold; the rotation carries on from that point.
71
87
  */
72
88
  export function useScenarioPlayer({
73
89
  scenarioCount,
@@ -77,9 +93,10 @@ export function useScenarioPlayer({
77
93
  enabled = true,
78
94
  initialScenario = 0,
79
95
  startAtEnd = false,
96
+ emptyFirstStep = true,
80
97
  }: UseScenarioPlayerOptions): UseScenarioPlayerResult {
81
98
  const auto = useAutoplay(enabled);
82
- const { reducedMotion, paused, setPaused } = auto;
99
+ const { reducedMotion, held, holdProps, release } = auto;
83
100
 
84
101
  const [scenario, setScenario] = useState(initialScenario);
85
102
  const [rawStep, setRawStep] = useState(startAtEnd ? Number.MAX_SAFE_INTEGER : 0);
@@ -89,6 +106,9 @@ export function useScenarioPlayer({
89
106
  const step = reducedMotion ? lastStep : Math.min(rawStep, lastStep);
90
107
 
91
108
  const playing = auto.playing && scenarioCount > 0;
109
+ // Latched the first time it may play (state derived during render).
110
+ const [started, setStarted] = useState(false);
111
+ if (playing && !started) setStarted(true);
92
112
 
93
113
  // True from the moment it becomes playable until its first tick has been scheduled.
94
114
  const starting = useRef(false);
@@ -98,9 +118,12 @@ export function useScenarioPlayer({
98
118
  wasPlaying.current = playing;
99
119
  if (!playing) return undefined;
100
120
  const atEnd = rawStep >= lastStep;
101
- // The first step after it becomes playable comes quickly. A finished
102
- // scenario still holds its full time: it is there to be read.
103
- const quickStart = starting.current && !atEnd;
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;
104
127
  starting.current = false;
105
128
  const timer = setTimeout(
106
129
  () => {
@@ -114,24 +137,25 @@ export function useScenarioPlayer({
114
137
  atEnd ? holdMs : quickStart ? Math.min(START_MS, stepMs) : stepMs,
115
138
  );
116
139
  return () => clearTimeout(timer);
117
- }, [playing, rawStep, lastStep, stepMs, holdMs, scenario, scenarioCount]);
140
+ }, [playing, rawStep, lastStep, stepMs, holdMs, scenario, scenarioCount, emptyFirstStep]);
118
141
 
142
+ // A visitor's choice is an instruction to play from there: it lets go of any hold.
119
143
  const setStep = useCallback(
120
144
  (next: number) => {
121
145
  setRawStep(Math.max(0, Math.min(lastStep, next)));
122
- setPaused(false);
146
+ release();
123
147
  },
124
- [lastStep, setPaused],
148
+ [lastStep, release],
125
149
  );
126
150
 
127
151
  const go = useCallback(
128
152
  (next: number) => {
129
153
  setScenario(next);
130
154
  setRawStep(0);
131
- setPaused(false);
155
+ release();
132
156
  },
133
- [setPaused],
157
+ [release],
134
158
  );
135
159
 
136
- return { scenario, step, paused, setPaused, setStep, go, reducedMotion };
160
+ return { scenario, step, started: started || reducedMotion, held, holdProps, setStep, go, reducedMotion };
137
161
  }
@@ -1,99 +0,0 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});"use client";
2
-
3
-
4
- var _chunkMANNIJ42cjs = require('./chunk-MANNIJ42.cjs');
5
-
6
- // src/components/tool-icon.tsx
7
- var _react = require('react');
8
- var TOOL_ICONS = /* @__PURE__ */ new Map();
9
- function toolIcon(toolType) {
10
- const known = TOOL_ICONS.get(toolType);
11
- if (known !== void 0) return known;
12
- const words = toolType.toLowerCase().split("_").filter(Boolean);
13
- let icon = null;
14
- for (let count = words.length; count >= 1 && !icon; count -= 1) {
15
- icon = _chunkMANNIJ42cjs.findIcon.call(void 0, `${words.slice(0, count).join("-")}-logo-grey`);
16
- }
17
- TOOL_ICONS.set(toolType, icon);
18
- return icon;
19
- }
20
- var ToolIcon = ({ toolType, size = 16, className }) => {
21
- const icon = toolIcon(toolType);
22
- return icon ? _react.createElement.call(void 0, icon, { size, className }) : null;
23
- };
24
- ToolIcon.displayName = "ToolIcon";
25
-
26
- // src/utils/common.ts
27
- var _clsx = require('clsx');
28
- var _tailwindmerge = require('tailwind-merge');
29
- function errorMessage(error, fallback = "Something went wrong") {
30
- if (error instanceof Error && error.message) return error.message;
31
- if (typeof error === "string" && error) return error;
32
- if (typeof error === "object" && error !== null) {
33
- const message = error.message;
34
- if (typeof message === "string" && message) return message;
35
- }
36
- return fallback;
37
- }
38
- function apiErrorMessage(payload, fallback) {
39
- if (typeof payload === "object" && payload !== null && "error" in payload) {
40
- const { error } = payload;
41
- if (typeof error === "string" && error) return error;
42
- }
43
- return errorMessage(payload, fallback);
44
- }
45
- function delay(ms) {
46
- return new Promise((resolve) => setTimeout(resolve, ms));
47
- }
48
- function generateRandomString(length = 8) {
49
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
50
- let result = "";
51
- for (let i = 0; i < length; i++) {
52
- result += chars.charAt(Math.floor(Math.random() * chars.length));
53
- }
54
- return result;
55
- }
56
- function truncateString(str, maxLength, suffix = "...") {
57
- if (str.length <= maxLength) return str;
58
- return str.substring(0, maxLength - suffix.length) + suffix;
59
- }
60
- function serializeJsonLd(schema) {
61
- return JSON.stringify(schema).replace(/</g, "\\u003c");
62
- }
63
- function deepClone(obj) {
64
- return JSON.parse(JSON.stringify(obj));
65
- }
66
- function getSlackCommunityJoinUrl() {
67
- const url = process.env.NEXT_PUBLIC_SLACK_COMMUNITY_JOIN_URL;
68
- if (!url) {
69
- console.warn("NEXT_PUBLIC_SLACK_COMMUNITY_JOIN_URL is not defined in environment variables");
70
- return "https://join.slack.com/t/openmsp/shared_invite/zt-36bl7mx0h-3~U2nFH6nqHqoTPXMaHEHA";
71
- }
72
- return url;
73
- }
74
- function clamp(value, min, max) {
75
- return Math.min(Math.max(value, min), max);
76
- }
77
- function pick(source, keys) {
78
- const out = {};
79
- for (const key of keys) {
80
- if (key in source) out[key] = source[key];
81
- }
82
- return out;
83
- }
84
- var NO_CLIENT_CACHE = { staleTime: 0 };
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
-
94
-
95
-
96
-
97
-
98
-
99
- exports.ToolIcon = ToolIcon; exports.errorMessage = errorMessage; exports.apiErrorMessage = apiErrorMessage; exports.delay = delay; exports.generateRandomString = generateRandomString; exports.truncateString = truncateString; exports.serializeJsonLd = serializeJsonLd; exports.deepClone = deepClone; exports.getSlackCommunityJoinUrl = getSlackCommunityJoinUrl; exports.clamp = clamp; exports.pick = pick; exports.NO_CLIENT_CACHE = NO_CLIENT_CACHE;
@@ -1,54 +0,0 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }"use client";
2
-
3
-
4
-
5
- var _chunkLCTDHLDLcjs = require('./chunk-LCTDHLDL.cjs');
6
-
7
- // src/components/agent-identity.tsx
8
- var _react = require('react');
9
- var _jsxruntime = require('react/jsx-runtime');
10
- var AGENT_GLYPH_COLOR = "var(--color-text-primary)";
11
- var AGENT_PICTURE_CLASS = "rounded-full object-cover";
12
- var AgentIdentityContext = _react.createContext.call(void 0, null);
13
- function Provide({ icons, children }) {
14
- const bySlug = _react.useMemo.call(void 0, () => {
15
- const map = /* @__PURE__ */ new Map();
16
- for (const [slug, icon] of Object.entries(_nullishCoalesce(icons, () => ( {})))) if (icon) map.set(slug, icon);
17
- return map;
18
- }, [icons]);
19
- return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, AgentIdentityContext.Provider, { value: bySlug, children });
20
- }
21
- function ProvideFromServer({
22
- endpoint,
23
- enabled,
24
- children
25
- }) {
26
- return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, Provide, { icons: _chunkLCTDHLDLcjs.useAgentIdentities.call(void 0, endpoint, enabled), children });
27
- }
28
- function AgentIdentityProvider({
29
- icons,
30
- endpoint = _chunkLCTDHLDLcjs.AGENT_IDENTITIES_API_PATH,
31
- enabled = true,
32
- children
33
- }) {
34
- if (icons) return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, Provide, { icons, children });
35
- return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, ProvideFromServer, { endpoint, enabled, children });
36
- }
37
- function useAgentIdentityIcon(slug) {
38
- const identities = _react.useContext.call(void 0, AgentIdentityContext);
39
- const unprovided = !identities && Boolean(slug);
40
- _react.useEffect.call(void 0, () => {
41
- if (!unprovided || process.env.NODE_ENV === "production") return;
42
- console.error(
43
- `[AgentIdentity] the mark of "${slug}" was asked for with no <AgentIdentityProvider> above it: agents are drawn from their server identities only, so mount the provider once at the root.`
44
- );
45
- }, [unprovided, slug]);
46
- return _optionalChain([identities, 'optionalAccess', _ => _.get, 'call', _2 => _2(_nullishCoalesce(slug, () => ( "")))]);
47
- }
48
-
49
-
50
-
51
-
52
-
53
-
54
- exports.AGENT_GLYPH_COLOR = AGENT_GLYPH_COLOR; exports.AGENT_PICTURE_CLASS = AGENT_PICTURE_CLASS; exports.AgentIdentityProvider = AgentIdentityProvider; exports.useAgentIdentityIcon = useAgentIdentityIcon;
@@ -1,43 +0,0 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }"use client";
2
-
3
-
4
-
5
- var _chunkVOX4E6AXcjs = require('./chunk-VOX4E6AX.cjs');
6
-
7
- // src/hooks/use-agent-identities.ts
8
- var _reactquery = require('@tanstack/react-query');
9
-
10
- // src/utils/embed-content-fetch.ts
11
- var contentFetch = (input, init) => {
12
- if (!_chunkVOX4E6AXcjs.hasEmbedAuthAdapter.call(void 0, )) return fetch(input, init);
13
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
14
- return _chunkVOX4E6AXcjs.embedAuthedFetch.call(void 0, url, init);
15
- };
16
-
17
- // src/hooks/use-agent-identities.ts
18
- var AGENT_IDENTITIES_API_PATH = "/api/ai-agents";
19
- async function fetchAgentIdentities(endpoint, signal) {
20
- const response = await contentFetch(endpoint, { signal });
21
- if (!response.ok) throw new Error(`agent identities read failed (${response.status})`);
22
- const body = await response.json();
23
- const icons = {};
24
- for (const agent of _nullishCoalesce(_optionalChain([body, 'optionalAccess', _ => _.agents]), () => ( []))) if (agent.slug && agent.icon) icons[agent.slug] = agent.icon;
25
- return icons;
26
- }
27
- function useAgentIdentities(endpoint = AGENT_IDENTITIES_API_PATH, enabled = true) {
28
- const { data } = _reactquery.useQuery.call(void 0, {
29
- queryKey: ["agent-identities", endpoint],
30
- queryFn: ({ signal }) => fetchAgentIdentities(endpoint, signal),
31
- staleTime: Infinity,
32
- gcTime: Infinity,
33
- retry: false,
34
- enabled
35
- });
36
- return data;
37
- }
38
-
39
-
40
-
41
-
42
-
43
- exports.contentFetch = contentFetch; exports.AGENT_IDENTITIES_API_PATH = AGENT_IDENTITIES_API_PATH; exports.useAgentIdentities = useAgentIdentities;