@flamingo-stack/openframe-frontend-core 0.0.705 → 0.0.706-snapshot.20261006190707

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 (206) hide show
  1. package/dist/{chunk-4KX6OED2.cjs → chunk-2G7UYK35.cjs} +46 -46
  2. package/dist/{chunk-KOWVJD4V.cjs → chunk-2KXTOP6G.cjs} +70 -57
  3. package/dist/{chunk-A75QY6CD.cjs → chunk-2RNB5DTN.cjs} +21 -21
  4. package/dist/{chunk-PN2RKPDF.js → chunk-36BCTE7D.js} +10 -10
  5. package/dist/{chunk-ZSAGLQOD.cjs → chunk-3AHTOSXJ.cjs} +17 -17
  6. package/dist/{chunk-VR2ZLFCP.js → chunk-4IBPRIQH.js} +1 -1
  7. package/dist/{chunk-FCGC42TS.js → chunk-4L6XCMOU.js} +66 -62
  8. package/dist/{chunk-X5MQ7JRP.js → chunk-4UM2ETLM.js} +9 -9
  9. package/dist/{chunk-6NJKKO73.cjs → chunk-6I4ZAFO2.cjs} +15 -15
  10. package/dist/{chunk-RBDP4WDE.js → chunk-6IW7TWPV.js} +6 -6
  11. package/dist/{chunk-MMOODW57.cjs → chunk-6NTPY5QA.cjs} +1887 -94
  12. package/dist/{chunk-QFXRPRFT.js → chunk-6YKAT7NV.js} +3 -3
  13. package/dist/{chunk-HPWWNJE7.cjs → chunk-AOWDNSTM.cjs} +89 -89
  14. package/dist/{chunk-IHERL5V4.cjs → chunk-B73373AG.cjs} +34 -34
  15. package/dist/{chunk-DENB2XJC.js → chunk-CNWOMQQJ.js} +2 -2
  16. package/dist/{chunk-MP2C3LJ4.js → chunk-CUME3VTQ.js} +1 -1
  17. package/dist/{chunk-PDWVPI3S.js → chunk-CZ7Y7UG4.js} +4 -72
  18. package/dist/{chunk-YHM5AWTT.js → chunk-CZSZSDD4.js} +6 -6
  19. package/dist/{chunk-F6I6XZMN.js → chunk-DA5UDTLG.js} +5 -5
  20. package/dist/{chunk-MFFCUVWG.js → chunk-F57ECE2I.js} +1 -1
  21. package/dist/{chunk-CH5232HV.js → chunk-FI6GMDO7.js} +2455 -662
  22. package/dist/{chunk-NDESEPW5.js → chunk-FXJ3WWUB.js} +68 -0
  23. package/dist/{chunk-G5A3PSPV.js → chunk-HDOYSEOJ.js} +1080 -691
  24. package/dist/{chunk-VA5T32XA.js → chunk-I3ST4PLM.js} +2 -2
  25. package/dist/{chunk-5HBHO5TB.js → chunk-JPG7OHEG.js} +1 -1
  26. package/dist/{chunk-YFBM2K6X.cjs → chunk-JRU2CXLB.cjs} +4 -4
  27. package/dist/{chunk-MKXO4Q5P.js → chunk-KUC673HM.js} +383 -142
  28. package/dist/{chunk-JEZKGCZI.cjs → chunk-L6Y6OOKG.cjs} +26 -26
  29. package/dist/{chunk-F6AFPNDF.js → chunk-LFFB6EMB.js} +29 -16
  30. package/dist/{chunk-X72J4ACL.cjs → chunk-LLSUVPQC.cjs} +252 -8
  31. package/dist/{chunk-RKV5B7EU.cjs → chunk-LM7TXMLU.cjs} +72 -4
  32. package/dist/{chunk-6SEANMOU.js → chunk-LNW45N2C.js} +11 -11
  33. package/dist/{chunk-OUY3HHW6.cjs → chunk-LRQTJOF2.cjs} +928 -539
  34. package/dist/{chunk-DW7G4KG3.js → chunk-MR3A4MZV.js} +3 -3
  35. package/dist/{chunk-2YXVWZVF.js → chunk-N45D4ZXQ.js} +959 -715
  36. package/dist/{chunk-Z2VNQQ74.js → chunk-NCPTID44.js} +978 -942
  37. package/dist/{chunk-WA6GUYQ4.cjs → chunk-NSGXARGJ.cjs} +104 -104
  38. package/dist/{chunk-ABU3TGXA.cjs → chunk-OF6GEI5A.cjs} +3 -3
  39. package/dist/{chunk-IKTQ3ZAG.cjs → chunk-OJLR5MEO.cjs} +6 -6
  40. package/dist/{chunk-VESKITRQ.cjs → chunk-P42V6N34.cjs} +736 -732
  41. package/dist/{chunk-OHFS7VOU.cjs → chunk-PLYEKV3N.cjs} +2 -2
  42. package/dist/{chunk-7VVFKJY5.js → chunk-PO33GZVY.js} +1799 -3449
  43. package/dist/{chunk-7LS22BZU.cjs → chunk-QAOFMK2A.cjs} +14 -14
  44. package/dist/{chunk-VHCACDKP.cjs → chunk-RQRX4YC3.cjs} +39 -39
  45. package/dist/{chunk-3MX5S4CF.cjs → chunk-SEXWHIMS.cjs} +899 -2549
  46. package/dist/{chunk-AQWZNP6A.js → chunk-SS5MBHDP.js} +4 -4
  47. package/dist/{chunk-ZSAO2HBW.js → chunk-TDYIREDJ.js} +15 -15
  48. package/dist/{chunk-XOYH2YQE.cjs → chunk-UDFRUKLC.cjs} +2 -2
  49. package/dist/{chunk-TYWLNCNH.cjs → chunk-UVTD52MD.cjs} +14 -82
  50. package/dist/{chunk-N3COXDBH.cjs → chunk-VX4E6YSB.cjs} +41 -5
  51. package/dist/{chunk-2QEUFXNS.cjs → chunk-WCVZCGK5.cjs} +312 -71
  52. package/dist/{chunk-6XIF2Z7I.js → chunk-WMX26HB5.js} +3 -3
  53. package/dist/{chunk-D7BTSVMS.cjs → chunk-Y2GSOUCS.cjs} +2 -2
  54. package/dist/{chunk-4I45Y3DN.cjs → chunk-YBUT4AMG.cjs} +6 -6
  55. package/dist/components/case-studies/index.cjs +30 -32
  56. package/dist/components/case-studies/index.js +14 -16
  57. package/dist/components/chat/entity-cards/case-study-card.d.ts +11 -4
  58. package/dist/components/chat/entity-cards/case-study-card.d.ts.map +1 -1
  59. package/dist/components/chat/index.cjs +11 -11
  60. package/dist/components/chat/index.d.ts +1 -1
  61. package/dist/components/chat/index.d.ts.map +1 -1
  62. package/dist/components/chat/index.js +16 -16
  63. package/dist/components/chat/nav-link-anchor-via-runtime.d.ts +34 -5
  64. package/dist/components/chat/nav-link-anchor-via-runtime.d.ts.map +1 -1
  65. package/dist/components/chat/utils/icon-library.d.ts.map +1 -1
  66. package/dist/components/contact/index.cjs +12 -14
  67. package/dist/components/contact/index.js +11 -13
  68. package/dist/components/docs/index.cjs +15 -17
  69. package/dist/components/docs/index.js +14 -16
  70. package/dist/components/embeds/index.cjs +12 -14
  71. package/dist/components/embeds/index.js +11 -13
  72. package/dist/components/faq/index.cjs +13 -15
  73. package/dist/components/faq/index.js +12 -14
  74. package/dist/components/features/index.cjs +12 -14
  75. package/dist/components/features/index.js +21 -23
  76. package/dist/components/help-center-pages/index.cjs +118 -120
  77. package/dist/components/help-center-pages/index.js +33 -35
  78. package/dist/components/icons/index.cjs +3 -5
  79. package/dist/components/icons/index.js +9 -11
  80. package/dist/components/icons-v2-generated/index.cjs +3 -5
  81. package/dist/components/icons-v2-generated/index.js +2 -4
  82. package/dist/components/index.cjs +87 -158
  83. package/dist/components/index.d.ts +0 -2
  84. package/dist/components/index.d.ts.map +1 -1
  85. package/dist/components/index.js +331 -402
  86. package/dist/components/loading/page-layout-skeleton.d.ts +1 -1
  87. package/dist/components/meeting-scheduler/index.cjs +62 -64
  88. package/dist/components/meeting-scheduler/index.js +12 -14
  89. package/dist/components/navigation/index.cjs +28 -16
  90. package/dist/components/navigation/index.d.ts +14 -10
  91. package/dist/components/navigation/index.d.ts.map +1 -1
  92. package/dist/components/navigation/index.js +38 -26
  93. package/dist/components/navigation/mingo-ai-button.d.ts +38 -3
  94. package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
  95. package/dist/components/navigation/mobile-nav-sheet-id.d.ts +3 -0
  96. package/dist/components/navigation/mobile-nav-sheet-id.d.ts.map +1 -0
  97. package/dist/components/navigation/mobile-nav-sheet.d.ts +48 -0
  98. package/dist/components/navigation/mobile-nav-sheet.d.ts.map +1 -0
  99. package/dist/components/navigation/nav-item-row.d.ts +27 -0
  100. package/dist/components/navigation/nav-item-row.d.ts.map +1 -0
  101. package/dist/components/navigation/nav-panel.d.ts +43 -0
  102. package/dist/components/navigation/nav-panel.d.ts.map +1 -0
  103. package/dist/components/navigation/navigation-sidebar-item.d.ts +1 -2
  104. package/dist/components/navigation/navigation-sidebar-item.d.ts.map +1 -1
  105. package/dist/components/navigation/site-footer.d.ts +28 -0
  106. package/dist/components/navigation/site-footer.d.ts.map +1 -0
  107. package/dist/components/navigation/site-header.d.ts +61 -0
  108. package/dist/components/navigation/site-header.d.ts.map +1 -0
  109. package/dist/components/navigation/site-nav-link.d.ts +26 -0
  110. package/dist/components/navigation/site-nav-link.d.ts.map +1 -0
  111. package/dist/components/navigation/top-navigation.d.ts +2 -2
  112. package/dist/components/navigation/top-navigation.d.ts.map +1 -1
  113. package/dist/components/onboarding-guides/index.cjs +15 -17
  114. package/dist/components/onboarding-guides/index.js +14 -16
  115. package/dist/components/platform/ToolBadge.d.ts +6 -0
  116. package/dist/components/platform/ToolBadge.d.ts.map +1 -1
  117. package/dist/components/related-content/index.cjs +10 -8
  118. package/dist/components/related-content/index.js +9 -7
  119. package/dist/components/tickets/index.cjs +16 -18
  120. package/dist/components/tickets/index.js +15 -17
  121. package/dist/components/ui/brand-association-card.d.ts +7 -0
  122. package/dist/components/ui/brand-association-card.d.ts.map +1 -1
  123. package/dist/components/ui/index.cjs +14 -14
  124. package/dist/components/ui/index.js +19 -19
  125. package/dist/hooks/index.cjs +19 -11
  126. package/dist/hooks/index.d.ts +1 -0
  127. package/dist/hooks/index.d.ts.map +1 -1
  128. package/dist/hooks/index.js +19 -11
  129. package/dist/hooks/ui/index.d.ts +3 -0
  130. package/dist/hooks/ui/index.d.ts.map +1 -1
  131. package/dist/hooks/ui/use-active-section.d.ts +10 -0
  132. package/dist/hooks/ui/use-active-section.d.ts.map +1 -0
  133. package/dist/hooks/ui/use-nav-menus.d.ts +48 -0
  134. package/dist/hooks/ui/use-nav-menus.d.ts.map +1 -0
  135. package/dist/hooks/ui/use-visitor-os.d.ts +15 -0
  136. package/dist/hooks/ui/use-visitor-os.d.ts.map +1 -0
  137. package/dist/hooks/use-location-hash.d.ts +9 -0
  138. package/dist/hooks/use-location-hash.d.ts.map +1 -0
  139. package/dist/index.cjs +56 -21
  140. package/dist/index.js +231 -196
  141. package/dist/types/case-study.d.ts +9 -0
  142. package/dist/types/case-study.d.ts.map +1 -1
  143. package/dist/types/navigation.d.ts +95 -65
  144. package/dist/types/navigation.d.ts.map +1 -1
  145. package/dist/utils/index.cjs +55 -0
  146. package/dist/utils/index.d.ts +1 -0
  147. package/dist/utils/index.d.ts.map +1 -1
  148. package/dist/utils/index.js +50 -1
  149. package/dist/utils/source-icons.d.ts.map +1 -1
  150. package/dist/utils/visitor-os.d.ts +45 -0
  151. package/dist/utils/visitor-os.d.ts.map +1 -0
  152. package/package.json +1 -1
  153. package/src/components/chat/entity-cards/case-study-card.tsx +115 -5
  154. package/src/components/chat/index.ts +7 -1
  155. package/src/components/chat/nav-link-anchor-via-runtime.tsx +62 -9
  156. package/src/components/chat/utils/icon-library.tsx +35 -1
  157. package/src/components/index.ts +0 -2
  158. package/src/components/loading/page-layout-skeleton.tsx +1 -1
  159. package/src/components/navigation/index.ts +21 -14
  160. package/src/components/navigation/mingo-ai-button.tsx +139 -63
  161. package/src/components/navigation/mobile-nav-sheet-id.ts +2 -0
  162. package/src/components/navigation/mobile-nav-sheet.tsx +241 -0
  163. package/src/components/navigation/nav-item-row.tsx +129 -0
  164. package/src/components/navigation/nav-panel.tsx +196 -0
  165. package/src/components/navigation/navigation-sidebar-item.tsx +1 -2
  166. package/src/components/navigation/site-footer.tsx +256 -0
  167. package/src/components/navigation/site-header.tsx +339 -0
  168. package/src/components/navigation/site-nav-link.tsx +41 -0
  169. package/src/components/navigation/ticket-alerts-button.tsx +2 -2
  170. package/src/components/navigation/top-navigation.tsx +2 -2
  171. package/src/components/platform/ToolBadge.tsx +18 -1
  172. package/src/components/ui/brand-association-card.tsx +25 -3
  173. package/src/hooks/index.ts +1 -0
  174. package/src/hooks/ui/index.ts +3 -0
  175. package/src/hooks/ui/use-active-section.ts +44 -0
  176. package/src/hooks/ui/use-nav-menus.ts +200 -0
  177. package/src/hooks/ui/use-visitor-os.ts +33 -0
  178. package/src/hooks/use-location-hash.ts +34 -0
  179. package/src/styles/chat-animations.css +57 -2
  180. package/src/types/case-study.ts +16 -0
  181. package/src/types/navigation.ts +80 -66
  182. package/src/utils/index.ts +1 -0
  183. package/src/utils/source-icons.ts +13 -0
  184. package/src/utils/visitor-os.ts +84 -0
  185. package/dist/chunk-LMBC5FFP.js +0 -43
  186. package/dist/chunk-O43V3EOS.js +0 -252
  187. package/dist/chunk-QZVSJF2K.cjs +0 -252
  188. package/dist/chunk-RALRPFDW.cjs +0 -43
  189. package/dist/components/footer-waitlist-button.d.ts +0 -29
  190. package/dist/components/footer-waitlist-button.d.ts.map +0 -1
  191. package/dist/components/footer.d.ts +0 -47
  192. package/dist/components/footer.d.ts.map +0 -1
  193. package/dist/components/navigation/client-only-header.d.ts +0 -8
  194. package/dist/components/navigation/client-only-header.d.ts.map +0 -1
  195. package/dist/components/navigation/header-skeleton.d.ts +0 -12
  196. package/dist/components/navigation/header-skeleton.d.ts.map +0 -1
  197. package/dist/components/navigation/header.d.ts +0 -9
  198. package/dist/components/navigation/header.d.ts.map +0 -1
  199. package/dist/components/navigation/mobile-nav-panel.d.ts +0 -10
  200. package/dist/components/navigation/mobile-nav-panel.d.ts.map +0 -1
  201. package/src/components/footer-waitlist-button.tsx +0 -53
  202. package/src/components/footer.tsx +0 -195
  203. package/src/components/navigation/client-only-header.tsx +0 -22
  204. package/src/components/navigation/header-skeleton.tsx +0 -74
  205. package/src/components/navigation/header.tsx +0 -429
  206. package/src/components/navigation/mobile-nav-panel.tsx +0 -155
