@flamingo-stack/openframe-frontend-core 0.0.705-snapshot.20261006160157 → 0.0.705
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/{chunk-WCVZCGK5.cjs → chunk-2QEUFXNS.cjs} +71 -312
- package/dist/{chunk-N45D4ZXQ.js → chunk-2YXVWZVF.js} +715 -959
- package/dist/{chunk-7OFPU7WE.cjs → chunk-3MX5S4CF.cjs} +2547 -877
- package/dist/{chunk-MB4PNY7C.cjs → chunk-4I45Y3DN.cjs} +6 -6
- package/dist/{chunk-K6O43WDL.cjs → chunk-4KX6OED2.cjs} +46 -46
- package/dist/{chunk-JPG7OHEG.js → chunk-5HBHO5TB.js} +1 -1
- package/dist/{chunk-3PLDQDE6.cjs → chunk-6NJKKO73.cjs} +15 -15
- package/dist/{chunk-O2RSAA5M.js → chunk-6SEANMOU.js} +11 -11
- package/dist/{chunk-WMX26HB5.js → chunk-6XIF2Z7I.js} +3 -3
- package/dist/{chunk-TQ4LB2DS.cjs → chunk-7LS22BZU.cjs} +14 -14
- package/dist/{chunk-7GEBFJT2.js → chunk-7VVFKJY5.js} +3429 -1759
- package/dist/{chunk-DDVBX42E.cjs → chunk-A75QY6CD.cjs} +21 -21
- package/dist/{chunk-OF6GEI5A.cjs → chunk-ABU3TGXA.cjs} +3 -3
- package/dist/{chunk-4CJMQLB5.js → chunk-AQWZNP6A.js} +4 -4
- package/dist/{chunk-FI6GMDO7.js → chunk-CH5232HV.js} +662 -2455
- package/dist/{chunk-Y2GSOUCS.cjs → chunk-D7BTSVMS.cjs} +2 -2
- package/dist/{chunk-4ILM7W2R.js → chunk-DENB2XJC.js} +2 -2
- package/dist/{chunk-6232S532.js → chunk-DW7G4KG3.js} +3 -3
- package/dist/{chunk-3NCM4ZQB.js → chunk-F6AFPNDF.js} +15 -15
- package/dist/{chunk-PXOQIA5K.js → chunk-F6I6XZMN.js} +5 -5
- package/dist/{chunk-G6UMNAVI.js → chunk-FCGC42TS.js} +61 -57
- package/dist/{chunk-BHI6GVC4.js → chunk-G5A3PSPV.js} +691 -1080
- package/dist/{chunk-HZ7CBXPL.cjs → chunk-HPWWNJE7.cjs} +89 -89
- package/dist/{chunk-SW5ZV2BN.cjs → chunk-IHERL5V4.cjs} +34 -34
- package/dist/{chunk-UJ33ZXDT.cjs → chunk-IKTQ3ZAG.cjs} +6 -6
- package/dist/{chunk-SEKYEWQG.cjs → chunk-JEZKGCZI.cjs} +26 -26
- package/dist/{chunk-7SCNMP2B.cjs → chunk-KOWVJD4V.cjs} +56 -56
- package/dist/chunk-LMBC5FFP.js +43 -0
- package/dist/{chunk-F57ECE2I.js → chunk-MFFCUVWG.js} +1 -1
- package/dist/{chunk-KUC673HM.js → chunk-MKXO4Q5P.js} +142 -383
- package/dist/{chunk-6NTPY5QA.cjs → chunk-MMOODW57.cjs} +94 -1887
- package/dist/{chunk-CUME3VTQ.js → chunk-MP2C3LJ4.js} +1 -1
- package/dist/{chunk-VX4E6YSB.cjs → chunk-N3COXDBH.cjs} +5 -41
- package/dist/{chunk-FXJ3WWUB.js → chunk-NDESEPW5.js} +0 -68
- package/dist/chunk-O43V3EOS.js +252 -0
- package/dist/{chunk-PLYEKV3N.cjs → chunk-OHFS7VOU.cjs} +2 -2
- package/dist/{chunk-M75XEEGT.cjs → chunk-OUY3HHW6.cjs} +539 -928
- package/dist/{chunk-CZ7Y7UG4.js → chunk-PDWVPI3S.js} +72 -4
- package/dist/{chunk-TE773HEH.js → chunk-PN2RKPDF.js} +10 -10
- package/dist/{chunk-3EZ4HHAP.js → chunk-QFXRPRFT.js} +3 -3
- package/dist/chunk-QZVSJF2K.cjs +252 -0
- package/dist/chunk-RALRPFDW.cjs +43 -0
- package/dist/{chunk-YGJKKDDM.js → chunk-RBDP4WDE.js} +6 -6
- package/dist/{chunk-LM7TXMLU.cjs → chunk-RKV5B7EU.cjs} +4 -72
- package/dist/{chunk-UVTD52MD.cjs → chunk-TYWLNCNH.cjs} +82 -14
- package/dist/{chunk-IKJA3Y7Z.js → chunk-VA5T32XA.js} +2 -2
- package/dist/{chunk-GWQEYPYM.cjs → chunk-VESKITRQ.cjs} +732 -728
- package/dist/{chunk-RQRX4YC3.cjs → chunk-VHCACDKP.cjs} +39 -39
- package/dist/{chunk-4IBPRIQH.js → chunk-VR2ZLFCP.js} +1 -1
- package/dist/{chunk-RTIPR53L.cjs → chunk-WA6GUYQ4.cjs} +104 -104
- package/dist/{chunk-T5TMCHWM.js → chunk-X5MQ7JRP.js} +9 -9
- package/dist/{chunk-LLSUVPQC.cjs → chunk-X72J4ACL.cjs} +8 -252
- package/dist/{chunk-UDFRUKLC.cjs → chunk-XOYH2YQE.cjs} +2 -2
- package/dist/{chunk-LVSFKENJ.cjs → chunk-YFBM2K6X.cjs} +4 -4
- package/dist/{chunk-DWPAAWMR.js → chunk-YHM5AWTT.js} +6 -6
- package/dist/{chunk-NCPTID44.js → chunk-Z2VNQQ74.js} +942 -978
- package/dist/{chunk-STHZBOQI.cjs → chunk-ZSAGLQOD.cjs} +17 -17
- package/dist/{chunk-VZICIQW2.js → chunk-ZSAO2HBW.js} +15 -15
- package/dist/components/case-studies/index.cjs +32 -30
- package/dist/components/case-studies/index.js +16 -14
- package/dist/components/chat/entity-cards/case-study-card.d.ts +4 -11
- package/dist/components/chat/entity-cards/case-study-card.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +11 -9
- package/dist/components/chat/index.js +16 -14
- package/dist/components/chat/utils/icon-library.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +14 -12
- package/dist/components/contact/index.js +13 -11
- package/dist/components/docs/index.cjs +17 -15
- package/dist/components/docs/index.js +16 -14
- package/dist/components/embeds/index.cjs +14 -12
- package/dist/components/embeds/index.js +13 -11
- package/dist/components/faq/index.cjs +15 -13
- package/dist/components/faq/index.js +14 -12
- package/dist/components/features/index.cjs +14 -12
- package/dist/components/features/index.js +23 -21
- package/dist/components/footer-waitlist-button.d.ts +29 -0
- package/dist/components/footer-waitlist-button.d.ts.map +1 -0
- package/dist/components/footer.d.ts +47 -0
- package/dist/components/footer.d.ts.map +1 -0
- package/dist/components/help-center-pages/index.cjs +120 -118
- package/dist/components/help-center-pages/index.js +35 -33
- package/dist/components/icons/index.cjs +5 -3
- package/dist/components/icons/index.js +11 -9
- package/dist/components/icons-v2-generated/index.cjs +5 -3
- package/dist/components/icons-v2-generated/index.js +4 -2
- package/dist/components/index.cjs +159 -86
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +402 -329
- package/dist/components/loading/page-layout-skeleton.d.ts +1 -1
- package/dist/components/meeting-scheduler/index.cjs +64 -62
- package/dist/components/meeting-scheduler/index.js +14 -12
- package/dist/components/navigation/client-only-header.d.ts +8 -0
- package/dist/components/navigation/client-only-header.d.ts.map +1 -0
- package/dist/components/navigation/header-skeleton.d.ts +12 -0
- package/dist/components/navigation/header-skeleton.d.ts.map +1 -0
- package/dist/components/navigation/header.d.ts +9 -0
- package/dist/components/navigation/header.d.ts.map +1 -0
- package/dist/components/navigation/index.cjs +16 -28
- package/dist/components/navigation/index.d.ts +10 -14
- package/dist/components/navigation/index.d.ts.map +1 -1
- package/dist/components/navigation/index.js +26 -38
- package/dist/components/navigation/mingo-ai-button.d.ts +3 -38
- package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
- package/dist/components/navigation/mobile-nav-panel.d.ts +10 -0
- package/dist/components/navigation/mobile-nav-panel.d.ts.map +1 -0
- package/dist/components/navigation/navigation-sidebar-item.d.ts +2 -1
- package/dist/components/navigation/navigation-sidebar-item.d.ts.map +1 -1
- package/dist/components/navigation/top-navigation.d.ts +2 -2
- package/dist/components/navigation/top-navigation.d.ts.map +1 -1
- package/dist/components/onboarding-guides/index.cjs +17 -15
- package/dist/components/onboarding-guides/index.js +16 -14
- package/dist/components/related-content/index.cjs +8 -10
- package/dist/components/related-content/index.js +7 -9
- package/dist/components/tickets/index.cjs +18 -16
- package/dist/components/tickets/index.js +17 -15
- package/dist/components/ui/index.cjs +14 -12
- package/dist/components/ui/index.js +19 -17
- package/dist/hooks/index.cjs +11 -19
- package/dist/hooks/index.d.ts +0 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +11 -19
- package/dist/hooks/ui/index.d.ts +0 -3
- package/dist/hooks/ui/index.d.ts.map +1 -1
- package/dist/index.cjs +22 -55
- package/dist/index.js +196 -229
- package/dist/types/case-study.d.ts +0 -9
- package/dist/types/case-study.d.ts.map +1 -1
- package/dist/types/navigation.d.ts +65 -95
- package/dist/types/navigation.d.ts.map +1 -1
- package/dist/utils/index.cjs +0 -55
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -50
- package/dist/utils/source-icons.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/entity-cards/case-study-card.tsx +5 -115
- package/src/components/chat/utils/icon-library.tsx +1 -35
- package/src/components/footer-waitlist-button.tsx +53 -0
- package/src/components/footer.tsx +195 -0
- package/src/components/index.ts +2 -0
- package/src/components/loading/page-layout-skeleton.tsx +1 -1
- package/src/components/navigation/client-only-header.tsx +22 -0
- package/src/components/navigation/header-skeleton.tsx +74 -0
- package/src/components/navigation/header.tsx +429 -0
- package/src/components/navigation/index.ts +14 -21
- package/src/components/navigation/mingo-ai-button.tsx +63 -139
- package/src/components/navigation/mobile-nav-panel.tsx +155 -0
- package/src/components/navigation/navigation-sidebar-item.tsx +2 -1
- package/src/components/navigation/ticket-alerts-button.tsx +2 -2
- package/src/components/navigation/top-navigation.tsx +2 -2
- package/src/hooks/index.ts +0 -1
- package/src/hooks/ui/index.ts +0 -3
- package/src/styles/chat-animations.css +2 -57
- package/src/types/case-study.ts +0 -16
- package/src/types/navigation.ts +66 -80
- package/src/utils/index.ts +0 -1
- package/src/utils/source-icons.ts +0 -13
- package/dist/components/navigation/mobile-nav-sheet-id.d.ts +0 -3
- package/dist/components/navigation/mobile-nav-sheet-id.d.ts.map +0 -1
- package/dist/components/navigation/mobile-nav-sheet.d.ts +0 -48
- package/dist/components/navigation/mobile-nav-sheet.d.ts.map +0 -1
- package/dist/components/navigation/nav-item-row.d.ts +0 -27
- package/dist/components/navigation/nav-item-row.d.ts.map +0 -1
- package/dist/components/navigation/nav-panel.d.ts +0 -43
- package/dist/components/navigation/nav-panel.d.ts.map +0 -1
- package/dist/components/navigation/site-footer.d.ts +0 -28
- package/dist/components/navigation/site-footer.d.ts.map +0 -1
- package/dist/components/navigation/site-header.d.ts +0 -61
- package/dist/components/navigation/site-header.d.ts.map +0 -1
- package/dist/components/navigation/site-nav-link.d.ts +0 -26
- package/dist/components/navigation/site-nav-link.d.ts.map +0 -1
- package/dist/hooks/ui/use-active-section.d.ts +0 -10
- package/dist/hooks/ui/use-active-section.d.ts.map +0 -1
- package/dist/hooks/ui/use-nav-menus.d.ts +0 -48
- package/dist/hooks/ui/use-nav-menus.d.ts.map +0 -1
- package/dist/hooks/ui/use-visitor-os.d.ts +0 -15
- package/dist/hooks/ui/use-visitor-os.d.ts.map +0 -1
- package/dist/hooks/use-location-hash.d.ts +0 -9
- package/dist/hooks/use-location-hash.d.ts.map +0 -1
- package/dist/utils/visitor-os.d.ts +0 -45
- package/dist/utils/visitor-os.d.ts.map +0 -1
- package/src/components/navigation/mobile-nav-sheet-id.ts +0 -2
- package/src/components/navigation/mobile-nav-sheet.tsx +0 -241
- package/src/components/navigation/nav-item-row.tsx +0 -129
- package/src/components/navigation/nav-panel.tsx +0 -196
- package/src/components/navigation/site-footer.tsx +0 -256
- package/src/components/navigation/site-header.tsx +0 -339
- package/src/components/navigation/site-nav-link.tsx +0 -41
- package/src/hooks/ui/use-active-section.ts +0 -44
- package/src/hooks/ui/use-nav-menus.ts +0 -200
- package/src/hooks/ui/use-visitor-os.ts +0 -33
- package/src/hooks/use-location-hash.ts +0 -34
- package/src/utils/visitor-os.ts +0 -84
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* CaseStudyCard (pure presentation).
|
|
5
|
-
* detail)
|
|
6
|
-
* strips) and `menu` (the site menu's card, hover-plays the highlight cut).
|
|
4
|
+
* CaseStudyCard (pure presentation). Two densities — `default` (vertical
|
|
5
|
+
* detail) and `sm` (compact horizontal for chat-inline).
|
|
7
6
|
*
|
|
8
7
|
* The card writes NO click logic — callers wrap with their own anchor
|
|
9
8
|
* and pass the resolved detail URL via `href`.
|
|
@@ -13,15 +12,9 @@
|
|
|
13
12
|
* `useOgPlaceholderUrl(...)`) → `bg-ods-bg`.
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
|
-
import { useState } from 'react';
|
|
17
15
|
import Image from '../../../embed-shims/next-image';
|
|
18
|
-
import
|
|
19
|
-
import type { CaseStudyCardData } from '../../../types/case-study';
|
|
16
|
+
import type { CaseStudy } from '../../../types/case-study';
|
|
20
17
|
import { cn } from '../../../utils/cn';
|
|
21
|
-
import { sortBitesByFeaturedAtDesc } from '../../features/video-bites-shared';
|
|
22
|
-
import { CardHoverPlay } from '../../features/video-center-badge';
|
|
23
|
-
import { VideoHoverPreviewSurface } from '../../features/video-hover-preview';
|
|
24
|
-
import { EntityIcon } from '../../icon-display';
|
|
25
18
|
import { Card } from '../../ui/card';
|
|
26
19
|
import {
|
|
27
20
|
COMPACT_CARD_IMAGE_SLOT,
|
|
@@ -43,7 +36,7 @@ import { useEntityCardLink } from './use-entity-card-link';
|
|
|
43
36
|
import { useEntityCardPlaceholder } from './use-entity-card-placeholder';
|
|
44
37
|
|
|
45
38
|
export interface CaseStudyCardProps {
|
|
46
|
-
study:
|
|
39
|
+
study: CaseStudy;
|
|
47
40
|
/** Detail URL resolved by the caller. */
|
|
48
41
|
href: string;
|
|
49
42
|
/** When `_blank`, opens in a new tab. Set by chat dispatch via
|
|
@@ -53,14 +46,7 @@ export interface CaseStudyCardProps {
|
|
|
53
46
|
targetPlatform?: string | null;
|
|
54
47
|
/** OG placeholder URL, used when `study.featured_image` is missing. */
|
|
55
48
|
placeholderUrl?: string | null;
|
|
56
|
-
|
|
57
|
-
* focus (the video bites' hover grammar), then who it is about and who tells it. */
|
|
58
|
-
size?: 'default' | 'sm' | 'portrait' | 'menu';
|
|
59
|
-
/** `menu`: mount the hover player (the host passes whether its panel is open,
|
|
60
|
-
* so a closed menu holds no player). Omitted: the card's own viewport gate. */
|
|
61
|
-
mediaMounted?: boolean;
|
|
62
|
-
/** `menu`: runs on click, before the navigation (the host closes its panel). */
|
|
63
|
-
onNavigate?: () => void;
|
|
49
|
+
size?: 'default' | 'sm' | 'portrait';
|
|
64
50
|
/** Portrait density: render the content-type chip. Mixed rails only; single-type rails pass false. Default true. */
|
|
65
51
|
showTypeBadge?: boolean;
|
|
66
52
|
className?: string;
|
|
@@ -106,22 +92,6 @@ export function CaseStudyCardSkeleton({ size = 'default' }: { size?: 'default' |
|
|
|
106
92
|
);
|
|
107
93
|
}
|
|
108
94
|
|
|
109
|
-
/**
|
|
110
|
-
* A story's HIGHLIGHT cut, the short video a card previews: its highlight
|
|
111
|
-
* video, else the bite an admin featured (the newest when several are; featuring
|
|
112
|
-
* is independent of the bite's own `published` flag, as on every featured surface). Never
|
|
113
|
-
* the full video: that plays on the story's own page. Null when it has neither.
|
|
114
|
-
*/
|
|
115
|
-
function caseStudyHighlight(study: CaseStudyCardData): { url: string; posterUrl: string | null } | null {
|
|
116
|
-
if (study.highlight_video_url) {
|
|
117
|
-
return { url: study.highlight_video_url, posterUrl: study.highlight_video_thumbnail ?? null };
|
|
118
|
-
}
|
|
119
|
-
const bite = (study.video_bites ?? [])
|
|
120
|
-
.filter(candidate => candidate.featured === true && candidate.url)
|
|
121
|
-
.sort(sortBitesByFeaturedAtDesc)[0];
|
|
122
|
-
return bite ? { url: bite.url, posterUrl: bite.thumbnail_url ?? null } : null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
95
|
export function CaseStudyCard({
|
|
126
96
|
study,
|
|
127
97
|
href,
|
|
@@ -131,11 +101,8 @@ export function CaseStudyCard({
|
|
|
131
101
|
placeholderUrl: placeholderUrlProp,
|
|
132
102
|
size = 'default',
|
|
133
103
|
showTypeBadge = true,
|
|
134
|
-
mediaMounted,
|
|
135
|
-
onNavigate,
|
|
136
104
|
className,
|
|
137
105
|
}: CaseStudyCardProps) {
|
|
138
|
-
const [hovered, setHovered] = useState(false);
|
|
139
106
|
const { target, rel } = useEntityCardLink({
|
|
140
107
|
href,
|
|
141
108
|
targetPlatform,
|
|
@@ -182,83 +149,6 @@ export function CaseStudyCard({
|
|
|
182
149
|
);
|
|
183
150
|
}
|
|
184
151
|
|
|
185
|
-
if (size === 'menu') {
|
|
186
|
-
const preview = caseStudyHighlight(study);
|
|
187
|
-
const videoUrl = preview?.url ?? null;
|
|
188
|
-
const hasAnyVideo = !!(videoUrl || study.main_video_url);
|
|
189
|
-
// The cover is the FULL video's poster (wide, like the card); the highlight
|
|
190
|
-
// cut's own frame is usually upright and would not fill a 16:9 slot.
|
|
191
|
-
const poster = study.main_video_thumbnail || study.featured_image || preview?.posterUrl || null;
|
|
192
|
-
return (
|
|
193
|
-
<Link
|
|
194
|
-
href={href}
|
|
195
|
-
target={target}
|
|
196
|
-
rel={rel}
|
|
197
|
-
onClick={onNavigate}
|
|
198
|
-
onPointerEnter={() => setHovered(true)}
|
|
199
|
-
onPointerLeave={() => setHovered(false)}
|
|
200
|
-
onFocus={() => setHovered(true)}
|
|
201
|
-
onBlur={() => setHovered(false)}
|
|
202
|
-
className={cn(
|
|
203
|
-
// `group/card` is what the shared play glyph accents on.
|
|
204
|
-
'group/card group flex min-w-0 flex-col gap-[var(--spacing-system-sf)] rounded-xl border border-ods-border bg-ods-bg p-[var(--spacing-system-mf)] text-ods-text-primary transition-colors hover:bg-ods-bg-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ods-accent',
|
|
205
|
-
className,
|
|
206
|
-
)}
|
|
207
|
-
>
|
|
208
|
-
{/* The media never takes the click: the whole card is the link. */}
|
|
209
|
-
<span className="pointer-events-none relative block aspect-video w-full shrink-0 overflow-hidden rounded-md bg-ods-bg">
|
|
210
|
-
{videoUrl ? (
|
|
211
|
-
// The shared hover surface, filled the way the walkthrough card fills
|
|
212
|
-
// its own 16:9 slot (`cover`): an upright cut is cropped to the slot,
|
|
213
|
-
// never letterboxed. Sound on hover, muted when the browser blocks it.
|
|
214
|
-
<VideoHoverPreviewSurface
|
|
215
|
-
url={videoUrl}
|
|
216
|
-
posterUrl={poster}
|
|
217
|
-
active={hovered}
|
|
218
|
-
playerMounted={mediaMounted}
|
|
219
|
-
fit="cover"
|
|
220
|
-
posterSizes="360px"
|
|
221
|
-
/>
|
|
222
|
-
) : (
|
|
223
|
-
<>
|
|
224
|
-
{(poster ?? placeholderUrl) && (
|
|
225
|
-
<Image
|
|
226
|
-
src={poster ?? placeholderUrl ?? ''}
|
|
227
|
-
alt=""
|
|
228
|
-
className="object-cover"
|
|
229
|
-
fill
|
|
230
|
-
sizes="360px"
|
|
231
|
-
unoptimized
|
|
232
|
-
onError={hideOnError}
|
|
233
|
-
/>
|
|
234
|
-
)}
|
|
235
|
-
{hasAnyVideo && <CardHoverPlay size="md" />}
|
|
236
|
-
</>
|
|
237
|
-
)}
|
|
238
|
-
</span>
|
|
239
|
-
{study.msp?.name && (
|
|
240
|
-
<span className="flex items-center gap-[var(--spacing-system-xsf)] text-ods-text-secondary text-h6">
|
|
241
|
-
{study.msp.icon_url && (
|
|
242
|
-
<EntityIcon
|
|
243
|
-
icon={{ url: study.msp.icon_url }}
|
|
244
|
-
size={20}
|
|
245
|
-
className="size-5 shrink-0 rounded object-contain"
|
|
246
|
-
/>
|
|
247
|
-
)}
|
|
248
|
-
<span className="min-w-0 truncate">{study.msp.name}</span>
|
|
249
|
-
</span>
|
|
250
|
-
)}
|
|
251
|
-
<span className="line-clamp-2 text-h6">{study.title}</span>
|
|
252
|
-
{study.user?.full_name && (
|
|
253
|
-
<span className="truncate text-ods-text-muted text-h6">
|
|
254
|
-
{study.user.job_title ? `${study.user.full_name}, ${study.user.job_title}` : study.user.full_name}
|
|
255
|
-
</span>
|
|
256
|
-
)}
|
|
257
|
-
<span className="mt-auto text-ods-accent text-h6">{hasAnyVideo ? 'Watch the story' : 'Read the story'}</span>
|
|
258
|
-
</Link>
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
152
|
if (size === 'portrait') {
|
|
263
153
|
// Rail/strip density — shared <EntityPortraitCard> shell.
|
|
264
154
|
return (
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
|
|
31
31
|
import { Megaphone, Bell, Info, Star, Package as PackageGlyph } from 'lucide-react';
|
|
32
32
|
import type { ComponentType, CSSProperties } from 'react';
|
|
33
|
-
import { platformAccentVarName } from '../../../utils/platform-identity';
|
|
34
33
|
import * as IconsV2 from '../../icons-v2-generated';
|
|
35
34
|
import { HeadphoneIcon } from '../../icons-v2-generated/audio-and-visual/headphone-icon';
|
|
36
35
|
import { VideoRecorderIcon } from '../../icons-v2-generated/audio-and-visual/video-recorder-icon';
|
|
@@ -53,8 +52,6 @@ import { GraduationCapIcon } from '../../icons-v2-generated/school/graduation-ca
|
|
|
53
52
|
import { TicketIcon } from '../../icons-v2-generated/shopping/ticket-icon';
|
|
54
53
|
import { Rocket02Icon } from '../../icons-v2-generated/vehicles-and-delivery/rocket-02-icon';
|
|
55
54
|
import { FlamingoLogo } from '../../icons/flamingo-logo';
|
|
56
|
-
import { MiamiCyberGangLogoFaceOnly } from '../../icons/miami-cyber-gang-logo-face-only';
|
|
57
|
-
import { MlgLogo } from '../../icons/mlg-logo';
|
|
58
55
|
import { OpenFrameLogo } from '../../icons/openframe-logo';
|
|
59
56
|
import { OpenmspLogo } from '../../icons/openmsp-logo';
|
|
60
57
|
import { getIconComponent } from './icon-registry';
|
|
@@ -74,37 +71,8 @@ function sizedLogo(Logo: ComponentType<{ className?: string; style?: CSSProperti
|
|
|
74
71
|
return <Logo className={className} style={{ width: size, height: size }} />;
|
|
75
72
|
};
|
|
76
73
|
}
|
|
77
|
-
|
|
78
|
-
// platform brand registry), never in the accent of the page showing it: on
|
|
79
|
-
// flamingo.run the OpenMSP mark stays OpenMSP yellow, not flamingo pink.
|
|
80
|
-
const brandColor = (platform: string) => `var(${platformAccentVarName(platform)})`;
|
|
81
|
-
// The OpenMSP mark's own defaults are black and white (made for a light page):
|
|
82
|
-
// on the dark surfaces every glyph sits on, the bubbles take the theme's text
|
|
83
|
-
// and background tokens and the back bubble OpenMSP's brand colour.
|
|
84
|
-
const OpenmspLogoIcon: IconComponent = ({ size = 16, className }) => (
|
|
85
|
-
<OpenmspLogo
|
|
86
|
-
className={className}
|
|
87
|
-
style={{ width: size, height: size }}
|
|
88
|
-
frontBubbleColor="var(--color-text-primary)"
|
|
89
|
-
innerFrontBubbleColor="var(--color-bg)"
|
|
90
|
-
backBubbleColor={brandColor('openmsp')}
|
|
91
|
-
/>
|
|
92
|
-
);
|
|
74
|
+
const OpenmspLogoIcon = sizedLogo(OpenmspLogo);
|
|
93
75
|
const FlamingoLogoIcon = sizedLogo(FlamingoLogo);
|
|
94
|
-
// The two community marks size themselves through a `size` prop, the resolver's own contract.
|
|
95
|
-
// Major League GitHub draws its mark in the theme's text colour on its own site
|
|
96
|
-
// (its header, hero and footer), never in an accent: so does every glyph of it.
|
|
97
|
-
const MlgLogoIcon: IconComponent = ({ size = 16, className, color }) => (
|
|
98
|
-
<MlgLogo size={size} className={className} color={color ?? 'var(--color-text-primary)'} />
|
|
99
|
-
);
|
|
100
|
-
const TmcgLogoIcon: IconComponent = ({ size = 16, className }) => (
|
|
101
|
-
<MiamiCyberGangLogoFaceOnly
|
|
102
|
-
size={size}
|
|
103
|
-
className={className}
|
|
104
|
-
originalPink={brandColor('tmcg')}
|
|
105
|
-
originalWhite="var(--color-text-primary)"
|
|
106
|
-
/>
|
|
107
|
-
);
|
|
108
76
|
|
|
109
77
|
/**
|
|
110
78
|
* `OpenFrameLogo` from `components/icons/` predates the v2 set and:
|
|
@@ -170,8 +138,6 @@ export const ICON_ALIASES: Record<string, IconComponent> = {
|
|
|
170
138
|
// (was the separate `renderSvgIcon` map; now ONE resolver everywhere).
|
|
171
139
|
'openframe-logo': LogoOpenframeIcon,
|
|
172
140
|
'openmsp-logo': OpenmspLogoIcon,
|
|
173
|
-
'mlg-logo': MlgLogoIcon,
|
|
174
|
-
'tmcg-logo': TmcgLogoIcon,
|
|
175
141
|
flamingo: FlamingoLogoIcon,
|
|
176
142
|
'flamingo-logo': FlamingoLogoIcon,
|
|
177
143
|
megaphone: Megaphone,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
|
+
import { useChatRuntime } from '../contexts/chat-runtime-context';
|
|
5
|
+
import { useRouter } from '../embed-shims/next-navigation';
|
|
6
|
+
import { executeNavigationImperative } from './chat/utils/execute-navigation';
|
|
7
|
+
import { OpenFrameLogo } from './icons';
|
|
8
|
+
import { Button } from './ui/button';
|
|
9
|
+
|
|
10
|
+
export interface FooterWaitlistButtonProps {
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Small wrapper around JoinWaitlistButton for use inside the footer.
|
|
16
|
+
*
|
|
17
|
+
* Routes through the host's unified-navigation hook
|
|
18
|
+
* (`runtime.navigation.navigate`) when a `ChatRuntimeContext` is
|
|
19
|
+
* mounted — that's the same path EVERY other in-app navigation
|
|
20
|
+
* surface uses (source chips, inline cards, search-autocomplete,
|
|
21
|
+
* action cards). One rule, one decision tree across the whole app.
|
|
22
|
+
* The hub's `HubRuntimeProvider` wires `navigate` to its `useUnifiedNav`
|
|
23
|
+
* helper, so this button picks up cross-platform new-tab decisions,
|
|
24
|
+
* same-URL re-scroll handling, embed-mode short-circuiting, and any
|
|
25
|
+
* future host-side nav rules for free.
|
|
26
|
+
*
|
|
27
|
+
* Falls back to the embed-shim's `router.push` when no runtime is
|
|
28
|
+
* mounted (third-party embedders who haven't set up
|
|
29
|
+
* `ChatRuntimeContext` — the lib stays usable without forcing them
|
|
30
|
+
* to wire the full chat-runtime).
|
|
31
|
+
*
|
|
32
|
+
* Target URL: `/waitlist#top`. `#top` is the canonical "scroll to
|
|
33
|
+
* page top" anchor — the destination page has an explicit
|
|
34
|
+
* `<div id="top">` at the top of `<main>` so native browser anchor
|
|
35
|
+
* scroll works in every browser regardless of the HTML5 magic-anchor
|
|
36
|
+
* behavior.
|
|
37
|
+
*/
|
|
38
|
+
export function FooterWaitlistButton({ className }: FooterWaitlistButtonProps) {
|
|
39
|
+
const router = useRouter();
|
|
40
|
+
const runtime = useChatRuntime();
|
|
41
|
+
|
|
42
|
+
const handleClick = useCallback(() => {
|
|
43
|
+
// The unified nav primitive: host `navigation.navigate` if wired, else the
|
|
44
|
+
// embed-shim router; new-tab/embed decision handled internally.
|
|
45
|
+
executeNavigationImperative({ runtime, href: '/waitlist#top', fallbackNavigate: router.push });
|
|
46
|
+
}, [router, runtime]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Button onClick={handleClick} className={className} leftIcon={<OpenFrameLogo />}>
|
|
50
|
+
Join Waitlist
|
|
51
|
+
</Button>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Suspense } from 'react';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
import { copyrightLine } from '../utils/copyright-line';
|
|
6
|
+
import { SocialIconRow } from './social-icon-row';
|
|
7
|
+
import { Skeleton } from './ui/skeleton';
|
|
8
|
+
|
|
9
|
+
interface FooterLink {
|
|
10
|
+
href: string;
|
|
11
|
+
label: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface FooterSection {
|
|
15
|
+
title: string;
|
|
16
|
+
links: FooterLink[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface FooterConfig {
|
|
20
|
+
name: string;
|
|
21
|
+
legalName: string;
|
|
22
|
+
description: string;
|
|
23
|
+
logo?: ReactNode;
|
|
24
|
+
sections: FooterSection[];
|
|
25
|
+
customComponent?: ReactNode; // Inject any custom component here
|
|
26
|
+
nameElement?: ReactNode; // Custom element for platform name with specific font
|
|
27
|
+
hideSocialRow?: boolean; // Hide the default social row
|
|
28
|
+
rightColumnContent?: ReactNode; // Custom content for right column
|
|
29
|
+
belowDescriptionContent?: ReactNode; // Custom content below description
|
|
30
|
+
moveDescriptionToRight?: boolean; // Move description and belowDescriptionContent to right column
|
|
31
|
+
keepBelowDescriptionLeft?: boolean; // Keep belowDescriptionContent on left even when moveDescriptionToRight is true
|
|
32
|
+
backgroundColor?: string; // ODS background color (e.g., 'bg-ods-card', 'bg-ods-bg')
|
|
33
|
+
social?: {
|
|
34
|
+
github?: string;
|
|
35
|
+
twitter?: string;
|
|
36
|
+
linkedin?: string;
|
|
37
|
+
reddit?: string;
|
|
38
|
+
youtube?: string;
|
|
39
|
+
instagram?: string;
|
|
40
|
+
facebook?: string;
|
|
41
|
+
discord?: string;
|
|
42
|
+
telegram?: string;
|
|
43
|
+
whatsapp?: string;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface FooterProps {
|
|
48
|
+
config?: FooterConfig;
|
|
49
|
+
renderLink?: (link: FooterLink) => ReactNode;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function NavLinkSkeleton() {
|
|
53
|
+
return <Skeleton className="h-5 w-20 md:h-6 md:w-24" />;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Platform-Aware Footer Component
|
|
58
|
+
* Accepts configuration from app-config.ts
|
|
59
|
+
*/
|
|
60
|
+
export function Footer({ config, renderLink }: FooterProps) {
|
|
61
|
+
// Config is required - no hardcoded fallbacks
|
|
62
|
+
if (!config) {
|
|
63
|
+
console.warn('Footer: No config provided');
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return <UniversalFooter config={config} renderLink={renderLink} />;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Universal Footer Component
|
|
72
|
+
* Renders footer based on provided config
|
|
73
|
+
*/
|
|
74
|
+
function UniversalFooter({
|
|
75
|
+
config,
|
|
76
|
+
renderLink,
|
|
77
|
+
}: {
|
|
78
|
+
config: FooterConfig;
|
|
79
|
+
renderLink?: (link: FooterLink) => ReactNode;
|
|
80
|
+
}) {
|
|
81
|
+
const defaultRenderLink = (link: FooterLink) => (
|
|
82
|
+
<a
|
|
83
|
+
href={link.href}
|
|
84
|
+
className="text-md md:text-md font-body font-medium leading-[1.33] text-ods-text-primary transition-colors hover:text-ods-accent"
|
|
85
|
+
>
|
|
86
|
+
{link.label}
|
|
87
|
+
</a>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const linkRenderer = renderLink || defaultRenderLink;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<footer
|
|
94
|
+
className={`flex w-full flex-col items-center justify-center ${config.backgroundColor || 'bg-ods-card'} relative z-[44] min-h-[auto] gap-6 border-t border-ods-border px-6 py-10 md:min-h-[248px] md:gap-6`}
|
|
95
|
+
>
|
|
96
|
+
<div className="grid w-full grid-cols-2 items-start gap-6 md:gap-8 lg:grid-cols-4">
|
|
97
|
+
{/* Column 1: Logo and optionally description */}
|
|
98
|
+
<div className="col-span-2 flex flex-col items-start gap-4 text-left md:col-span-1 md:gap-6 lg:col-span-1">
|
|
99
|
+
{/* Logo and name */}
|
|
100
|
+
<div className="flex items-center gap-2">
|
|
101
|
+
{config.logo && <Suspense fallback={<Skeleton className="h-8 w-8" />}>{config.logo}</Suspense>}
|
|
102
|
+
{config.nameElement || (
|
|
103
|
+
<span className="whitespace-nowrap font-heading text-heading-5 font-bold text-ods-text-primary">
|
|
104
|
+
{config.name}
|
|
105
|
+
</span>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Only show description here if NOT moving to right */}
|
|
110
|
+
{!config.moveDescriptionToRight && (
|
|
111
|
+
<>
|
|
112
|
+
<p className="font-body text-sm font-medium leading-[1.43] text-ods-text-primary md:text-sm">
|
|
113
|
+
{config.description}
|
|
114
|
+
</p>
|
|
115
|
+
|
|
116
|
+
{/* Custom content below description */}
|
|
117
|
+
{config.belowDescriptionContent && (
|
|
118
|
+
<Suspense fallback={<Skeleton className="h-8 w-full" />}>{config.belowDescriptionContent}</Suspense>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{/* Conditional social row - show by default unless hideSocialRow is true */}
|
|
122
|
+
{!config.hideSocialRow && (
|
|
123
|
+
<SocialIconRow
|
|
124
|
+
className="pt-2"
|
|
125
|
+
links={
|
|
126
|
+
config.social
|
|
127
|
+
? Object.entries(config.social)
|
|
128
|
+
.filter(([_, href]) => href)
|
|
129
|
+
.map(([platform, href]) => ({ platform, href: href }))
|
|
130
|
+
: undefined
|
|
131
|
+
}
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
134
|
+
</>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{/* Show belowDescriptionContent on left even when description is moved to right */}
|
|
138
|
+
{config.moveDescriptionToRight && config.keepBelowDescriptionLeft && config.belowDescriptionContent && (
|
|
139
|
+
<Suspense fallback={<Skeleton className="h-8 w-full" />}>{config.belowDescriptionContent}</Suspense>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{/* Dynamic sections - 1 column each on all screens */}
|
|
144
|
+
{config.sections.map((section, index) => (
|
|
145
|
+
<div key={index} className="col-span-1 flex flex-col items-start gap-3 text-left">
|
|
146
|
+
<h3 className="tracking-[-0.02em] text-ods-text-muted text-h5">{section.title}</h3>
|
|
147
|
+
<div className="flex flex-col gap-3">
|
|
148
|
+
{section.links.map((link, linkIndex) => (
|
|
149
|
+
<Suspense key={linkIndex} fallback={<NavLinkSkeleton />}>
|
|
150
|
+
{linkRenderer(link)}
|
|
151
|
+
</Suspense>
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
))}
|
|
156
|
+
|
|
157
|
+
{/* Custom component column - full width on mobile and medium, 1 column on large */}
|
|
158
|
+
{config.customComponent && (
|
|
159
|
+
<div className="col-span-2 flex flex-col justify-center md:col-span-1 lg:col-span-1">
|
|
160
|
+
<Suspense fallback={<Skeleton className="h-32 w-full" />}>{config.customComponent}</Suspense>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{/* Right column content - shows if rightColumnContent is provided OR if moving description to right */}
|
|
165
|
+
{(config.rightColumnContent || config.moveDescriptionToRight) && (
|
|
166
|
+
<div className="col-span-2 flex flex-col justify-start gap-4 md:col-span-1 md:gap-6 lg:col-span-1">
|
|
167
|
+
{/* Show description in right column if moveDescriptionToRight is true */}
|
|
168
|
+
{config.moveDescriptionToRight && (
|
|
169
|
+
<>
|
|
170
|
+
<p className="font-body text-sm font-medium leading-[1.43] text-ods-text-primary md:text-sm">
|
|
171
|
+
{config.description}
|
|
172
|
+
</p>
|
|
173
|
+
|
|
174
|
+
{/* Custom content below description - only if NOT keeping it on left */}
|
|
175
|
+
{config.belowDescriptionContent && !config.keepBelowDescriptionLeft && (
|
|
176
|
+
<Suspense fallback={<Skeleton className="h-8 w-full" />}>{config.belowDescriptionContent}</Suspense>
|
|
177
|
+
)}
|
|
178
|
+
</>
|
|
179
|
+
)}
|
|
180
|
+
|
|
181
|
+
{/* Regular right column content */}
|
|
182
|
+
{config.rightColumnContent && (
|
|
183
|
+
<Suspense fallback={<Skeleton className="h-32 w-full" />}>{config.rightColumnContent}</Suspense>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{/* Copyright */}
|
|
190
|
+
<p className="text-md md:text-md w-full pt-4 text-center font-body font-medium leading-[1.33] text-ods-text-muted md:pt-0">
|
|
191
|
+
{copyrightLine(config.legalName)}
|
|
192
|
+
</p>
|
|
193
|
+
</footer>
|
|
194
|
+
);
|
|
195
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -33,8 +33,10 @@ export * from './faq-accordion';
|
|
|
33
33
|
export * from './faq';
|
|
34
34
|
export * from './related-content';
|
|
35
35
|
export * from './filter-chip';
|
|
36
|
+
export * from './footer';
|
|
36
37
|
export * from './unified-filter-logic';
|
|
37
38
|
export * from './unified-pagination';
|
|
39
|
+
export * from './footer-waitlist-button';
|
|
38
40
|
export * from './hero-image-uploader';
|
|
39
41
|
export * from './icons-block';
|
|
40
42
|
export * from './image-cropper';
|
|
@@ -33,7 +33,7 @@ export function AnnouncementBarSkeleton() {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Header skeleton
|
|
36
|
+
* Header skeleton that matches the ClientOnlyHeader placeholder but with proper animations
|
|
37
37
|
*/
|
|
38
38
|
export function HeaderSkeleton() {
|
|
39
39
|
return (
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { useIsHydrated } from '../../hooks/ui/use-is-hydrated';
|
|
5
|
+
import { Header, type HeaderConfig } from './header';
|
|
6
|
+
import { HeaderSkeleton } from './header-skeleton';
|
|
7
|
+
|
|
8
|
+
export interface ClientOnlyHeaderProps {
|
|
9
|
+
config: HeaderConfig;
|
|
10
|
+
skeleton?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ClientOnlyHeader({ config, skeleton }: ClientOnlyHeaderProps) {
|
|
14
|
+
const isClient = useIsHydrated();
|
|
15
|
+
|
|
16
|
+
if (!isClient) {
|
|
17
|
+
// Return custom skeleton or default skeleton while client-side JavaScript loads
|
|
18
|
+
return skeleton || <HeaderSkeleton config={config} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return <Header config={config} />;
|
|
22
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { HeaderConfig } from '../../types/navigation';
|
|
2
|
+
import { cn } from '../../utils';
|
|
3
|
+
|
|
4
|
+
export interface HeaderSkeletonProps {
|
|
5
|
+
config?: HeaderConfig;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Loading skeleton mirroring the unified ODS top-navigation geometry
|
|
10
|
+
* (`TopNavigation`, Figma 2797-5978): 48px mobile / 56px md+, cell model.
|
|
11
|
+
* Keep in sync with `header.tsx` — a diverging skeleton makes the
|
|
12
|
+
* skeleton→live handoff jump.
|
|
13
|
+
*/
|
|
14
|
+
export function HeaderSkeleton({ config }: HeaderSkeletonProps) {
|
|
15
|
+
const showNavigation = config?.navigation && config.navigation.items.length > 0;
|
|
16
|
+
const showActions = config?.actions?.right && config.actions.right.length > 0;
|
|
17
|
+
const showMobileMenu = config?.mobile?.enabled;
|
|
18
|
+
const showLeftActions = !!config?.actions?.left;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="sticky top-0 z-[50] w-full">
|
|
22
|
+
<header
|
|
23
|
+
className={cn(
|
|
24
|
+
'flex h-[72px] w-full items-center border-b border-t border-ods-border md:border-t-0',
|
|
25
|
+
config?.backgroundColor || 'bg-ods-card',
|
|
26
|
+
config?.className,
|
|
27
|
+
)}
|
|
28
|
+
>
|
|
29
|
+
{/* Leading cells: admin toggle / burger */}
|
|
30
|
+
{showLeftActions && (
|
|
31
|
+
<div className="flex h-full w-[72px] items-center justify-center border-r border-ods-border">
|
|
32
|
+
<div className="h-6 w-6 animate-pulse rounded bg-ods-border" />
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
{showMobileMenu && (
|
|
36
|
+
<div className="flex h-full w-[72px] items-center justify-center border-r border-ods-border lg:hidden">
|
|
37
|
+
<div className="h-6 w-6 animate-pulse rounded bg-ods-border" />
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
|
|
41
|
+
{/* Logo zone */}
|
|
42
|
+
<div className="flex h-full flex-1 items-center gap-2 p-[var(--spacing-system-m)] md:pl-[var(--spacing-system-l)] lg:flex-none lg:pl-[var(--spacing-system-xxl)]">
|
|
43
|
+
<div className="h-6 w-6 animate-pulse rounded bg-ods-border" />
|
|
44
|
+
<div className="h-5 w-24 animate-pulse rounded bg-ods-border" />
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{/* Center: navigation links (desktop only) */}
|
|
48
|
+
<div className="hidden h-full min-w-0 flex-1 items-center justify-center gap-2 lg:flex">
|
|
49
|
+
{showNavigation && (
|
|
50
|
+
<>
|
|
51
|
+
<div className="h-8 w-20 animate-pulse rounded bg-ods-border" />
|
|
52
|
+
<div className="h-8 w-28 animate-pulse rounded bg-ods-border" />
|
|
53
|
+
<div className="h-8 w-24 animate-pulse rounded bg-ods-border" />
|
|
54
|
+
</>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{/* CTA zone */}
|
|
59
|
+
{showActions && (
|
|
60
|
+
<div className="hidden h-full items-center px-[var(--spacing-system-m)] lg:flex">
|
|
61
|
+
<div className="h-8 w-24 animate-pulse rounded bg-ods-border" />
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
{/* Mingo cell */}
|
|
66
|
+
{config?.mingo?.enabled && (
|
|
67
|
+
<div className="flex h-full items-center border-l border-ods-border px-[var(--spacing-system-l)]">
|
|
68
|
+
<div className="h-8 w-8 animate-pulse rounded bg-ods-border md:w-28" />
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
</header>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|