@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,84 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Assistant runtime context: what a PAGE needs to offer the host's chat
5
+ * assistant ("Ask Mingo"): whether a chat is there to open, the assistant's
6
+ * name and glyph, where its questions are read from, and HOW the chat is
7
+ * opened and asked.
8
+ *
9
+ * A sibling of `ChatRuntimeContext` (which configures the chat panel itself)
10
+ * and `EndpointsRuntimeContext`. It is its own context because its values
11
+ * change with the visitor (signed in or not, chat mounted or not), and a new
12
+ * `ChatRuntime` identity re-renders the whole chat tree.
13
+ *
14
+ * The NEAREST provider wins, so a page region that carries its own chat (an
15
+ * embedded chat beside the content) mounts a provider whose `open` opens THAT
16
+ * chat: every "ask" surface under it (`FaqSection`'s card, `AskPrompts`,
17
+ * `AskCard`) then opens and fills it, with no prop on any of them.
18
+ *
19
+ * No provider, or `available: false`: the "ask" surfaces render nothing.
20
+ */
21
+
22
+ import { createContext, useContext, type ReactNode } from 'react';
23
+
24
+ /** What an "ask" surface asks the chat to do. */
25
+ export interface AssistantOpenRequest {
26
+ /** A question to send once the chat is open. Absent: the chat only opens. */
27
+ prompt?: string;
28
+ /** The topic of the surface that asked (a FAQ's topic, a page section). */
29
+ topic?: string;
30
+ }
31
+
32
+ /** What an "ask" surface reports after a question was sent (analytics). */
33
+ export interface AssistantAskEvent {
34
+ /** The id of the question that was asked. */
35
+ promptId: string;
36
+ /** The topic of the surface that asked. */
37
+ topic?: string;
38
+ }
39
+
40
+ /** The wording of the card beside a FAQ. `{assistant}` is replaced by the assistant's name. */
41
+ export interface AssistantAskCardCopy {
42
+ title?: string;
43
+ description?: string;
44
+ /** How many questions the card offers. */
45
+ count?: number;
46
+ }
47
+
48
+ export interface AssistantRuntime {
49
+ /** A chat is mounted for this visitor: an "ask" click has something to open. */
50
+ available: boolean;
51
+ /** The assistant's configured name. */
52
+ name?: string | null;
53
+ /** The assistant's configured glyph. Absent: the packaged Mingo mark. */
54
+ icon?: ReactNode;
55
+ /** The chat source the default `open` addresses its `ask-ai:open` event to. */
56
+ source?: string;
57
+ /**
58
+ * GET endpoint the questions are read from:
59
+ * `<url>?count=<n>&section=<topic>&exclude=<id,id>` → `{ prompts: AskPrompt[] }`
60
+ * `section` is ANY string: the host decides what topics exist (the hub: the
61
+ * `section` attribute of a quick action, set in chat config), so a new topic
62
+ * needs no change here. Hub: '/api/quick-actions/questions'. Absent: an "ask"
63
+ * surface shows its launcher with no questions.
64
+ */
65
+ askPromptsUrl?: string;
66
+ /**
67
+ * Open the chat and, with `prompt`, ask it. Absent: the `ask-ai:open` event
68
+ * of `source` (`openAskAi`), which the mounted `EmbeddableChat` listens for.
69
+ * A host whose chat is opened another way (an embedded chat, a different
70
+ * panel) supplies its own.
71
+ */
72
+ open?: (request: AssistantOpenRequest) => void;
73
+ /** After a question was sent (analytics). */
74
+ onAsk?: (event: AssistantAskEvent) => void;
75
+ /** The wording of the card beside a FAQ. Absent: the lib's own. */
76
+ askCard?: AssistantAskCardCopy;
77
+ }
78
+
79
+ export const AssistantRuntimeContext = createContext<AssistantRuntime | null>(null);
80
+
81
+ /** The nearest assistant runtime, or null when no provider is mounted. */
82
+ export function useAssistantRuntime(): AssistantRuntime | null {
83
+ return useContext(AssistantRuntimeContext);
84
+ }
@@ -30,4 +30,13 @@ export {
30
30
 
31
31
  export { ChatRuntimeContext, useChatRuntime, useRequiredChatRuntime, type ChatRuntime } from './chat-runtime-context';
32
32
 
33
+ export {
34
+ AssistantRuntimeContext,
35
+ useAssistantRuntime,
36
+ type AssistantAskCardCopy,
37
+ type AssistantAskEvent,
38
+ type AssistantOpenRequest,
39
+ type AssistantRuntime,
40
+ } from './assistant-runtime-context';
41
+
33
42
  export { useOuterOrDefault } from './use-outer-or-default';
@@ -4,6 +4,7 @@ export * from './platform';
4
4
  export * from './use-toast';
5
5
  export * from './use-contact-submission';
6
6
  export * from './use-copy-to-clipboard';
7
+ export * from './use-downloads';
7
8
 
8
9
  // Image authentication hooks
9
10
  export * from './use-batch-images';
@@ -0,0 +1,138 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useSyncExternalStore } from 'react';
4
+ import { DOWNLOADS_API_PATH, type AppDownload, type DownloadsPublic } from '../types/downloads';
5
+ import { contentFetch } from '../utils/embed-content-fetch';
6
+ import { desktopOsOf } from '../utils/visitor-os';
7
+ import { useVisitorOs } from './ui/use-visitor-os';
8
+
9
+ export interface DownloadsState {
10
+ /** The server's answer; null until it lands. */
11
+ data: DownloadsPublic | null;
12
+ isLoading: boolean;
13
+ error: boolean;
14
+ }
15
+
16
+ interface Entry {
17
+ state: DownloadsState;
18
+ request: Promise<void> | null;
19
+ listeners: Set<() => void>;
20
+ }
21
+
22
+ const IDLE: DownloadsState = { data: null, isLoading: false, error: false };
23
+
24
+ /**
25
+ * One entry per endpoint for the life of the page. Every download button on a
26
+ * page asks the same question, so they share ONE request and one answer: the
27
+ * list changes only when the server is redeployed, never while a page is open.
28
+ */
29
+ const entries = new Map<string, Entry>();
30
+
31
+ function entryFor(endpoint: string): Entry {
32
+ let entry = entries.get(endpoint);
33
+ if (!entry) {
34
+ entry = { state: IDLE, request: null, listeners: new Set() };
35
+ entries.set(endpoint, entry);
36
+ }
37
+ return entry;
38
+ }
39
+
40
+ function publish(endpoint: string, state: DownloadsState) {
41
+ const entry = entryFor(endpoint);
42
+ entry.state = state;
43
+ for (const listener of entry.listeners) listener();
44
+ }
45
+
46
+ function load(endpoint: string, force = false): void {
47
+ const entry = entryFor(endpoint);
48
+ if (entry.request || (entry.state.data && !force)) return;
49
+ publish(endpoint, { data: entry.state.data, isLoading: true, error: false });
50
+ entry.request = contentFetch(endpoint)
51
+ .then(async response => {
52
+ if (!response.ok) throw new Error(`Request failed (${response.status})`);
53
+ publish(endpoint, { data: (await response.json()) as DownloadsPublic, isLoading: false, error: false });
54
+ })
55
+ .catch(() => publish(endpoint, { data: entry.state.data, isLoading: false, error: true }))
56
+ .finally(() => {
57
+ entry.request = null;
58
+ });
59
+ }
60
+
61
+ /** Test seam: forget every answer. */
62
+ export function resetDownloadsStore(): void {
63
+ entries.clear();
64
+ }
65
+
66
+ export interface UseDownloadsOptions {
67
+ /** GET endpoint for the public projection. Default `DOWNLOADS_API_PATH`; embedders pass their proxy path. */
68
+ endpoint?: string;
69
+ /** The server's copy (a host that read it during SSR): no request is made. */
70
+ initialData?: DownloadsPublic;
71
+ /** False: nothing is requested (a host that does not offer the desktop app). Default true. */
72
+ enabled?: boolean;
73
+ }
74
+
75
+ /**
76
+ * What the OpenFrame desktop app is installed from, read FROM THE SERVER
77
+ * (`GET /api/downloads`). The links and package names live in the server's
78
+ * environment; no client holds a copy of its own. Every caller on a page shares
79
+ * one request.
80
+ */
81
+ export function useDownloads({ endpoint = DOWNLOADS_API_PATH, initialData, enabled = true }: UseDownloadsOptions = {}) {
82
+ const entry = entryFor(endpoint);
83
+ // A server copy is the answer: held from the first render (server and client alike), and never re-requested.
84
+ // Written during render so THIS caller's first paint has it; a render cannot notify other callers.
85
+ if (initialData && entry.state.data !== initialData && !entry.request) {
86
+ entry.state = { data: initialData, isLoading: false, error: false };
87
+ }
88
+ // So the callers that rendered before this one (a header button above the page) are told after
89
+ // the commit: they re-read the shared answer instead of staying on the empty one they first saw.
90
+ useEffect(() => {
91
+ if (!initialData) return;
92
+ for (const listener of entry.listeners) listener();
93
+ }, [entry, initialData]);
94
+ const subscribe = useCallback(
95
+ (listener: () => void) => {
96
+ entry.listeners.add(listener);
97
+ return () => {
98
+ entry.listeners.delete(listener);
99
+ };
100
+ },
101
+ [entry],
102
+ );
103
+ const state = useSyncExternalStore(
104
+ subscribe,
105
+ () => entry.state,
106
+ () => entry.state,
107
+ );
108
+
109
+ useEffect(() => {
110
+ if (enabled) load(endpoint);
111
+ }, [enabled, endpoint]);
112
+
113
+ const reload = useCallback(() => load(endpoint, true), [endpoint]);
114
+ // Until the effect has asked, an enabled caller with no answer is loading, not empty.
115
+ const isLoading = state.isLoading || (enabled && !state.data && !state.error);
116
+ return { data: state.data, isLoading, error: state.error, reload };
117
+ }
118
+
119
+ export interface VisitorDesktopDownload {
120
+ /** False until the visitor's system AND the server's list are both known. */
121
+ known: boolean;
122
+ /** This visitor's installer (the first one for their system); null when nothing installs on it. */
123
+ download: AppDownload | null;
124
+ }
125
+
126
+ /**
127
+ * The installer a download button offers THIS visitor: the server's first
128
+ * installer for the system they are on. `enabled: false` asks for nothing.
129
+ */
130
+ export function useVisitorDesktopDownload(options: UseDownloadsOptions = {}): VisitorDesktopDownload {
131
+ const enabled = options.enabled ?? true;
132
+ const visitor = useVisitorOs();
133
+ const { data, error } = useDownloads(options);
134
+ if (!enabled || !visitor.known || (!data && !error)) return { known: false, download: null };
135
+ const os = desktopOsOf(visitor.os);
136
+ const download = data?.desktop.find(row => row.kind === 'binary' && row.url && row.os === os) ?? null;
137
+ return { known: true, download };
138
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The download page's wire shape and vocabulary: what the OpenFrame apps are
3
+ * installed from. The hub answers `DOWNLOADS_API_PATH` with `DownloadsPublic`;
4
+ * the lib's `<DownloadAppsPage>` renders it (hub SSR, openframe-frontend through
5
+ * its `/content` proxy, the embedding example).
6
+ */
7
+ import type { DesktopOs } from '../utils/visitor-os';
8
+
9
+ /** GET endpoint for the public downloads projection (the hub's path; embedders prefix their proxy). */
10
+ export const DOWNLOADS_API_PATH = '/api/downloads';
11
+
12
+ /** How long a copy stays fresh: the CDN window of the route, and the page's revalidation age. */
13
+ export const DOWNLOADS_CACHE_SECONDS = 300;
14
+
15
+ export const DOWNLOADS_TITLE = 'Download OpenFrame';
16
+ export const DOWNLOADS_TAGLINE =
17
+ 'Install it on the computer you manage IT from, and on your phone. One account everywhere.';
18
+ /** The title and subtitle when a host offers the mobile app only. */
19
+ export const DOWNLOADS_MOBILE_TITLE = 'Get the OpenFrame app';
20
+ export const DOWNLOADS_MOBILE_TAGLINE =
21
+ 'Alerts and tickets on your phone, signed in with the account you already have.';
22
+
23
+ /**
24
+ * What a row is: an installer file, or a one-line package manager install. THE
25
+ * one declaration: the type, the server's rows and every reader derive from it.
26
+ */
27
+ export const APP_DOWNLOAD_KINDS = ['binary', 'command'] as const;
28
+ export type AppDownloadKind = (typeof APP_DOWNLOAD_KINDS)[number];
29
+
30
+ /** One way to install the desktop app on one system. */
31
+ export interface AppDownload {
32
+ id: string;
33
+ kind: AppDownloadKind;
34
+ os: DesktopOs;
35
+ /** `universal`, `x64`, `arm64`; null for a command. */
36
+ architecture: string | null;
37
+ /** The installer's address (`binary`). */
38
+ url: string | null;
39
+ /** The whole command to paste (`command`). */
40
+ command: string | null;
41
+ /** "macOS 13+". */
42
+ minOsLabel: string | null;
43
+ /** The package manager's name (`command`), or the button's own words (`binary`). */
44
+ label: string | null;
45
+ }
46
+
47
+ /** Where the mobile app is installed from. A store the server names no listing for is null, and is not offered. */
48
+ export interface MobileAppLinks {
49
+ appStoreUrl: string | null;
50
+ googlePlayUrl: string | null;
51
+ }
52
+
53
+ /**
54
+ * Everything the download page shows. All of it is the server's: the lib holds
55
+ * no link of its own and falls back to none, so what is missing is not drawn.
56
+ */
57
+ export interface DownloadsPublic {
58
+ /** The desktop installers and install commands, in the order they are offered. Per system, the first installer is the default. */
59
+ desktop: AppDownload[];
60
+ /** The mobile app's store listings. */
61
+ mobile: MobileAppLinks;
62
+ }
63
+
64
+ /** A system's name on a button and a card. */
65
+ export const DESKTOP_OS_LABELS: Record<DesktopOs, string> = { mac: 'Mac', windows: 'Windows', linux: 'Linux' };
66
+
67
+ /** An architecture's name beside a button, when a system has more than one installer. */
68
+ export const DOWNLOAD_ARCHITECTURE_LABELS: Record<string, string> = {
69
+ universal: 'Apple Silicon and Intel',
70
+ x64: 'x64',
71
+ arm64: 'ARM',
72
+ };
73
+
74
+ /** What a host learns when a visitor takes an install action on the page. */
75
+ export interface DownloadActionEvent {
76
+ /** `installer`: a download button. `command`: a copied install command. */
77
+ action: 'installer' | 'command';
78
+ /** The row's id (`mac`, `windows-arm64`, `homebrew`). */
79
+ id: string;
80
+ os: DesktopOs;
81
+ architecture: string | null;
82
+ }
@@ -55,6 +55,7 @@ export * from './waitlist';
55
55
  // Business logic types