@@ -1,104 +1,180 @@
1
1
  'use client';
2
2
 
3
3
  import type React from 'react';
4
+ import { useEffect } from 'react';
5
+ import { useVisitorOs } from '../../hooks/ui/use-visitor-os';
4
6
  import { cn } from '../../utils';
7
+ import { shortcutLabel, usesCommandKey } from '../../utils/visitor-os';
5
8
  import { MingoIcon } from '../icons';
9
+ import { Button } from '../ui/button';
6
10
 
7
11
  export interface MingoAiButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
8
12
  source?: string;
9
- /** The platform's Mingo identity glyph — pass the SAME server-configured
13
+ /** The platform's Mingo identity glyph: pass the SAME server-configured
10
14
  * icon the chat panel renders (host-side: `EntityIcon` fed by the admin
11
15
  * `assistantIcon`), so the launcher and the panel can never diverge.
12
16
  * Falls back to the packaged Mingo mark when the server has none. */
13
17
  icon?: React.ReactNode;
14
- /** The launcher's wordmark + aria-label — pass the server-configured
18
+ /** The launcher's wordmark + aria-label: pass the server-configured
15
19
  * assistant name (same `assistantName` the chat panel shows) so the
16
20
  * launcher never hardcodes an identity the admin has renamed. */
