@flamingo-stack/openframe-frontend-core 0.0.773 → 0.0.774
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-7P2QXBWE.cjs → chunk-4JZC624S.cjs} +226 -206
- package/dist/{chunk-JJNOAYD4.cjs → chunk-5AOKXXHV.cjs} +4 -4
- package/dist/{chunk-MT5Z2LSV.cjs → chunk-5Z3XL2VU.cjs} +72 -72
- package/dist/{chunk-BPAYT72L.cjs → chunk-BTVO676B.cjs} +148 -131
- package/dist/{chunk-TZ4GKUO2.cjs → chunk-CZIMISAR.cjs} +5 -1
- package/dist/{chunk-YSKX42LW.cjs → chunk-E5E7IE7X.cjs} +12 -12
- package/dist/{chunk-XVFM6SJA.cjs → chunk-E64HPPRH.cjs} +31 -31
- package/dist/{chunk-CTRJNTYU.cjs → chunk-EQBJHJS7.cjs} +2 -2
- package/dist/{chunk-BMFU2CUL.cjs → chunk-HTSRAQVG.cjs} +4 -4
- 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-6LLF2QCS.cjs → chunk-LEVA5WO3.cjs} +39 -39
- package/dist/{chunk-CW3ML76U.cjs → chunk-NTPX5GXI.cjs} +4 -0
- package/dist/{chunk-VW2QQKQ6.cjs → chunk-NX7P2T2U.cjs} +15 -15
- package/dist/{chunk-7J4YWDT6.cjs → chunk-Q6B2DC65.cjs} +3 -3
- package/dist/{chunk-NPCFVTA2.cjs → chunk-Q7EXOVMT.cjs} +31 -31
- package/dist/{chunk-2A56MLLL.cjs → chunk-SWEPHSIP.cjs} +4 -4
- package/dist/{chunk-KSCEAIBS.cjs → chunk-TOPGBOCN.cjs} +2 -2
- package/dist/{chunk-I4ZVMNHO.cjs → chunk-TTQHSSHF.cjs} +6 -6
- package/dist/{chunk-FPCGKH5K.cjs → chunk-TUXPFF4P.cjs} +31 -29
- package/dist/{chunk-42YLIZFD.cjs → chunk-U332FL4C.cjs} +22 -22
- package/dist/{chunk-3FREBLAJ.cjs → chunk-UAKXZ2KD.cjs} +47 -45
- package/dist/{chunk-SE76YGR3.cjs → chunk-USMR73ND.cjs} +4 -4
- package/dist/{chunk-X5Z4HRH7.cjs → chunk-VISK6LKE.cjs} +2 -2
- package/dist/{chunk-HEEK42BL.cjs → chunk-VWUQVJ2D.cjs} +81 -81
- package/dist/{chunk-5V7BIUGA.cjs → chunk-Y3EBUFX4.cjs} +92 -21
- package/dist/{chunk-5HBGBLJT.cjs → chunk-ZHGOXDA3.cjs} +63 -59
- package/dist/{cloud-tenants-EE5ULFOK.cjs → cloud-tenants-7RNZ6GHM.cjs} +11 -11
- 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 +63 -61
- 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/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 +11 -7
- package/dist/components/tickets/index.cjs +17 -17
- package/dist/components/ui/index.cjs +13 -11
- package/dist/components/ui/photo-wall.d.ts +7 -0
- package/dist/components/ui/photo-wall.d.ts.map +1 -1
- package/dist/components/ui/photo-wall.js +16 -2
- 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-NNDBC6IH.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-DPUMWQED.cjs} +12 -12
- package/dist/{logs-GUV3BMGC.cjs → logs-EE25MWYM.cjs} +19 -19
- package/dist/{policies-HPMTDOAS.cjs → policies-H5LPZWEM.cjs} +15 -15
- package/dist/{scripts-QYYKCIUW.cjs → scripts-YGO4LOES.cjs} +16 -16
- package/dist/{software-update-NC2KZKEI.cjs → software-update-5BEB62ZH.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/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/index.ts +2 -1
- package/src/components/related-content/related-content-section.tsx +26 -12
- package/src/components/ui/photo-wall.tsx +24 -2
- 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
|
@@ -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"
|
|
@@ -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,
|
|
@@ -259,10 +260,18 @@ function CardForType({
|
|
|
259
260
|
// refetch, no cross-mount cache.
|
|
260
261
|
// =============================================================================
|
|
261
262
|
|
|
262
|
-
function useGroupItems(
|
|
263
|
+
function useGroupItems(
|
|
264
|
+
type: string,
|
|
265
|
+
refs: ContentRef[],
|
|
266
|
+
buildUrl: (type: string, ids: string[]) => string | null,
|
|
267
|
+
seedRows?: ReadonlyArray<unknown>,
|
|
268
|
+
) {
|
|
263
269
|
const ids = refs.map(r => r.id);
|
|
264
270
|
const url = ids.length > 0 ? buildUrl(type, ids) : null;
|
|
265
|
-
|
|
271
|
+
// The page the server already read (`initialRows`) is in hand from the first render, the server's included.
|
|
272
|
+
const [pages, setPages] = useState<ReadonlyMap<string, unknown[]>>(() =>
|
|
273
|
+
seedRows && url !== null ? new Map([[url, [...seedRows]]]) : new Map(),
|
|
274
|
+
);
|
|
266
275
|
const cached = url !== null ? pages.get(url) : undefined;
|
|
267
276
|
const { data, dataUrl, error } = useSelfFetch<unknown>(cached ? null : url);
|
|
268
277
|
if (data != null && dataUrl !== null && !pages.has(dataUrl)) {
|
|
@@ -302,15 +311,8 @@ function resolveGroupConfig(type: string): ContentRefGroupConfig {
|
|
|
302
311
|
);
|
|
303
312
|
}
|
|
304
313
|
|
|
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;
|
|
314
|
+
// The page size (`GROUP_PAGE_SIZE`) lives with the groups' other settings (`utils/content-ref-groups`, server-safe), so
|
|
315
|
+
// a server page can read a group's first page for `initialRows`.
|
|
314
316
|
|
|
315
317
|
/** A group's layout, read by the group and by the rail's skeleton, so the skeleton is the rail's size. */
|
|
316
318
|
const GROUP_CLASS = 'space-y-4';
|
|
@@ -358,9 +360,12 @@ function ContentGroup({
|
|
|
358
360
|
adminCampaignCard,
|
|
359
361
|
heading,
|
|
360
362
|
onLoadingChange,
|
|
363
|
+
seedRows,
|
|
361
364
|
}: {
|
|
362
365
|
type: string;
|
|
363
366
|
refs: ContentRef[];
|
|
367
|
+
/** The rows of this group's FIRST page, read by the server (`initialRows`). */
|
|
368
|
+
seedRows?: ReadonlyArray<unknown>;
|
|
364
369
|
columns: 2 | 3;
|
|
365
370
|
buildUrl: (type: string, ids: string[]) => string | null;
|
|
366
371
|
resolveHref: (ref: ContentRef) => { href: string | null; targetPlatform: string | null };
|
|
@@ -392,7 +397,7 @@ function ContentGroup({
|
|
|
392
397
|
}
|
|
393
398
|
const totalGroupPages = pageCount(refs.length, GROUP_PAGE_SIZE);
|
|
394
399
|
const pageRefs = refs.slice((page - 1) * GROUP_PAGE_SIZE, page * GROUP_PAGE_SIZE);
|
|
395
|
-
const { items, isLoading } = useGroupItems(type, pageRefs, buildUrl);
|
|
400
|
+
const { items, isLoading } = useGroupItems(type, pageRefs, buildUrl, seedRows);
|
|
396
401
|
const groupPagination =
|
|
397
402
|
totalGroupPages > 1 ? <Pagination currentPage={page} totalPages={totalGroupPages} onPageChange={setPage} /> : null;
|
|
398
403
|
useEffect(() => {
|
|
@@ -540,6 +545,13 @@ export interface RelatedContentSectionProps {
|
|
|
540
545
|
* drills the refs here; the first client fetch is skipped (useSelfFetch
|
|
541
546
|
* initialData contract). */
|
|
542
547
|
initialItems?: ContentRefWithReason[];
|
|
548
|
+
/**
|
|
549
|
+
* The rows of each group's FIRST page, read by the server, keyed by the refs' `type`: exactly what the group's list
|
|
550
|
+
* URL answers for those ids (`buildListUrl(type, firstPageIds)`). The cards are then in the server HTML instead of
|
|
551
|
+
* appearing after a request per group in the browser, which on an author page made a card the page's largest paint
|
|
552
|
+
* seconds after load (CU-17tkuw5xp4q). A group with no entry fetches as before; later pages always fetch.
|
|
553
|
+
*/
|
|
554
|
+
initialRows?: Readonly<Record<string, ReadonlyArray<unknown>>>;
|
|
543
555
|
/** Section title (default: "Related Content") */
|
|
544
556
|
title?: string;
|
|
545
557
|
/**
|
|
@@ -612,6 +624,7 @@ export function RelatedContentSection({
|
|
|
612
624
|
sameTypeMinResults,
|
|
613
625
|
includeTypes,
|
|
614
626
|
initialItems,
|
|
627
|
+
initialRows,
|
|
615
628
|
title = 'Related Content',
|
|
616
629
|
columns = 2,
|
|
617
630
|
excludeTypes,
|
|
@@ -787,6 +800,7 @@ export function RelatedContentSection({
|
|
|
787
800
|
adminCampaignCard={adminCampaignCard}
|
|
788
801
|
heading={<GroupHeading type={type} />}
|
|
789
802
|
onLoadingChange={onGroupLoadingChange}
|
|
803
|
+
seedRows={initialRows?.[type]}
|
|
790
804
|
/>
|
|
791
805
|
))}
|
|
792
806
|
</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';
|
|
@@ -228,8 +235,23 @@ export function PhotoWall({
|
|
|
228
235
|
// `optimizedWidth`: the host's resizer at the column's width), so it is downloaded once.
|
|
229
236
|
const drawnWidth = requestWidth ?? undefined;
|
|
230
237
|
const drawnUrl = useCallback((url: string) => (drawnWidth ? resizedImageUrl(url, drawnWidth) : url), [drawnWidth]);
|
|
231
|
-
|
|
232
|
-
const
|
|
238
|
+
// A picture whose size the host stated has its orientation now; only the others are measured.
|
|
239
|
+
const known = useMemo(() => {
|
|
240
|
+
const byUrl = new Map<string, PhotoOrientation>();
|
|
241
|
+
for (const image of unique) {
|
|
242
|
+
if (image.width && image.height) byUrl.set(drawnUrl(image.url), photoOrientation(image.width, image.height));
|
|
243
|
+
}
|
|
244
|
+
return byUrl;
|
|
245
|
+
}, [unique, drawnUrl]);
|
|
246
|
+
const urls = useMemo(
|
|
247
|
+
() => unique.map(image => drawnUrl(image.url)).filter(url => !known.has(url)),
|
|
248
|
+
[unique, drawnUrl, known],
|
|
249
|
+
);
|
|
250
|
+
const probed = usePhotoOrientations(urls);
|
|
251
|
+
const orientations = useMemo(() => {
|
|
252
|
+
if (urls.length === 0) return known;
|
|
253
|
+
return probed ? new Map([...known, ...probed]) : null;
|
|
254
|
+
}, [urls, known, probed]);
|
|
233
255
|
|
|
234
256
|
const layout = useMemo(() => {
|
|
235
257
|
if (!orientations) return null;
|
|
@@ -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
|
+
}
|