56
56
  export * from './faq';
57
57
  export * from './trust-center';
58
+ export * from './downloads';
58
59
  export * from './content-ref';
59
60
  export * from './report';
60
61
  export * from './stack';
@@ -17,6 +17,7 @@ export {
17
17
  // source of truth). Re-exported here so existing callers that pull from
18
18
  // the barrel keep working without changing imports.
19
19
  export { formatDate, formatNumber, formatPrice, formatBytes } from './format';
20
+ export { TRIAL_DAYS, TRIAL_LABELS } from './trial';
20
21
  export {
21
22
  VENDOR_CLASSIFICATIONS,
22
23
  OPENFRAME_CLASSIFICATIONS,
@@ -117,6 +118,8 @@ export * from './video-bite-id';
117
118
  // OS type utilities
118
119
  export * from './os-utils';
119
120
  export * from './visitor-os';
121
+ // Also exported via the granular subpath `./utils/mobile-app` (server and Edge consumers).
122
+ export * from './mobile-app';
120
123
  // Phone utilities
121
124
  export * from './country-phone-utils';
122
125
  export * from './compliance-standards';
@@ -116,6 +116,10 @@ const BUILDERS: Record<string, (ids: string[], base: string) => string> = {
116
116
  design_doc: (ids, b) => `${b}/api/design-docs?ids=${ids.join(',')}`,
117
117
  openframe_tenant: (ids, b) => `${b}/api/openframe-tenants?ids=${ids.join(',')}`,
118
118
  prospect_call: (ids, b) => `${b}/api/prospect-calls?ids=${ids.join(',')}`,
119
+ // The public website's own pages (live source; ChatRef-shaped items like the objects above).
120
+ site_page: (ids, b) => `${b}/api/site-pages?ids=${ids.join(',')}`,
121
+ // The OpenMSP vendor directory (indexed source).
122
+ vendor: (ids, b) => `${b}/api/vendors/cards?ids=${ids.join(',')}`,
119
123
  // Code intelligence — per-object card hydration, same `handleEntityCardList`
120
124
  // shape as the internal objects above.
121
125
  code_rule: (ids, b) => `${b}/api/code-rules/cards?ids=${ids.join(',')}`,
@@ -0,0 +1,57 @@
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`).
6
+ *
7
+ * Pure and server-safe; also published as the granular subpath
8
+ * `./utils/mobile-app`, for an Edge proxy and other server-only consumers.
9
+ */
10
+ import type { MobileAppLinks } from '../types/downloads';
11
+ import { detectVisitorOs } from './visitor-os';
12
+
13
+ /** The website's download page: every installer and the mobile app. */
14
+ export const DOWNLOAD_PAGE_PATH = '/download';
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
+ /**
41
+ * The store for a phone or tablet, or `null` for every other system and for a
42
+ * store the server names no listing for (`links` is the server's answer).
43
+ *
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.
47
+ */
48
+ export function resolveMobileStoreUrl(
49
+ links: MobileAppLinks,
50
+ userAgent: string | null | undefined,
51
+ maxTouchPoints?: number,
52
+ ): string | null {
53
+ const os = detectVisitorOs(userAgent, { maxTouchPoints });
54
+ if (os === 'ios') return links.appStoreUrl;
55
+ if (os === 'android') return links.googlePlayUrl;
56
+ return null;
57
+ }
@@ -54,8 +54,12 @@ export const SOURCE_ICON_NAMES: Record<string, string> = {
54
54
  'audience-mssp': 'shield-lock',
55
55
  'audience-it-team': 'buildings',
56
56
 
57
- // Trust (single-record live source over the public Vanta projection)
58
- 'trust-center': 'shield',
57
+ // Trust (single-record live source over the public Vanta projection): the icon its menu entry draws
58
+ 'trust-center': 'shield-check',
59
+ // The public website's own pages (live source): the site's own mark, as a glyph in the slot's colour
60
+ 'website-pages': 'flamingo-logo-grey',
61
+ // The OpenMSP vendor directory: the icon the site menu's "Tool directory" entry draws (it reads this one)
62
+ vendors: 'package-search',
59
63
 
60
64
  // Financials
61
65
  'investor-updates': 'mail',
@@ -145,6 +149,8 @@ export const SOURCE_LABELS_BY_TABLE: Record<string, string> = {
145
149
 
146
150
  // Trust
147
151
  'trust-center': 'Trust Center',
152
+ 'website-pages': 'Website',
153
+ vendors: 'Vendor Directory',
148
154
 
149
155
  // Financials
150
156
  'investor-updates': 'Investor Updates',
@@ -248,6 +254,8 @@ export const DEFAULT_DOCUMENT_TYPE_TO_TABLE_ID: Record<string, string> = {
248
254
 
249
255
  // Trust
250
256
  trust_center: 'trust-center',
257
+ site_page: 'website-pages',
258
+ vendor: 'vendors',
251
259
 
252
260
  // Financials
253
261
  investor_update: 'investor-updates',
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The free trial's facts and the words every surface says them with: the
3
+ * sign-up panel here, and the host site's trial buttons, trust rows and pricing
4
+ * copy. Stated once, so a trial that changes length changes everywhere.
5
+ *
6
+ * Server-safe (no React, no DOM).
7
+ */
8
+
9
+ /** How long the free trial runs, in days. */
10
+ export const TRIAL_DAYS = 14;
11
+
12
+ /** The reassurance labels shown beside a trial button. */
13
+ export const TRIAL_LABELS = {
14
+ /** "14 day free trial" */
15
+ length: `${TRIAL_DAYS} day free trial`,
16
+ noCard: 'No card required',
17
+ cancel: 'Cancel Anytime',
18
+ noContract: 'No Contract',
19
+ } as const;
@@ -1,89 +0,0 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }"use client";
2
-
3
-
4
- var _chunkAUMKE74Mcjs = require('./chunk-AUMKE74M.cjs');
5
-
6
- // src/hooks/use-self-fetch.ts
7
- var _react = require('react');
8
- function useSelfFetch(url, options) {
9
- const initialData = _optionalChain([options, 'optionalAccess', _ => _.initialData]);
10
- const revalidateOnVisibleAfterMs = _optionalChain([options, 'optionalAccess', _2 => _2.revalidateOnVisibleAfterMs]);
11
- const [data, setData] = _react.useState.call(void 0, _nullishCoalesce(initialData, () => ( null)));
12
- const [dataUrl, setDataUrl] = _react.useState.call(void 0, initialData !== void 0 ? url : null);
13
- const [isLoading, setIsLoading] = _react.useState.call(void 0, initialData === void 0 && url !== null);
14
- const [error, setError] = _react.useState.call(void 0, false);
15
- const [reloadKey, setReloadKey] = _react.useState.call(void 0, 0);
16
- const [mountedAt] = _react.useState.call(void 0, () => Date.now());
17
- const dataAtRef = _react.useRef.call(void 0, initialData !== void 0 ? mountedAt : 0);
18
- const dataUrlRef = _react.useRef.call(void 0, initialData !== void 0 ? url : null);
19
- const [syncedProps, setSyncedProps] = _react.useState.call(void 0, { url, initialData });
20
- if (syncedProps.url !== url || syncedProps.initialData !== initialData) {
21
- setSyncedProps({ url, initialData });
22
- if (syncedProps.initialData !== initialData && initialData !== void 0) {
23
- setData(initialData);
24
- setDataUrl(url);
25
- }
26
- if (syncedProps.url !== url && url === null) {
27
- setIsLoading(false);
28
- }
29
- }
30
- _react.useEffect.call(void 0, () => {
31
- if (url === null) return void 0;
32
- if (dataUrlRef.current === url) return void 0;
33
- const ctrl = new AbortController();
34
- let cancelled = false;
35
- async function load() {
36
- try {
37
- setIsLoading(true);
38
- setError(false);
39
- const res = await _chunkAUMKE74Mcjs.contentFetch.call(void 0, url, { signal: ctrl.signal });
40
- if (!res.ok) throw new Error(`Request failed (${res.status})`);
41
- const json = await res.json();
42
- if (!cancelled) {
43
- setData(json);
44
- setDataUrl(url);
45
- dataUrlRef.current = url;
46
- dataAtRef.current = Date.now();
47
- }
48
- } catch (err) {
49
- if (cancelled || _optionalChain([err, 'optionalAccess', _3 => _3.name]) === "AbortError") return;
50
- setError(true);
51
- console.error("useSelfFetch:", url, err);
52
- } finally {
53
- if (!cancelled) setIsLoading(false);
54
- }
55
- }
56
- void load();
57
- return () => {
58
- cancelled = true;
59
- ctrl.abort();
60
- };
61
- }, [url, reloadKey]);
62
- const forceReload = () => {
63
- dataUrlRef.current = null;
64
- setReloadKey((k) => k + 1);
65
- };
66
- _react.useEffect.call(void 0, () => {
67
- if (!revalidateOnVisibleAfterMs || url === null) return void 0;
68
- const onVisible = () => {
69
- if (document.visibilityState !== "visible") return;
70
- if (Date.now() - dataAtRef.current < revalidateOnVisibleAfterMs) return;
71
- forceReload();
72
- };
73
- document.addEventListener("visibilitychange", onVisible);
74
- return () => document.removeEventListener("visibilitychange", onVisible);
75
- }, [url, revalidateOnVisibleAfterMs]);
76
- return {
77
- data,
78
- dataUrl,
79
- setData,
80
- isLoading,
81
- error,
82
- // Force a re-fetch of the current url (error-retry affordance).
83
- reload: forceReload
84
- };
85
- }
86
-
87
-
88
-
89
- exports.useSelfFetch = useSelfFetch;
@@ -1,16 +0,0 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});"use client";
2
-
3
-
4
-
5
- var _chunkULWVKHSIcjs = require('./chunk-ULWVKHSI.cjs');
6
-
7
- // src/utils/embed-content-fetch.ts
8
- var contentFetch = (input, init) => {
9
- if (!_chunkULWVKHSIcjs.hasEmbedAuthAdapter.call(void 0, )) return fetch(input, init);
10
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
11
- return _chunkULWVKHSIcjs.embedAuthedFetch.call(void 0, url, init);
12
- };
13
-
14
-
15
-
16
- exports.contentFetch = contentFetch;
@@ -1,16 +0,0 @@
1
- "use client";
2
- import {
3
- embedAuthedFetch,
4
- hasEmbedAuthAdapter
5
- } from "./chunk-ADBE7QF3.js";
6
-
7
- // src/utils/embed-content-fetch.ts
8
- var contentFetch = (input, init) => {
9
- if (!hasEmbedAuthAdapter()) return fetch(input, init);
10
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
11
- return embedAuthedFetch(url, init);
12
- };
13
-
14
- export {
15
- contentFetch
16
- };