@flamingo-stack/openframe-frontend-core 0.0.737 → 0.0.739
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/build/export-map.json +1 -1
- package/dist/{chunk-AZL332TK.cjs → chunk-2QMBXGMQ.cjs} +15 -2
- package/dist/{chunk-HZJRERJW.cjs → chunk-3RFQPQ7Z.cjs} +3 -3
- package/dist/{chunk-YADG5Q7C.cjs → chunk-63N4FN5V.cjs} +3 -3
- package/dist/{chunk-FTTLBMHU.cjs → chunk-7ZFCR7UY.cjs} +2 -2
- package/dist/{chunk-MJGX357L.cjs → chunk-B27QEHYR.cjs} +2 -2
- package/dist/{chunk-VRNNIV5Y.cjs → chunk-B3XBJCOD.cjs} +9 -9
- package/dist/{chunk-VL4JKXU2.cjs → chunk-BV5WZVOP.cjs} +243 -243
- package/dist/{chunk-KVGBDWYV.cjs → chunk-CUDTMCXK.cjs} +36 -36
- package/dist/{chunk-L4ZNR5V7.cjs → chunk-DSVNFRY5.cjs} +87 -87
- package/dist/{chunk-LW3QSP47.cjs → chunk-DTLHY2ZF.cjs} +27 -27
- package/dist/{chunk-4ISP3XP4.cjs → chunk-EGN4I5Q7.cjs} +76 -76
- package/dist/{chunk-XPP7VCNY.cjs → chunk-ENCAIAMY.cjs} +10 -10
- package/dist/{chunk-FA2LATDM.cjs → chunk-FL6UO7IR.cjs} +4 -4
- package/dist/{chunk-ZRYKDN2J.cjs → chunk-GABDE3FM.cjs} +24 -24
- package/dist/{chunk-MYBTNOS6.cjs → chunk-GSWQZX23.cjs} +2 -2
- package/dist/{chunk-L535OD7V.cjs → chunk-HF7Z2E4W.cjs} +2 -2
- package/dist/chunk-HOTM4UM6.cjs +84 -0
- package/dist/{chunk-EAY6ZUES.cjs → chunk-I5MSCD57.cjs} +2 -2
- package/dist/{chunk-FCGLSWKR.cjs → chunk-IFS4AH77.cjs} +19 -1
- package/dist/{chunk-MGW5DO6P.cjs → chunk-KLI4OSRA.cjs} +7 -7
- package/dist/{chunk-O4MG7FXW.cjs → chunk-NX6JQT25.cjs} +6 -6
- package/dist/{chunk-ZJNYWNCB.cjs → chunk-O67V3HJR.cjs} +29 -29
- package/dist/{chunk-DUXLZPMJ.cjs → chunk-PQ2M7P2U.cjs} +7 -7
- package/dist/{chunk-SGVQO5J6.cjs → chunk-Q7GG3WC5.cjs} +26 -26
- package/dist/{chunk-TJQYX3QC.cjs → chunk-RGFT3P3L.cjs} +97 -97
- package/dist/{chunk-4FJZ7RJD.cjs → chunk-S2CA5WE4.cjs} +161 -93
- package/dist/{chunk-E2JCZU36.cjs → chunk-SB4VWA5V.cjs} +1044 -857
- package/dist/{chunk-XLSVQJBA.cjs → chunk-SFY4UYPY.cjs} +44 -44
- package/dist/{chunk-4RMLAHU2.cjs → chunk-TF7J6VXG.cjs} +20 -20
- package/dist/{chunk-CM35DLKS.cjs → chunk-UI3HROSI.cjs} +35 -35
- package/dist/{chunk-DA4VS6TS.cjs → chunk-UXFJXIUT.cjs} +4 -4
- package/dist/{chunk-N6YOOGCD.cjs → chunk-UYP3G6PB.cjs} +12 -12
- package/dist/{chunk-URIJ6YUG.cjs → chunk-V2ZFH42N.cjs} +22 -17
- package/dist/{chunk-77SD7RXY.cjs → chunk-WBANGORJ.cjs} +7 -7
- package/dist/{chunk-VYODTHMR.cjs → chunk-WMKNZI4G.cjs} +54 -54
- package/dist/{chunk-EX2GYIGW.cjs → chunk-YRE3O65E.cjs} +12 -12
- package/dist/{chunk-EJOXXJJB.cjs → chunk-YXKB5WRE.cjs} +95 -104
- package/dist/{cloud-tenants-IWQ5FFLY.cjs → cloud-tenants-QSFSO3PA.cjs} +20 -20
- package/dist/components/agent-identity.d.ts +8 -1
- package/dist/components/agent-identity.d.ts.map +1 -1
- package/dist/components/agent-identity.js +6 -1
- package/dist/components/calendar.cjs +4 -4
- package/dist/components/case-studies/index.cjs +32 -32
- package/dist/components/chat/chat-turn.d.ts +23 -0
- package/dist/components/chat/chat-turn.d.ts.map +1 -0
- package/dist/components/chat/chat-turn.js +30 -0
- package/dist/components/chat/index.cjs +18 -16
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +1 -0
- package/dist/components/contact/index.cjs +21 -21
- package/dist/components/docs/index.cjs +24 -24
- package/dist/components/embeds/index.cjs +21 -21
- package/dist/components/faq/faq-section.d.ts.map +1 -1
- package/dist/components/faq/faq-section.js +9 -4
- package/dist/components/faq/index.cjs +22 -22
- package/dist/components/features/auth/auth-benefits-panel.d.ts +3 -5
- package/dist/components/features/auth/auth-benefits-panel.d.ts.map +1 -1
- package/dist/components/features/auth/auth-benefits-panel.js +3 -19
- package/dist/components/features/conversation-card.d.ts +12 -4
- package/dist/components/features/conversation-card.d.ts.map +1 -1
- package/dist/components/features/conversation-card.js +30 -38
- package/dist/components/features/index.cjs +20 -20
- package/dist/components/features/walkthrough-video.d.ts +3 -0
- package/dist/components/features/walkthrough-video.d.ts.map +1 -1
- package/dist/components/features/walkthrough-video.js +36 -13
- package/dist/components/help-center-pages/index.cjs +127 -127
- package/dist/components/index.cjs +114 -98
- package/dist/components/meeting-scheduler/index.cjs +67 -67
- package/dist/components/navigation/index.cjs +23 -23
- package/dist/components/onboarding-guides/index.cjs +23 -23
- package/dist/components/platform/ToolBadge.d.ts.map +1 -1
- package/dist/components/platform/ToolBadge.js +4 -3
- package/dist/components/product-demo/index.cjs +8 -8
- package/dist/components/related-content/index.cjs +12 -12
- package/dist/components/tickets/index.cjs +26 -26
- package/dist/components/ui/app-window-frame.d.ts +7 -1
- package/dist/components/ui/app-window-frame.d.ts.map +1 -1
- package/dist/components/ui/app-window-frame.js +10 -2
- package/dist/components/ui/benefit-list.d.ts +42 -0
- package/dist/components/ui/benefit-list.d.ts.map +1 -0
- package/dist/components/ui/benefit-list.js +62 -0
- package/dist/components/ui/button/button.d.ts +1 -0
- package/dist/components/ui/button/button.d.ts.map +1 -1
- package/dist/components/ui/button/button.js +15 -2
- package/dist/components/ui/dropdown-button.d.ts +20 -2
- package/dist/components/ui/dropdown-button.d.ts.map +1 -1
- package/dist/components/ui/dropdown-button.js +57 -1
- package/dist/components/ui/file-manager/index.cjs +11 -11
- package/dist/components/ui/index.cjs +34 -20
- package/dist/components/ui/index.d.ts +3 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +3 -0
- package/dist/components/ui/leader-row.d.ts +17 -0
- package/dist/components/ui/leader-row.d.ts.map +1 -0
- package/dist/components/ui/leader-row.js +14 -0
- package/dist/components/ui/strip-card.d.ts +23 -0
- package/dist/components/ui/strip-card.d.ts.map +1 -0
- package/dist/components/ui/strip-card.js +56 -0
- package/dist/{devices-OUHGWA6Y.cjs → devices-C7XLWJFC.cjs} +21 -21
- package/dist/embed-shims/index.cjs +2 -2
- package/dist/embed-shims/next-image.cjs +2 -2
- package/dist/embed-shims/next-image.d.ts.map +1 -1
- package/dist/embed-shims/next-image.js +6 -2
- package/dist/{embeddable-chat-U3C4WWB7.cjs → embeddable-chat-6JKKFQEB.cjs} +89 -89
- package/dist/hooks/index.cjs +3 -3
- package/dist/index.cjs +46 -25
- package/dist/{knowledge-3ZASPWMF.cjs → knowledge-3QKWTOE7.cjs} +21 -21
- package/dist/{logs-E6Y54P6K.cjs → logs-JXD75NR4.cjs} +29 -29
- package/dist/{policies-LAV75B5B.cjs → policies-ZPN53PFQ.cjs} +24 -24
- package/dist/{remote-session-Z4YZFG5E.cjs → remote-session-DNFZ5OLY.cjs} +10 -10
- package/dist/{software-update-2CAL3AMJ.cjs → software-update-CRSIBS3K.cjs} +24 -24
- package/dist/{tickets-board-JOSYLOS7.cjs → tickets-board-CKZRB6YV.cjs} +12 -12
- package/dist/utils/demo-clock.d.ts +22 -0
- package/dist/utils/demo-clock.d.ts.map +1 -0
- package/dist/utils/demo-clock.js +40 -0
- package/dist/utils/identity-tint.d.ts +13 -0
- package/dist/utils/identity-tint.d.ts.map +1 -0
- package/dist/utils/identity-tint.js +14 -0
- package/dist/utils/index.cjs +54 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -0
- package/dist/{video-file-player-6NWGXZ3W.cjs → video-file-player-ZH2ILAPO.cjs} +12 -12
- package/package.json +1 -1
- package/src/components/agent-identity.tsx +12 -1
- package/src/components/chat/chat-turn.tsx +52 -0
- package/src/components/chat/index.ts +1 -0
- package/src/components/faq/faq-section.tsx +4 -1
- package/src/components/features/auth/auth-benefits-panel.tsx +5 -32
- package/src/components/features/conversation-card.tsx +48 -46
- package/src/components/features/walkthrough-video.tsx +59 -18
- package/src/components/platform/ToolBadge.tsx +4 -3
- package/src/components/ui/app-window-frame.tsx +18 -2
- package/src/components/ui/benefit-list.tsx +97 -0
- package/src/components/ui/button/button.tsx +14 -1
- package/src/components/ui/dropdown-button.tsx +73 -1
- package/src/components/ui/index.ts +3 -0
- package/src/components/ui/leader-row.tsx +28 -0
- package/src/components/ui/strip-card.tsx +68 -0
- package/src/embed-shims/next-image.tsx +15 -4
- package/src/utils/demo-clock.ts +62 -0
- package/src/utils/identity-tint.ts +23 -0
- package/src/utils/index.ts +2 -0
- package/dist/chunk-UR4K2NSL.cjs +0 -69
|
@@ -106,6 +106,13 @@ export interface WalkthroughVideoData {
|
|
|
106
106
|
position?: 'left' | 'right' | null;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* What ends Largest Contentful Paint: the browser reports no new candidate after
|
|
111
|
+
* the visitor's first scroll, tap or key press. The floating card renders only
|
|
112
|
+
* after one of these.
|
|
113
|
+
*/
|
|
114
|
+
const FIRST_INTERACTION_EVENTS = ['scroll', 'pointerdown', 'keydown'] as const;
|
|
115
|
+
|
|
109
116
|
/** Everything the two placements share — the widget's actual contract. */
|
|
110
117
|
interface WalkthroughVideoBaseProps {
|
|
111
118
|
video: WalkthroughVideoData | null | undefined;
|
|
@@ -148,6 +155,9 @@ interface WalkthroughVideoBaseProps {
|
|
|
148
155
|
* not in the base.
|
|
149
156
|
*/
|
|
150
157
|
export interface FloatingWalkthroughVideoProps extends WalkthroughVideoBaseProps {
|
|
158
|
+
/** The card appears no sooner than this after load, and only once the visitor
|
|
159
|
+
* has scrolled, tapped or pressed a key (so it can never be the page's
|
|
160
|
+
* Largest Contentful Paint). Default 3000. */
|
|
151
161
|
appearDelayMs?: number;
|
|
152
162
|
/** Cookie-based dismissal (id-match, mirrors the announcement bar). `false`
|
|
153
163
|
* disables the X entirely. `storageKey` is the per-platform cookie name. */
|
|
@@ -208,9 +218,10 @@ function WalkthroughVideo({
|
|
|
208
218
|
autoPlayCard = false,
|
|
209
219
|
label = 'Play Demo Video',
|
|
210
220
|
placement,
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
221
|
+
// The FLOATING card never appears sooner than this (and never before the
|
|
222
|
+
// visitor's first interaction: see the mount gate). The inline preset passes
|
|
223
|
+
// 0: an in-page block is part of a page the visitor is already looking at,
|
|
224
|
+
// so staggering it in would only jump the layout.
|
|
214
225
|
appearDelayMs = 3000,
|
|
215
226
|
dismissal = {},
|
|
216
227
|
hideNearSelector = 'footer',
|
|
@@ -224,7 +235,16 @@ function WalkthroughVideo({
|
|
|
224
235
|
// (embedder) → a stable presence marker so dismissal still works.
|
|
225
236
|
const dismissalId = video?.id != null ? String(video.id) : 'dismissed';
|
|
226
237
|
|
|
227
|
-
// --- mount gate (
|
|
238
|
+
// --- mount gate (cookie read happens post-delay, no hydration mismatch) ---
|
|
239
|
+
// The FLOATING card waits for two things: the appear delay AND the visitor's
|
|
240
|
+
// first scroll, tap or key press. The delay alone never kept it out of Largest
|
|
241
|
+
// Contentful Paint: the metric takes candidates until the first interaction,
|
|
242
|
+
// so a late card that was the largest thing in a short window became the
|
|
243
|
+
// page's LCP (blog post, 1440x215: the heading at 3.2 s, then the card's
|
|
244
|
+
// poster at 7.1 s). No attribute opts an element out, and a timer does not
|
|
245
|
+
// either; rendering only after the first interaction is the documented way,
|
|
246
|
+
// because the metric is final by then. The inline block is part of the page's
|
|
247
|
+
// own content and waits for neither.
|
|
228
248
|
const [mounted, setMounted] = useState(false);
|
|
229
249
|
const [dismissed, setDismissed] = useState(false);
|
|
230
250
|
const hasVideo = video != null;
|
|
@@ -235,31 +255,52 @@ function WalkthroughVideo({
|
|
|
235
255
|
// dismissal id was 'dismissed' on the first pass and a dismissed card
|
|
236
256
|
// flashed for the length of a second appear-delay before hiding.
|
|
237
257
|
if (!hasVideo) return undefined; // wait for data — see the id-based dep below
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
258
|
+
let delayElapsed = false;
|
|
259
|
+
let interacted = inline;
|
|
260
|
+
const reveal = () => {
|
|
261
|
+
if (dismissEnabled && isWalkthroughDismissed(storageKey, dismissalId)) {
|
|
262
|
+
setDismissed(true);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
// Cleared on the way through: a client-only embedder can swap to a NEW
|
|
266
|
+
// video id in-session (the cookie is id-matched, so the new one isn't
|
|
267
|
+
// dismissed), and a sticky `dismissed` kept the card hidden until a
|
|
268
|
+
// full reload — contradicting the id-match behaviour documented above.
|
|
269
|
+
setDismissed(false);
|
|
270
|
+
setMounted(true);
|
|
271
|
+
};
|
|
272
|
+
const revealWhenReady = () => {
|
|
273
|
+
if (!delayElapsed || !interacted) return;
|
|
251
274
|
const ric = (window as unknown as { requestIdleCallback?: (cb: () => void) => number }).requestIdleCallback;
|
|
252
275
|
if (typeof ric === 'function') idleHandle = ric(reveal);
|
|
253
276
|
else reveal();
|
|
277
|
+
};
|
|
278
|
+
const stopListening = () => {
|
|
279
|
+
for (const type of FIRST_INTERACTION_EVENTS) window.removeEventListener(type, onInteraction, true);
|
|
280
|
+
};
|
|
281
|
+
function onInteraction() {
|
|
282
|
+
interacted = true;
|
|
283
|
+
stopListening();
|
|
284
|
+
revealWhenReady();
|
|
285
|
+
}
|
|
286
|
+
if (!interacted) {
|
|
287
|
+
for (const type of FIRST_INTERACTION_EVENTS) {
|
|
288
|
+
window.addEventListener(type, onInteraction, { capture: true, passive: true });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const timer = setTimeout(() => {
|
|
292
|
+
delayElapsed = true;
|
|
293
|
+
revealWhenReady();
|
|
254
294
|
}, appearDelayMs);
|
|
255
295
|
return () => {
|
|
256
296
|
clearTimeout(timer);
|
|
297
|
+
stopListening();
|
|
257
298
|
const cic = (window as unknown as { cancelIdleCallback?: (h: number) => void }).cancelIdleCallback;
|
|
258
299
|
if (idleHandle !== null && typeof cic === 'function') cic(idleHandle);
|
|
259
300
|
};
|
|
260
301
|
// `videoId`, not the object: a new identity (embedder refetch, re-seeded
|
|
261
302
|
// RSC payload) would otherwise restart the appear delay.
|
|
262
|
-
}, [appearDelayMs, dismissEnabled, storageKey, dismissalId, hasVideo, videoId]);
|
|
303
|
+
}, [appearDelayMs, dismissEnabled, storageKey, dismissalId, hasVideo, videoId, inline]);
|
|
263
304
|
|
|
264
305
|
// --- environment preferences (read after hydration; no SSR access) ---
|
|
265
306
|
// Derived, not stored: both inputs are plain reads of the environment, and
|
|
@@ -85,13 +85,14 @@ export const ToolBadge: React.FC<ToolBadgeProps> = ({
|
|
|
85
85
|
return linked(
|
|
86
86
|
<span
|
|
87
87
|
className={cn(
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
// The house tag (Figma `tag`): 32px, on the card surface, the caption face.
|
|
89
|
+
'inline-flex h-8 items-center gap-[var(--spacing-system-xs)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-xsf)] text-ods-text-primary text-h5',
|
|
90
|
+
href && 'transition-colors hover:border-ods-border-hover',
|
|
90
91
|
className,
|
|
91
92
|
)}
|
|
92
93
|
>
|
|
93
94
|
{mark && (
|
|
94
|
-
<span className="flex h-
|
|
95
|
+
<span className="flex h-4 w-4 shrink-0 items-center justify-center [&_img]:h-full [&_img]:w-full [&_img]:rounded-sm [&_img]:object-contain [&_svg]:h-full [&_svg]:w-full">
|
|
95
96
|
{mark}
|
|
96
97
|
</span>
|
|
97
98
|
)}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
1
3
|
import type { ReactNode } from 'react';
|
|
2
4
|
import { cn } from '../../utils/cn';
|
|
5
|
+
import { useAgentTint } from '../agent-identity';
|
|
3
6
|
|
|
4
7
|
export interface AppWindowFrameProps {
|
|
5
8
|
/** Centered in the title bar. */
|
|
@@ -7,6 +10,12 @@ export interface AppWindowFrameProps {
|
|
|
7
10
|
children: ReactNode;
|
|
8
11
|
/** A bar pinned under the body (a status line, an action). */
|
|
9
12
|
footer?: ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* The slug of the agent whose window this is. The window takes that agent's
|
|
15
|
+
* tint from its identity, like every card of the agent's. A window names its
|
|
16
|
+
* agent, never a colour.
|
|
17
|
+
*/
|
|
18
|
+
agent?: string | null;
|
|
10
19
|
className?: string;
|
|
11
20
|
/** The scrolling body between the title bar and the footer. */
|
|
12
21
|
bodyClassName?: string;
|
|
@@ -19,10 +28,17 @@ export interface AppWindowFrameProps {
|
|
|
19
28
|
*
|
|
20
29
|
* The frame is a column, so give it a height and the body fills what is left.
|
|
21
30
|
*/
|
|
22
|
-
export function AppWindowFrame({ title, children, footer, className, bodyClassName }: AppWindowFrameProps) {
|
|
31
|
+
export function AppWindowFrame({ title, children, footer, agent, className, bodyClassName }: AppWindowFrameProps) {
|
|
32
|
+
const tint = useAgentTint(agent);
|
|
23
33
|
return (
|
|
24
34
|
<div
|
|
25
|
-
className={cn(
|
|
35
|
+
className={cn(
|
|
36
|
+
'flex min-w-0 flex-col overflow-hidden rounded-xl border border-ods-border',
|
|
37
|
+
// An agent's window sits on the page surface under that agent's tint.
|
|
38
|
+
tint ? 'bg-ods-bg' : 'bg-ods-card',
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
style={tint}
|
|
26
42
|
>
|
|
27
43
|
<div className="relative flex h-10 shrink-0 items-center gap-2 border-b border-ods-border px-4" aria-hidden>
|
|
28
44
|
<span className="h-2.5 w-2.5 rounded-full bg-ods-border" />
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { cn } from '../../utils/cn';
|
|
3
|
+
import { trialLabels } from '../../utils/trial';
|
|
4
|
+
import { FileOffIcon } from '../icons-v2-generated/documents/file-off-icon';
|
|
5
|
+
import { CreditCardXmarkIcon } from '../icons-v2-generated/finance/credit-card-xmark-icon';
|
|
6
|
+
import { FlaskVialIcon } from '../icons-v2-generated/school/flask-vial-icon';
|
|
7
|
+
import { XmarkAltIcon } from '../icons-v2-generated/signs-and-symbols/xmark-alt-icon';
|
|
8
|
+
|
|
9
|
+
export interface BenefitItem {
|
|
10
|
+
/** Drawn at 16px in the quiet colour. */
|
|
11
|
+
icon?: ReactNode;
|
|
12
|
+
label: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The mark of each of the trial's terms, wherever a term is listed. */
|
|
16
|
+
export const TRIAL_TERM_ICONS = {
|
|
17
|
+
noCard: <CreditCardXmarkIcon />,
|
|
18
|
+
cancel: <XmarkAltIcon />,
|
|
19
|
+
length: <FlaskVialIcon />,
|
|
20
|
+
noContract: <FileOffIcon />,
|
|
21
|
+
} as const;
|
|
22
|
+
export type TrialTerm = keyof typeof TRIAL_TERM_ICONS;
|
|
23
|
+
|
|
24
|
+
/** The trial's terms a list shows when its host names none: cancel anytime, the trial's length, no contract. */
|
|
25
|
+
export const DEFAULT_TRIAL_TERMS: readonly TrialTerm[] = ['cancel', 'length', 'noContract'];
|
|
26
|
+
|
|
27
|
+
/** The named terms of the trial as benefits, in the order given. The length is left out when the host does not know it. */
|
|
28
|
+
export function trialBenefits(
|
|
29
|
+
trialDays: number | string | null | undefined,
|
|
30
|
+
terms: readonly TrialTerm[] = DEFAULT_TRIAL_TERMS,
|
|
31
|
+
): BenefitItem[] {
|
|
32
|
+
const labels = trialLabels(trialDays);
|
|
33
|
+
return terms.flatMap(term => {
|
|
34
|
+
const label = labels[term];
|
|
35
|
+
return label ? [{ icon: TRIAL_TERM_ICONS[term], label }] : [];
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BenefitListProps {
|
|
40
|
+
/**
|
|
41
|
+
* Which of the trial's terms to list, in order ("cancel", "length",
|
|
42
|
+
* "noContract", "noCard"). Omitted: none, the list is `items` alone.
|
|
43
|
+
*/
|
|
44
|
+
terms?: readonly TrialTerm[];
|
|
45
|
+
/** The free trial's length in days, for the `length` term. Without it that term is left out. */
|
|
46
|
+
trialDays?: number | string | null;
|
|
47
|
+
/** The host's own benefits. With `terms`, `itemsFirst` says which come first. */
|
|
48
|
+
items?: BenefitItem[];
|
|
49
|
+
/** Draw `items` before the trial's terms. Default: after them. */
|
|
50
|
+
itemsFirst?: boolean;
|
|
51
|
+
/** `center` centres the row (under a centred hero). Default `start`. */
|
|
52
|
+
align?: 'start' | 'center';
|
|
53
|
+
className?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* THE row of short benefits beside a call to action ("Cancel Anytime", "No
|
|
58
|
+
* Contract"): a small mark and a few words each, wrapping as a row. The one
|
|
59
|
+
* such row everywhere: the sign-up panel, every hero, every closing call to
|
|
60
|
+
* action. A host chooses WHICH benefits (`terms`, `items`), never how they look.
|
|
61
|
+
*/
|
|
62
|
+
export function BenefitList({
|
|
63
|
+
terms,
|
|
64
|
+
trialDays,
|
|
65
|
+
items = [],
|
|
66
|
+
itemsFirst = false,
|
|
67
|
+
align = 'start',
|
|
68
|
+
className,
|
|
69
|
+
}: BenefitListProps) {
|
|
70
|
+
const termItems = terms ? trialBenefits(trialDays, terms) : [];
|
|
71
|
+
const all = itemsFirst ? [...items, ...termItems] : [...termItems, ...items];
|
|
72
|
+
if (all.length === 0) return null;
|
|
73
|
+
return (
|
|
74
|
+
<ul
|
|
75
|
+
className={cn(
|
|
76
|
+
// A full step between benefits, a tighter one between wrapped rows: each reads as its own point.
|
|
77
|
+
'm-0 flex list-none flex-wrap content-center items-center gap-x-[var(--spacing-system-l)] gap-y-[var(--spacing-system-m)] p-0',
|
|
78
|
+
align === 'center' && 'justify-center',
|
|
79
|
+
className,
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{all.map(item => (
|
|
83
|
+
<li key={item.label} className="flex items-center gap-[var(--spacing-system-xs)]">
|
|
84
|
+
{item.icon && (
|
|
85
|
+
<span
|
|
86
|
+
className="flex size-4 shrink-0 items-center justify-center text-ods-text-secondary [&_svg]:size-4"
|
|
87
|
+
aria-hidden
|
|
88
|
+
>
|
|
89
|
+
{item.icon}
|
|
90
|
+
</span>
|
|
91
|
+
)}
|
|
92
|
+
<span className="whitespace-nowrap text-ods-text-primary text-h4">{item.label}</span>
|
|
93
|
+
</li>
|
|
94
|
+
))}
|
|
95
|
+
</ul>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -90,8 +90,19 @@ const buttonVariants = cva(
|
|
|
90
90
|
true: 'px-0',
|
|
91
91
|
false: '',
|
|
92
92
|
},
|
|
93
|
+
// Two buttons drawn as ONE surface with a seam between them (a main action
|
|
94
|
+
// and a chevron that opens its alternatives: `DropdownButton` with an
|
|
95
|
+
// `action`). `start` is the leading half, `end` the trailing one: each
|
|
96
|
+
// gives up the corners and the border on its inner edge.
|
|
97
|
+
attach: {
|
|
98
|
+
none: '',
|
|
99
|
+
start: 'rounded-r-none',
|
|
100
|
+
end: 'rounded-l-none border-l-0',
|
|
101
|
+
},
|
|
93
102
|
},
|
|
94
103
|
compoundVariants: [
|
|
104
|
+
// The trailing half as an icon button is the design's 40px chevron cell, at every breakpoint.
|
|
105
|
+
{ attach: 'end', size: 'icon', class: 'w-10 px-0 md:w-10' },
|
|
95
106
|
{ size: 'small', class: '[&_svg]:h-4 [&_svg]:w-4' },
|
|
96
107
|
// Comes after the size classes, so cn()'s tailwind-merge resolves the
|
|
97
108
|
// ods-typography group in favour of text-h4.
|
|
@@ -103,6 +114,7 @@ const buttonVariants = cva(
|
|
|
103
114
|
font: 'bold',
|
|
104
115
|
fullWidth: false,
|
|
105
116
|
noPaddingX: false,
|
|
117
|
+
attach: 'none',
|
|
106
118
|
},
|
|
107
119
|
},
|
|
108
120
|
);
|
|
@@ -267,6 +279,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function ButtonI
|
|
|
267
279
|
font,
|
|
268
280
|
fullWidth,
|
|
269
281
|
noPaddingX,
|
|
282
|
+
attach,
|
|
270
283
|
asChild,
|
|
271
284
|
href,
|
|
272
285
|
openInNewTab,
|
|
@@ -389,7 +402,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function ButtonI
|
|
|
389
402
|
);
|
|
390
403
|
}
|
|
391
404
|
|
|
392
|
-
const classes = cn(buttonVariants({ variant, size, font, fullWidth, noPaddingX }), className);
|
|
405
|
+
const classes = cn(buttonVariants({ variant, size, font, fullWidth, noPaddingX, attach }), className);
|
|
393
406
|
|
|
394
407
|
// asChild: consumer fully controls the rendered element; we just stamp our classes.
|
|
395
408
|
if (asChild) {
|
|
@@ -6,9 +6,26 @@ import { Chevron02DownIcon } from '../icons-v2-generated';
|
|
|
6
6
|
import { ActionsMenuDropdown, type ActionsMenuGroup, type ActionsMenuItem } from './actions-menu';
|
|
7
7
|
import { Button, type ButtonProps } from './button';
|
|
8
8
|
|
|
9
|
+
/** The button's own action, when its label does something itself and the chevron holds the alternatives. */
|
|
10
|
+
export interface DropdownButtonAction {
|
|
11
|
+
href?: string;
|
|
12
|
+
/** Fetch `href` as a file (an installer) instead of navigating to it. */
|
|
13
|
+
download?: boolean | string;
|
|
14
|
+
openInNewTab?: boolean;
|
|
15
|
+
onClick?: React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
export interface DropdownButtonProps {
|
|
10
19
|
/** Button label rendered next to the chevron. */
|
|
11
20
|
label: React.ReactNode;
|
|
21
|
+
/**
|
|
22
|
+
* Makes the label a button of its own (Figma `button-action`): it runs this
|
|
23
|
+
* action, and the chevron beside it opens the menu of alternatives. Two click
|
|
24
|
+
* targets on one surface. Without it the whole button opens the menu.
|
|
25
|
+
*/
|
|
26
|
+
action?: DropdownButtonAction;
|
|
27
|
+
/** The chevron's accessible name when `action` is set (it has no text of its own). Default: "More options". */
|
|
28
|
+
menuAriaLabel?: string;
|
|
12
29
|
/** Optional leading icon rendered before the label. */
|
|
13
30
|
icon?: React.ReactNode;
|
|
14
31
|
/**
|
|
@@ -55,7 +72,9 @@ export interface DropdownButtonProps {
|
|
|
55
72
|
* `ActionsMenu`. One component, so the trigger styling, chevron, and menu
|
|
56
73
|
* chrome cannot drift between surfaces.
|
|
57
74
|
*
|
|
58
|
-
*
|
|
75
|
+
* With `action`, the label is a button of its own and the chevron alone opens
|
|
76
|
+
* the menu (two targets, one surface). Otherwise, two trigger looks — BOTH
|
|
77
|
+
* render through the unified `Button`:
|
|
59
78
|
* - default (no `variant`): the card-colored seam trigger — `Button
|
|
60
79
|
* variant="outline"` with the chevron in its `splitIcon` slot, so the label
|
|
61
80
|
* and chevron are separated by a divider, the whole surface is one click
|
|
@@ -70,6 +89,8 @@ export interface DropdownButtonProps {
|
|
|
70
89
|
*/
|
|
71
90
|
export function DropdownButton({
|
|
72
91
|
label,
|
|
92
|
+
action,
|
|
93
|
+
menuAriaLabel = 'More options',
|
|
73
94
|
icon,
|
|
74
95
|
items,
|
|
75
96
|
groups,
|
|
@@ -107,6 +128,57 @@ export function DropdownButton({
|
|
|
107
128
|
|
|
108
129
|
const resolvedAriaLabel = ariaLabel || (typeof label === 'string' ? label : undefined);
|
|
109
130
|
|
|
131
|
+
// With its own action: two common `Button`s attached into one surface. The
|
|
132
|
+
// label runs the action, the chevron is the menu's trigger.
|
|
133
|
+
if (action) {
|
|
134
|
+
const skin = variant ?? 'outline';
|
|
135
|
+
return (
|
|
136
|
+
<div
|
|
137
|
+
role="group"
|
|
138
|
+
aria-label={resolvedAriaLabel}
|
|
139
|
+
className={cn('inline-flex items-stretch', fullWidth && 'w-full', className)}
|
|
140
|
+
>
|
|
141
|
+
<Button
|
|
142
|
+
variant={skin}
|
|
143
|
+
attach="start"
|
|
144
|
+
href={action.href}
|
|
145
|
+
download={action.download}
|
|
146
|
+
openInNewTab={action.openInNewTab}
|
|
147
|
+
onClick={action.onClick}
|
|
148
|
+
disabled={disabled}
|
|
149
|
+
loading={loading}
|
|
150
|
+
leftIcon={icon}
|
|
151
|
+
className={fullWidth ? 'flex-1' : undefined}
|
|
152
|
+
>
|
|
153
|
+
{label}
|
|
154
|
+
</Button>
|
|
155
|
+
<ActionsMenuDropdown
|
|
156
|
+
groups={groups ?? [{ items: items ?? [] }]}
|
|
157
|
+
open={open}
|
|
158
|
+
onOpenChange={handleOpenChange}
|
|
159
|
+
align={align}
|
|
160
|
+
side={side}
|
|
161
|
+
onCloseAutoFocus={onCloseAutoFocus}
|
|
162
|
+
triggerAriaLabel={menuAriaLabel}
|
|
163
|
+
customTrigger={
|
|
164
|
+
<Button
|
|
165
|
+
type="button"
|
|
166
|
+
variant={skin}
|
|
167
|
+
size="icon"
|
|
168
|
+
attach="end"
|
|
169
|
+
disabled={disabled || loading}
|
|
170
|
+
aria-label={menuAriaLabel}
|
|
171
|
+
className={cn(open && 'bg-ods-bg-hover')}
|
|
172
|
+
leftIcon={
|
|
173
|
+
<Chevron02DownIcon className={cn('transition-transform duration-fast', open && 'rotate-180')} />
|
|
174
|
+
}
|
|
175
|
+
/>
|
|
176
|
+
}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
110
182
|
const trigger = variant ? (
|
|
111
183
|
<Button
|
|
112
184
|
variant={variant}
|
|
@@ -240,6 +240,9 @@ export * from './product-screen-frame';
|
|
|
240
240
|
export * from './phone-frame';
|
|
241
241
|
export * from './notification-stack';
|
|
242
242
|
export * from './numbered-step';
|
|
243
|
+
export * from './strip-card';
|
|
244
|
+
export * from './benefit-list';
|
|
245
|
+
export * from './leader-row';
|
|
243
246
|
export * from './device-wallpaper';
|
|
244
247
|
export * from './analyzing-state';
|
|
245
248
|
export * from './incident-feed';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { cn } from '../../utils/cn';
|
|
3
|
+
|
|
4
|
+
export interface LeaderRowProps {
|
|
5
|
+
label: ReactNode;
|
|
6
|
+
value: ReactNode;
|
|
7
|
+
/** After the value, at 16px (a check, a warning). */
|
|
8
|
+
status?: ReactNode;
|
|
9
|
+
/** The row's element. Default `div`; `li` inside a list. */
|
|
10
|
+
as?: 'div' | 'li';
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A label and its value joined by a leader line (Figma `info-card` row): the
|
|
16
|
+
* quiet label, a hairline that takes the free space, the value, and a status
|
|
17
|
+
* mark at the end. One line, always: a long value clips.
|
|
18
|
+
*/
|
|
19
|
+
export function LeaderRow({ label, value, status, as: Row = 'div', className }: LeaderRowProps) {
|
|
20
|
+
return (
|
|
21
|
+
<Row className={cn('flex min-w-0 items-center gap-[var(--spacing-system-xsf)] text-h4', className)}>
|
|
22
|
+
<span className="shrink-0 whitespace-nowrap text-ods-text-secondary">{label}</span>
|
|
23
|
+
<span className="h-px min-w-4 flex-1 bg-ods-border" aria-hidden />
|
|
24
|
+
<span className="min-w-0 truncate text-ods-text-primary">{value}</span>
|
|
25
|
+
{status && <span className="flex shrink-0 items-center">{status}</span>}
|
|
26
|
+
</Row>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { cn } from '../../utils/cn';
|
|
3
|
+
|
|
4
|
+
export interface StripCardProps {
|
|
5
|
+
/** Before the title, at 24px (an icon, a step's number). */
|
|
6
|
+
lead?: ReactNode;
|
|
7
|
+
title: ReactNode;
|
|
8
|
+
/** One quieter line under the title. */
|
|
9
|
+
caption?: ReactNode;
|
|
10
|
+
/** The title's element. Default `h3`. */
|
|
11
|
+
titleAs?: 'h3' | 'h4' | 'p';
|
|
12
|
+
/** `roomy` gives the head and the body the larger inset (a card whose head carries a caption). */
|
|
13
|
+
density?: 'default' | 'roomy';
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
bodyClassName?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* THE card with a head strip (Figma "Info-card" with a `tag` head): a head on
|
|
21
|
+
* the card surface holding a mark, a title and an optional caption, a hairline,
|
|
22
|
+
* then the body on the page surface. One frame for every such card, so a row
|
|
23
|
+
* of them shares its radius, hairlines and insets.
|
|
24
|
+
*/
|
|
25
|
+
export function StripCard({
|
|
26
|
+
lead,
|
|
27
|
+
title,
|
|
28
|
+
caption,
|
|
29
|
+
titleAs: Title = 'h3',
|
|
30
|
+
density = 'default',
|
|
31
|
+
children,
|
|
32
|
+
className,
|
|
33
|
+
bodyClassName,
|
|
34
|
+
}: StripCardProps) {
|
|
35
|
+
const roomy = density === 'roomy';
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
className={cn(
|
|
39
|
+
'flex h-full min-w-0 flex-col overflow-hidden rounded-md border border-ods-border bg-ods-card',
|
|
40
|
+
className,
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
<div
|
|
44
|
+
className={cn(
|
|
45
|
+
'flex items-center gap-[var(--spacing-system-mf)] border-b border-ods-border',
|
|
46
|
+
roomy ? 'p-[var(--spacing-system-l)]' : 'p-[var(--spacing-system-m)]',
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
{lead && <span className="flex shrink-0 items-center">{lead}</span>}
|
|
50
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
51
|
+
<Title className="m-0 text-ods-text-primary text-h3">{title}</Title>
|
|
52
|
+
{caption && <span className="text-ods-text-secondary text-h6">{caption}</span>}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div
|
|
56
|
+
className={cn(
|
|
57
|
+
'flex min-w-0 flex-1 flex-col bg-ods-bg',
|
|
58
|
+
roomy
|
|
59
|
+
? 'gap-[var(--spacing-system-l)] p-[var(--spacing-system-l)]'
|
|
60
|
+
: 'gap-[var(--spacing-system-m)] px-[var(--spacing-system-m)] pb-[var(--spacing-system-l)] pt-[var(--spacing-system-m)]',
|
|
61
|
+
bodyClassName,
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
{children}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
'use client';
|
|
23
23
|
import { forwardRef, type ComponentType, type ImgHTMLAttributes, type Ref } from 'react';
|
|
24
24
|
|
|
25
|
+
import { fetchPriorityProp } from '../utils/fetch-priority';
|
|
26
|
+
|
|
25
27
|
type ImageProps = Omit<ImgHTMLAttributes<HTMLImageElement>, 'src' | 'placeholder'> & {
|
|
26
28
|
src?: string | { src: string };
|
|
27
29
|
alt?: string;
|
|
@@ -91,12 +93,18 @@ const Image = forwardRef<HTMLImageElement, ImageProps>(function NextImageShim({
|
|
|
91
93
|
optimizedWidth && resizer && typeof given.src === 'string'
|
|
92
94
|
? { ...given, src: resizer(given.src, optimizedWidth), unoptimized: true }
|
|
93
95
|
: given;
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
96
|
+
// `priority` means "this is the picture the page is waiting for", so its
|
|
97
|
+
// request is ranked high on EVERY host. Stated here, once: since Next 16
|
|
98
|
+
// `priority` only preloads and no longer ranks the request, and the plain
|
|
99
|
+
// <img> below dropped the prop altogether, so an embedder's hero went out at
|
|
100
|
+
// the default priority. A caller that names its own `fetchPriority` keeps it.
|
|
101
|
+
const fetchPriority = props.fetchPriority ?? (props.priority ? 'high' : undefined);
|
|
102
|
+
|
|
103
|
+
// Real impl path — registered by the host. Every Next-specific prop
|
|
104
|
+
// (priority, placeholder, sizes, loader…) reaches the real component intact.
|
|
97
105
|
if (impl) {
|
|
98
106
|
const Real = impl;
|
|
99
|
-
return <Real ref={ref} {...props} />;
|
|
107
|
+
return <Real ref={ref} {...props} fetchPriority={fetchPriority} />;
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
// Fallback path — plain <img>. Drops Next-only props and reduces
|
|
@@ -115,6 +123,7 @@ const Image = forwardRef<HTMLImageElement, ImageProps>(function NextImageShim({
|
|
|
115
123
|
unoptimized: _unoptimized,
|
|
116
124
|
onLoadingComplete,
|
|
117
125
|
loader: _loader,
|
|
126
|
+
fetchPriority: _fetchPriority,
|
|
118
127
|
style,
|
|
119
128
|
...rest
|
|
120
129
|
} = props;
|
|
@@ -147,6 +156,8 @@ const Image = forwardRef<HTMLImageElement, ImageProps>(function NextImageShim({
|
|
|
147
156
|
style={finalStyle}
|
|
148
157
|
onLoad={onLoadingComplete ? e => onLoadingComplete(e.currentTarget) : undefined}
|
|
149
158
|
{...rest}
|
|
159
|
+
// The attribute's spelling depends on the host's React major (`fetchPriorityProp`).
|
|
160
|
+
{...(fetchPriority ? fetchPriorityProp(fetchPriority) : {})}
|
|
150
161
|
/>
|
|
151
162
|
);
|
|
152
163
|
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Times for a scripted conversation (a product demo, an example card): a
|
|
3
|
+
* believable time of day for each turn, chosen from a seed.
|
|
4
|
+
*
|
|
5
|
+
* The same seed always gives the same times, so a server render and the
|
|
6
|
+
* browser agree and a card keeps its time on every visit. Turns of one
|
|
7
|
+
* conversation follow each other: each is the one before it plus a short gap.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** A 32-bit hash of the seed (FNV-1a), the generator's starting state. */
|
|
11
|
+
function hashSeed(seed: string): number {
|
|
12
|
+
let hash = 0x811c9dc5;
|
|
13
|
+
for (let i = 0; i < seed.length; i++) {
|
|
14
|
+
hash ^= seed.charCodeAt(i);
|
|
15
|
+
hash = Math.imul(hash, 0x01000193);
|
|
16
|
+
}
|
|
17
|
+
return hash >>> 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A seeded generator of numbers in [0, 1) (mulberry32). */
|
|
21
|
+
function seededRandom(seed: string): () => number {
|
|
22
|
+
let state = hashSeed(seed);
|
|
23
|
+
return () => {
|
|
24
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
25
|
+
let t = state;
|
|
26
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
27
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
28
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DemoClockOptions {
|
|
33
|
+
/** How many turns to time. Default 2. */
|
|
34
|
+
count?: number;
|
|
35
|
+
/** The first turn falls between these hours of the day (24h, end exclusive). Default 8 to 18. */
|
|
36
|
+
fromHour?: number;
|
|
37
|
+
toHour?: number;
|
|
38
|
+
/** The longest gap between two turns, in minutes. Default 3. A gap may be 0: an agent answers within the minute. */
|
|
39
|
+
maxGapMinutes?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Minutes of the day as "2:47 PM". */
|
|
43
|
+
export function formatDemoClock(minutesOfDay: number): string {
|
|
44
|
+
const minutes = ((Math.floor(minutesOfDay) % 1440) + 1440) % 1440;
|
|
45
|
+
const hour24 = Math.floor(minutes / 60);
|
|
46
|
+
const hour12 = hour24 % 12 === 0 ? 12 : hour24 % 12;
|
|
47
|
+
return `${hour12}:${String(minutes % 60).padStart(2, '0')} ${hour24 < 12 ? 'AM' : 'PM'}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The time of each turn of one scripted conversation, in order, for `seed`. */
|
|
51
|
+
export function demoClockTimes(seed: string, options: DemoClockOptions = {}): string[] {
|
|
52
|
+
const { count = 2, fromHour = 8, toHour = 18, maxGapMinutes = 3 } = options;
|
|
53
|
+
const random = seededRandom(seed);
|
|
54
|
+
const span = Math.max(1, (toHour - fromHour) * 60);
|
|
55
|
+
let minutes = fromHour * 60 + Math.floor(random() * span);
|
|
56
|
+
const times: string[] = [];
|
|
57
|
+
for (let i = 0; i < Math.max(0, count); i++) {
|
|
58
|
+
if (i > 0) minutes += Math.floor(random() * (maxGapMinutes + 1));
|
|
59
|
+
times.push(formatDemoClock(minutes));
|
|
60
|
+
}
|
|
61
|
+
return times;
|
|
62
|
+
}
|