@flamingo-stack/openframe-frontend-core 0.0.721 → 0.0.722

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 (134) hide show
  1. package/dist/assets/index.cjs +0 -6
  2. package/dist/assets/index.d.ts +0 -1
  3. package/dist/assets/index.d.ts.map +1 -1
  4. package/dist/assets/index.js +1 -5
  5. package/dist/{chunk-KFOAV77K.js → chunk-5D2K7TFT.js} +1 -1
  6. package/dist/{chunk-NMPVK53I.cjs → chunk-5K6YKRP5.cjs} +23 -23
  7. package/dist/{chunk-S63QBBT7.js → chunk-65C4WHG4.js} +5 -5
  8. package/dist/{chunk-XE2K53GH.cjs → chunk-6HRQLM6T.cjs} +17 -17
  9. package/dist/{chunk-NPSJE7J3.js → chunk-7UZ2JZJY.js} +2 -2
  10. package/dist/{chunk-7Z5FMQYR.cjs → chunk-ABSWD4PQ.cjs} +14 -14
  11. package/dist/{chunk-RCI3PALX.js → chunk-AWWLEYAU.js} +6 -6
  12. package/dist/{chunk-NJ5TBTJG.js → chunk-BCU6UUNQ.js} +5 -5
  13. package/dist/{chunk-3LY7V3RF.js → chunk-BUDZWXRG.js} +2 -2
  14. package/dist/{chunk-UZTOI6FJ.cjs → chunk-DWSBTGKT.cjs} +590 -598
  15. package/dist/{chunk-QQCFPMAA.js → chunk-DX3W7DPC.js} +1 -1
  16. package/dist/{chunk-HXB7SEYG.js → chunk-E7NIZXDT.js} +7 -16
  17. package/dist/{chunk-74N742ZG.js → chunk-ETQVXIYW.js} +4 -4
  18. package/dist/{chunk-742BGXNX.js → chunk-EWED6ISQ.js} +1 -1
  19. package/dist/{chunk-LAXKEBDD.js → chunk-F33RMJOL.js} +7 -7
  20. package/dist/{chunk-QLTXT562.cjs → chunk-FDWYBJYD.cjs} +6 -6
  21. package/dist/{chunk-6AJIOW3S.cjs → chunk-GAFK3WNT.cjs} +37 -37
  22. package/dist/{chunk-3R32ZNA2.cjs → chunk-GYSTZD6X.cjs} +35 -35
  23. package/dist/{chunk-57UTI7ZH.js → chunk-HA2ASGJM.js} +4 -4
  24. package/dist/{chunk-OKE5BIQH.cjs → chunk-HP4UUBJC.cjs} +8 -8
  25. package/dist/{chunk-CO3FOZIH.js → chunk-I3UUMRO7.js} +1 -1
  26. package/dist/{chunk-ZLUFEWZZ.js → chunk-IF72N4PE.js} +6 -6
  27. package/dist/{chunk-GCK75D7F.cjs → chunk-IITZ6JSC.cjs} +28 -28
  28. package/dist/{chunk-QYS4YVEP.cjs → chunk-JQ55FFIK.cjs} +31 -31
  29. package/dist/{chunk-2P2M27EW.js → chunk-KBPGSPXQ.js} +13 -21
  30. package/dist/{chunk-JDBZ4ZHU.js → chunk-KS3YLYVL.js} +2 -2
  31. package/dist/{chunk-RJMWJAJO.cjs → chunk-KWBH63GV.cjs} +19 -19
  32. package/dist/{chunk-IYVFSLWJ.js → chunk-KZQO5EJQ.js} +4 -0
  33. package/dist/{chunk-IMTY3GRR.cjs → chunk-LV6UJD2K.cjs} +2 -2
  34. package/dist/{chunk-XXO52RZR.cjs → chunk-MJ4SXOG2.cjs} +18 -18
  35. package/dist/{chunk-BYVVE7HP.cjs → chunk-NFABAOMD.cjs} +34 -36
  36. package/dist/{chunk-UXU7Y5BH.js → chunk-NMGWNUKB.js} +3 -3
  37. package/dist/{chunk-HJMSNOBT.cjs → chunk-P7Y626YJ.cjs} +258 -267
  38. package/dist/{chunk-RQZQHLNS.cjs → chunk-RVQV6J5K.cjs} +66 -66
  39. package/dist/{chunk-E6SMMMKV.js → chunk-SZ3CBFSH.js} +9 -9
  40. package/dist/{chunk-LGKMR7NZ.js → chunk-SZF7ENIW.js} +3 -3
  41. package/dist/{chunk-F5BPGWGT.cjs → chunk-TKFTWM3V.cjs} +5 -1
  42. package/dist/{chunk-GYAXV3MZ.js → chunk-UYMTAFHG.js} +4 -6
  43. package/dist/{chunk-F7ORKKEF.cjs → chunk-VC7COEWF.cjs} +83 -83
  44. package/dist/{chunk-44Q6MV7C.js → chunk-VSYQJCNR.js} +1 -1
  45. package/dist/{chunk-O5EHOJZD.cjs → chunk-VU7RU3LL.cjs} +7 -7
  46. package/dist/{chunk-D2DH6VZL.cjs → chunk-VV5INIQB.cjs} +3 -3
  47. package/dist/{chunk-I2254QN6.cjs → chunk-VYHCN5NU.cjs} +3 -3
  48. package/dist/{chunk-ZZEKXGFS.js → chunk-WY4SLTMZ.js} +2 -2
  49. package/dist/{chunk-LODMNSQY.cjs → chunk-XJYAUIOL.cjs} +11 -11
  50. package/dist/{chunk-GGBYZKMS.cjs → chunk-XR7NVJXS.cjs} +5 -5
  51. package/dist/{chunk-VFICDEYJ.cjs → chunk-ZHC6MDGF.cjs} +41 -41
  52. package/dist/{chunk-5QXE5HMV.js → chunk-ZMHP4Y6M.js} +3 -3
  53. package/dist/{cloud-tenants-Q4TCJS5Y.js → cloud-tenants-4Z36KWSX.js} +9 -9
  54. package/dist/{cloud-tenants-HZHKRUIW.cjs → cloud-tenants-5NIC5VXK.cjs} +10 -10
  55. package/dist/components/case-studies/index.cjs +23 -23
  56. package/dist/components/case-studies/index.js +10 -10
  57. package/dist/components/chat/index.cjs +7 -7
  58. package/dist/components/chat/index.js +6 -6
  59. package/dist/components/contact/index.cjs +11 -11
  60. package/dist/components/contact/index.js +10 -10
  61. package/dist/components/docs/index.cjs +14 -14
  62. package/dist/components/docs/index.js +13 -13
  63. package/dist/components/embeds/index.cjs +11 -11
  64. package/dist/components/embeds/index.js +10 -10
  65. package/dist/components/faq/index.cjs +11 -11
  66. package/dist/components/faq/index.js +10 -10
  67. package/dist/components/features/index.cjs +10 -10
  68. package/dist/components/features/index.js +9 -9
  69. package/dist/components/features/remote-session/remote-access-request-card.d.ts.map +1 -1
  70. package/dist/components/help-center-pages/download-apps-page.d.ts +19 -6
  71. package/dist/components/help-center-pages/download-apps-page.d.ts.map +1 -1
  72. package/dist/components/help-center-pages/index.cjs +145 -130
  73. package/dist/components/help-center-pages/index.d.ts +1 -1
  74. package/dist/components/help-center-pages/index.d.ts.map +1 -1
  75. package/dist/components/help-center-pages/index.js +59 -44
  76. package/dist/components/index.cjs +64 -64
  77. package/dist/components/index.js +21 -21
  78. package/dist/components/meeting-scheduler/index.cjs +47 -47
  79. package/dist/components/meeting-scheduler/index.js +9 -9
  80. package/dist/components/navigation/index.cjs +13 -13
  81. package/dist/components/navigation/index.js +12 -12
  82. package/dist/components/onboarding-guides/index.cjs +13 -13
  83. package/dist/components/onboarding-guides/index.js +12 -12
  84. package/dist/components/product-demo/index.cjs +8 -8
  85. package/dist/components/product-demo/index.js +8 -8
  86. package/dist/components/related-content/index.cjs +5 -5
  87. package/dist/components/related-content/index.js +4 -4
  88. package/dist/components/tickets/index.cjs +14 -14
  89. package/dist/components/tickets/index.js +13 -13
  90. package/dist/components/ui/index.cjs +10 -10
  91. package/dist/components/ui/index.js +9 -9
  92. package/dist/components/ui/mobile-app-qr.d.ts +13 -15
  93. package/dist/components/ui/mobile-app-qr.d.ts.map +1 -1
  94. package/dist/{devices-NOUGVTES.js → devices-2TK3LAKM.js} +9 -9
  95. package/dist/{devices-6XHUFIC6.cjs → devices-7IKT72DK.cjs} +11 -11
  96. package/dist/index.cjs +16 -24
  97. package/dist/index.js +15 -23
  98. package/dist/{knowledge-2YTUXQ2Z.js → knowledge-3GJ4MU3V.js} +9 -9
  99. package/dist/{knowledge-ANEKPMO2.cjs → knowledge-HYSEV35H.cjs} +11 -11
  100. package/dist/{logs-WVQIONUT.js → logs-TATVM62H.js} +9 -9
  101. package/dist/{logs-U3IB3HUM.cjs → logs-ZX3NADQC.cjs} +18 -18
  102. package/dist/{policies-UFRN2EQF.cjs → policies-J4FSDKPI.cjs} +14 -14
  103. package/dist/{policies-66EGDW2H.js → policies-MRRKLF4Q.js} +9 -9
  104. package/dist/{remote-session-6WEUJZDC.js → remote-session-WCLR47RN.js} +4 -4
  105. package/dist/{remote-session-A75X5NPC.cjs → remote-session-Z2YCUPHU.cjs} +6 -6
  106. package/dist/{software-update-2MHKL76D.cjs → software-update-CXUBWYD2.cjs} +14 -14
  107. package/dist/{software-update-3ZD3MXF3.js → software-update-KM46WUVQ.js} +9 -9
  108. package/dist/{tickets-board-IFWMIZ35.cjs → tickets-board-R5OQSEEC.cjs} +8 -8
  109. package/dist/{tickets-board-MYXG3V72.js → tickets-board-U3RF3AO4.js} +3 -3
  110. package/dist/types/downloads.d.ts +16 -0
  111. package/dist/types/downloads.d.ts.map +1 -1
  112. package/dist/utils/format.d.ts +11 -0
  113. package/dist/utils/format.d.ts.map +1 -1
  114. package/dist/utils/index.cjs +4 -9
  115. package/dist/utils/index.d.ts +1 -1
  116. package/dist/utils/index.d.ts.map +1 -1
  117. package/dist/utils/index.js +4 -7
  118. package/dist/utils/mobile-app.cjs +0 -9
  119. package/dist/utils/mobile-app.d.ts +8 -25
  120. package/dist/utils/mobile-app.d.ts.map +1 -1
  121. package/dist/utils/mobile-app.js +1 -7
  122. package/package.json +1 -1
  123. package/src/assets/index.ts +0 -1
  124. package/src/components/features/remote-session/remote-access-request-card.tsx +6 -6
  125. package/src/components/help-center-pages/download-apps-page.tsx +53 -29
  126. package/src/components/help-center-pages/index.ts +1 -1
  127. package/src/components/ui/mobile-app-qr.tsx +17 -22
  128. package/src/types/downloads.ts +17 -0
  129. package/src/utils/format.ts +15 -0
  130. package/src/utils/index.ts +1 -1
  131. package/src/utils/mobile-app.ts +8 -31
  132. package/dist/assets/mobile-app-qr.d.ts +0 -17
  133. package/dist/assets/mobile-app-qr.d.ts.map +0 -1
  134. package/src/assets/mobile-app-qr.ts +0 -19
