@flamingo-stack/openframe-frontend-core 0.0.719 → 0.0.721
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/assets/index.cjs +6 -0
- package/dist/assets/index.d.ts +1 -0
- package/dist/assets/index.d.ts.map +1 -1
- package/dist/assets/index.js +5 -1
- package/dist/assets/mobile-app-qr.d.ts +17 -0
- package/dist/assets/mobile-app-qr.d.ts.map +1 -0
- package/dist/{chunk-2JLP2X2M.js → chunk-2P2M27EW.js} +2396 -2369
- package/dist/{chunk-IWCPXJEE.js → chunk-2TXGR7IK.js} +138 -63
- package/dist/{chunk-FVQJBII6.js → chunk-3LY7V3RF.js} +3 -3
- package/dist/{chunk-HCFNEKO3.cjs → chunk-3R32ZNA2.cjs} +46 -46
- package/dist/{chunk-WSDT7LYL.js → chunk-3UBWDR65.js} +1 -1
- package/dist/{chunk-NZ5BHDFZ.js → chunk-44Q6MV7C.js} +2 -2
- package/dist/{chunk-CMZ3QTFQ.js → chunk-4I3K6LEH.js} +24 -0
- package/dist/{chunk-ZXCZYMN4.cjs → chunk-4LY6R4TN.cjs} +137 -62
- package/dist/{chunk-EAHOB64X.js → chunk-57UTI7ZH.js} +106 -42
- package/dist/{chunk-XGHGGBM3.js → chunk-5QXE5HMV.js} +4 -4
- package/dist/{chunk-NYWESHOQ.cjs → chunk-6AJIOW3S.cjs} +57 -57
- package/dist/{chunk-4QFWGLZO.js → chunk-6YBUQAOH.js} +1 -1
- package/dist/{chunk-JHQ334WS.js → chunk-742BGXNX.js} +2 -0
- package/dist/{chunk-D6PMVWDI.js → chunk-74N742ZG.js} +6 -6
- package/dist/{chunk-AECTRMGQ.cjs → chunk-7GRPR7XA.cjs} +13 -1
- package/dist/{chunk-4HJRHEXD.cjs → chunk-7Z5FMQYR.cjs} +22 -22
- package/dist/chunk-AFTRPEXO.js +13 -0
- package/dist/{chunk-WBNAEYTQ.cjs → chunk-BYUIFT7Q.cjs} +6 -6
- package/dist/{chunk-LCLDFMDF.cjs → chunk-BYVVE7HP.cjs} +58 -58
- package/dist/{chunk-JPD4NXH6.js → chunk-CO3FOZIH.js} +1 -1
- package/dist/{chunk-E7YCPY7K.cjs → chunk-D2DH6VZL.cjs} +3 -3
- package/dist/{chunk-HWVUR735.cjs → chunk-DFM572VH.cjs} +2 -2
- package/dist/{chunk-YDPKUAP4.js → chunk-DJ4IFQNQ.js} +2 -0
- package/dist/{chunk-5U3KWQXP.js → chunk-E6SMMMKV.js} +13 -13
- package/dist/chunk-EWPR7662.cjs +13 -0
- package/dist/{chunk-SWAVO7BB.cjs → chunk-F7ORKKEF.cjs} +108 -108
- package/dist/{chunk-AGVXKD4K.js → chunk-FSQQZFVG.js} +12 -0
- package/dist/{chunk-ETYLOZOB.cjs → chunk-GCK75D7F.cjs} +109 -22
- package/dist/{chunk-ZKNZ2CXW.cjs → chunk-GGBYZKMS.cjs} +11 -11
- package/dist/{chunk-Y4MB25KW.js → chunk-GYAXV3MZ.js} +3 -3
- package/dist/{chunk-UWQWMGJ7.cjs → chunk-HJMSNOBT.cjs} +1040 -696
- package/dist/{chunk-DFQYAYBH.cjs → chunk-HQIOMFJH.cjs} +2 -0
- package/dist/{chunk-JH2SIWPQ.js → chunk-HXB7SEYG.js} +1611 -1267
- package/dist/{chunk-LE5WKGJV.cjs → chunk-I2254QN6.cjs} +16 -16
- package/dist/{chunk-PSVSEN4J.cjs → chunk-IMTY3GRR.cjs} +2 -0
- package/dist/{chunk-ILNN6QRW.js → chunk-JDBZ4ZHU.js} +3 -3
- package/dist/{chunk-GQ7JJUIT.cjs → chunk-K2JUJ37D.cjs} +25 -1
- package/dist/{chunk-6YPTUHM7.js → chunk-K76ACBR3.js} +3 -3
- package/dist/{chunk-2NASBO7O.js → chunk-KFOAV77K.js} +2 -2
- package/dist/{chunk-CU6NWO2M.js → chunk-LAXKEBDD.js} +14 -18
- package/dist/{chunk-5YAGX5DY.js → chunk-LGKMR7NZ.js} +7 -7
- package/dist/{chunk-E2V2EH2G.cjs → chunk-LODMNSQY.cjs} +16 -16
- package/dist/{chunk-4D6NMPOO.js → chunk-NJ5TBTJG.js} +7 -7
- package/dist/{chunk-TDZ76NCB.cjs → chunk-NMPVK53I.cjs} +29 -29
- package/dist/{chunk-TTVOLEUS.js → chunk-NPSJE7J3.js} +90 -3
- package/dist/{chunk-H5VIMBRN.cjs → chunk-O5EHOJZD.cjs} +4 -4
- package/dist/{chunk-KT3U3ZRP.cjs → chunk-OKE5BIQH.cjs} +8 -8
- package/dist/{chunk-DC4RTYWK.cjs → chunk-QLTXT562.cjs} +5 -5
- package/dist/{chunk-H5V7GRN5.js → chunk-QQCFPMAA.js} +5 -7
- package/dist/{chunk-7BREFV7G.cjs → chunk-QYS4YVEP.cjs} +34 -36
- package/dist/{chunk-7BCY77W6.js → chunk-RCI3PALX.js} +8 -10
- package/dist/{chunk-P3JMAMHP.js → chunk-REETLEL5.js} +1 -1
- package/dist/{chunk-4JIGEK7C.cjs → chunk-RJMWJAJO.cjs} +97 -33
- package/dist/{chunk-WS7WRR5Z.cjs → chunk-RQZQHLNS.cjs} +86 -90
- package/dist/{chunk-FFT56TYB.cjs → chunk-RUZIICXT.cjs} +2 -2
- package/dist/{chunk-AJHSQ5XK.js → chunk-S63QBBT7.js} +6 -6
- package/dist/{chunk-TELAANY6.js → chunk-UXU7Y5BH.js} +2 -2
- package/dist/{chunk-NQVS4G7G.cjs → chunk-UZTOI6FJ.cjs} +773 -746
- package/dist/{chunk-DFEPUYTF.cjs → chunk-VFICDEYJ.cjs} +45 -47
- package/dist/{chunk-LOZLHG2U.cjs → chunk-XE2K53GH.cjs} +20 -20
- package/dist/{chunk-QBCDSTLE.cjs → chunk-XXO52RZR.cjs} +26 -26
- package/dist/{chunk-TTEFN6VO.js → chunk-ZLUFEWZZ.js} +10 -10
- package/dist/{chunk-MA4Q5RBP.cjs → chunk-ZSOFLPQN.cjs} +9 -9
- package/dist/{chunk-KBJJQFYU.js → chunk-ZZEKXGFS.js} +2 -2
- package/dist/{cloud-tenants-FBUO66GW.cjs → cloud-tenants-HZHKRUIW.cjs} +16 -16
- package/dist/{cloud-tenants-DNBPNQNP.js → cloud-tenants-Q4TCJS5Y.js} +15 -15
- package/dist/components/calendar.cjs +4 -4
- package/dist/components/calendar.js +1 -1
- package/dist/components/case-studies/index.cjs +30 -30
- package/dist/components/case-studies/index.js +16 -16
- package/dist/components/chat/ask-prompts.d.ts +10 -2
- package/dist/components/chat/ask-prompts.d.ts.map +1 -1
- package/dist/components/chat/assistant-ask-prompts.d.ts +27 -0
- package/dist/components/chat/assistant-ask-prompts.d.ts.map +1 -0
- package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
- package/dist/components/chat/hooks/index.d.ts +1 -0
- package/dist/components/chat/hooks/index.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-ask-prompts.d.ts +63 -0
- package/dist/components/chat/hooks/use-ask-prompts.d.ts.map +1 -0
- package/dist/components/chat/index.cjs +27 -10
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +26 -9
- package/dist/components/chat/utils/icon-library.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +17 -17
- package/dist/components/contact/index.js +16 -16
- package/dist/components/docs/index.cjs +20 -20
- package/dist/components/docs/index.js +19 -19
- package/dist/components/embeds/index.cjs +17 -17
- package/dist/components/embeds/index.js +16 -16
- package/dist/components/faq/faq-ask-card.d.ts +44 -0
- package/dist/components/faq/faq-ask-card.d.ts.map +1 -0
- package/dist/components/faq/faq-document-page.d.ts +5 -1
- package/dist/components/faq/faq-document-page.d.ts.map +1 -1
- package/dist/components/faq/faq-section.d.ts +18 -5
- package/dist/components/faq/faq-section.d.ts.map +1 -1
- package/dist/components/faq/index.cjs +19 -18
- package/dist/components/faq/index.d.ts +1 -0
- package/dist/components/faq/index.d.ts.map +1 -1
- package/dist/components/faq/index.js +18 -17
- package/dist/components/features/auth/auth-benefits-panel.d.ts.map +1 -1
- package/dist/components/features/index.cjs +16 -16
- package/dist/components/features/index.js +15 -15
- package/dist/components/help-center-pages/download-apps-page.d.ts +55 -0
- package/dist/components/help-center-pages/download-apps-page.d.ts.map +1 -0
- package/dist/components/help-center-pages/index.cjs +333 -119
- package/dist/components/help-center-pages/index.d.ts +1 -0
- package/dist/components/help-center-pages/index.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.js +251 -37
- package/dist/components/help-center-pages/trust-center-page.d.ts +4 -1
- package/dist/components/help-center-pages/trust-center-page.d.ts.map +1 -1
- package/dist/components/index.cjs +131 -217
- package/dist/components/index.js +236 -322
- package/dist/components/meeting-scheduler/index.cjs +61 -62
- package/dist/components/meeting-scheduler/index.js +18 -19
- package/dist/components/navigation/index.cjs +19 -19
- package/dist/components/navigation/index.js +18 -18
- package/dist/components/navigation/mingo-ai-button.d.ts +1 -1
- package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
- package/dist/components/onboarding-guides/index.cjs +19 -20
- package/dist/components/onboarding-guides/index.js +18 -19
- package/dist/components/product-demo/index.cjs +8 -8
- package/dist/components/product-demo/index.js +8 -8
- package/dist/components/related-content/index.cjs +7 -9
- package/dist/components/related-content/index.js +6 -8
- package/dist/components/tickets/index.cjs +21 -21
- package/dist/components/tickets/index.js +20 -20
- package/dist/components/ui/button/button.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +11 -11
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/index.cjs +34 -16
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +33 -15
- package/dist/components/ui/mobile-app-qr.d.ts +23 -0
- package/dist/components/ui/mobile-app-qr.d.ts.map +1 -0
- package/dist/components/ui/store-badges.d.ts +7 -4
- package/dist/components/ui/store-badges.d.ts.map +1 -1
- package/dist/contexts/assistant-runtime-context.d.ts +74 -0
- package/dist/contexts/assistant-runtime-context.d.ts.map +1 -0
- package/dist/contexts/index.cjs +7 -1
- package/dist/contexts/index.d.ts +1 -0
- package/dist/contexts/index.d.ts.map +1 -1
- package/dist/contexts/index.js +6 -0
- package/dist/{devices-5A5TY4JD.cjs → devices-6XHUFIC6.cjs} +17 -17
- package/dist/{devices-PLDAEUSL.js → devices-NOUGVTES.js} +15 -15
- package/dist/hooks/index.cjs +10 -5
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +9 -4
- package/dist/hooks/use-downloads.d.ts +41 -0
- package/dist/hooks/use-downloads.d.ts.map +1 -0
- package/dist/index.cjs +80 -20
- package/dist/index.js +79 -19
- package/dist/{knowledge-G6Q5KRWV.js → knowledge-2YTUXQ2Z.js} +15 -15
- package/dist/{knowledge-FKTJ723Z.cjs → knowledge-ANEKPMO2.cjs} +17 -17
- package/dist/{logs-CJCXUBU6.cjs → logs-U3IB3HUM.cjs} +25 -25
- package/dist/{logs-QMV6XIC4.js → logs-WVQIONUT.js} +15 -15
- package/dist/{policies-G6D7AX6F.js → policies-66EGDW2H.js} +15 -15
- package/dist/{policies-QMRTK3CY.cjs → policies-UFRN2EQF.cjs} +20 -20
- package/dist/{remote-session-DBD6OSTX.js → remote-session-6WEUJZDC.js} +5 -5
- package/dist/{remote-session-2JVI7RVA.cjs → remote-session-A75X5NPC.cjs} +7 -7
- package/dist/{software-update-PNZMRDBT.cjs → software-update-2MHKL76D.cjs} +20 -20
- package/dist/{software-update-6BQM6BKH.js → software-update-3ZD3MXF3.js} +15 -15
- package/dist/{tickets-board-BHLPAVNJ.cjs → tickets-board-IFWMIZ35.cjs} +9 -9
- package/dist/{tickets-board-PYDNELSK.js → tickets-board-MYXG3V72.js} +4 -4
- package/dist/types/downloads.d.ts +67 -0
- package/dist/types/downloads.d.ts.map +1 -0
- package/dist/types/index.cjs +24 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +16 -1
- package/dist/utils/index.cjs +46 -2
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +40 -3
- package/dist/utils/list-url.d.ts.map +1 -1
- package/dist/utils/mobile-app.cjs +46 -0
- package/dist/utils/mobile-app.d.ts +40 -0
- package/dist/utils/mobile-app.d.ts.map +1 -0
- package/dist/utils/mobile-app.js +40 -0
- package/dist/utils/source-icons.d.ts.map +1 -1
- package/dist/utils/trial.d.ts +18 -0
- package/dist/utils/trial.d.ts.map +1 -0
- package/package.json +7 -1
- package/src/assets/index.ts +1 -0
- package/src/assets/mobile-app-qr.ts +19 -0
- package/src/components/chat/ask-prompts.tsx +25 -3
- package/src/components/chat/assistant-ask-prompts.tsx +78 -0
- package/src/components/chat/entity-cards/dispatch.tsx +38 -4
- package/src/components/chat/hooks/index.ts +12 -0
- package/src/components/chat/hooks/use-ask-prompts.ts +188 -0
- package/src/components/chat/index.ts +1 -0
- package/src/components/chat/utils/icon-library.tsx +6 -0
- package/src/components/faq/faq-ask-card.tsx +115 -0
- package/src/components/faq/faq-document-page.tsx +6 -1
- package/src/components/faq/faq-section.tsx +27 -5
- package/src/components/faq/index.ts +1 -0
- package/src/components/features/auth/auth-benefits-panel.tsx +5 -4
- package/src/components/help-center-pages/download-apps-page.tsx +364 -0
- package/src/components/help-center-pages/index.ts +1 -0
- package/src/components/help-center-pages/trust-center-page.tsx +5 -2
- package/src/components/navigation/mingo-ai-button.tsx +27 -6
- package/src/components/ui/button/button.tsx +3 -0
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/mobile-app-qr.tsx +40 -0
- package/src/components/ui/store-badges.tsx +17 -9
- package/src/contexts/assistant-runtime-context.tsx +84 -0
- package/src/contexts/index.ts +9 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-downloads.ts +138 -0
- package/src/types/downloads.ts +82 -0
- package/src/types/index.ts +1 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/list-url.ts +4 -0
- package/src/utils/mobile-app.ts +57 -0
- package/src/utils/source-icons.ts +10 -2
- package/src/utils/trial.ts +19 -0
- package/dist/chunk-6X3O67OL.cjs +0 -89
- package/dist/chunk-AUMKE74M.cjs +0 -16
- package/dist/chunk-G3GRYDMS.js +0 -16
- package/dist/chunk-RR6PY34K.js +0 -89
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `<DownloadAppsPage>`: every way to install OpenFrame, on one page.
|
|
5
|
+
*
|
|
6
|
+
* - The desktop app: every system at once, one card each (a download page
|
|
7
|
+
* lists its platforms; it does not hide them behind tabs, so nothing moves
|
|
8
|
+
* when a visitor looks for another system). In a card the installer is THE
|
|
9
|
+
* action (the other architecture is a quieter button) and each package
|
|
10
|
+
* manager is one line under it, with its copy button.
|
|
11
|
+
* - The mobile app: the store badges, and the install QR code beside them.
|
|
12
|
+
* - `footer`: the host's own last word (the website's "start on the web" card).
|
|
13
|
+
*
|
|
14
|
+
* DATA: all of it is the server's answer (`DownloadsPublic`), read by
|
|
15
|
+
* `useDownloads` against `endpoint` (default `DOWNLOADS_API_PATH`; embedders
|
|
16
|
+
* pass their `/content` proxy path), the one request every download button on
|
|
17
|
+
* the page shares. `initialData` (hub SSR) is the answer, with no fetch. The
|
|
18
|
+
* lib holds no link of its own and no fallback: the page ADAPTS, so a system
|
|
19
|
+
* with no installer and no command has no card, a store with no listing has no
|
|
20
|
+
* badge, and a section with nothing to offer is not drawn.
|
|
21
|
+
*
|
|
22
|
+
* A HOST DECIDES `showDesktop`: the website shows the desktop app only to a
|
|
23
|
+
* visitor whose main action is the download; the desktop shell hides it (it is
|
|
24
|
+
* the app already running).
|
|
25
|
+
*
|
|
26
|
+
* The chrome is the canonical `PageShell` + frozen `PageLayout`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { useEffect, type ComponentType, type ReactNode } from 'react';
|
|
30
|
+
import { useRouter } from '../../embed-shims/next-navigation';
|
|
31
|
+
import { useCopyToClipboard } from '../../hooks/use-copy-to-clipboard';
|
|
32
|
+
import { useDownloads } from '../../hooks/use-downloads';
|
|
33
|
+
import {
|
|
34
|
+
DESKTOP_OS_LABELS,
|
|
35
|
+
DOWNLOAD_ARCHITECTURE_LABELS,
|
|
36
|
+
DOWNLOADS_API_PATH,
|
|
37
|
+
DOWNLOADS_MOBILE_TAGLINE,
|
|
38
|
+
DOWNLOADS_MOBILE_TITLE,
|
|
39
|
+
DOWNLOADS_TAGLINE,
|
|
40
|
+
DOWNLOADS_TITLE,
|
|
41
|
+
type AppDownload,
|
|
42
|
+
type DownloadActionEvent,
|
|
43
|
+
type DownloadsPublic,
|
|
44
|
+
type MobileAppLinks,
|
|
45
|
+
} from '../../types/downloads';
|
|
46
|
+
import { cn } from '../../utils/cn';
|
|
47
|
+
import { DOWNLOAD_PAGE_STORE_PARAM, MOBILE_APP_INSTALL_HOST_PATH, resolveMobileStoreUrl } from '../../utils/mobile-app';
|
|
48
|
+
import { DESKTOP_OSES, type DesktopOs } from '../../utils/visitor-os';
|
|
49
|
+
import { CommandBox } from '../features/command-box';
|
|
50
|
+
import { AppleLogoIcon } from '../icons-v2-generated/brand-logos/apple-logo-icon';
|
|
51
|
+
import { WindowsLogoGreyIcon } from '../icons-v2-generated/brand-logos/windows-logo-grey-icon';
|
|
52
|
+
import { Download01Icon } from '../icons-v2-generated/interface/download-01-icon';
|
|
53
|
+
import { PageShell } from '../layout/article-detail-layout';
|
|
54
|
+
import { PageLayout } from '../layout/page-layout';
|
|
55
|
+
import { UnifiedSkeleton } from '../loading/unified-skeleton';
|
|
56
|
+
import { Button } from '../ui/button/button';
|
|
57
|
+
import { LoadError } from '../ui/error-state';
|
|
58
|
+
import { MobileAppQr } from '../ui/mobile-app-qr';
|
|
59
|
+
import { StoreBadgeLinks } from '../ui/store-badges';
|
|
60
|
+
|
|
61
|
+
export interface DownloadAppsPageProps {
|
|
62
|
+
/** GET endpoint for the public projection. Default `DOWNLOADS_API_PATH`. */
|
|
63
|
+
endpoint?: string;
|
|
64
|
+
/** Optional SSR hydrate (hub server-read): skips the initial client fetch. */
|
|
65
|
+
initialData?: DownloadsPublic;
|
|
66
|
+
/** Render the standalone `<PageShell>`. Default true. Pass false when the host
|
|
67
|
+
* layout already provides the page container (e.g. openframe-frontend). */
|
|
68
|
+
shell?: boolean;
|
|
69
|
+
/** Back-button config. Default: none. */
|
|
70
|
+
backButton?: { label?: string; href?: string } | false;
|
|
71
|
+
/** Page title. Default by what is offered (`DOWNLOADS_TITLE` / `DOWNLOADS_MOBILE_TITLE`). */
|
|
72
|
+
title?: string;
|
|
73
|
+
/** Page subtitle. Default by what is offered. */
|
|
74
|
+
subtitle?: string;
|
|
75
|
+
/** Offer the desktop app. Default true. False: the mobile app only. */
|
|
76
|
+
showDesktop?: boolean;
|
|
77
|
+
/** Open the store listings in a new tab. Default true. */
|
|
78
|
+
openStoresInNewTab?: boolean;
|
|
79
|
+
/** Called when a visitor downloads an installer or copies an install command (the host's analytics). */
|
|
80
|
+
onDownloadAction?: (event: DownloadActionEvent) => void;
|
|
81
|
+
/** The host's own closing block, under the mobile app. */
|
|
82
|
+
footer?: ReactNode;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const SECTIONS_CLASS = 'flex flex-col gap-[var(--spacing-system-xlf)]';
|
|
86
|
+
const SECTION_CLASS = 'flex flex-col gap-[var(--spacing-system-lf)]';
|
|
87
|
+
const CARDS_GRID_CLASS = 'grid grid-cols-1 gap-[var(--spacing-system-lf)] content-md:grid-cols-2';
|
|
88
|
+
/** One card takes the row; two or more share it. */
|
|
89
|
+
const cardsGridClass = (cards: number) => (cards > 1 ? CARDS_GRID_CLASS : 'grid grid-cols-1');
|
|
90
|
+
const CARD_CLASS =
|
|
91
|
+
'flex flex-col gap-[var(--spacing-system-lf)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-lf)]';
|
|
92
|
+
const CARD_HEADER_CLASS = 'flex items-center gap-[var(--spacing-system-sf)]';
|
|
93
|
+
const BUTTONS_CLASS = 'flex flex-wrap gap-[var(--spacing-system-sf)]';
|
|
94
|
+
const COMMANDS_CLASS = 'flex flex-col gap-[var(--spacing-system-mf)]';
|
|
95
|
+
|
|
96
|
+
const OS_ICONS: Partial<Record<DesktopOs, ComponentType<{ className?: string }>>> = {
|
|
97
|
+
mac: AppleLogoIcon,
|
|
98
|
+
windows: WindowsLogoGreyIcon,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** A card's heading row: a small logo, the name, and one quiet line of detail at the far end. */
|
|
102
|
+
function CardHeader({
|
|
103
|
+
icon: Icon,
|
|
104
|
+
title,
|
|
105
|
+
detail,
|
|
106
|
+
}: {
|
|
107
|
+
icon?: ComponentType<{ className?: string }>;
|
|
108
|
+
title: string;
|
|
109
|
+
detail?: string;
|
|
110
|
+
}) {
|
|
111
|
+
return (
|
|
112
|
+
<div className={CARD_HEADER_CLASS}>
|
|
113
|
+
{Icon && <Icon className="h-6 w-6 shrink-0 text-ods-text-secondary" />}
|
|
114
|
+
<h3 className="m-0 text-ods-text-primary text-h3">{title}</h3>
|
|
115
|
+
{detail && <p className="m-0 ml-auto text-right text-ods-text-secondary text-h6">{detail}</p>}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface SystemRows {
|
|
121
|
+
os: DesktopOs;
|
|
122
|
+
installers: AppDownload[];
|
|
123
|
+
commands: AppDownload[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The rows per system, in the order they arrived. The page adapts to what the
|
|
128
|
+
* deployment names: a system is offered when it has an installer OR a package
|
|
129
|
+
* manager command, and a system with neither has no card at all.
|
|
130
|
+
*/
|
|
131
|
+
function rowsBySystem(downloads: readonly AppDownload[]): SystemRows[] {
|
|
132
|
+
return DESKTOP_OSES.map(os => ({
|
|
133
|
+
os,
|
|
134
|
+
installers: downloads.filter(d => d.os === os && d.kind === 'binary' && d.url),
|
|
135
|
+
commands: downloads.filter(d => d.os === os && d.kind === 'command' && d.command),
|
|
136
|
+
})).filter(system => system.installers.length > 0 || system.commands.length > 0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function installerLabel(download: AppDownload, siblings: number): string {
|
|
140
|
+
if (download.label) return download.label;
|
|
141
|
+
const label = `Download for ${DESKTOP_OS_LABELS[download.os]}`;
|
|
142
|
+
const architecture = download.architecture ? DOWNLOAD_ARCHITECTURE_LABELS[download.architecture] : null;
|
|
143
|
+
// One installer for a system needs no qualifier; two are told apart by their architecture.
|
|
144
|
+
return siblings > 1 && architecture ? `${label} (${architecture})` : label;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** One system's card: its logo and name, its installers side by side, then each package manager as one line. */
|
|
148
|
+
function SystemCard({
|
|
149
|
+
system,
|
|
150
|
+
onDownloadAction,
|
|
151
|
+
}: {
|
|
152
|
+
system: SystemRows;
|
|
153
|
+
onDownloadAction?: DownloadAppsPageProps['onDownloadAction'];
|
|
154
|
+
}) {
|
|
155
|
+
const { copy } = useCopyToClipboard({
|
|
156
|
+
successTitle: 'Copied',
|
|
157
|
+
successDescription: 'Paste it into your terminal and press Return.',
|
|
158
|
+
});
|
|
159
|
+
const requirements = [...new Set(system.installers.map(d => d.minOsLabel).filter((l): l is string => !!l))];
|
|
160
|
+
const report = (action: DownloadActionEvent['action'], row: AppDownload) =>
|
|
161
|
+
onDownloadAction?.({ action, id: row.id, os: row.os, architecture: row.architecture });
|
|
162
|
+
return (
|
|
163
|
+
<div className={CARD_CLASS}>
|
|
164
|
+
<CardHeader
|
|
165
|
+
icon={OS_ICONS[system.os]}
|
|
166
|
+
title={DESKTOP_OS_LABELS[system.os]}
|
|
167
|
+
detail={requirements.length > 0 ? `Requires ${requirements.join(' or ')}` : undefined}
|
|
168
|
+
/>
|
|
169
|
+
{system.installers.length > 0 && (
|
|
170
|
+
<div className={BUTTONS_CLASS}>
|
|
171
|
+
{system.installers.map((download, index) => (
|
|
172
|
+
<Button
|
|
173
|
+
key={download.id}
|
|
174
|
+
variant={index === 0 ? 'accent' : 'outline'}
|
|
175
|
+
href={download.url ?? undefined}
|
|
176
|
+
download
|
|
177
|
+
leftIcon={<Download01Icon className="h-5 w-5" />}
|
|
178
|
+
onClick={() => report('installer', download)}
|
|
179
|
+
>
|
|
180
|
+
{installerLabel(download, system.installers.length)}
|
|
181
|
+
</Button>
|
|
182
|
+
))}
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
{system.commands.length > 0 && (
|
|
186
|
+
<div className={COMMANDS_CLASS}>
|
|
187
|
+
{system.commands.map(row =>
|
|
188
|
+
row.command ? (
|
|
189
|
+
<CommandBox
|
|
190
|
+
key={row.id}
|
|
191
|
+
title={row.label ?? undefined}
|
|
192
|
+
command={row.command}
|
|
193
|
+
maxLines={1}
|
|
194
|
+
commandClassName="text-ods-accent"
|
|
195
|
+
copyAriaLabel={`Copy the ${row.label ?? 'install'} command`}
|
|
196
|
+
onCopy={() => {
|
|
197
|
+
void copy(row.command as string);
|
|
198
|
+
report('command', row);
|
|
199
|
+
}}
|
|
200
|
+
/>
|
|
201
|
+
) : null,
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** A section's cards while the server's answer loads: the same grid and card frame, a button's box in each. */
|
|
210
|
+
function CardsSkeleton({ label }: { label: string }) {
|
|
211
|
+
return (
|
|
212
|
+
<div className={CARDS_GRID_CLASS} aria-busy="true" aria-label={label}>
|
|
213
|
+
{[0, 1].map(card => (
|
|
214
|
+
<div key={card} className={CARD_CLASS}>
|
|
215
|
+
<UnifiedSkeleton className="h-6 w-32 rounded" />
|
|
216
|
+
<UnifiedSkeleton className="h-12 w-56 rounded-md" />
|
|
217
|
+
<UnifiedSkeleton className="h-14 w-full rounded-md" />
|
|
218
|
+
</div>
|
|
219
|
+
))}
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function Section({ id, title, children }: { id: string; title: string; children: ReactNode }) {
|
|
225
|
+
return (
|
|
226
|
+
<section aria-labelledby={id} className={SECTION_CLASS}>
|
|
227
|
+
<h2 id={id} className="m-0 text-ods-text-primary text-h2">
|
|
228
|
+
{title}
|
|
229
|
+
</h2>
|
|
230
|
+
{children}
|
|
231
|
+
</section>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Which devices the store card names: only the stores the server has a listing for. */
|
|
236
|
+
function storeCardTitle(links: MobileAppLinks): string {
|
|
237
|
+
if (links.appStoreUrl && links.googlePlayUrl) return 'iPhone, iPad and Android';
|
|
238
|
+
return links.appStoreUrl ? 'iPhone and iPad' : 'Android';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function DownloadAppsPage({
|
|
242
|
+
endpoint = DOWNLOADS_API_PATH,
|
|
243
|
+
initialData,
|
|
244
|
+
shell = true,
|
|
245
|
+
backButton = false,
|
|
246
|
+
title,
|
|
247
|
+
subtitle,
|
|
248
|
+
showDesktop = true,
|
|
249
|
+
openStoresInNewTab = true,
|
|
250
|
+
onDownloadAction,
|
|
251
|
+
footer,
|
|
252
|
+
}: DownloadAppsPageProps) {
|
|
253
|
+
const { data, isLoading, error, reload } = useDownloads({ endpoint, initialData });
|
|
254
|
+
// The page adapts to the server's answer: a section exists only for what it names.
|
|
255
|
+
const systems = showDesktop && data ? rowsBySystem(data.desktop) : [];
|
|
256
|
+
const mobile = data?.mobile ?? null;
|
|
257
|
+
const hasMobile = !!mobile && (!!mobile.appStoreUrl || !!mobile.googlePlayUrl);
|
|
258
|
+
// Until the answer lands, a host that offers the desktop app is titled for it.
|
|
259
|
+
const offersDesktop = showDesktop && (!data || systems.length > 0);
|
|
260
|
+
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
// The install QR code arrives with this parameter: a phone or a tablet goes
|
|
263
|
+
// straight on to its store. `replace`, so Back returns to whatever sent the
|
|
264
|
+
// visitor here. Anyone else who opens the page on a phone reads it.
|
|
265
|
+
if (!mobile || !new URLSearchParams(window.location.search).has(DOWNLOAD_PAGE_STORE_PARAM)) return;
|
|
266
|
+
const storeUrl = resolveMobileStoreUrl(mobile, navigator.userAgent, navigator.maxTouchPoints);
|
|
267
|
+
if (storeUrl) window.location.replace(storeUrl);
|
|
268
|
+
}, [mobile]);
|
|
269
|
+
|
|
270
|
+
let body: ReactNode;
|
|
271
|
+
if (error && !data) {
|
|
272
|
+
body = <LoadError message="Could not load the downloads" onRetry={reload} />;
|
|
273
|
+
} else if (!data) {
|
|
274
|
+
body = isLoading ? (
|
|
275
|
+
<>
|
|
276
|
+
{showDesktop && (
|
|
277
|
+
<Section id="download-desktop" title="Desktop app">
|
|
278
|
+
<CardsSkeleton label="Loading the installers" />
|
|
279
|
+
</Section>
|
|
280
|
+
)}
|
|
281
|
+
<Section id="download-mobile" title="Mobile app">
|
|
282
|
+
<CardsSkeleton label="Loading the mobile app" />
|
|
283
|
+
</Section>
|
|
284
|
+
</>
|
|
285
|
+
) : null;
|
|
286
|
+
} else {
|
|
287
|
+
body = (
|
|
288
|
+
<>
|
|
289
|
+
{systems.length > 0 && (
|
|
290
|
+
<Section id="download-desktop" title="Desktop app">
|
|
291
|
+
<div className={cardsGridClass(systems.length)}>
|
|
292
|
+
{systems.map(system => (
|
|
293
|
+
<SystemCard key={system.os} system={system} onDownloadAction={onDownloadAction} />
|
|
294
|
+
))}
|
|
295
|
+
</div>
|
|
296
|
+
</Section>
|
|
297
|
+
)}
|
|
298
|
+
|
|
299
|
+
{mobile && hasMobile && (
|
|
300
|
+
<Section id="download-mobile" title="Mobile app">
|
|
301
|
+
<div className={CARDS_GRID_CLASS}>
|
|
302
|
+
<div className={CARD_CLASS}>
|
|
303
|
+
<CardHeader title={storeCardTitle(mobile)} />
|
|
304
|
+
<p className="m-0 text-ods-text-secondary text-h4">Get alerts and respond to tickets on the go.</p>
|
|
305
|
+
<StoreBadgeLinks
|
|
306
|
+
appStoreUrl={mobile.appStoreUrl}
|
|
307
|
+
googlePlayUrl={mobile.googlePlayUrl}
|
|
308
|
+
openInNewTab={openStoresInNewTab}
|
|
309
|
+
/>
|
|
310
|
+
</div>
|
|
311
|
+
<div className={cn(CARD_CLASS, 'content-md:flex-row content-md:items-center content-md:justify-between')}>
|
|
312
|
+
<div className="flex flex-col gap-[var(--spacing-system-sf)]">
|
|
313
|
+
<CardHeader title="Scan with your phone" />
|
|
314
|
+
<p className="m-0 text-ods-text-secondary text-h4">
|
|
315
|
+
Point your camera at the code, or open {MOBILE_APP_INSTALL_HOST_PATH} on your phone.
|
|
316
|
+
</p>
|
|
317
|
+
</div>
|
|
318
|
+
{/* The plate is light on purpose: a QR code is read dark on light. */}
|
|
319
|
+
<div className="w-fit shrink-0 rounded-md bg-ods-bg-inverted p-[var(--spacing-system-sf)]">
|
|
320
|
+
<MobileAppQr className="h-[120px] w-[120px]" />
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</Section>
|
|
325
|
+
)}
|
|
326
|
+
</>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<DownloadAppsChrome
|
|
332
|
+
shell={shell}
|
|
333
|
+
backButton={backButton}
|
|
334
|
+
title={title ?? (offersDesktop ? DOWNLOADS_TITLE : DOWNLOADS_MOBILE_TITLE)}
|
|
335
|
+
subtitle={subtitle ?? (offersDesktop ? DOWNLOADS_TAGLINE : DOWNLOADS_MOBILE_TAGLINE)}
|
|
336
|
+
>
|
|
337
|
+
<div className={SECTIONS_CLASS}>
|
|
338
|
+
{body}
|
|
339
|
+
{footer}
|
|
340
|
+
</div>
|
|
341
|
+
</DownloadAppsChrome>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** The page's chrome: the standalone `PageShell` or the host's padded box, and the frozen `PageLayout` header. */
|
|
346
|
+
function DownloadAppsChrome({
|
|
347
|
+
shell,
|
|
348
|
+
title,
|
|
349
|
+
subtitle,
|
|
350
|
+
backButton,
|
|
351
|
+
children,
|
|
352
|
+
}: Required<Pick<DownloadAppsPageProps, 'shell' | 'title' | 'subtitle' | 'backButton'>> & { children: ReactNode }) {
|
|
353
|
+
const router = useRouter();
|
|
354
|
+
const backCfg =
|
|
355
|
+
backButton === false
|
|
356
|
+
? undefined
|
|
357
|
+
: { label: backButton.label ?? 'Back to home', onClick: () => router.push(backButton.href ?? '/') };
|
|
358
|
+
const inner = (
|
|
359
|
+
<PageLayout title={title} subtitle={subtitle} backButton={backCfg} titleSize="h1" titleWrap>
|
|
360
|
+
{children}
|
|
361
|
+
</PageLayout>
|
|
362
|
+
);
|
|
363
|
+
return shell ? <PageShell>{inner}</PageShell> : <div className="page-shell-content">{inner}</div>;
|
|
364
|
+
}
|
|
@@ -22,6 +22,7 @@ export { ProductReleasesListPage, type ProductReleasesListPageProps } from './pr
|
|
|
22
22
|
export { DeliveryPage, type DeliveryPageProps } from './delivery-page';
|
|
23
23
|
export { OnboardingGuidesCatalogPage, type OnboardingGuidesCatalogPageProps } from './onboarding-guides-catalog-page';
|
|
24
24
|
export { TrustCenterPage, TrustCenterPageSkeleton, type TrustCenterPageProps } from './trust-center-page';
|
|
25
|
+
export { DownloadAppsPage, type DownloadAppsPageProps } from './download-apps-page';
|
|
25
26
|
|
|
26
27
|
// Existing full-pages re-exported for a single Help Center import site.
|
|
27
28
|
export { FaqDocumentPage, type FaqDocumentPageProps } from '../faq';
|
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
} from '../../types/trust-center';
|
|
46
46
|
import { STICKY_HEADER_OFFSET_PX } from '../../utils/same-page-hash-nav';
|
|
47
47
|
import { useScrollSpy } from '../docs/use-scroll-spy';
|
|
48
|
-
import { FaqSection } from '../faq/faq-section';
|
|
48
|
+
import { FaqSection, type FaqSectionProps } from '../faq/faq-section';
|
|
49
49
|
import { PageShell } from '../layout/article-detail-layout';
|
|
50
50
|
import { PageLayout } from '../layout/page-layout';
|
|
51
51
|
import { StickySectionNav } from '../navigation/sticky-section-nav';
|
|
@@ -83,6 +83,8 @@ export interface TrustCenterPageProps {
|
|
|
83
83
|
title?: string;
|
|
84
84
|
/** Page subtitle. Default the brand-neutral `TRUST_CENTER_TAGLINE` (a host adds its brand here). */
|
|
85
85
|
subtitle?: string;
|
|
86
|
+
/** The FAQ section's "ask the assistant" card (`FaqSection`'s `ask`): the host states its topic. Absent: no card. */
|
|
87
|
+
ask?: FaqSectionProps['ask'];
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
/** One section's page content. Its `h2` is the `TRUST_CENTER_SECTIONS` label — never a literal here. */
|
|
@@ -149,6 +151,7 @@ export function TrustCenterPage({
|
|
|
149
151
|
backButton = false,
|
|
150
152
|
title = TRUST_CENTER_TITLE,
|
|
151
153
|
subtitle = TRUST_CENTER_TAGLINE,
|
|
154
|
+
ask,
|
|
152
155
|
}: TrustCenterPageProps) {
|
|
153
156
|
const { data, isLoading, error, reload } = useSelfFetch<TrustCenterPublic>(endpoint, {
|
|
154
157
|
initialData,
|
|
@@ -232,7 +235,7 @@ export function TrustCenterPage({
|
|
|
232
235
|
lead: TRUST_SECTION_LEADS.subprocessors,
|
|
233
236
|
render: d => <SubprocessorsSection subprocessors={d.subprocessors} />,
|
|
234
237
|
},
|
|
235
|
-
faq: { render: d => <FaqSection initialFaqs={d.faqs} heading={null} /> },
|
|
238
|
+
faq: { render: d => <FaqSection initialFaqs={d.faqs} heading={null} ask={ask} /> },
|
|
236
239
|
contact: { render: () => <ContactSection onContact={() => openRequest(null)} /> },
|
|
237
240
|
};
|
|
238
241
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import type React from 'react';
|
|
4
|
-
import { useEffect } from 'react';
|
|
4
|
+
import { useEffect, useRef } from 'react';
|
|
5
|
+
import { useAssistantRuntime } from '../../contexts/assistant-runtime-context';
|
|
5
6
|
import { useVisitorOs } from '../../hooks/ui/use-visitor-os';
|
|
6
7
|
import { cn } from '../../utils';
|
|
7
8
|
import { shortcutLabel, usesCommandKey } from '../../utils/visitor-os';
|
|
@@ -78,8 +79,8 @@ export function openAskAi(source?: string, options?: { prompt?: string }): void
|
|
|
78
79
|
*/
|
|
79
80
|
export function MingoAiButton({
|
|
80
81
|
source,
|
|
81
|
-
icon,
|
|
82
|
-
label
|
|
82
|
+
icon: iconProp,
|
|
83
|
+
label: labelProp,
|
|
83
84
|
shortcutHint = false,
|
|
84
85
|
variant = 'inline',
|
|
85
86
|
className,
|
|
@@ -93,6 +94,22 @@ export function MingoAiButton({
|
|
|
93
94
|
const inPage = variant === 'button';
|
|
94
95
|
|
|
95
96
|
const commandKey = visitor.known && usesCommandKey(visitor.os);
|
|
97
|
+
// The launcher is the assistant's: its name, glyph and the chat it opens are
|
|
98
|
+
// the assistant runtime's (the host's server-resolved identity and its own
|
|
99
|
+
// opener), so no launcher types a name or assumes which chat exists. A prop
|
|
100
|
+
// still wins (a header given its own). The lib holds no name of its own:
|
|
101
|
+
// with none from either, there is no launcher to render (below). With no
|
|
102
|
+
// runtime opener the open is the `ask-ai:open` event of `source`.
|
|
103
|
+
const assistant = useAssistantRuntime();
|
|
104
|
+
const label = labelProp ?? assistant?.name;
|
|
105
|
+
const icon = iconProp ?? assistant?.icon;
|
|
106
|
+
const runtimeOpen = assistant?.open;
|
|
107
|
+
const eventSource = source ?? assistant?.source;
|
|
108
|
+
// The latest opener, for the shortcut listener: a host's inline callback must not re-bind it every render.
|
|
109
|
+
const openRef = useRef<() => void>(() => undefined);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
openRef.current = () => (runtimeOpen ? runtimeOpen({}) : openAskAi(eventSource));
|
|
112
|
+
});
|
|
96
113
|
|
|
97
114
|
// The shortcut the key cap shows opens the same chat the click opens.
|
|
98
115
|
useEffect(() => {
|
|
@@ -105,11 +122,14 @@ export function MingoAiButton({
|
|
|
105
122
|
const modifier = commandKey ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;
|
|
106
123
|
if (event.key.toLowerCase() !== 'k' || !modifier) return;
|
|
107
124
|
event.preventDefault();
|
|
108
|
-
|
|
125
|
+
openRef.current();
|
|
109
126
|
};
|
|
110
127
|
window.addEventListener('keydown', onKeyDown);
|
|
111
128
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
112
|
-
}, [shortcutHint,
|
|
129
|
+
}, [shortcutHint, commandKey]);
|
|
130
|
+
|
|
131
|
+
// No name from the host or the runtime: nothing to launch under. The lib invents none.
|
|
132
|
+
if (!label) return null;
|
|
113
133
|
|
|
114
134
|
return (
|
|
115
135
|
<Button
|
|
@@ -121,7 +141,8 @@ export function MingoAiButton({
|
|
|
121
141
|
aria-label={label}
|
|
122
142
|
aria-keyshortcuts={shortcutHint && visitor.known ? (commandKey ? 'Meta+K' : 'Control+K') : undefined}
|
|
123
143
|
onClick={e => {
|
|
124
|
-
|
|
144
|
+
if (runtimeOpen) runtimeOpen({});
|
|
145
|
+
else openAskAi(eventSource);
|
|
125
146
|
onClick?.(e);
|
|
126
147
|
}}
|
|
127
148
|
className={cn(
|
|
@@ -339,6 +339,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function ButtonI
|
|
|
339
339
|
download={download}
|
|
340
340
|
prefetch={false}
|
|
341
341
|
className={cn(shellClasses, isDisabled && 'pointer-events-none')}
|
|
342
|
+
// The click still reaches the caller (a download is what analytics counts).
|
|
343
|
+
onClick={onClick as unknown as React.MouseEventHandler<HTMLAnchorElement> | undefined}
|
|
342
344
|
aria-label={props['aria-label']}
|
|
343
345
|
>
|
|
344
346
|
{splitContent}
|
|
@@ -423,6 +425,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function ButtonI
|
|
|
423
425
|
download={download}
|
|
424
426
|
prefetch={false}
|
|
425
427
|
className={cn(classes, isDisabled && 'pointer-events-none')}
|
|
428
|
+
onClick={onClick as unknown as React.MouseEventHandler<HTMLAnchorElement> | undefined}
|
|
426
429
|
aria-label={props['aria-label']}
|
|
427
430
|
>
|
|
428
431
|
{content}
|
|
@@ -73,6 +73,7 @@ export * from './status-badge';
|
|
|
73
73
|
export * from './palette-badge';
|
|
74
74
|
export * from './department-badge';
|
|
75
75
|
export * from './store-badges';
|
|
76
|
+
export * from './mobile-app-qr';
|
|
76
77
|
export * from './status-indicator';
|
|
77
78
|
export * from './toaster';
|
|
78
79
|
// TODO: Add other UI components as they are moved to ui-kit
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { MOBILE_APP_QR_PATH, MOBILE_APP_QR_VIEWBOX_SIZE } from '../../assets/mobile-app-qr';
|
|
2
|
+
import { cn } from '../../utils/cn';
|
|
3
|
+
import { MOBILE_APP_INSTALL_URL } from '../../utils/mobile-app';
|
|
4
|
+
|
|
5
|
+
export interface MobileAppQrProps {
|
|
6
|
+
/** Sizes the code; the caller owns its footprint. */
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The install QR code, drawn inline rather than loaded from `/public` so the
|
|
12
|
+
* modules take their colour from the surface they sit on.
|
|
13
|
+
*
|
|
14
|
+
* Dark modules, and the caller supplies a light plate. Decoders binarize the
|
|
15
|
+
* image and expect dark-on-light; inverting that is one of the two things they
|
|
16
|
+
* reliably fail on. The other is a missing quiet zone, which is why the 4-module
|
|
17
|
+
* margin is baked into the viewBox — keep it even when the plate is larger.
|
|
18
|
+
*
|
|
19
|
+
* That polarity holds only under the dark theme, where `text-ods-bg` is the dark
|
|
20
|
+
* value and `bg-ods-bg-inverted` the light one; both flip under `.theme-light`.
|
|
21
|
+
* Do not render this inside a light-theme scope: an inverted code is silently
|
|
22
|
+
* unscannable rather than visibly broken.
|
|
23
|
+
*
|
|
24
|
+
* The artwork is the asset `MOBILE_APP_QR_PATH` (`assets/mobile-app-qr`), which
|
|
25
|
+
* encodes {@link MOBILE_APP_INSTALL_URL}; the regeneration note lives there.
|
|
26
|
+
*/
|
|
27
|
+
export function MobileAppQr({ className }: MobileAppQrProps) {
|
|
28
|
+
return (
|
|
29
|
+
<svg
|
|
30
|
+
viewBox={`0 0 ${MOBILE_APP_QR_VIEWBOX_SIZE} ${MOBILE_APP_QR_VIEWBOX_SIZE}`}
|
|
31
|
+
fill="none"
|
|
32
|
+
shapeRendering="crispEdges"
|
|
33
|
+
role="img"
|
|
34
|
+
aria-label={`QR code for ${MOBILE_APP_INSTALL_URL}`}
|
|
35
|
+
className={cn('h-[120px] w-[120px] text-ods-bg', className)}
|
|
36
|
+
>
|
|
37
|
+
<path stroke="currentColor" d={MOBILE_APP_QR_PATH} />
|
|
38
|
+
</svg>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -299,32 +299,40 @@ export function GooglePlayBadge({ className }: StoreBadgeProps) {
|
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
export interface StoreBadgeLinksProps {
|
|
302
|
-
|
|
303
|
-
|
|
302
|
+
/** The App Store listing. Absent or null: that badge is not drawn. */
|
|
303
|
+
appStoreUrl?: string | null;
|
|
304
|
+
/** The Google Play listing. Absent or null: that badge is not drawn. */
|
|
305
|
+
googlePlayUrl?: string | null;
|
|
304
306
|
/** Opens both listings in a new tab. */
|
|
305
307
|
openInNewTab?: boolean;
|
|
306
308
|
className?: string;
|
|
307
309
|
}
|
|
308
310
|
|
|
309
311
|
/**
|
|
310
|
-
* The
|
|
312
|
+
* The store badges as links — the canonical "get our mobile app" row. A store
|
|
313
|
+
* with no listing has no badge, and with neither the row is not rendered.
|
|
311
314
|
*
|
|
312
315
|
* Both are rendered at the same height because Apple and Google each require
|
|
313
316
|
* their badge to be no smaller than any other store's badge beside it, which is
|
|
314
317
|
* only satisfiable by drawing them equal.
|
|
315
318
|
*/
|
|
316
319
|
export function StoreBadgeLinks({ appStoreUrl, googlePlayUrl, openInNewTab = true, className }: StoreBadgeLinksProps) {
|
|
320
|
+
if (!appStoreUrl && !googlePlayUrl) return null;
|
|
317
321
|
return (
|
|
318
322
|
<div className={cn('flex flex-wrap items-center gap-[var(--spacing-system-lf)]', className)}>
|
|
319
323
|
{/* `rel` on both branches: `noopener` is inert without a target, but `noreferrer` is
|
|
320
324
|
not — dropping it on the same-window branch sent the tenant's hostname to Apple
|
|
321
325
|
and Google as a Referer. */}
|
|
322
|
-
|
|
323
|
-
<
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
326
|
+
{appStoreUrl && (
|
|
327
|
+
<a href={appStoreUrl} target={openInNewTab ? '_blank' : undefined} rel="noopener noreferrer">
|
|
328
|
+
<AppStoreBadge className="h-12 w-auto" />
|
|
329
|
+
</a>
|
|
330
|
+
)}
|
|
331
|
+
{googlePlayUrl && (
|
|
332
|
+
<a href={googlePlayUrl} target={openInNewTab ? '_blank' : undefined} rel="noopener noreferrer">
|
|
333
|
+
<GooglePlayBadge className="h-12 w-auto" />
|
|
334
|
+
</a>
|
|
335
|
+
)}
|
|
328
336
|
</div>
|
|
329
337
|
);
|
|
330
338
|
}
|