@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.
Files changed (228) hide show
  1. package/dist/assets/index.cjs +6 -0
  2. package/dist/assets/index.d.ts +1 -0
  3. package/dist/assets/index.d.ts.map +1 -1
  4. package/dist/assets/index.js +5 -1
  5. package/dist/assets/mobile-app-qr.d.ts +17 -0
  6. package/dist/assets/mobile-app-qr.d.ts.map +1 -0
  7. package/dist/{chunk-2JLP2X2M.js → chunk-2P2M27EW.js} +2396 -2369
  8. package/dist/{chunk-IWCPXJEE.js → chunk-2TXGR7IK.js} +138 -63
  9. package/dist/{chunk-FVQJBII6.js → chunk-3LY7V3RF.js} +3 -3
  10. package/dist/{chunk-HCFNEKO3.cjs → chunk-3R32ZNA2.cjs} +46 -46
  11. package/dist/{chunk-WSDT7LYL.js → chunk-3UBWDR65.js} +1 -1
  12. package/dist/{chunk-NZ5BHDFZ.js → chunk-44Q6MV7C.js} +2 -2
  13. package/dist/{chunk-CMZ3QTFQ.js → chunk-4I3K6LEH.js} +24 -0
  14. package/dist/{chunk-ZXCZYMN4.cjs → chunk-4LY6R4TN.cjs} +137 -62
  15. package/dist/{chunk-EAHOB64X.js → chunk-57UTI7ZH.js} +106 -42
  16. package/dist/{chunk-XGHGGBM3.js → chunk-5QXE5HMV.js} +4 -4
  17. package/dist/{chunk-NYWESHOQ.cjs → chunk-6AJIOW3S.cjs} +57 -57
  18. package/dist/{chunk-4QFWGLZO.js → chunk-6YBUQAOH.js} +1 -1
  19. package/dist/{chunk-JHQ334WS.js → chunk-742BGXNX.js} +2 -0
  20. package/dist/{chunk-D6PMVWDI.js → chunk-74N742ZG.js} +6 -6
  21. package/dist/{chunk-AECTRMGQ.cjs → chunk-7GRPR7XA.cjs} +13 -1
  22. package/dist/{chunk-4HJRHEXD.cjs → chunk-7Z5FMQYR.cjs} +22 -22
  23. package/dist/chunk-AFTRPEXO.js +13 -0
  24. package/dist/{chunk-WBNAEYTQ.cjs → chunk-BYUIFT7Q.cjs} +6 -6
  25. package/dist/{chunk-LCLDFMDF.cjs → chunk-BYVVE7HP.cjs} +58 -58
  26. package/dist/{chunk-JPD4NXH6.js → chunk-CO3FOZIH.js} +1 -1
  27. package/dist/{chunk-E7YCPY7K.cjs → chunk-D2DH6VZL.cjs} +3 -3
  28. package/dist/{chunk-HWVUR735.cjs → chunk-DFM572VH.cjs} +2 -2
  29. package/dist/{chunk-YDPKUAP4.js → chunk-DJ4IFQNQ.js} +2 -0
  30. package/dist/{chunk-5U3KWQXP.js → chunk-E6SMMMKV.js} +13 -13
  31. package/dist/chunk-EWPR7662.cjs +13 -0
  32. package/dist/{chunk-SWAVO7BB.cjs → chunk-F7ORKKEF.cjs} +108 -108
  33. package/dist/{chunk-AGVXKD4K.js → chunk-FSQQZFVG.js} +12 -0
  34. package/dist/{chunk-ETYLOZOB.cjs → chunk-GCK75D7F.cjs} +109 -22
  35. package/dist/{chunk-ZKNZ2CXW.cjs → chunk-GGBYZKMS.cjs} +11 -11
  36. package/dist/{chunk-Y4MB25KW.js → chunk-GYAXV3MZ.js} +3 -3
  37. package/dist/{chunk-UWQWMGJ7.cjs → chunk-HJMSNOBT.cjs} +1040 -696
  38. package/dist/{chunk-DFQYAYBH.cjs → chunk-HQIOMFJH.cjs} +2 -0
  39. package/dist/{chunk-JH2SIWPQ.js → chunk-HXB7SEYG.js} +1611 -1267
  40. package/dist/{chunk-LE5WKGJV.cjs → chunk-I2254QN6.cjs} +16 -16
  41. package/dist/{chunk-PSVSEN4J.cjs → chunk-IMTY3GRR.cjs} +2 -0
  42. package/dist/{chunk-ILNN6QRW.js → chunk-JDBZ4ZHU.js} +3 -3
  43. package/dist/{chunk-GQ7JJUIT.cjs → chunk-K2JUJ37D.cjs} +25 -1
  44. package/dist/{chunk-6YPTUHM7.js → chunk-K76ACBR3.js} +3 -3
  45. package/dist/{chunk-2NASBO7O.js → chunk-KFOAV77K.js} +2 -2
  46. package/dist/{chunk-CU6NWO2M.js → chunk-LAXKEBDD.js} +14 -18
  47. package/dist/{chunk-5YAGX5DY.js → chunk-LGKMR7NZ.js} +7 -7
  48. package/dist/{chunk-E2V2EH2G.cjs → chunk-LODMNSQY.cjs} +16 -16
  49. package/dist/{chunk-4D6NMPOO.js → chunk-NJ5TBTJG.js} +7 -7
  50. package/dist/{chunk-TDZ76NCB.cjs → chunk-NMPVK53I.cjs} +29 -29
  51. package/dist/{chunk-TTVOLEUS.js → chunk-NPSJE7J3.js} +90 -3
  52. package/dist/{chunk-H5VIMBRN.cjs → chunk-O5EHOJZD.cjs} +4 -4
  53. package/dist/{chunk-KT3U3ZRP.cjs → chunk-OKE5BIQH.cjs} +8 -8
  54. package/dist/{chunk-DC4RTYWK.cjs → chunk-QLTXT562.cjs} +5 -5
  55. package/dist/{chunk-H5V7GRN5.js → chunk-QQCFPMAA.js} +5 -7
  56. package/dist/{chunk-7BREFV7G.cjs → chunk-QYS4YVEP.cjs} +34 -36
  57. package/dist/{chunk-7BCY77W6.js → chunk-RCI3PALX.js} +8 -10
  58. package/dist/{chunk-P3JMAMHP.js → chunk-REETLEL5.js} +1 -1
  59. package/dist/{chunk-4JIGEK7C.cjs → chunk-RJMWJAJO.cjs} +97 -33
  60. package/dist/{chunk-WS7WRR5Z.cjs → chunk-RQZQHLNS.cjs} +86 -90
  61. package/dist/{chunk-FFT56TYB.cjs → chunk-RUZIICXT.cjs} +2 -2
  62. package/dist/{chunk-AJHSQ5XK.js → chunk-S63QBBT7.js} +6 -6
  63. package/dist/{chunk-TELAANY6.js → chunk-UXU7Y5BH.js} +2 -2
  64. package/dist/{chunk-NQVS4G7G.cjs → chunk-UZTOI6FJ.cjs} +773 -746
  65. package/dist/{chunk-DFEPUYTF.cjs → chunk-VFICDEYJ.cjs} +45 -47
  66. package/dist/{chunk-LOZLHG2U.cjs → chunk-XE2K53GH.cjs} +20 -20
  67. package/dist/{chunk-QBCDSTLE.cjs → chunk-XXO52RZR.cjs} +26 -26
  68. package/dist/{chunk-TTEFN6VO.js → chunk-ZLUFEWZZ.js} +10 -10
  69. package/dist/{chunk-MA4Q5RBP.cjs → chunk-ZSOFLPQN.cjs} +9 -9
  70. package/dist/{chunk-KBJJQFYU.js → chunk-ZZEKXGFS.js} +2 -2
  71. package/dist/{cloud-tenants-FBUO66GW.cjs → cloud-tenants-HZHKRUIW.cjs} +16 -16
  72. package/dist/{cloud-tenants-DNBPNQNP.js → cloud-tenants-Q4TCJS5Y.js} +15 -15
  73. package/dist/components/calendar.cjs +4 -4
  74. package/dist/components/calendar.js +1 -1
  75. package/dist/components/case-studies/index.cjs +30 -30
  76. package/dist/components/case-studies/index.js +16 -16
  77. package/dist/components/chat/ask-prompts.d.ts +10 -2
  78. package/dist/components/chat/ask-prompts.d.ts.map +1 -1
  79. package/dist/components/chat/assistant-ask-prompts.d.ts +27 -0
  80. package/dist/components/chat/assistant-ask-prompts.d.ts.map +1 -0
  81. package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
  82. package/dist/components/chat/hooks/index.d.ts +1 -0
  83. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  84. package/dist/components/chat/hooks/use-ask-prompts.d.ts +63 -0
  85. package/dist/components/chat/hooks/use-ask-prompts.d.ts.map +1 -0
  86. package/dist/components/chat/index.cjs +27 -10
  87. package/dist/components/chat/index.d.ts +1 -0
  88. package/dist/components/chat/index.d.ts.map +1 -1
  89. package/dist/components/chat/index.js +26 -9
  90. package/dist/components/chat/utils/icon-library.d.ts.map +1 -1
  91. package/dist/components/contact/index.cjs +17 -17
  92. package/dist/components/contact/index.js +16 -16
  93. package/dist/components/docs/index.cjs +20 -20
  94. package/dist/components/docs/index.js +19 -19
  95. package/dist/components/embeds/index.cjs +17 -17
  96. package/dist/components/embeds/index.js +16 -16
  97. package/dist/components/faq/faq-ask-card.d.ts +44 -0
  98. package/dist/components/faq/faq-ask-card.d.ts.map +1 -0
  99. package/dist/components/faq/faq-document-page.d.ts +5 -1
  100. package/dist/components/faq/faq-document-page.d.ts.map +1 -1
  101. package/dist/components/faq/faq-section.d.ts +18 -5
  102. package/dist/components/faq/faq-section.d.ts.map +1 -1
  103. package/dist/components/faq/index.cjs +19 -18
  104. package/dist/components/faq/index.d.ts +1 -0
  105. package/dist/components/faq/index.d.ts.map +1 -1
  106. package/dist/components/faq/index.js +18 -17
  107. package/dist/components/features/auth/auth-benefits-panel.d.ts.map +1 -1
  108. package/dist/components/features/index.cjs +16 -16
  109. package/dist/components/features/index.js +15 -15
  110. package/dist/components/help-center-pages/download-apps-page.d.ts +55 -0
  111. package/dist/components/help-center-pages/download-apps-page.d.ts.map +1 -0
  112. package/dist/components/help-center-pages/index.cjs +333 -119
  113. package/dist/components/help-center-pages/index.d.ts +1 -0
  114. package/dist/components/help-center-pages/index.d.ts.map +1 -1
  115. package/dist/components/help-center-pages/index.js +251 -37
  116. package/dist/components/help-center-pages/trust-center-page.d.ts +4 -1
  117. package/dist/components/help-center-pages/trust-center-page.d.ts.map +1 -1
  118. package/dist/components/index.cjs +131 -217
  119. package/dist/components/index.js +236 -322
  120. package/dist/components/meeting-scheduler/index.cjs +61 -62
  121. package/dist/components/meeting-scheduler/index.js +18 -19
  122. package/dist/components/navigation/index.cjs +19 -19
  123. package/dist/components/navigation/index.js +18 -18
  124. package/dist/components/navigation/mingo-ai-button.d.ts +1 -1
  125. package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
  126. package/dist/components/onboarding-guides/index.cjs +19 -20
  127. package/dist/components/onboarding-guides/index.js +18 -19
  128. package/dist/components/product-demo/index.cjs +8 -8
  129. package/dist/components/product-demo/index.js +8 -8
  130. package/dist/components/related-content/index.cjs +7 -9
  131. package/dist/components/related-content/index.js +6 -8
  132. package/dist/components/tickets/index.cjs +21 -21
  133. package/dist/components/tickets/index.js +20 -20
  134. package/dist/components/ui/button/button.d.ts.map +1 -1
  135. package/dist/components/ui/file-manager/index.cjs +11 -11
  136. package/dist/components/ui/file-manager/index.js +1 -1
  137. package/dist/components/ui/index.cjs +34 -16
  138. package/dist/components/ui/index.d.ts +1 -0
  139. package/dist/components/ui/index.d.ts.map +1 -1
  140. package/dist/components/ui/index.js +33 -15
  141. package/dist/components/ui/mobile-app-qr.d.ts +23 -0
  142. package/dist/components/ui/mobile-app-qr.d.ts.map +1 -0
  143. package/dist/components/ui/store-badges.d.ts +7 -4
  144. package/dist/components/ui/store-badges.d.ts.map +1 -1
  145. package/dist/contexts/assistant-runtime-context.d.ts +74 -0
  146. package/dist/contexts/assistant-runtime-context.d.ts.map +1 -0
  147. package/dist/contexts/index.cjs +7 -1
  148. package/dist/contexts/index.d.ts +1 -0
  149. package/dist/contexts/index.d.ts.map +1 -1
  150. package/dist/contexts/index.js +6 -0
  151. package/dist/{devices-5A5TY4JD.cjs → devices-6XHUFIC6.cjs} +17 -17
  152. package/dist/{devices-PLDAEUSL.js → devices-NOUGVTES.js} +15 -15
  153. package/dist/hooks/index.cjs +10 -5
  154. package/dist/hooks/index.d.ts +1 -0
  155. package/dist/hooks/index.d.ts.map +1 -1
  156. package/dist/hooks/index.js +9 -4
  157. package/dist/hooks/use-downloads.d.ts +41 -0
  158. package/dist/hooks/use-downloads.d.ts.map +1 -0
  159. package/dist/index.cjs +80 -20
  160. package/dist/index.js +79 -19
  161. package/dist/{knowledge-G6Q5KRWV.js → knowledge-2YTUXQ2Z.js} +15 -15
  162. package/dist/{knowledge-FKTJ723Z.cjs → knowledge-ANEKPMO2.cjs} +17 -17
  163. package/dist/{logs-CJCXUBU6.cjs → logs-U3IB3HUM.cjs} +25 -25
  164. package/dist/{logs-QMV6XIC4.js → logs-WVQIONUT.js} +15 -15
  165. package/dist/{policies-G6D7AX6F.js → policies-66EGDW2H.js} +15 -15
  166. package/dist/{policies-QMRTK3CY.cjs → policies-UFRN2EQF.cjs} +20 -20
  167. package/dist/{remote-session-DBD6OSTX.js → remote-session-6WEUJZDC.js} +5 -5
  168. package/dist/{remote-session-2JVI7RVA.cjs → remote-session-A75X5NPC.cjs} +7 -7
  169. package/dist/{software-update-PNZMRDBT.cjs → software-update-2MHKL76D.cjs} +20 -20
  170. package/dist/{software-update-6BQM6BKH.js → software-update-3ZD3MXF3.js} +15 -15
  171. package/dist/{tickets-board-BHLPAVNJ.cjs → tickets-board-IFWMIZ35.cjs} +9 -9
  172. package/dist/{tickets-board-PYDNELSK.js → tickets-board-MYXG3V72.js} +4 -4
  173. package/dist/types/downloads.d.ts +67 -0
  174. package/dist/types/downloads.d.ts.map +1 -0
  175. package/dist/types/index.cjs +24 -0
  176. package/dist/types/index.d.ts +1 -0
  177. package/dist/types/index.d.ts.map +1 -1
  178. package/dist/types/index.js +16 -1
  179. package/dist/utils/index.cjs +46 -2
  180. package/dist/utils/index.d.ts +2 -0
  181. package/dist/utils/index.d.ts.map +1 -1
  182. package/dist/utils/index.js +40 -3
  183. package/dist/utils/list-url.d.ts.map +1 -1
  184. package/dist/utils/mobile-app.cjs +46 -0
  185. package/dist/utils/mobile-app.d.ts +40 -0
  186. package/dist/utils/mobile-app.d.ts.map +1 -0
  187. package/dist/utils/mobile-app.js +40 -0
  188. package/dist/utils/source-icons.d.ts.map +1 -1
  189. package/dist/utils/trial.d.ts +18 -0
  190. package/dist/utils/trial.d.ts.map +1 -0
  191. package/package.json +7 -1
  192. package/src/assets/index.ts +1 -0
  193. package/src/assets/mobile-app-qr.ts +19 -0
  194. package/src/components/chat/ask-prompts.tsx +25 -3
  195. package/src/components/chat/assistant-ask-prompts.tsx +78 -0
  196. package/src/components/chat/entity-cards/dispatch.tsx +38 -4
  197. package/src/components/chat/hooks/index.ts +12 -0
  198. package/src/components/chat/hooks/use-ask-prompts.ts +188 -0
  199. package/src/components/chat/index.ts +1 -0
  200. package/src/components/chat/utils/icon-library.tsx +6 -0
  201. package/src/components/faq/faq-ask-card.tsx +115 -0
  202. package/src/components/faq/faq-document-page.tsx +6 -1
  203. package/src/components/faq/faq-section.tsx +27 -5
  204. package/src/components/faq/index.ts +1 -0
  205. package/src/components/features/auth/auth-benefits-panel.tsx +5 -4
  206. package/src/components/help-center-pages/download-apps-page.tsx +364 -0
  207. package/src/components/help-center-pages/index.ts +1 -0
  208. package/src/components/help-center-pages/trust-center-page.tsx +5 -2
  209. package/src/components/navigation/mingo-ai-button.tsx +27 -6
  210. package/src/components/ui/button/button.tsx +3 -0
  211. package/src/components/ui/index.ts +1 -0
  212. package/src/components/ui/mobile-app-qr.tsx +40 -0
  213. package/src/components/ui/store-badges.tsx +17 -9
  214. package/src/contexts/assistant-runtime-context.tsx +84 -0
  215. package/src/contexts/index.ts +9 -0
  216. package/src/hooks/index.ts +1 -0
  217. package/src/hooks/use-downloads.ts +138 -0
  218. package/src/types/downloads.ts +82 -0
  219. package/src/types/index.ts +1 -0
  220. package/src/utils/index.ts +3 -0
  221. package/src/utils/list-url.ts +4 -0
  222. package/src/utils/mobile-app.ts +57 -0
  223. package/src/utils/source-icons.ts +10 -2
  224. package/src/utils/trial.ts +19 -0
  225. package/dist/chunk-6X3O67OL.cjs +0 -89
  226. package/dist/chunk-AUMKE74M.cjs +0 -16
  227. package/dist/chunk-G3GRYDMS.js +0 -16
  228. 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 = 'Mingo AI',
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
- openAskAi(source);
125
+ openRef.current();
109
126
  };
110
127
  window.addEventListener('keydown', onKeyDown);
111
128
  return () => window.removeEventListener('keydown', onKeyDown);
112
- }, [shortcutHint, source, commandKey]);
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
- openAskAi(source);
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
- appStoreUrl: string;
303
- googlePlayUrl: string;
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 pair of store badges as links — the canonical "get our mobile app" row.
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
- <a href={appStoreUrl} target={openInNewTab ? '_blank' : undefined} rel="noopener noreferrer">
323
- <AppStoreBadge className="h-12 w-auto" />
324
- </a>
325
- <a href={googlePlayUrl} target={openInNewTab ? '_blank' : undefined} rel="noopener noreferrer">
326
- <GooglePlayBadge className="h-12 w-auto" />
327
- </a>
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
  }