17
21
  label?: string;
22
+ /** Bind Cmd+K (Ctrl+K off Apple platforms) to open the chat, and show the
23
+ * key cap beside the label. Default false. */
24
+ shortcutHint?: boolean;
25
+ /** `inline` (default): the header launcher, the height and type of the menu
26
+ * items beside it. `field`: the full-width row that opens the mobile menu
27
+ * (the name reads as the field's prompt). ONE component, so every Mingo
28
+ * launcher carries the same identity, ring and event. */
29
+ variant?: 'inline' | 'field';
30
+ }
31
+
32
+ const MINGO_ACCENT = 'var(--ods-flamingo-cyan-base)';
33
+
34
+ /** The event the mounted chat panel (`EmbeddableChat`) opens on. */
35
+ export const ASK_AI_OPEN_EVENT = 'ask-ai:open';
36
+
37
+ /**
38
+ * Open the chat of `source`. Coalesced to '' so a source-less call still
39
+ * matches the panel's own `runtime.source ?? ''` comparison (undefined !== ''
40
+ * would make the panel silently ignore the event).
41
+ */
42
+ export function openAskAi(source?: string): void {
43
+ window.dispatchEvent(new CustomEvent(ASK_AI_OPEN_EVENT, { detail: { source: source ?? '' } }));
18
44
  }
