@flamingo-stack/openframe-frontend-core 0.0.773 → 0.0.775
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-VW2QQKQ6.cjs → chunk-36GYLB3Z.cjs} +15 -15
- package/dist/{chunk-5HBGBLJT.cjs → chunk-3XLDYEQD.cjs} +74 -63
- package/dist/{chunk-7P2QXBWE.cjs → chunk-42JGOJCB.cjs} +234 -213
- package/dist/{chunk-I4ZVMNHO.cjs → chunk-4LFB4YY3.cjs} +6 -6
- package/dist/{chunk-JJNOAYD4.cjs → chunk-5AOKXXHV.cjs} +4 -4
- package/dist/{chunk-MT5Z2LSV.cjs → chunk-5YJWT5ZQ.cjs} +72 -72
- package/dist/{chunk-42YLIZFD.cjs → chunk-6SJYFUGD.cjs} +22 -22
- package/dist/{chunk-BPAYT72L.cjs → chunk-BTVO676B.cjs} +148 -131
- package/dist/{chunk-TZ4GKUO2.cjs → chunk-CZIMISAR.cjs} +5 -1
- package/dist/{chunk-XVFM6SJA.cjs → chunk-E64HPPRH.cjs} +31 -31
- package/dist/{chunk-CTRJNTYU.cjs → chunk-EQBJHJS7.cjs} +2 -2
- package/dist/{chunk-6LLF2QCS.cjs → chunk-F67SEXCW.cjs} +39 -39
- package/dist/{chunk-HEEK42BL.cjs → chunk-FHTTRWAK.cjs} +81 -81
- package/dist/{chunk-NRFNZ35Z.cjs → chunk-IQQ3SLUK.cjs} +19 -19
- package/dist/{chunk-RNLRS4ID.cjs → chunk-KKAY6GPU.cjs} +2 -2
- package/dist/{chunk-ILZEXCJH.cjs → chunk-KWZ62LPZ.cjs} +124 -104
- package/dist/{chunk-KDMWXXLE.cjs → chunk-L5CKY5WA.cjs} +15 -15
- package/dist/{chunk-CW3ML76U.cjs → chunk-NTPX5GXI.cjs} +4 -0
- package/dist/{chunk-NPCFVTA2.cjs → chunk-ODF3KCRK.cjs} +31 -31
- package/dist/{chunk-YSKX42LW.cjs → chunk-PMX6QNNN.cjs} +12 -12
- package/dist/{chunk-2A56MLLL.cjs → chunk-SWEPHSIP.cjs} +4 -4
- package/dist/{chunk-7J4YWDT6.cjs → chunk-TF2XYDH3.cjs} +3 -3
- package/dist/{chunk-KSCEAIBS.cjs → chunk-TOPGBOCN.cjs} +2 -2
- package/dist/{chunk-FPCGKH5K.cjs → chunk-TUXPFF4P.cjs} +31 -29
- package/dist/{chunk-SE76YGR3.cjs → chunk-USMR73ND.cjs} +4 -4
- package/dist/{chunk-BMFU2CUL.cjs → chunk-VAGZJRYV.cjs} +4 -4
- package/dist/{chunk-3FREBLAJ.cjs → chunk-VDXBACD3.cjs} +47 -45
- package/dist/{chunk-X5Z4HRH7.cjs → chunk-VISK6LKE.cjs} +2 -2
- package/dist/{chunk-5V7BIUGA.cjs → chunk-Y3EBUFX4.cjs} +92 -21
- package/dist/{cloud-tenants-EE5ULFOK.cjs → cloud-tenants-ISGAMTJD.cjs} +11 -11
- package/dist/components/authors/author-detail-view.d.ts.map +1 -1
- package/dist/components/authors/author-detail-view.js +12 -2
- package/dist/components/case-studies/index.cjs +28 -28
- package/dist/components/chat/ask-prompts.d.ts.map +1 -1
- package/dist/components/chat/ask-prompts.js +28 -19
- package/dist/components/chat/entity-cards/entity-portrait-card.d.ts +3 -1
- package/dist/components/chat/entity-cards/entity-portrait-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/entity-portrait-card.js +2 -0
- package/dist/components/chat/entity-cards/program-card.d.ts +6 -1
- package/dist/components/chat/entity-cards/program-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/program-card.js +19 -2
- package/dist/components/chat/index.cjs +10 -8
- package/dist/components/chat/index.d.ts +1 -1
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +10 -4
- package/dist/components/chat/utils/ask-ai-events.d.ts +10 -0
- package/dist/components/chat/utils/ask-ai-events.d.ts.map +1 -1
- package/dist/components/chat/utils/ask-ai-events.js +21 -1
- package/dist/components/contact/index.cjs +12 -12
- package/dist/components/docs/index.cjs +15 -15
- package/dist/components/embeds/index.cjs +12 -12
- package/dist/components/faq/index.cjs +13 -13
- package/dist/components/features/index.cjs +11 -11
- package/dist/components/features/video-hover-preview.d.ts.map +1 -1
- package/dist/components/features/video-hover-preview.js +5 -2
- package/dist/components/features/video-shared.d.ts +2 -1
- package/dist/components/features/video-shared.d.ts.map +1 -1
- package/dist/components/features/video.d.ts.map +1 -1
- package/dist/components/features/video.js +64 -13
- package/dist/components/help-center-pages/index.cjs +100 -100
- package/dist/components/index.cjs +74 -62
- package/dist/components/meeting-scheduler/index.cjs +46 -46
- package/dist/components/navigation/index.cjs +14 -14
- package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
- package/dist/components/navigation/mingo-ai-button.js +2 -1
- package/dist/components/navigation/site-header.d.ts.map +1 -1
- package/dist/components/navigation/site-header.js +3 -1
- package/dist/components/onboarding-guides/index.cjs +14 -14
- package/dist/components/product-demo/index.cjs +20 -20
- package/dist/components/related-content/card-registry.d.ts +5 -0
- package/dist/components/related-content/card-registry.d.ts.map +1 -1
- package/dist/components/related-content/card-registry.js +2 -0
- package/dist/components/related-content/index.cjs +7 -6
- package/dist/components/related-content/index.d.ts +2 -1
- package/dist/components/related-content/index.d.ts.map +1 -1
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/related-content/related-content-section.d.ts +8 -10
- package/dist/components/related-content/related-content-section.d.ts.map +1 -1
- package/dist/components/related-content/related-content-section.js +20 -11
- package/dist/components/tickets/index.cjs +17 -17
- package/dist/components/ui/index.cjs +13 -11
- package/dist/components/ui/photo-wall.d.ts +16 -1
- package/dist/components/ui/photo-wall.d.ts.map +1 -1
- package/dist/components/ui/photo-wall.js +24 -9
- package/dist/components/ui/tab-navigation.d.ts +8 -1
- package/dist/components/ui/tab-navigation.d.ts.map +1 -1
- package/dist/components/ui/tab-navigation.js +9 -3
- package/dist/{devices-EBGGHEGR.cjs → devices-AHJX6ACJ.cjs} +12 -12
- package/dist/{embeddable-chat-HCT4QV35.cjs → embeddable-chat-DI7ZZTBP.cjs} +75 -75
- package/dist/hooks/index.cjs +4 -4
- package/dist/hooks/use-near-viewport.d.ts.map +1 -1
- package/dist/hooks/use-near-viewport.js +4 -0
- package/dist/index.cjs +23 -15
- package/dist/{knowledge-5ZSMKKTL.cjs → knowledge-FH5GVRVP.cjs} +12 -12
- package/dist/{logs-GUV3BMGC.cjs → logs-JBIHHLUQ.cjs} +19 -19
- package/dist/{policies-HPMTDOAS.cjs → policies-XLJ2ABY5.cjs} +15 -15
- package/dist/{scripts-QYYKCIUW.cjs → scripts-CQLXM7PP.cjs} +16 -16
- package/dist/{software-update-NC2KZKEI.cjs → software-update-7NYZ7L76.cjs} +14 -14
- package/dist/utils/content-ref-groups.d.ts +8 -0
- package/dist/utils/content-ref-groups.d.ts.map +1 -1
- package/dist/utils/content-ref-groups.js +2 -0
- package/dist/utils/dismiss-cookie.d.ts +14 -0
- package/dist/utils/dismiss-cookie.d.ts.map +1 -1
- package/dist/utils/dismiss-cookie.js +4 -0
- package/dist/utils/index.cjs +10 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -0
- package/dist/utils/visitor-os.d.ts +9 -0
- package/dist/utils/visitor-os.d.ts.map +1 -1
- package/dist/utils/visitor-os.js +4 -0
- package/dist/{video-file-player-COXRF4M5.cjs → video-file-player-4TXA7BZF.cjs} +3 -3
- package/package.json +1 -1
- package/src/components/authors/author-detail-view.tsx +10 -2
- package/src/components/chat/ask-prompts.tsx +6 -1
- package/src/components/chat/entity-cards/entity-portrait-card.tsx +4 -0
- package/src/components/chat/entity-cards/program-card.tsx +24 -2
- package/src/components/chat/index.ts +16 -9
- package/src/components/chat/utils/ask-ai-events.ts +26 -0
- package/src/components/features/video-hover-preview.tsx +46 -37
- package/src/components/features/video-shared.tsx +2 -1
- package/src/components/features/video.tsx +102 -19
- package/src/components/navigation/mingo-ai-button.tsx +2 -1
- package/src/components/navigation/site-header.tsx +3 -1
- package/src/components/related-content/card-registry.tsx +7 -0
- package/src/components/related-content/index.ts +2 -1
- package/src/components/related-content/related-content-section.tsx +38 -14
- package/src/components/ui/photo-wall.tsx +43 -10
- package/src/components/ui/tab-navigation.tsx +18 -3
- package/src/hooks/use-near-viewport.ts +6 -0
- package/src/utils/content-ref-groups.ts +9 -0
- package/src/utils/dismiss-cookie.ts +17 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/visitor-os.ts +12 -0
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
import type React from 'react';
|
|
33
33
|
import {
|
|
34
34
|
createElement,
|
|
35
|
+
forwardRef,
|
|
35
36
|
useCallback,
|
|
36
37
|
useEffect,
|
|
37
38
|
useMemo,
|
|
@@ -40,10 +41,12 @@ import {
|
|
|
40
41
|
useSyncExternalStore,
|
|
41
42
|
type ComponentType,
|
|
42
43
|
} from 'react';
|
|
44
|
+
import Image from '../../embed-shims/next-image';
|
|
43
45
|
import { useNearViewport } from '../../hooks/use-near-viewport';
|
|
44
46
|
import { cn } from '../../utils/cn';
|
|
45
47
|
import { fetchPriorityProp } from '../../utils/fetch-priority';
|
|
46
48
|
import { UnifiedSkeleton } from '../loading/unified-skeleton';
|
|
49
|
+
import { Button } from '../ui/button/button';
|
|
47
50
|
import { useLoadingScope } from '../ui/loading-scope';
|
|
48
51
|
import { VideoPlayBadge } from './video-center-badge';
|
|
49
52
|
import type { FilePlayerProps } from './video-file-player';
|
|
@@ -97,26 +100,105 @@ function subscribeToFilePlayer(listener: () => void): () => void {
|
|
|
97
100
|
const loadedFilePlayer = () => filePlayer;
|
|
98
101
|
const noFilePlayer = () => null;
|
|
99
102
|
|
|
100
|
-
/**
|
|
101
|
-
|
|
103
|
+
/**
|
|
104
|
+
* The video's box with its poster in it, until the player is drawn. The poster is a real image, not a CSS background:
|
|
105
|
+
* a background is found only after layout and is never ranked, so on a page whose video is the largest thing painted
|
|
106
|
+
* the poster arrived last (CU-17tkuw5xp4q). A `priority` video's poster is fetched at once and ranked high.
|
|
107
|
+
*/
|
|
108
|
+
const PosterBox = forwardRef<
|
|
109
|
+
HTMLDivElement,
|
|
110
|
+
{ poster?: string | null; className?: string; priority?: boolean; fill?: boolean }
|
|
111
|
+
>(function PosterBoxView({ poster, className, priority, fill }, ref) {
|
|
102
112
|
return (
|
|
103
113
|
<div
|
|
114
|
+
ref={ref}
|
|
104
115
|
aria-hidden="true"
|
|
105
|
-
className={className}
|
|
106
|
-
style={{
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
116
|
+
className={cn('relative overflow-hidden', className)}
|
|
117
|
+
style={fill ? { width: '100%', height: '100%' } : undefined}
|
|
118
|
+
>
|
|
119
|
+
{poster ? (
|
|
120
|
+
<Image src={poster} alt="" fill sizes="100vw" priority={priority} unoptimized className="object-cover" />
|
|
121
|
+
) : null}
|
|
122
|
+
</div>
|
|
112
123
|
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/** The player's box while its module loads: the poster at the player's own size, so nothing moves. */
|
|
127
|
+
function FilePlayerFallback({
|
|
128
|
+
poster,
|
|
129
|
+
className,
|
|
130
|
+
priority,
|
|
131
|
+
}: {
|
|
132
|
+
poster?: string | null;
|
|
133
|
+
className?: string;
|
|
134
|
+
priority?: boolean;
|
|
135
|
+
}) {
|
|
136
|
+
return <PosterBox poster={poster} className={className} priority={priority} fill />;
|
|
113
137
|
}
|
|
114
138
|
|
|
115
|
-
|
|
139
|
+
/**
|
|
140
|
+
* A video nobody has asked to play yet starts as its poster and a play button, the way a YouTube video does
|
|
141
|
+
* (`YouTubeFacade`): the player and hls.js are the largest script a page can load, and on a page whose video waits for
|
|
142
|
+
* a click (a session, an episode, a release) loading them up front cost every visitor a long main-thread task before
|
|
143
|
+
* anyone pressed play (CU-17tkuw5xp4q). A pointer, focus or touch on the button starts the download, so the player is
|
|
144
|
+
* usually there by the click, and the click plays it with sound. A video that plays by itself (autoplay, hover
|
|
145
|
+
* playback, a resumed position, chrome-less) or whose host drives it through a handle mounts its player as before.
|
|
146
|
+
*/
|
|
147
|
+
function waitsForPlay(props: FilePlayerProps): boolean {
|
|
148
|
+
return (
|
|
149
|
+
!props.autoPlay &&
|
|
150
|
+
!props.autoPlayUnmuted &&
|
|
151
|
+
!props.playOnHover &&
|
|
152
|
+
props.playWhenHovered === undefined &&
|
|
153
|
+
props.startTime === undefined &&
|
|
154
|
+
!props.playerHandleRef &&
|
|
155
|
+
!props.chromeless
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function FilePlayer({
|
|
160
|
+
title,
|
|
161
|
+
priority,
|
|
162
|
+
...props
|
|
163
|
+
}: FilePlayerProps & { title?: string; priority?: boolean }): React.ReactElement {
|
|
164
|
+
// Decided once: a video that started as a facade stays the player once pressed, whatever its props do later.
|
|
165
|
+
const [facade] = useState(() => waitsForPlay(props));
|
|
166
|
+
const [pressed, setPressed] = useState(false);
|
|
167
|
+
if (facade && !pressed) {
|
|
168
|
+
return (
|
|
169
|
+
<div className={cn('relative', props.className)} style={{ width: '100%', height: '100%' }}>
|
|
170
|
+
<FilePlayerFallback poster={props.poster} className="absolute inset-0" priority={priority} />
|
|
171
|
+
<Button
|
|
172
|
+
type="button"
|
|
173
|
+
variant="glyph"
|
|
174
|
+
size="wrap"
|
|
175
|
+
aria-label={title ? `Play: ${title}` : 'Play video'}
|
|
176
|
+
onPointerEnter={loadFilePlayer}
|
|
177
|
+
onFocus={loadFilePlayer}
|
|
178
|
+
onTouchStart={loadFilePlayer}
|
|
179
|
+
onClick={() => {
|
|
180
|
+
loadFilePlayer();
|
|
181
|
+
setPressed(true);
|
|
182
|
+
}}
|
|
183
|
+
className="group absolute inset-0 rounded-none"
|
|
184
|
+
>
|
|
185
|
+
<VideoPlayBadge
|
|
186
|
+
size="lg"
|
|
187
|
+
className="transition-transform duration-200 group-hover:scale-110 group-hover:text-ods-accent"
|
|
188
|
+
/>
|
|
189
|
+
</Button>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
// Pressed: the click is the visitor's gesture, so the player starts with sound (muted if the browser refuses).
|
|
194
|
+
return <LoadedFilePlayer {...props} priority={priority} {...(facade ? { autoPlayUnmuted: true } : {})} />;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function LoadedFilePlayer({ priority, ...props }: FilePlayerProps & { priority?: boolean }): React.ReactElement {
|
|
116
198
|
// The player's component when its module has arrived (a loaded module, drawn through `createElement`).
|
|
117
199
|
const arrived = useSyncExternalStore(subscribeToFilePlayer, loadedFilePlayer, noFilePlayer);
|
|
118
200
|
if (arrived) return createElement(arrived, props);
|
|
119
|
-
return <FilePlayerFallback poster={props.poster} className={props.className} />;
|
|
201
|
+
return <FilePlayerFallback poster={props.poster} className={props.className} priority={priority} />;
|
|
120
202
|
}
|
|
121
203
|
|
|
122
204
|
// =============================================================================
|
|
@@ -207,18 +289,17 @@ export function Video(props: VideoProps): React.ReactElement | null {
|
|
|
207
289
|
|
|
208
290
|
const effectiveKind = resolveKind(props, url);
|
|
209
291
|
|
|
210
|
-
|
|
292
|
+
// Drawn when it comes near the screen, like it plays when it is on it: the player and hls.js are the largest
|
|
293
|
+
// script a page can load, so a video below the fold costs its poster until it is approached. `lazy={false}`
|
|
294
|
+
// mounts the player at once (a caller that drives playback itself before it is seen).
|
|
295
|
+
if ((props.lazy ?? true) && !isNear) {
|
|
211
296
|
// Same box, poster only: the observer target IS the reserved space.
|
|
212
297
|
return wrapWithLayout(
|
|
213
|
-
<
|
|
298
|
+
<PosterBox
|
|
214
299
|
ref={nearRef}
|
|
300
|
+
poster={props.poster}
|
|
215
301
|
className={props.className}
|
|
216
|
-
|
|
217
|
-
props.poster
|
|
218
|
-
? { backgroundImage: `url(${props.poster})`, backgroundSize: 'cover', backgroundPosition: 'center' }
|
|
219
|
-
: undefined
|
|
220
|
-
}
|
|
221
|
-
aria-hidden="true"
|
|
302
|
+
priority={'priority' in props ? props.priority : undefined}
|
|
222
303
|
/>,
|
|
223
304
|
layout,
|
|
224
305
|
);
|
|
@@ -246,6 +327,8 @@ export function Video(props: VideoProps): React.ReactElement | null {
|
|
|
246
327
|
<FilePlayer
|
|
247
328
|
url={url}
|
|
248
329
|
poster={props.poster}
|
|
330
|
+
title={props.title}
|
|
331
|
+
priority={props.priority}
|
|
249
332
|
muted={props.muted}
|
|
250
333
|
{...pickFilePlayback(props)}
|
|
251
334
|
className={props.className}
|
|
@@ -7,7 +7,7 @@ import { useVisitorOs } from '../../hooks/ui/use-visitor-os';
|
|
|
7
7
|
import { cn } from '../../utils';
|
|
8
8
|
import { shortcutLabel, usesCommandKey } from '../../utils/visitor-os';
|
|
9
9
|
import { AgentMark } from '../agent-mark';
|
|
10
|
-
import { ASK_AI_OPEN_EVENT } from '../chat/utils/ask-ai-events';
|
|
10
|
+
import { ASK_AI_OPEN_EVENT, ASK_AI_TRIGGER_ATTRIBUTE } from '../chat/utils/ask-ai-events';
|
|
11
11
|
import { Button } from '../ui/button';
|
|
12
12
|
|
|
13
13
|
export interface MingoAiButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
@@ -132,6 +132,7 @@ export function MingoAiButton({
|
|
|
132
132
|
<Button
|
|
133
133
|
{...props}
|
|
134
134
|
type="button"
|
|
135
|
+
{...{ [ASK_AI_TRIGGER_ATTRIBUTE]: '' }}
|
|
135
136
|
variant={field ? 'glyph' : 'transparent'}
|
|
136
137
|
size="wrap"
|
|
137
138
|
font="regular"
|
|
@@ -314,7 +314,9 @@ export function SiteHeader({
|
|
|
314
314
|
/>
|
|
315
315
|
)}
|
|
316
316
|
{nav.signIn && (
|
|
317
|
-
|
|
317
|
+
// `data-site-signin`: a host whose page is the same for every visitor (served from a CDN) hides it
|
|
318
|
+
// before paint for a visitor with a session, so a signed-in visitor never sees it flash.
|
|
319
|
+
<div className="hidden lg:flex" data-site-signin="">
|
|
318
320
|
<Button
|
|
319
321
|
asChild
|
|
320
322
|
variant="transparent"
|
|
@@ -580,6 +580,11 @@ export interface RelatedCardContext {
|
|
|
580
580
|
anchorAttrs: Pick<CardLinkAnchorProps, 'target' | 'rel'>;
|
|
581
581
|
/** Branded OG fallback for cards whose row has no featured image. */
|
|
582
582
|
placeholderUrl?: string;
|
|
583
|
+
/**
|
|
584
|
+
* The rail's first card: its image is likely the page's largest paint (an author page opens on it), so a card that
|
|
585
|
+
* can rank its image fetches it at once instead of lazily.
|
|
586
|
+
*/
|
|
587
|
+
priority?: boolean;
|
|
583
588
|
extras?: ChatCardDispatchExtras;
|
|
584
589
|
}
|
|
585
590
|
|
|
@@ -645,6 +650,7 @@ function programEntry(configKey: 'podcast' | 'webinar' | 'event'): RelatedCardRe
|
|
|
645
650
|
href={ctx.href}
|
|
646
651
|
targetPlatform={ctx.targetPlatform}
|
|
647
652
|
placeholderUrl={ctx.placeholderUrl}
|
|
653
|
+
priority={ctx.priority}
|
|
648
654
|
{...ctx.anchorAttrs}
|
|
649
655
|
/>
|
|
650
656
|
);
|
|
@@ -684,6 +690,7 @@ export const RELATED_CARD_REGISTRY: Record<string, RelatedCardRegistryEntry> = {
|
|
|
684
690
|
href={ctx.href}
|
|
685
691
|
targetPlatform={ctx.targetPlatform}
|
|
686
692
|
placeholderUrl={ctx.placeholderUrl}
|
|
693
|
+
priority={ctx.priority}
|
|
687
694
|
{...ctx.anchorAttrs}
|
|
688
695
|
/>
|
|
689
696
|
),
|
|
@@ -3,10 +3,11 @@ export {
|
|
|
3
3
|
RelatedContentSection,
|
|
4
4
|
RelatedContentSectionSkeleton,
|
|
5
5
|
type RelatedContentSectionSkeletonProps,
|
|
6
|
-
GROUP_PAGE_SIZE,
|
|
7
6
|
type RelatedContentSectionProps,
|
|
8
7
|
type CardLinkProvider,
|
|
9
8
|
type CardLinkProviderProps,
|
|
10
9
|
type AdminCampaignCardSlot,
|
|
11
10
|
} from './related-content-section';
|
|
12
11
|
export { type CardLinkAnchorProps } from './card-registry';
|
|
12
|
+
// The page size is server-safe data (utils/content-ref-groups); kept on this subpath for its existing importers.
|
|
13
|
+
export { GROUP_PAGE_SIZE } from '../../utils/content-ref-groups';
|
|
@@ -54,6 +54,7 @@ import { useSelfFetch } from '../../hooks/use-self-fetch';
|
|
|
54
54
|
import type { ContentRef, ContentRefWithReason } from '../../types/content-ref';
|
|
55
55
|
import {
|
|
56
56
|
CONTENT_REF_GROUPS,
|
|
57
|
+
GROUP_PAGE_SIZE,
|
|
57
58
|
getContentRefLabelOrTitleCase,
|
|
58
59
|
orderContentRefTypes,
|
|
59
60
|
type ContentRefGroupConfig,
|
|
@@ -199,8 +200,11 @@ function CardForType({
|
|
|
199
200
|
linkProps,
|
|
200
201
|
extras,
|
|
201
202
|
adminCampaignCard,
|
|
203
|
+
priority,
|
|
202
204
|
}: {
|
|
203
205
|
type: string;
|
|
206
|
+
/** The rail's first card: its image is fetched at once (see `RelatedCardContext.priority`). */
|
|
207
|
+
priority?: boolean;
|
|
204
208
|
/** One fetched row, still unvalidated. Its shape is decided at RUNTIME by
|
|
205
209
|
* `type` (each group fetches from its own per-type list API), so it stays
|
|
206
210
|
* `unknown` right up to the registry entry for that type — which owns the
|
|
@@ -246,6 +250,7 @@ function CardForType({
|
|
|
246
250
|
// a numeric title reached `buildOgPlaceholderUrl` as a number. `rowString`
|
|
247
251
|
// makes the claim true.
|
|
248
252
|
placeholderUrl: extras?.buildOgPlaceholderUrl?.(rowString(item, 'title') ?? '') ?? undefined,
|
|
253
|
+
priority,
|
|
249
254
|
extras,
|
|
250
255
|
});
|
|
251
256
|
}
|
|
@@ -259,10 +264,18 @@ function CardForType({
|
|
|
259
264
|
// refetch, no cross-mount cache.
|
|
260
265
|
// =============================================================================
|
|
261
266
|
|
|
262
|
-
function useGroupItems(
|
|
267
|
+
function useGroupItems(
|
|
268
|
+
type: string,
|
|
269
|
+
refs: ContentRef[],
|
|
270
|
+
buildUrl: (type: string, ids: string[]) => string | null,
|
|
271
|
+
seedRows?: ReadonlyArray<unknown>,
|
|
272
|
+
) {
|
|
263
273
|
const ids = refs.map(r => r.id);
|
|
264
274
|
const url = ids.length > 0 ? buildUrl(type, ids) : null;
|
|
265
|
-
|
|
275
|
+
// The page the server already read (`initialRows`) is in hand from the first render, the server's included.
|
|
276
|
+
const [pages, setPages] = useState<ReadonlyMap<string, unknown[]>>(() =>
|
|
277
|
+
seedRows && url !== null ? new Map([[url, [...seedRows]]]) : new Map(),
|
|
278
|
+
);
|
|
266
279
|
const cached = url !== null ? pages.get(url) : undefined;
|
|
267
280
|
const { data, dataUrl, error } = useSelfFetch<unknown>(cached ? null : url);
|
|
268
281
|
if (data != null && dataUrl !== null && !pages.has(dataUrl)) {
|
|
@@ -302,15 +315,8 @@ function resolveGroupConfig(type: string): ContentRefGroupConfig {
|
|
|
302
315
|
);
|
|
303
316
|
}
|
|
304
317
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
* scrollbox inside the page traps wheel events and hides the sections
|
|
308
|
-
* below it). MUST stay at or above the largest suggestion fill
|
|
309
|
-
* (RELATED_SAME_TYPE_COUNT in the hub's lib/constants/suggestions.ts) so
|
|
310
|
-
* current rails never paginate — only genuinely big groups (author pages)
|
|
311
|
-
* do. Exported through the subpath barrel for the hub's module-load
|
|
312
|
-
* assertion of that relation (entity-suggestion-sections.tsx). */
|
|
313
|
-
export const GROUP_PAGE_SIZE = 12;
|
|
318
|
+
// The page size (`GROUP_PAGE_SIZE`) lives with the groups' other settings (`utils/content-ref-groups`, server-safe), so
|
|
319
|
+
// a server page can read a group's first page for `initialRows`.
|
|
314
320
|
|
|
315
321
|
/** A group's layout, read by the group and by the rail's skeleton, so the skeleton is the rail's size. */
|
|
316
322
|
const GROUP_CLASS = 'space-y-4';
|
|
@@ -358,9 +364,15 @@ function ContentGroup({
|
|
|
358
364
|
adminCampaignCard,
|
|
359
365
|
heading,
|
|
360
366
|
onLoadingChange,
|
|
367
|
+
seedRows,
|
|
368
|
+
leadsRail = false,
|
|
361
369
|
}: {
|
|
362
370
|
type: string;
|
|
363
371
|
refs: ContentRef[];
|
|
372
|
+
/** The rows of this group's FIRST page, read by the server (`initialRows`). */
|
|
373
|
+
seedRows?: ReadonlyArray<unknown>;
|
|
374
|
+
/** The rail's first group: its first card's image is fetched at once. */
|
|
375
|
+
leadsRail?: boolean;
|
|
364
376
|
columns: 2 | 3;
|
|
365
377
|
buildUrl: (type: string, ids: string[]) => string | null;
|
|
366
378
|
resolveHref: (ref: ContentRef) => { href: string | null; targetPlatform: string | null };
|
|
@@ -392,7 +404,7 @@ function ContentGroup({
|
|
|
392
404
|
}
|
|
393
405
|
const totalGroupPages = pageCount(refs.length, GROUP_PAGE_SIZE);
|
|
394
406
|
const pageRefs = refs.slice((page - 1) * GROUP_PAGE_SIZE, page * GROUP_PAGE_SIZE);
|
|
395
|
-
const { items, isLoading } = useGroupItems(type, pageRefs, buildUrl);
|
|
407
|
+
const { items, isLoading } = useGroupItems(type, pageRefs, buildUrl, seedRows);
|
|
396
408
|
const groupPagination =
|
|
397
409
|
totalGroupPages > 1 ? <Pagination currentPage={page} totalPages={totalGroupPages} onPageChange={setPage} /> : null;
|
|
398
410
|
useEffect(() => {
|
|
@@ -443,7 +455,7 @@ function ContentGroup({
|
|
|
443
455
|
const itemById = new Map(items.map(it => [rowKey(it), it]));
|
|
444
456
|
|
|
445
457
|
const cards = pageRefs
|
|
446
|
-
.map(contentRef => {
|
|
458
|
+
.map((contentRef, index) => {
|
|
447
459
|
const itemId = String(contentRef.id);
|
|
448
460
|
const item = itemById.get(itemId);
|
|
449
461
|
if (!item) return null;
|
|
@@ -466,6 +478,8 @@ function ContentGroup({
|
|
|
466
478
|
linkProps={linkProps}
|
|
467
479
|
extras={extras}
|
|
468
480
|
adminCampaignCard={adminCampaignCard}
|
|
481
|
+
// The rail's first card, on its first page: the top of the rail, often the page's largest paint.
|
|
482
|
+
priority={leadsRail && page === 1 && index === 0}
|
|
469
483
|
/>
|
|
470
484
|
)}
|
|
471
485
|
</LinkProvider>
|
|
@@ -540,6 +554,13 @@ export interface RelatedContentSectionProps {
|
|
|
540
554
|
* drills the refs here; the first client fetch is skipped (useSelfFetch
|
|
541
555
|
* initialData contract). */
|
|
542
556
|
initialItems?: ContentRefWithReason[];
|
|
557
|
+
/**
|
|
558
|
+
* The rows of each group's FIRST page, read by the server, keyed by the refs' `type`: exactly what the group's list
|
|
559
|
+
* URL answers for those ids (`buildListUrl(type, firstPageIds)`). The cards are then in the server HTML instead of
|
|
560
|
+
* appearing after a request per group in the browser, which on an author page made a card the page's largest paint
|
|
561
|
+
* seconds after load (CU-17tkuw5xp4q). A group with no entry fetches as before; later pages always fetch.
|
|
562
|
+
*/
|
|
563
|
+
initialRows?: Readonly<Record<string, ReadonlyArray<unknown>>>;
|
|
543
564
|
/** Section title (default: "Related Content") */
|
|
544
565
|
title?: string;
|
|
545
566
|
/**
|
|
@@ -612,6 +633,7 @@ export function RelatedContentSection({
|
|
|
612
633
|
sameTypeMinResults,
|
|
613
634
|
includeTypes,
|
|
614
635
|
initialItems,
|
|
636
|
+
initialRows,
|
|
615
637
|
title = 'Related Content',
|
|
616
638
|
columns = 2,
|
|
617
639
|
excludeTypes,
|
|
@@ -774,7 +796,7 @@ export function RelatedContentSection({
|
|
|
774
796
|
return (
|
|
775
797
|
<LoadingRegion as="section" label="Loading related content" announce={loadingTypes.size > 0} className={RAIL_CLASS}>
|
|
776
798
|
<h2 className={RAIL_TITLE_CLASS}>{accentSentenceMarks(title)}</h2>
|
|
777
|
-
{orderedTypes.map(type => (
|
|
799
|
+
{orderedTypes.map((type, groupIndex) => (
|
|
778
800
|
<ContentGroup
|
|
779
801
|
key={type}
|
|
780
802
|
type={type}
|
|
@@ -787,6 +809,8 @@ export function RelatedContentSection({
|
|
|
787
809
|
adminCampaignCard={adminCampaignCard}
|
|
788
810
|
heading={<GroupHeading type={type} />}
|
|
789
811
|
onLoadingChange={onGroupLoadingChange}
|
|
812
|
+
seedRows={initialRows?.[type]}
|
|
813
|
+
leadsRail={groupIndex === 0}
|
|
790
814
|
/>
|
|
791
815
|
))}
|
|
792
816
|
</LoadingRegion>
|
|
@@ -26,6 +26,13 @@ import { MarqueeWall } from './marquee-wall';
|
|
|
26
26
|
export interface PhotoWallImage {
|
|
27
27
|
url: string;
|
|
28
28
|
alt?: string;
|
|
29
|
+
/**
|
|
30
|
+
* The picture's pixel size, when the host knows it (a server that read the file). A picture with a size is laid
|
|
31
|
+
* out at once; one without is measured in the browser first, and the wall waits for that, so it is the page's
|
|
32
|
+
* largest paint only after every picture has loaded (CU-17tkuw5xp4q).
|
|
33
|
+
*/
|
|
34
|
+
width?: number;
|
|
35
|
+
height?: number;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
export type PhotoOrientation = 'tall' | 'wide' | 'square';
|
|
@@ -174,6 +181,14 @@ export interface PhotoWallProps {
|
|
|
174
181
|
className?: string;
|
|
175
182
|
/** The pictures are still being fetched: show the wall's own placeholder frames. */
|
|
176
183
|
loading?: boolean;
|
|
184
|
+
/**
|
|
185
|
+
* The width a column is drawn at, as an image `sizes` value (e.g. `(min-width: 1024px) 25vw, 50vw`). With it, and a
|
|
186
|
+
* size on every picture, the wall is laid out at once, on the server too, and each picture is drawn with the host's
|
|
187
|
+
* `srcset`: the first frames are in the HTML and fetched before any script runs, instead of after the wall has been
|
|
188
|
+
* measured in the browser (CU-17tkuw5xp4q: the careers hero's largest paint waited for that). Two walls given the
|
|
189
|
+
* same pictures and `sizes` (one per breakpoint) ask for the same files.
|
|
190
|
+
*/
|
|
191
|
+
sizes?: string;
|
|
177
192
|
}
|
|
178
193
|
|
|
179
194
|
export function PhotoWall({
|
|
@@ -183,6 +198,7 @@ export function PhotoWall({
|
|
|
183
198
|
fadeColor,
|
|
184
199
|
className,
|
|
185
200
|
loading: loadingProp = false,
|
|
201
|
+
sizes,
|
|
186
202
|
}: PhotoWallProps) {
|
|
187
203
|
// In a page's loading state the pictures are read: the wall draws its placeholder frames and measures nothing.
|
|
188
204
|
const inScope = useLoadingScope();
|
|
@@ -218,18 +234,36 @@ export function PhotoWall({
|
|
|
218
234
|
: undefined;
|
|
219
235
|
// One frame per picture: a url listed twice is its first entry. Nothing is
|
|
220
236
|
// asked for before the wall knows its width.
|
|
237
|
+
// Laid out without measuring: every picture's size is stated and the caller states the column's width.
|
|
238
|
+
const sizedUpFront = !!sizes && images.length > 0 && images.every(image => image.width && image.height);
|
|
221
239
|
const unique = useMemo(() => {
|
|
222
|
-
if (requestWidth === undefined
|
|
240
|
+
if (inScope || (!sizedUpFront && requestWidth === undefined)) return [];
|
|
223
241
|
const byUrl = new Map<string, PhotoWallImage>();
|
|
224
242
|
for (const image of images) if (!byUrl.has(image.url)) byUrl.set(image.url, image);
|
|
225
243
|
return Array.from(byUrl.values());
|
|
226
|
-
}, [images, requestWidth, inScope]);
|
|
244
|
+
}, [images, requestWidth, inScope, sizedUpFront]);
|
|
227
245
|
// Each picture is measured at the url it is drawn from (the boxed `Image`'s
|
|
228
246
|
// `optimizedWidth`: the host's resizer at the column's width), so it is downloaded once.
|
|
229
|
-
|
|
247
|
+
// Laid out up front, a picture is drawn from its own url with the host's `srcset` (`sizes`), on the server too.
|
|
248
|
+
const drawnWidth = sizedUpFront ? undefined : (requestWidth ?? undefined);
|
|
230
249
|
const drawnUrl = useCallback((url: string) => (drawnWidth ? resizedImageUrl(url, drawnWidth) : url), [drawnWidth]);
|
|
231
|
-
|
|
232
|
-
const
|
|
250
|
+
// A picture whose size the host stated has its orientation now; only the others are measured.
|
|
251
|
+
const known = useMemo(() => {
|
|
252
|
+
const byUrl = new Map<string, PhotoOrientation>();
|
|
253
|
+
for (const image of unique) {
|
|
254
|
+
if (image.width && image.height) byUrl.set(drawnUrl(image.url), photoOrientation(image.width, image.height));
|
|
255
|
+
}
|
|
256
|
+
return byUrl;
|
|
257
|
+
}, [unique, drawnUrl]);
|
|
258
|
+
const urls = useMemo(
|
|
259
|
+
() => unique.map(image => drawnUrl(image.url)).filter(url => !known.has(url)),
|
|
260
|
+
[unique, drawnUrl, known],
|
|
261
|
+
);
|
|
262
|
+
const probed = usePhotoOrientations(urls);
|
|
263
|
+
const orientations = useMemo(() => {
|
|
264
|
+
if (urls.length === 0) return known;
|
|
265
|
+
return probed ? new Map([...known, ...probed]) : null;
|
|
266
|
+
}, [urls, known, probed]);
|
|
233
267
|
|
|
234
268
|
const layout = useMemo(() => {
|
|
235
269
|
if (!orientations) return null;
|
|
@@ -254,7 +288,7 @@ export function PhotoWall({
|
|
|
254
288
|
|
|
255
289
|
if (images.length === 0 && !loading) return null;
|
|
256
290
|
// Until the pictures arrive AND their sizes are known, the same placeholder frames.
|
|
257
|
-
const columnsToDraw = loading || requestWidth === undefined ? null : layout;
|
|
291
|
+
const columnsToDraw = loading || (!sizedUpFront && requestWidth === undefined) ? null : layout;
|
|
258
292
|
|
|
259
293
|
const gridStyle = {
|
|
260
294
|
gridTemplateColumns: `repeat(${columnsToDraw?.length || columns}, minmax(0, 1fr))`,
|
|
@@ -290,10 +324,9 @@ export function PhotoWall({
|
|
|
290
324
|
alt={photo.alt ?? ''}
|
|
291
325
|
fill
|
|
292
326
|
className="object-cover"
|
|
293
|
-
// Asked for at the column's width
|
|
294
|
-
//
|
|
295
|
-
optimizedWidth
|
|
296
|
-
unoptimized
|
|
327
|
+
// Asked for at the column's width: through the host's `srcset` when the wall was laid
|
|
328
|
+
// out up front, else at the measured width (the caller's url when no step covers it).
|
|
329
|
+
{...(sizedUpFront ? { sizes } : { optimizedWidth: drawnWidth, unoptimized: true })}
|
|
297
330
|
// A column's first frames are on screen when the wall appears (one of them is
|
|
298
331
|
// usually the page's largest paint), so they are not left to lazy loading.
|
|
299
332
|
loading={i < EAGER_PHOTOS_PER_COLUMN ? 'eager' : 'lazy'}
|
|
@@ -657,7 +657,22 @@ export function TabNavigationSkeleton({
|
|
|
657
657
|
);
|
|
658
658
|
}
|
|
659
659
|
|
|
660
|
-
|
|
660
|
+
/**
|
|
661
|
+
* Only a URL-synced bar reads the URL. `useSearchParams()` in a component makes Next render the whole page in the
|
|
662
|
+
* browser when it is prerendered (or fail the build without a Suspense boundary), so a page whose tabs are plain
|
|
663
|
+
* controlled state (`urlSync` off, the default) must not call it: that is what kept every page with a tab strip
|
|
664
|
+
* (the flamingo homepage) from being served from a CDN (CU-17tkuw5xp4q). `urlSync` is fixed per bar; switching it
|
|
665
|
+
* remounts the bar.
|
|
666
|
+
*/
|
|
667
|
+
export function TabNavigation(props: TabNavigationProps) {
|
|
668
|
+
return props.urlSync ? <UrlSyncedTabNavigation {...props} /> : <TabNavigationBody {...props} searchParams={null} />;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function UrlSyncedTabNavigation(props: TabNavigationProps) {
|
|
672
|
+
return <TabNavigationBody {...props} searchParams={useSearchParams()} />;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function TabNavigationBody({
|
|
661
676
|
activeTab: controlledActiveTab,
|
|
662
677
|
onTabChange: controlledOnTabChange,
|
|
663
678
|
tabs,
|
|
@@ -673,12 +688,12 @@ export function TabNavigation({
|
|
|
673
688
|
defaultTab,
|
|
674
689
|
staticLabels = false,
|
|
675
690
|
children,
|
|
676
|
-
|
|
691
|
+
searchParams,
|
|
692
|
+
}: TabNavigationProps & { searchParams: ReturnType<typeof useSearchParams> | null }) {
|
|
677
693
|
const providedVariant = useContext(TabNavigationVariantContext);
|
|
678
694
|
const variant = variantProp ?? providedVariant;
|
|
679
695
|
const router = useRouter();
|
|
680
696
|
const pathname = usePathname();
|
|
681
|
-
const searchParams = useSearchParams();
|
|
682
697
|
|
|
683
698
|
// Determine URL sync settings
|
|
684
699
|
const isUrlSyncEnabled = !!urlSync;
|
|
@@ -126,6 +126,12 @@ export function useNearViewport<T extends Element = HTMLElement>(
|
|
|
126
126
|
|
|
127
127
|
elRef.current = node;
|
|
128
128
|
if (!node) return;
|
|
129
|
+
// No observer (an old browser, a test DOM): draw now. A gate that can never open would hide the content for
|
|
130
|
+
// good, and every caller's fallback is the content itself.
|
|
131
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
132
|
+
setIsNear(true);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
129
135
|
|
|
130
136
|
const cb = () => setIsNear(true);
|
|
131
137
|
subscribers.set(node, cb);
|
|
@@ -87,3 +87,12 @@ export function orderContentRefTypes(present: Iterable<string>): string[] {
|
|
|
87
87
|
const unregistered = [...presentSet].filter(type => !CONTENT_REF_GROUPS[type]);
|
|
88
88
|
return [...registeredInOrder, ...unregistered];
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
/** Items per page within one type group of the related-content rail. Groups larger than this paginate
|
|
92
|
+
* with the standard Pagination control (NO nested scrolling — a bounded
|
|
93
|
+
* scrollbox inside the page traps wheel events and hides the sections
|
|
94
|
+
* below it). MUST stay at or above the largest suggestion fill
|
|
95
|
+
* (RELATED_SAME_TYPE_COUNT in the hub's lib/constants/suggestions.ts) so
|
|
96
|
+
* current rails never paginate — only genuinely big groups (author pages)
|
|
97
|
+
* do. Server-safe here (no client boundary), so a server page can read a group's first page (`initialRows`). */
|
|
98
|
+
export const GROUP_PAGE_SIZE = 12;
|
|
@@ -125,3 +125,20 @@ export function clearDismissCookie(name: string): void {
|
|
|
125
125
|
fallbackStore(name).clear();
|
|
126
126
|
document.cookie = `${name}=; path=/; max-age=0`;
|
|
127
127
|
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The inline script that applies a dismissal BEFORE the first paint, for a page
|
|
131
|
+
* the server renders the same for every visitor (and a CDN may cache), so the
|
|
132
|
+
* server cannot read the cookie: placed early in `<body>`, it marks the
|
|
133
|
+
* document root with `attribute` when the cookie holds `id`, and the host's CSS
|
|
134
|
+
* hides the surface on that attribute. Without it the surface paints and then
|
|
135
|
+
* collapses once the bundle hydrates: a flash and a layout shift.
|
|
136
|
+
*
|
|
137
|
+
* Same match rule as `isDismissedCookieValue` (the id, never mere presence) and
|
|
138
|
+
* the same encoding `writeDismissCookie` uses. Cookie tier only: a
|
|
139
|
+
* cookie-averse document (a native shell) is not a cached web page. Any throw
|
|
140
|
+
* leaves the surface shown, the failing-open side.
|
|
141
|
+
*/
|
|
142
|
+
export function dismissedBeforePaintScript(name: string, id: string, attribute: string): string {
|
|
143
|
+
return `(function(){try{var n=${JSON.stringify(`${name}=`)},c=document.cookie.split('; ');for(var i=0;i<c.length;i++){if(c[i].indexOf(n)===0&&decodeURIComponent(c[i].slice(n.length))===${JSON.stringify(id)}){document.documentElement.setAttribute(${JSON.stringify(attribute)},'');break}}}catch(e){}})();`;
|
|
144
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -383,6 +383,7 @@ export type { FaqHashTarget } from './faq-anchor';
|
|
|
383
383
|
// pure + server-safe; the hub re-exports these from its config/util shims.
|
|
384
384
|
export {
|
|
385
385
|
CONTENT_REF_GROUPS,
|
|
386
|
+
GROUP_PAGE_SIZE,
|
|
386
387
|
getContentRefLabel,
|
|
387
388
|
getContentRefLabelOrTitleCase,
|
|
388
389
|
orderContentRefTypes,
|
package/src/utils/visitor-os.ts
CHANGED
|
@@ -82,3 +82,15 @@ export function usesCommandKey(os: VisitorOs | null): boolean {
|
|
|
82
82
|
export function shortcutLabel(os: VisitorOs | null, key: string): string {
|
|
83
83
|
return usesCommandKey(os) ? `⌘${key}` : `Ctrl ${key}`;
|
|
84
84
|
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The inline script that marks the document with the visitor's DESKTOP system before the first paint
|
|
88
|
+
* (`attribute` = `mac` | `windows` | `linux`, absent for a phone, a tablet or an unknown system), for a page the
|
|
89
|
+
* server renders the same for every visitor (and a CDN may cache): it draws every system's variant and the host's
|
|
90
|
+
* CSS shows the one the attribute names, so the right one is there from the first frame with no hydration
|
|
91
|
+
* mismatch. The same rule as `detectVisitorOs` (Client Hints platform, then the User-Agent, touch points telling an
|
|
92
|
+
* iPad from a Mac), held equal to it by the tests. Any throw leaves the attribute off: the host's no-system variant.
|
|
93
|
+
*/
|
|
94
|
+
export function desktopOsBeforePaintScript(attribute: string): string {
|
|
95
|
+
return `(function(){try{var n=window.navigator,h=n.userAgentData&&n.userAgentData.platform,p=String(h||'').replace(/"/g,'').trim().toLowerCase(),t=(n.maxTouchPoints||0)>1,o=null,m=${JSON.stringify(HINT_PLATFORMS)};if(m[p])o=m[p];else{var u=n.userAgent||'';if(/iPhone|iPad|iPod/i.test(u))o='ios';else if(/Android/i.test(u))o='android';else if(/Windows NT|Win64|Win32/i.test(u))o='windows';else if(/Macintosh|Mac OS X/i.test(u))o='mac';else if(/Linux|X11|CrOS/i.test(u))o='linux'}if(o==='mac'&&t)o='ios';if(o==='mac'||o==='windows'||o==='linux')document.documentElement.setAttribute(${JSON.stringify(attribute)},o)}catch(e){}})();`;
|
|
96
|
+
}
|