@@ -8,8 +8,14 @@
8
8
  * when a visitor looks for another system). In a card the installer is THE
9
9
  * action (the other architecture is a quieter button) and each package
10
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).
11
+ * - The mobile app: the store badges, and the install QR code beside them. The
12
+ * code and the address it encodes are the server's (`mobile.install`): the
13
+ * website's install link, which the server answers with a redirect to the
14
+ * phone's store. This page redirects nobody, and with no install link from
15
+ * the server it draws no code.
16
+ * - `web`: the way in for someone who installs nothing (the website's trial
17
+ * signup). The host names the words and the button; the page draws the card,
18
+ * in the frame of every other card on it.
13
19
  *
14
20
  * DATA: all of it is the server's answer (`DownloadsPublic`), read by
15
21
  * `useDownloads` against `endpoint` (default `DOWNLOADS_API_PATH`; embedders
@@ -26,7 +32,7 @@
26
32
  * The chrome is the canonical `PageShell` + frozen `PageLayout`.
27
33
  */
28
34
 
29
- import { useEffect, type ComponentType, type ReactNode } from 'react';
35
+ import type { ComponentType, ReactNode } from 'react';
30
36
  import { useRouter } from '../../embed-shims/next-navigation';
31
37
  import { useCopyToClipboard } from '../../hooks/use-copy-to-clipboard';
32
38
  import { useDownloads } from '../../hooks/use-downloads';
@@ -44,7 +50,7 @@ import {
44
50
  type MobileAppLinks,
45
51
  } from '../../types/downloads';
46
52
  import { cn } from '../../utils/cn';
47
- import { DOWNLOAD_PAGE_STORE_PARAM, MOBILE_APP_INSTALL_HOST_PATH, resolveMobileStoreUrl } from '../../utils/mobile-app';
53
+ import { formatUrlForDisplay } from '../../utils/format';
48
54
  import { DESKTOP_OSES, type DesktopOs } from '../../utils/visitor-os';
49
55
  import { CommandBox } from '../features/command-box';
50
56
  import { AppleLogoIcon } from '../icons-v2-generated/brand-logos/apple-logo-icon';
@@ -78,8 +84,16 @@ export interface DownloadAppsPageProps {
78
84
  openStoresInNewTab?: boolean;
79
85
  /** Called when a visitor downloads an installer or copies an install command (the host's analytics). */
80
86
  onDownloadAction?: (event: DownloadActionEvent) => void;
81
- /** The host's own closing block, under the mobile app. */
82
- footer?: ReactNode;
87
+ /** The closing card, under the mobile app: using OpenFrame without installing anything. Default: none. */
88
+ web?: DownloadAppsWebOption;
89
+ }
90
+
91
+ /** The "no install" card: the host's words and its own button (the page holds no signup link). */
92
+ export interface DownloadAppsWebOption {
93
+ title: string;
94
+ description: string;
95
+ /** The host's button (the website's trial signup). */
96
+ action: ReactNode;
83
97
  }
84
98
 
85
99
  const SECTIONS_CLASS = 'flex flex-col gap-[var(--spacing-system-xlf)]';
@@ -206,6 +220,24 @@ function SystemCard({
206
220
  );
207
221
  }
208
222
 
223
+ /** The closing card: one line of what the web offers, and the host's button at the far end. */
224
+ function WebCard({ web }: { web: DownloadAppsWebOption }) {
225
+ return (
226
+ <section
227
+ aria-labelledby="download-web"
228
+ className={cn(CARD_CLASS, 'content-md:flex-row content-md:items-center content-md:justify-between')}
229
+ >
230
+ <div className="flex flex-col gap-[var(--spacing-system-xsf)]">
231
+ <h2 id="download-web" className="m-0 text-ods-text-primary text-h3">
232
+ {web.title}
233
+ </h2>
234
+ <p className="m-0 text-ods-text-secondary text-h4">{web.description}</p>
235
+ </div>
236
+ <div className="shrink-0">{web.action}</div>
237
+ </section>
238
+ );
239
+ }
240
+
209
241
  /** A section's cards while the server's answer loads: the same grid and card frame, a button's box in each. */
210
242
  function CardsSkeleton({ label }: { label: string }) {
211
243
  return (
@@ -248,7 +280,7 @@ export function DownloadAppsPage({
248
280
  showDesktop = true,
249
281
  openStoresInNewTab = true,
250
282
  onDownloadAction,
251
- footer,
283
+ web,
252
284
  }: DownloadAppsPageProps) {
253
285
  const { data, isLoading, error, reload } = useDownloads({ endpoint, initialData });
254
286
  // The page adapts to the server's answer: a section exists only for what it names.
@@ -258,15 +290,6 @@ export function DownloadAppsPage({
258
290
  // Until the answer lands, a host that offers the desktop app is titled for it.
259
291
  const offersDesktop = showDesktop && (!data || systems.length > 0);
260
292
 
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
293
  let body: ReactNode;
271
294
  if (error && !data) {
272
295
  body = <LoadError message="Could not load the downloads" onRetry={reload} />;
@@ -298,7 +321,7 @@ export function DownloadAppsPage({
298
321
 
299
322
  {mobile && hasMobile && (
300
323
  <Section id="download-mobile" title="Mobile app">
301
- <div className={CARDS_GRID_CLASS}>
324
+ <div className={cardsGridClass(mobile.install ? 2 : 1)}>
302
325
  <div className={CARD_CLASS}>
303
326
  <CardHeader title={storeCardTitle(mobile)} />
304
327
  <p className="m-0 text-ods-text-secondary text-h4">Get alerts and respond to tickets on the go.</p>
@@ -308,18 +331,19 @@ export function DownloadAppsPage({
308
331
  openInNewTab={openStoresInNewTab}
309
332
  />
310
333
  </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]" />
334
+ {mobile.install && (
335
+ <div
336
+ className={cn(CARD_CLASS, 'content-md:flex-row content-md:items-center content-md:justify-between')}
337
+ >
338
+ <div className="flex flex-col gap-[var(--spacing-system-sf)]">
339
+ <CardHeader title="Scan with your phone" />
340
+ <p className="m-0 text-ods-text-secondary text-h4">
341
+ Point your camera at the code, or open {formatUrlForDisplay(mobile.install.url)} on your phone.
342
+ </p>
343
+ </div>
344
+ <MobileAppQr install={mobile.install} className="h-[144px] w-[144px] shrink-0" />
321
345
  </div>
322
- </div>
346
+ )}
323
347
  </div>
324
348
  </Section>
325
349
  )}
@@ -336,7 +360,7 @@ export function DownloadAppsPage({
336
360
  >
337
361
  <div className={SECTIONS_CLASS}>
338
362
  {body}
339
- {footer}
363
+ {web && <WebCard web={web} />}
340
364
  </div>
341
365
  </DownloadAppsChrome>
342
366
  );
@@ -22,7 +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
+ export { DownloadAppsPage, type DownloadAppsPageProps, type DownloadAppsWebOption } from './download-apps-page';
26
26
 
27
27
  // Existing full-pages re-exported for a single Help Center import site.
28
28
  export { FaqDocumentPage, type FaqDocumentPageProps } from '../faq';
@@ -1,40 +1,35 @@
1
- import { MOBILE_APP_QR_PATH, MOBILE_APP_QR_VIEWBOX_SIZE } from '../../assets/mobile-app-qr';
1
+ import type { MobileAppInstallLink } from '../../types/downloads';
2
2
  import { cn } from '../../utils/cn';
3
- import { MOBILE_APP_INSTALL_URL } from '../../utils/mobile-app';
4
3
 
5
4
  export interface MobileAppQrProps {
5
+ /** The server's install link: the address and the artwork that encodes it. */
6
+ install: MobileAppInstallLink;
6
7
  /** Sizes the code; the caller owns its footprint. */
7
8
  className?: string;
8
9
  }
9
10
 
10
11
  /**
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.
12
+ * The install QR code, drawn inline: the modules are the ODS accent colour and
13
+ * the background is transparent, so the code sits directly on whatever surface
14
+ * it is placed on. The artwork and the address it encodes are the server's
15
+ * (`MobileAppInstallLink`): this component holds neither.
13
16
  *
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.
17
+ * On a dark surface that is a light-on-dark code. Phone cameras read both
18
+ * polarities; what they need is contrast between the accent and the surface, and
19
+ * the quiet zone, which the server bakes into the viewBox as a transparent
20
+ * margin. Place it on a plain surface (a card, the page), never over an image
21
+ * or a surface close to the accent in brightness.
26
22
  */
27
- export function MobileAppQr({ className }: MobileAppQrProps) {
23
+ export function MobileAppQr({ install, className }: MobileAppQrProps) {
28
24
  return (
29
25
  <svg
30
- viewBox={`0 0 ${MOBILE_APP_QR_VIEWBOX_SIZE} ${MOBILE_APP_QR_VIEWBOX_SIZE}`}
31
- fill="none"
26
+ viewBox={`0 0 ${install.qrViewBoxSize} ${install.qrViewBoxSize}`}
32
27
  shapeRendering="crispEdges"
33
28
  role="img"
34
- aria-label={`QR code for ${MOBILE_APP_INSTALL_URL}`}
35
- className={cn('h-[120px] w-[120px] text-ods-bg', className)}
29
+ aria-label={`QR code for ${install.url}`}
30
+ className={cn('h-[120px] w-[120px] text-ods-accent', className)}
36
31
  >
37
- <path stroke="currentColor" d={MOBILE_APP_QR_PATH} />
32
+ <path fill="currentColor" d={install.qrPath} />
38
33
  </svg>
39
34
  );
40
35
  }
@@ -44,10 +44,27 @@ export interface AppDownload {
44
44
  label: string | null;
45
45
  }
46
46
 
47
+ /**
48
+ * The install link a phone opens to reach its store, and the QR code that
49
+ * encodes it. Both are the server's: the link is an address on the website
50
+ * (answered there with a redirect by device), and the artwork is drawn from it
51
+ * on the server, so the two can never disagree and the lib names no host.
52
+ */
53
+ export interface MobileAppInstallLink {
54
+ /** The absolute address the code encodes. */
55
+ url: string;
56
+ /** The side of the artwork's square viewBox, quiet zone included. */
57
+ qrViewBoxSize: number;
58
+ /** The dark modules as one SVG path of filled rectangles (`fill` draws them). */
59
+ qrPath: string;
60
+ }
61
+
47
62
  /** Where the mobile app is installed from. A store the server names no listing for is null, and is not offered. */
48
63
  export interface MobileAppLinks {
49
64
  appStoreUrl: string | null;
50
65
  googlePlayUrl: string | null;
66
+ /** The install link and its QR code. Absent or null: no code is drawn. */
67
+ install?: MobileAppInstallLink | null;
51
68
  }
52
69
 
53
70
  /**
@@ -1059,3 +1059,18 @@ export function formatWebinarTimeMeta(
1059
1059
  const time = formatProgramTimeRange(at, { startAt: opts.startAt, withZoneLabel: opts.withZoneLabel });
1060
1060
  return [time, duration].filter(Boolean).join(' · ');
1061
1061
  }
1062
+
1063
+ /**
1064
+ * A web address as it is shown to a person to read or type, not to follow: no
1065
+ * scheme, no query or fragment, no trailing slash.
1066
+ * `https://www.example.com/mobile?from=qr` reads `example.com/mobile`.
1067
+ *
1068
+ * `www` is dropped by default, because nobody types it; `keepWww` keeps it where
1069
+ * the address is shown as the site's own name.
1070
+ */
1071
+ export function formatUrlForDisplay(url: string, options: { keepWww?: boolean } = {}): string {
1072
+ return url
1073
+ .replace(options.keepWww ? /^https?:\/\//i : /^https?:\/\/(www\.)?/i, '')
1074
+ .replace(/[?#].*$/, '')
1075
+ .replace(/\/$/, '');
1076
+ }
@@ -16,7 +16,7 @@ export {
16
16
  // Number / currency / byte / date formatters live in `./format` (single
17
17
  // source of truth). Re-exported here so existing callers that pull from
18
18
  // the barrel keep working without changing imports.
19
- export { formatDate, formatNumber, formatPrice, formatBytes } from './format';
19
+ export { formatDate, formatNumber, formatPrice, formatBytes, formatUrlForDisplay } from './format';
20
20
  export { TRIAL_DAYS, TRIAL_LABELS } from './trial';
21
21
  export {
22
22
  VENDOR_CLASSIFICATIONS,
@@ -1,8 +1,8 @@
1
1
  /**
2
- * The download page's own addresses: where it lives, the parameter that sends a
3
- * phone on to its store, and the URL the install QR code encodes. The store
4
- * listings and installer links are NOT here: they are the server's
5
- * (`DownloadsPublic`, read through `useDownloads`).
2
+ * The download page's address, and what a server needs to send a phone on to
3
+ * its store. No link lives here: the store listings, the installer links and
4
+ * the install link the QR code encodes are all the server's (`DownloadsPublic`,
5
+ * read through `useDownloads`).
6
6
  *
7
7
  * Pure and server-safe; also published as the granular subpath
8
8
  * `./utils/mobile-app`, for an Edge proxy and other server-only consumers.
@@ -13,37 +13,14 @@ import { detectVisitorOs } from './visitor-os';
13
13
  /** The website's download page: every installer and the mobile app. */
14
14
  export const DOWNLOAD_PAGE_PATH = '/download';
15
15
 
16
- /**
17
- * On the download page: "send a phone or a tablet on to its store". The install
18
- * QR code carries it, because whoever scans a code is holding the phone the app
19
- * goes on. Anyone who opens the page without it reads the page.
20
- */
21
- export const DOWNLOAD_PAGE_STORE_PARAM = 'store';
22
-
23
- /**
24
- * The one URL the install QR code encodes: the website's download page, asking
25
- * it to send a phone on to its store.
26
- *
27
- * Absolute and on the production host on purpose: a code must point at
28
- * production whichever deployment rendered the page it came from. `MobileAppQr`
29
- * draws this value as committed path data, so the two change together: see the
30
- * regeneration note on that component.
31
- */
32
- export const MOBILE_APP_INSTALL_URL = `https://www.flamingo.run${DOWNLOAD_PAGE_PATH}?${DOWNLOAD_PAGE_STORE_PARAM}=1`;
33
-
34
- /** The download page as it is printed for a human to read or type: no scheme, no `www`, no query. */
35
- export const MOBILE_APP_INSTALL_HOST_PATH = MOBILE_APP_INSTALL_URL.replace(/^https:\/\/(www\.)?/, '').replace(
36
- /\?.*$/,
37
- '',
38
- );
39
-
40
16
  /**
41
17
  * The store for a phone or tablet, or `null` for every other system and for a
42
18
  * store the server names no listing for (`links` is the server's answer).
43
19
  *
44
- * The server passes the request's User-Agent; a browser also passes
45
- * `navigator.maxTouchPoints`, the only signal that tells an iPad asking for the
46
- * desktop site (it calls itself a Macintosh) from a real Mac.
20
+ * The server passes the request's User-Agent (the install link's redirect).
21
+ * A browser may also pass `navigator.maxTouchPoints`, the only signal that tells
22
+ * an iPad asking for the desktop site (it calls itself a Macintosh) from a real
23
+ * Mac; the server cannot, so that iPad lands on the download page and its badges.
47
24
  */
48
25
  export function resolveMobileStoreUrl(
49
26
  links: MobileAppLinks,
@@ -1,17 +0,0 @@
1
- /**
2
- * The install QR code's artwork: the modules of `MOBILE_APP_INSTALL_URL`
3
- * (`utils/mobile-app`) at version 3, ECC level M, as one SVG path. Packaged with
4
- * the library like the other assets, so every consumer draws the same code with
5
- * no host-asset serving. `MobileAppQr` (components/ui) is the component that
6
- * renders it.
7
- *
8
- * It changes WITH that URL. To regenerate: with the `qrcode` package,
9
- * `QRCode.create(url, { errorCorrectionLevel: 'M' })` gives the modules; each
10
- * row is one `M{x+4} {y+4}.5h{run}` followed by `m{gap} 0h{run}` per further
11
- * run (the 4 is the quiet zone), and the viewBox side is `size + 8`.
12
- */
13
- /** The side of the artwork's square viewBox: 29 modules plus a 4-module quiet zone on each side. */
14
- export declare const MOBILE_APP_QR_VIEWBOX_SIZE = 37;
15
- /** The dark modules, as horizontal strokes one module tall (`stroke` draws them). */
16
- export declare const MOBILE_APP_QR_PATH = "M4 4.5h7m2 0h2m2 0h1m2 0h1m2 0h2m1 0h7M4 5.5h1m5 0h1m2 0h2m2 0h3m6 0h1m5 0h1M4 6.5h1m1 0h3m1 0h1m1 0h3m1 0h1m1 0h3m2 0h1m2 0h1m1 0h3m1 0h1M4 7.5h1m1 0h3m1 0h1m1 0h1m1 0h1m1 0h6m2 0h1m1 0h1m1 0h3m1 0h1M4 8.5h1m1 0h3m1 0h1m1 0h3m1 0h1m1 0h1m1 0h5m1 0h1m1 0h3m1 0h1M4 9.5h1m5 0h1m1 0h1m1 0h2m1 0h1m1 0h3m4 0h1m5 0h1M4 10.5h7m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h7M12 11.5h1m2 0h2m5 0h3M4 12.5h1m1 0h5m3 0h1m1 0h8m2 0h5M4 13.5h4m4 0h2m2 0h1m3 0h4m1 0h4m3 0h1M6 14.5h1m3 0h3m1 0h1m3 0h4m2 0h1M4 15.5h1m2 0h3m2 0h2m1 0h2m1 0h1m1 0h1m2 0h5m1 0h1m1 0h1M4 16.5h1m4 0h2m1 0h2m1 0h1m1 0h1m2 0h2m1 0h1m5 0h2M16 17.5h3m3 0h5m1 0h1m3 0h1M5 18.5h1m3 0h2m1 0h1m5 0h2m4 0h1m1 0h2m1 0h2M4 19.5h6m1 0h1m3 0h1m2 0h3m1 0h4m1 0h2m2 0h1M8 20.5h1m1 0h1m2 0h2m1 0h2m1 0h1m1 0h1m5 0h1m1 0h2M4 21.5h1m1 0h2m7 0h1m2 0h1m1 0h9m1 0h1m1 0h1M4 22.5h1m3 0h3m2 0h1m1 0h2m2 0h3m3 0h1m1 0h1m2 0h1M4 23.5h1m6 0h4m3 0h2m3 0h2m2 0h2m2 0h1M4 24.5h1m2 0h4m4 0h1m3 0h1m1 0h1m1 0h6m1 0h3M12 25.5h6m2 0h1m3 0h1m3 0h5M4 26.5h7m3 0h8m1 0h2m1 0h1m1 0h3M4 27.5h1m5 0h1m1 0h2m1 0h3m2 0h1m2 0h2m3 0h1M4 28.5h1m1 0h3m1 0h1m1 0h2m2 0h1m1 0h1m2 0h1m2 0h5m1 0h3M4 29.5h1m1 0h3m1 0h1m1 0h2m1 0h2m3 0h4m3 0h1m1 0h4M4 30.5h1m1 0h3m1 0h1m1 0h1m3 0h4m1 0h1m2 0h8M4 31.5h1m5 0h1m4 0h1m1 0h1m1 0h2m1 0h1m1 0h1m1 0h2m1 0h1m1 0h1M4 32.5h7m1 0h1m2 0h2m4 0h1m1 0h1m3 0h4";
17
- //# sourceMappingURL=mobile-app-qr.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mobile-app-qr.d.ts","sourceRoot":"","sources":["../../src/assets/mobile-app-qr.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,oGAAoG;AACpG,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAE7C,qFAAqF;AACrF,eAAO,MAAM,kBAAkB,uyCACuwC,CAAC"}
@@ -1,19 +0,0 @@
1
- /**
2
- * The install QR code's artwork: the modules of `MOBILE_APP_INSTALL_URL`
3
- * (`utils/mobile-app`) at version 3, ECC level M, as one SVG path. Packaged with
4
- * the library like the other assets, so every consumer draws the same code with
5
- * no host-asset serving. `MobileAppQr` (components/ui) is the component that
6
- * renders it.
7
- *
8
- * It changes WITH that URL. To regenerate: with the `qrcode` package,
9
- * `QRCode.create(url, { errorCorrectionLevel: 'M' })` gives the modules; each
10
- * row is one `M{x+4} {y+4}.5h{run}` followed by `m{gap} 0h{run}` per further
11
- * run (the 4 is the quiet zone), and the viewBox side is `size + 8`.
12
- */
13
-
14
- /** The side of the artwork's square viewBox: 29 modules plus a 4-module quiet zone on each side. */
15
- export const MOBILE_APP_QR_VIEWBOX_SIZE = 37;
16
-
17
- /** The dark modules, as horizontal strokes one module tall (`stroke` draws them). */
18
- export const MOBILE_APP_QR_PATH =
19
- 'M4 4.5h7m2 0h2m2 0h1m2 0h1m2 0h2m1 0h7M4 5.5h1m5 0h1m2 0h2m2 0h3m6 0h1m5 0h1M4 6.5h1m1 0h3m1 0h1m1 0h3m1 0h1m1 0h3m2 0h1m2 0h1m1 0h3m1 0h1M4 7.5h1m1 0h3m1 0h1m1 0h1m1 0h1m1 0h6m2 0h1m1 0h1m1 0h3m1 0h1M4 8.5h1m1 0h3m1 0h1m1 0h3m1 0h1m1 0h1m1 0h5m1 0h1m1 0h3m1 0h1M4 9.5h1m5 0h1m1 0h1m1 0h2m1 0h1m1 0h3m4 0h1m5 0h1M4 10.5h7m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h7M12 11.5h1m2 0h2m5 0h3M4 12.5h1m1 0h5m3 0h1m1 0h8m2 0h5M4 13.5h4m4 0h2m2 0h1m3 0h4m1 0h4m3 0h1M6 14.5h1m3 0h3m1 0h1m3 0h4m2 0h1M4 15.5h1m2 0h3m2 0h2m1 0h2m1 0h1m1 0h1m2 0h5m1 0h1m1 0h1M4 16.5h1m4 0h2m1 0h2m1 0h1m1 0h1m2 0h2m1 0h1m5 0h2M16 17.5h3m3 0h5m1 0h1m3 0h1M5 18.5h1m3 0h2m1 0h1m5 0h2m4 0h1m1 0h2m1 0h2M4 19.5h6m1 0h1m3 0h1m2 0h3m1 0h4m1 0h2m2 0h1M8 20.5h1m1 0h1m2 0h2m1 0h2m1 0h1m1 0h1m5 0h1m1 0h2M4 21.5h1m1 0h2m7 0h1m2 0h1m1 0h9m1 0h1m1 0h1M4 22.5h1m3 0h3m2 0h1m1 0h2m2 0h3m3 0h1m1 0h1m2 0h1M4 23.5h1m6 0h4m3 0h2m3 0h2m2 0h2m2 0h1M4 24.5h1m2 0h4m4 0h1m3 0h1m1 0h1m1 0h6m1 0h3M12 25.5h6m2 0h1m3 0h1m3 0h5M4 26.5h7m3 0h8m1 0h2m1 0h1m1 0h3M4 27.5h1m5 0h1m1 0h2m1 0h3m2 0h1m2 0h2m3 0h1M4 28.5h1m1 0h3m1 0h1m1 0h2m2 0h1m1 0h1m2 0h1m2 0h5m1 0h3M4 29.5h1m1 0h3m1 0h1m1 0h2m1 0h2m3 0h4m3 0h1m1 0h4M4 30.5h1m1 0h3m1 0h1m1 0h1m3 0h4m1 0h1m2 0h8M4 31.5h1m5 0h1m4 0h1m1 0h1m1 0h2m1 0h1m1 0h1m1 0h2m1 0h1m1 0h1M4 32.5h7m1 0h1m2 0h2m4 0h1m1 0h1m3 0h4';