19
45
 
20
46
  /**
21
- * Marketing-header Mingo AI launcher: the flush, full-height button rendered
22
- * at the far right edge of the public `Header` (`config.mingo`). Stateless:
23
- * clicking dispatches an `ask-ai:open` CustomEvent (source-filtered) that the
24
- * mounted `EmbeddableChat` panel listens for.
47
+ * THE Mingo AI launcher, in the site header's right cluster and at the top of
48
+ * the mobile menu (`variant="field"`): the round identity glyph, the
49
+ * assistant's name and an optional shortcut hint, sized like the menu items
50
+ * beside it. Stateless: clicking dispatches an `ask-ai:open`
51
+ * CustomEvent (source-filtered) that the mounted `EmbeddableChat` panel
52
+ * listens for.
53
+ *
54
+ * It carries the AI edge light (`.mingo-edge-frame` / `.mingo-edge` in
55
+ * `styles/chat-animations.css`): an accent arc travelling around its outline,
56
+ * with a one-shot shimmer on hover. Static under reduced motion.
25
57
  *
26
58
  * Distinct from `header-mingo-button.tsx` (`HeaderMingoButton`), the
27
59
  * dashboard/AppHeader controlled toggle; different surface and contract, do
28
60
  * not merge them.
29
61
  *
30
- * Deliberately a raw `<button>` rather than the ui-kit `Button`: it needs
31
- * full-height flush cell layout, an absolutely-positioned animated ring, and
32
- * icon-only collapse that `Button` cannot express (same precedent as
33
- * `header-mingo-button.tsx`).
62
+ * It is the lib `Button` (`transparent`, or `glyph` for the menu row, at the
63
+ * `wrap` size): the launcher's own box, ring and collapse are classes on it.
34
64
  */
35
- const MINGO_ACCENT = 'var(--ods-flamingo-cyan-base)';
65
+ export function MingoAiButton({
66
+ source,
67
+ icon,
68
+ label = 'Mingo AI',
69
+ shortcutHint = false,
70
+ variant = 'inline',
71
+ className,
72
+ onClick,
73
+ ...props
74
+ }: MingoAiButtonProps) {
75
+ // Known after hydration only: until then the hint keeps its space, empty.
76
+ const visitor = useVisitorOs();
77
+ const field = variant === 'field';
78
+
79
+ const commandKey = visitor.known && usesCommandKey(visitor.os);
80
+
81
+ // The shortcut the key cap shows opens the same chat the click opens.
82
+ useEffect(() => {
83
+ if (!shortcutHint) return undefined;
84
+ const onKeyDown = (event: KeyboardEvent) => {
85
+ // Something on the page already took the shortcut (an editor's link shortcut): leave it alone.
86
+ if (event.defaultPrevented || event.altKey || event.shiftKey) return;
87
+ // The modifier the key cap shows, and only that one: on a Mac Ctrl+K is
88
+ // the text fields' own "delete to the end of the line".
89
+ const modifier = commandKey ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;
90
+ if (event.key.toLowerCase() !== 'k' || !modifier) return;
91
+ event.preventDefault();
92
+ openAskAi(source);
93
+ };
94
+ window.addEventListener('keydown', onKeyDown);
95
+ return () => window.removeEventListener('keydown', onKeyDown);
96
+ }, [shortcutHint, source, commandKey]);
36
97
 
37
- export function MingoAiButton({ source, icon, label = 'Mingo AI', className, onClick, ...props }: MingoAiButtonProps) {
38
98
  return (
39
- <button
99
+ <Button
40
100
  {...props}
41
101
  type="button"
102
+ variant={field ? 'glyph' : 'transparent'}
103
+ size="wrap"
104
+ font="regular"
42
105
  aria-label={label}
106
+ aria-keyshortcuts={shortcutHint && visitor.known ? (commandKey ? 'Meta+K' : 'Control+K') : undefined}
43
107
  onClick={e => {
44
- // Coalesce to '' so a source-less mount still matches EmbeddableChat's
45
- // own `runtime.source ?? ''` comparison (undefined !== '' would make
46
- // the panel silently ignore the event).
47
- window.dispatchEvent(new CustomEvent('ask-ai:open', { detail: { source: source ?? '' } }));
108
+ openAskAi(source);
48
109
  onClick?.(e);
49
110
  }}
50
111
  className={cn(
51
- // Unified ODS top-navigation cell (Figma 2797-6808 desktop /
52
- // 2797-7275 mobile): full-height cell with a leading divider,
53
- // transparent at rest so it inherits the bar's background.
54
- // No hover background on the cell: the animated ring + shimmer are
55
- // the ONLY hover treatment (a bg flip on top read as a double
56
- // animation).
57
- 'group/mingo relative flex h-full shrink-0 items-center border-l border-ods-border bg-transparent px-[var(--spacing-system-m)] focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent',
112
+ 'group/mingo flex shrink-0 justify-start focus-visible:ring-ods-accent',
113
+ field
114
+ ? // A row of the mobile menu like the groups under it: same height,
115
+ // type and divider, no surface and no ring of its own.
116
+ 'min-h-14 w-full gap-[var(--spacing-system-sf)] rounded-none border-b border-ods-border text-left text-h4 [&_svg]:h-6 [&_svg]:w-6'
117
+ : // Transparent at rest so it inherits the bar's background; the hover
118
+ // wash is the menu items' own.
119
+ 'h-10 gap-[var(--spacing-system-xsf)] rounded-md pl-[var(--spacing-system-xsf)] pr-2.5 text-ods-text-secondary hover:text-ods-text-primary',
58
120
  className,
59
121
  )}
60
122
  >
61
- {/* Inner box = 1:1 twin of the DEFAULT-size Button geometry (rounded-md,
62
- h-11 md:h-12, px-m) — the same footprint as the header's "Try for
63
- Free" CTA — so the animated ring traces exactly where a full button
64
- border would sit inside the 72px bar. */}
65
- <span className="relative flex h-11 items-center gap-[var(--spacing-system-xsf)] rounded-md px-[var(--spacing-system-m)] md:h-12">
66
- {/* AI edge light (Apple-Intelligence-style): a rotating accent-
67
- gradient arc clipped to a 3px ring on the box outline via CSS
68
- mask (.mingo-edge-frame) — NO opaque cover, so the launcher
69
- inherits whatever background sits behind it. Platform-tinted via
70
- the accent token. */}
71
- <span aria-hidden="true" className="mingo-edge-frame pointer-events-none absolute inset-0 rounded-md">
72
- <span className="mingo-edge" />
73
- </span>
74
- {/* One-shot light-streak shimmer on hover, clipped to the box. */}
75
- <span aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden rounded-md">
76
- <span
77
- className="mingo-shimmer absolute inset-y-0 left-0 w-1/2"
78
- style={{
79
- background:
80
- 'linear-gradient(105deg, transparent, color-mix(in srgb, var(--ods-system-greys-white) 12%, transparent), transparent)',
81
- }}
82
- />
123
+ {!field && (
124
+ <>
125
+ {/* AI edge light (Apple-Intelligence-style): a rotating accent-gradient
126
+ arc clipped to a hairline ring on the outline via CSS mask
127
+ (.mingo-edge-frame). NO opaque cover, so the launcher inherits
128
+ whatever background sits behind it. Platform-tinted via the accent
129
+ token. */}
130
+ <span aria-hidden="true" className="mingo-edge-frame pointer-events-none absolute inset-0 rounded-md">
131
+ <span className="mingo-edge" />
132
+ </span>
133
+ {/* One-shot light-streak shimmer on hover, clipped to the outline. */}
134
+ <span aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden rounded-md">
135
+ <span
136
+ className="mingo-shimmer absolute inset-y-0 left-0 w-1/2"
137
+ style={{
138
+ background:
139
+ 'linear-gradient(105deg, transparent, color-mix(in srgb, var(--ods-system-greys-white) 12%, transparent), transparent)',
140
+ }}
141
+ />
142
+ </span>
143
+ </>
144
+ )}
145
+ {icon ? (
146
+ <span
147
+ className={cn(
148
+ 'relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full',
149
+ field ? 'size-6' : 'size-5',
150
+ )}
151
+ >
152
+ {icon}
83
153
  </span>
84
- {icon ? (
85
- <span className="relative inline-flex size-6 shrink-0 items-center justify-center">{icon}</span>
86
- ) : (
87
- <MingoIcon
88
- color="currentColor"
89
- eyesColor={MINGO_ACCENT}
90
- cornerColor={MINGO_ACCENT}
91
- className="relative h-6 w-6 shrink-0 text-ods-text-primary"
92
- />
93
- )}
94
- {/* Wordmark collapses below md — the mobile cell is icon-only per
95
- spec. h3 bold, matching the default-size CTA label next to it
96
- (Figma 2936-6825). */}
97
- {/* Bare `text-h3` — the exact composite the default-size Button label
98
- uses (bold 700 and -0.02em tracking are built into the utility). */}
99
- <span className="relative hidden whitespace-nowrap text-ods-text-primary text-h3 md:inline">{label}</span>
154
+ ) : (
155
+ <MingoIcon
156
+ color="currentColor"
157
+ eyesColor={MINGO_ACCENT}
158
+ cornerColor={MINGO_ACCENT}
159
+ className={cn('relative shrink-0 text-ods-text-primary', field ? 'size-6' : 'size-5')}
160
+ />
161
+ )}
162
+ {/* In the header the name collapses below lg, with the menus: on a phone
163
+ the launcher is its glyph alone (the name stays the accessible name). */}
164
+ <span
165
+ className={cn('relative whitespace-nowrap', field ? 'min-w-0 flex-1 truncate' : 'hidden text-h6 lg:inline')}
166
+ >
167
+ {label}
100
168
  </span>
101
- </button>
169
+ {shortcutHint && !field && (
170
+ // A key cap in the label's own type (never smaller: a hint nobody can
171
+ // read is decoration) and the label's colour, which clears 4.5:1 on
172
+ // the bar. It keeps its width before the platform is known.
173
+ <kbd className="relative hidden h-6 min-w-9 items-center justify-center rounded border border-ods-border px-[var(--spacing-system-xxs)] text-h6 lg:inline-flex">
174
+ {visitor.known ? shortcutLabel(visitor.os, 'K') : ''}
175
+ </kbd>
176
+ )}
177
+ </Button>
102
178
  );
103
179
  }
104
180
 
@@ -0,0 +1,2 @@
1
+ /** DOM id of the mobile navigation sheet, referenced by the header burger's `aria-controls`. */
2
+ export const MOBILE_NAV_SHEET_ID = 'mobile-nav-sheet';
@@ -0,0 +1,241 @@
1
+ 'use client';
2
+
3
+ import { usePreventScroll } from '@react-aria/overlays';
4
+ import type React from 'react';
5
+ import { useRef, useState } from 'react';
6
+ import { resolveActiveSection } from '../../hooks/ui/use-active-section';
7
+ import { useFocusTrap } from '../../hooks/ui/use-focus-trap';
8
+ import type { NavLink, NavMenu, SiteNav } from '../../types/navigation';
9
+ import { cn } from '../../utils/cn';
10
+ import { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon';
11
+ import { Chevron02RightIcon } from '../icons-v2-generated/arrows/chevron-02-right-icon';
12
+ import { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon';
13
+ import { Button } from '../ui/button';
14
+ import { MingoAiButton } from './mingo-ai-button';
15
+ import { MOBILE_NAV_SHEET_ID } from './mobile-nav-sheet-id';
16
+ import { NavItemRow } from './nav-item-row';
17
+ import { defaultRenderSiteNavLink, NAV_FOCUS_CLASS, type SiteNavLinkRenderer } from './site-nav-link';
18
+
19
+ export { MOBILE_NAV_SHEET_ID } from './mobile-nav-sheet-id';
20
+
21
+ export interface MobileNavSheetProps {
22
+ nav: SiteNav;
23
+ isOpen: boolean;
24
+ onClose: () => void;
25
+ /** The host router's pathname: the group holding the current page starts open. */
26
+ pathname: string;
27
+ logo: React.ReactNode;
28
+ renderLink?: SiteNavLinkRenderer;
29
+ /** The primary CTA node when `nav.primaryCta` is `'trial'` or `'waitlist'`. */
30
+ cta?: React.ReactNode;
31
+ /** Host nodes shown above the pinned CTA (a sign-up button). */
32
+ actions?: React.ReactNode;
33
+ /** The Mingo row at the top (`MingoAiButton variant="field"`): the assistant's
34
+ * name and icon; `source` scopes the `ask-ai:open` event. Omitted: no row. */
35
+ askAI?: { source?: string; icon?: React.ReactNode; label?: string };
36
+ /** Host-added groups after the site's own (Profile, Admin). */
37
+ extraMenus?: NavMenu[];
38
+ /** Values for `NavLink.badgeKey`. */
39
+ badges?: Record<string, number>;
40
+ /** Rendered under the links of a menu with `showSocial`. */
41
+ social?: React.ReactNode;
42
+ /** Opaque ODS background class. Default `bg-ods-bg`. */
43
+ backgroundClassName?: string;
44
+ }
45
+
46
+ /**
47
+ * Would the sheet list anything from this navigation? The header's menu button
48
+ * and the sheet read this one answer (plus whatever the host adds: a profile
49
+ * entry, a sign-up button), so a button never opens an empty sheet.
50
+ */
51
+ export function siteNavHasMobileMenus(nav: Pick<SiteNav, 'menus' | 'mobileMenus'>): boolean {
52
+ return (nav.mobileMenus ?? nav.menus).length > 0;
53
+ }
54
+
55
+ const GROUP_ROW_CLASS =
56
+ 'flex min-h-14 w-full items-center justify-between gap-[var(--spacing-system-sf)] text-left text-ods-text-primary text-h4';
57
+
58
+ const menuLinks = (menu: NavMenu): NavLink[] => [
59
+ ...(menu.columns ?? []).flatMap(group => group.links),
60
+ ...(menu.sideLinks ?? []),
61
+ ];
62
+
63
+ /**
64
+ * The site navigation below `lg`: a full-screen sheet drawn from the same
65
+ * `SiteNav` as the header. Each menu with links is an accordion that expands
66
+ * in place; the primary CTA and Sign in stay pinned at the bottom while the
67
+ * body scrolls.
68
+ *
69
+ * Unmounted while closed (the header's panels are what crawlers read).
70
+ */
71
+ export function MobileNavSheet(props: MobileNavSheetProps) {
72
+ // The open groups are seeded from the page the sheet opens on, so the body
73
+ // mounts with the sheet: every open starts from the current section.
74
+ if (!props.isOpen) return null;
75
+ return <MobileNavSheetBody {...props} />;
76
+ }
77
+
78
+ function MobileNavSheetBody({
79
+ nav,
80
+ onClose,
81
+ pathname,
82
+ logo,
83
+ renderLink = defaultRenderSiteNavLink,
84
+ cta,
85
+ actions,
86
+ askAI,
87
+ extraMenus,
88
+ badges,
89
+ social,
90
+ backgroundClassName,
91
+ }: MobileNavSheetProps) {
92
+ const sheetRef = useRef<HTMLDivElement>(null);
93
+ const menus = [...(nav.mobileMenus ?? nav.menus), ...(extraMenus ?? [])];
94
+
95
+ // Shared ref-counted, iOS-aware scroll lock (react-aria): one counter with
96
+ // the modals and chat overlays, restores prior styles on release.
97
+ usePreventScroll();
98
+ // Initial focus, Tab containment, Escape-to-close, guarded focus restore.
99
+ useFocusTrap(sheetRef, true, { onEscape: onClose });
100
+
101
+ const [openGroups, setOpenGroups] = useState<Record<string, boolean>>(() => {
102
+ const active = resolveActiveSection(menus, pathname);
103
+ return active ? { [active]: true } : {};
104
+ });
105
+
106
+ const primaryCta =
107
+ typeof nav.primaryCta === 'object' ? (
108
+ <Button variant="accent" href={nav.primaryCta.href} onClick={onClose} className="w-full">
109
+ {nav.primaryCta.label}
110
+ </Button>
111
+ ) : nav.primaryCta === 'none' ? null : (
112
+ (cta ?? null)
113
+ );
114
+ const hasFooter = !!actions || !!primaryCta || !!nav.signIn;
115
+
116
+ return (
117
+ <div
118
+ ref={sheetRef}
119
+ id={MOBILE_NAV_SHEET_ID}
120
+ role="dialog"
121
+ aria-modal="true"
122
+ aria-label="Navigation menu"
123
+ tabIndex={-1}
124
+ // Small-viewport height: 100vh overflowed under mobile browser chrome,
125
+ // leaving the pinned CTA unreachable while the body was scroll-locked.
126
+ className={cn(
127
+ 'fixed inset-0 z-[9999] flex h-[100svh] flex-col text-ods-text-primary outline-none',
128
+ backgroundClassName ?? 'bg-ods-bg',
129
+ )}
130
+ >
131
+ <div className="flex h-14 shrink-0 items-center justify-between border-b border-ods-border pl-5 pr-[var(--spacing-system-xsf)]">
132
+ <div className="flex min-w-0 items-center">{logo}</div>
133
+ <Button
134
+ type="button"
135
+ variant="transparent"
136
+ size="icon"
137
+ aria-label="Close menu"
138
+ onClick={onClose}
139
+ className={cn('h-11 w-11 shrink-0 md:h-11 md:w-11', NAV_FOCUS_CLASS)}
140
+ >
141
+ <XmarkIcon aria-hidden="true" className="!h-6 !w-6" />
142
+ </Button>
143
+ </div>
144
+
145
+ <div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-5">
146
+ {askAI && (
147
+ <MingoAiButton
148
+ variant="field"
149
+ source={askAI.source}
150
+ icon={askAI.icon}
151
+ label={askAI.label}
152
+ onClick={onClose}
153
+ />
154
+ )}
155
+
156
+ <nav aria-label="Main">
157
+ {menus.map(menu => {
158
+ const links = menuLinks(menu);
159
+ if (links.length === 0) {
160
+ if (!menu.href) return null;
161
+ return (
162
+ <div key={menu.id} className="border-b border-ods-border">
163
+ {renderLink({
164
+ link: { href: menu.href },
165
+ className: cn(GROUP_ROW_CLASS, NAV_FOCUS_CLASS),
166
+ onClick: onClose,
167
+ children: (
168
+ <>
169
+ {menu.label}
170
+ <Chevron02RightIcon aria-hidden="true" className="h-5 w-5 shrink-0 text-ods-text-muted" />
171
+ </>
172
+ ),
173
+ })}
174
+ </div>
175
+ );
176
+ }
177
+
178
+ const open = !!openGroups[menu.id];
179
+ const listId = `${MOBILE_NAV_SHEET_ID}-${menu.id}`;
180
+ return (
181
+ <div key={menu.id} className="border-b border-ods-border">
182
+ <Button
183
+ type="button"
184
+ variant="glyph"
185
+ size="wrap"
186
+ font="regular"
187
+ aria-expanded={open}
188
+ aria-controls={open ? listId : undefined}
189
+ onClick={() => setOpenGroups(prev => ({ ...prev, [menu.id]: !prev[menu.id] }))}
190
+ className={cn(GROUP_ROW_CLASS, NAV_FOCUS_CLASS)}
191
+ >
192
+ {menu.label}
193
+ <Chevron02DownIcon
194
+ aria-hidden="true"
195
+ className={cn(
196
+ 'h-5 w-5 shrink-0 text-ods-text-muted transition-transform duration-150',
197
+ open && 'rotate-180',
198
+ )}
199
+ />
200
+ </Button>
201
+ {open && (
202
+ <div id={listId} className="flex flex-col pb-[var(--spacing-system-sf)]">
203
+ {links.map(link => (
204
+ <NavItemRow
205
+ key={link.id}
206
+ link={link}
207
+ variant="sheet"
208
+ renderLink={renderLink}
209
+ badge={link.badgeKey ? badges?.[link.badgeKey] : undefined}
210
+ onNavigate={onClose}
211
+ />
212
+ ))}
213
+ {menu.showSocial && social && <div className="pt-[var(--spacing-system-xsf)]">{social}</div>}
214
+ </div>
215
+ )}
216
+ </div>
217
+ );
218
+ })}
219
+ </nav>
220
+ </div>
221
+
222
+ {/* Pinned: stays visible while the body scrolls, clears the iOS home indicator. */}
223
+ {hasFooter && (
224
+ <div className="flex shrink-0 flex-col gap-[var(--spacing-system-xxs)] border-t border-ods-border px-5 pb-[max(1.75rem,env(safe-area-inset-bottom))] pt-[var(--spacing-system-mf)] [&_a]:w-full [&_button]:w-full">
225
+ {actions}
226
+ {primaryCta}
227
+ {nav.signIn &&
228
+ renderLink({
229
+ link: nav.signIn,
230
+ className: cn(
231
+ 'flex h-12 items-center justify-center rounded-md text-ods-text-primary text-h4',
232
+ NAV_FOCUS_CLASS,
233
+ ),
234
+ onClick: onClose,
235
+ children: nav.signIn.label,
236
+ })}
237
+ </div>
238
+ )}
239
+ </div>
240
+ );
241
+ }
@@ -0,0 +1,129 @@
1
+ 'use client';
2
+
3
+ import type React from 'react';
4
+ import type { NavLink } from '../../types/navigation';
5
+ import { cn } from '../../utils/cn';
6
+ import { EntityIcon } from '../icon-display';
7
+ import { defaultRenderSiteNavLink, NAV_FOCUS_CLASS, navLinkLabel, type SiteNavLinkRenderer } from './site-nav-link';
8
+ import { UnreadCountBadge } from './unread-dot';
9
+
10
+ export type NavItemRowVariant = 'panel' | 'sheet' | 'side' | 'label';
11
+
12
+ export interface NavItemRowProps {
13
+ link: NavLink;
14
+ /**
15
+ * - `panel` (default): a desktop menu row, 36px icon tile, label, description.
16
+ * - `sheet`: the mobile sheet's row, 32px tile, at least 52px tall.
17
+ * - `side`: a mega menu's side list, a small bare icon and the label.
18
+ * - `label`: the label only (footer columns).
19
+ */
20
+ variant?: NavItemRowVariant;
21
+ renderLink?: SiteNavLinkRenderer;
22
+ /** The live counter for `link.badgeKey`; nothing renders at 0 or undefined. */
23
+ badge?: number;
24
+ /** Called on activation, so the menu or sheet holding the row can close. */
25
+ onNavigate?: () => void;
26
+ className?: string;
27
+ }
28
+
29
+ // Box and colour per variant; every text step is an ODS composite.
30
+ const ROW_CLASSES: Record<NavItemRowVariant, string> = {
31
+ panel:
32
+ 'flex items-start gap-[var(--spacing-system-sf)] rounded-lg p-[var(--spacing-system-sf)] text-ods-text-primary transition-colors hover:bg-ods-bg-hover',
33
+ sheet: 'flex min-h-[52px] items-center gap-[var(--spacing-system-sf)] py-1.5 text-ods-text-primary',
34
+ side: 'flex items-center gap-[var(--spacing-system-xsf)] rounded-md px-[var(--spacing-system-sf)] py-2.5 text-ods-text-secondary transition-colors text-h6 hover:bg-ods-bg-hover hover:text-ods-text-primary',
35
+ label:
36
+ 'inline-flex items-center gap-[var(--spacing-system-xsf)] text-ods-text-secondary transition-colors text-h6 hover:text-ods-text-primary',
37
+ };
38
+
39
+ function RowGlyph({ link, box, glyph }: { link: NavLink; box: string; glyph: number }) {
40
+ // A server-configured icon (an agent's avatar) is drawn round and whole.
41
+ if (link.icon) {
42
+ return (
43
+ <span
44
+ aria-hidden="true"
45
+ className={cn('flex shrink-0 items-center justify-center overflow-hidden rounded-full', box)}
46
+ >
47
+ <EntityIcon icon={link.icon} className="h-full w-full" />
48
+ </span>
49
+ );
50
+ }
51
+ if (!link.iconName) return null;
52
+ return (
53
+ <span
54
+ aria-hidden="true"
55
+ className={cn(
56
+ 'flex shrink-0 items-center justify-center rounded-lg border border-ods-border bg-ods-bg text-ods-text-primary',
57
+ box,
58
+ )}
59
+ >
60
+ <EntityIcon icon={{ name: link.iconName }} size={glyph} />
61
+ </span>
62
+ );
63
+ }
64
+
65
+ /**
66
+ * One navigation row, shared by the desktop panels, the mobile sheet and the
67
+ * footer columns: icon tile or agent avatar, label, description, the
68
+ * "Built on ..." line and an optional live counter.
69
+ */
70
+ export function NavItemRow({
71
+ link,
72
+ variant = 'panel',
73
+ renderLink = defaultRenderSiteNavLink,
74
+ badge,
75
+ onNavigate,
76
+ className,
77
+ }: NavItemRowProps) {
78
+ const label = navLinkLabel(link);
79
+ const counter = badge ? <UnreadCountBadge count={badge} className="static shrink-0" /> : null;
80
+ // A row with one line of text centres it on its icon; with a description or
81
+ // a "Built on" line the label stays level with the icon's top.
82
+ const singleLine = !link.description && !link.builtOn;
83
+ const rowClassName = cn(
84
+ ROW_CLASSES[variant],
85
+ variant === 'panel' && singleLine && 'items-center',
86
+ NAV_FOCUS_CLASS,
87
+ className,
88
+ );
89
+ const onClick = onNavigate ? () => onNavigate() : undefined;
90
+
91
+ let children: React.ReactNode;
92
+ if (variant === 'label') {
93
+ children = (
94
+ <>
95
+ {label}
96
+ {counter}
97
+ </>
98
+ );
99
+ } else if (variant === 'side') {
100
+ children = (
101
+ <>
102
+ {link.iconName && (
103
+ <span aria-hidden="true" className="flex shrink-0">
104
+ <EntityIcon icon={{ name: link.iconName }} size={18} />
105
+ </span>
106
+ )}
107
+ <span className="min-w-0 flex-1">{label}</span>
108
+ {counter}
109
+ </>
110
+ );
111
+ } else {
112
+ const sheet = variant === 'sheet';
113
+ children = (
114
+ <>
115
+ <RowGlyph link={link} box={sheet ? 'h-8 w-8' : 'h-9 w-9'} glyph={sheet ? 18 : 20} />
116
+ <span className="flex min-w-0 flex-1 flex-col">
117
+ <span className="flex items-center gap-[var(--spacing-system-xsf)] text-h6">
118
+ <span className="min-w-0">{label}</span>
119
+ {counter}
120
+ </span>
121
+ {link.description && <span className="text-ods-text-muted text-h6">{link.description}</span>}
122
+ {link.builtOn && <span className="text-ods-text-muted text-h6">Built on {link.builtOn}</span>}
123
+ </span>
124
+ </>
125
+ );
126
+ }
127
+
128
+ return <>{renderLink({ link, className: rowClassName, children, onClick })}</>;
129
+ }