@flamingo-stack/openframe-frontend-core 0.0.503 → 0.0.504-snapshot.20260801181709

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 (191) hide show
  1. package/dist/{chunk-SGWPW2AO.cjs → chunk-2A74VRAF.cjs} +18 -18
  2. package/dist/{chunk-SGWPW2AO.cjs.map → chunk-2A74VRAF.cjs.map} +1 -1
  3. package/dist/{chunk-LILQJ3UZ.cjs → chunk-3ENGUBJP.cjs} +1 -1
  4. package/dist/chunk-3ENGUBJP.cjs.map +1 -0
  5. package/dist/{chunk-AWDHRIL3.cjs → chunk-4ST2BOWJ.cjs} +75 -75
  6. package/dist/{chunk-AWDHRIL3.cjs.map → chunk-4ST2BOWJ.cjs.map} +1 -1
  7. package/dist/{chunk-SFJRMBJC.js → chunk-5SVNA7IU.js} +2 -2
  8. package/dist/{chunk-OAWFYZZT.js → chunk-7DOSAJDK.js} +3 -3
  9. package/dist/{chunk-J4T5ZBWG.cjs → chunk-BZRP54IO.cjs} +19 -19
  10. package/dist/{chunk-J4T5ZBWG.cjs.map → chunk-BZRP54IO.cjs.map} +1 -1
  11. package/dist/{chunk-BZF63JXP.js → chunk-CRJVO4KO.js} +6 -6
  12. package/dist/{chunk-5RIHLXEP.cjs → chunk-DDVQ5NKK.cjs} +33 -33
  13. package/dist/{chunk-5RIHLXEP.cjs.map → chunk-DDVQ5NKK.cjs.map} +1 -1
  14. package/dist/{chunk-HR6F6IP4.js → chunk-DIKJLAMY.js} +78 -43
  15. package/dist/chunk-DIKJLAMY.js.map +1 -0
  16. package/dist/{chunk-VO2ZONMR.js → chunk-FNTFWP2A.js} +3 -3
  17. package/dist/{chunk-2OXJ6NHI.cjs → chunk-GJZ42TTG.cjs} +7 -7
  18. package/dist/{chunk-2OXJ6NHI.cjs.map → chunk-GJZ42TTG.cjs.map} +1 -1
  19. package/dist/{chunk-JIZJDAXI.cjs → chunk-GXRVBKYX.cjs} +12 -12
  20. package/dist/{chunk-JIZJDAXI.cjs.map → chunk-GXRVBKYX.cjs.map} +1 -1
  21. package/dist/{chunk-F7AD5CUU.js → chunk-H3N4P3CG.js} +3 -3
  22. package/dist/chunk-H3N4P3CG.js.map +1 -0
  23. package/dist/{chunk-VCPTU4IW.js → chunk-HWER6KGU.js} +7 -7
  24. package/dist/{chunk-4SR2QHMB.js → chunk-ITVWX5HX.js} +3 -3
  25. package/dist/{chunk-FFXNRJ5S.cjs → chunk-LOBR7IC6.cjs} +6 -6
  26. package/dist/chunk-LOBR7IC6.cjs.map +1 -0
  27. package/dist/{chunk-72DF3QTA.cjs → chunk-MJQ263KS.cjs} +14 -14
  28. package/dist/{chunk-72DF3QTA.cjs.map → chunk-MJQ263KS.cjs.map} +1 -1
  29. package/dist/{chunk-FTOVZ7O2.js → chunk-NPO6CHCR.js} +7 -7
  30. package/dist/{chunk-XV5SXALY.js → chunk-NT5FX7GZ.js} +3 -3
  31. package/dist/{chunk-UCV4LFUG.cjs → chunk-NY75FKN6.cjs} +40 -40
  32. package/dist/{chunk-UCV4LFUG.cjs.map → chunk-NY75FKN6.cjs.map} +1 -1
  33. package/dist/{chunk-4UNQMYP7.cjs → chunk-O32Y4E4R.cjs} +3 -3
  34. package/dist/{chunk-4UNQMYP7.cjs.map → chunk-O32Y4E4R.cjs.map} +1 -1
  35. package/dist/{chunk-CLQEBWV7.js → chunk-QE55ROZW.js} +4 -4
  36. package/dist/{chunk-WHJGRRK2.js → chunk-QW7SW5HY.js} +52 -43
  37. package/dist/chunk-QW7SW5HY.js.map +1 -0
  38. package/dist/{chunk-JGXAVNZQ.js → chunk-R6CKL7IR.js} +2464 -1886
  39. package/dist/chunk-R6CKL7IR.js.map +1 -0
  40. package/dist/{chunk-L5CI57RM.cjs → chunk-T3BF6JTQ.cjs} +52 -43
  41. package/dist/chunk-T3BF6JTQ.cjs.map +1 -0
  42. package/dist/{chunk-G43CEISB.js → chunk-TA3VJ2QO.js} +4 -4
  43. package/dist/{chunk-7L2RVINX.cjs → chunk-TRS4CIAS.cjs} +35 -35
  44. package/dist/{chunk-7L2RVINX.cjs.map → chunk-TRS4CIAS.cjs.map} +1 -1
  45. package/dist/{chunk-DMXB3VE2.cjs → chunk-U3YLML53.cjs} +1837 -1259
  46. package/dist/chunk-U3YLML53.cjs.map +1 -0
  47. package/dist/{chunk-3T4SMJM5.cjs → chunk-VDKNUC3N.cjs} +181 -146
  48. package/dist/chunk-VDKNUC3N.cjs.map +1 -0
  49. package/dist/{chunk-RR3Z52X5.cjs → chunk-W3QCFQ56.cjs} +42 -42
  50. package/dist/{chunk-RR3Z52X5.cjs.map → chunk-W3QCFQ56.cjs.map} +1 -1
  51. package/dist/{chunk-VYEDTVIZ.js → chunk-XJPMTJAR.js} +4 -4
  52. package/dist/{chunk-6GT557KN.js → chunk-XKIA4XVE.js} +1 -1
  53. package/dist/chunk-XKIA4XVE.js.map +1 -0
  54. package/dist/components/case-studies/index.cjs +11 -11
  55. package/dist/components/case-studies/index.js +5 -5
  56. package/dist/components/chat/index.cjs +5 -5
  57. package/dist/components/chat/index.js +4 -4
  58. package/dist/components/contact/index.cjs +6 -6
  59. package/dist/components/contact/index.js +5 -5
  60. package/dist/components/docs/index.cjs +8 -8
  61. package/dist/components/docs/index.js +7 -7
  62. package/dist/components/embeds/index.cjs +6 -6
  63. package/dist/components/embeds/index.js +5 -5
  64. package/dist/components/faq/index.cjs +7 -7
  65. package/dist/components/faq/index.js +6 -6
  66. package/dist/components/features/index.cjs +5 -5
  67. package/dist/components/features/index.js +4 -4
  68. package/dist/components/help-center-pages/index.cjs +24 -24
  69. package/dist/components/help-center-pages/index.js +15 -15
  70. package/dist/components/index.cjs +169 -165
  71. package/dist/components/index.cjs.map +1 -1
  72. package/dist/components/index.js +17 -13
  73. package/dist/components/index.js.map +1 -1
  74. package/dist/components/navigation/app-header.d.ts +8 -0
  75. package/dist/components/navigation/app-header.d.ts.map +1 -1
  76. package/dist/components/navigation/header-button.d.ts +5 -1
  77. package/dist/components/navigation/header-button.d.ts.map +1 -1
  78. package/dist/components/navigation/header.d.ts.map +1 -1
  79. package/dist/components/navigation/index.cjs +9 -5
  80. package/dist/components/navigation/index.cjs.map +1 -1
  81. package/dist/components/navigation/index.d.ts +4 -0
  82. package/dist/components/navigation/index.d.ts.map +1 -1
  83. package/dist/components/navigation/index.js +8 -4
  84. package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
  85. package/dist/components/navigation/navigation-sidebar-item.d.ts.map +1 -1
  86. package/dist/components/navigation/ticket-alerts-button.d.ts +29 -0
  87. package/dist/components/navigation/ticket-alerts-button.d.ts.map +1 -0
  88. package/dist/components/navigation/top-navigation.d.ts +5 -1
  89. package/dist/components/navigation/top-navigation.d.ts.map +1 -1
  90. package/dist/components/navigation/unread-dot.d.ts +19 -0
  91. package/dist/components/navigation/unread-dot.d.ts.map +1 -0
  92. package/dist/components/onboarding-guides/index.cjs +9 -9
  93. package/dist/components/onboarding-guides/index.js +8 -8
  94. package/dist/components/related-content/index.cjs +7 -7
  95. package/dist/components/related-content/index.js +6 -6
  96. package/dist/components/tickets/help-center-card.d.ts.map +1 -1
  97. package/dist/components/tickets/hooks/use-ticket-actions.d.ts.map +1 -1
  98. package/dist/components/tickets/hooks/use-ticket-engagements.d.ts +1 -9
  99. package/dist/components/tickets/hooks/use-ticket-engagements.d.ts.map +1 -1
  100. package/dist/components/tickets/hooks/use-tickets-list.d.ts +0 -7
  101. package/dist/components/tickets/hooks/use-tickets-list.d.ts.map +1 -1
  102. package/dist/components/tickets/index.cjs +15 -8
  103. package/dist/components/tickets/index.cjs.map +1 -1
  104. package/dist/components/tickets/index.d.ts +2 -1
  105. package/dist/components/tickets/index.d.ts.map +1 -1
  106. package/dist/components/tickets/index.js +14 -7
  107. package/dist/components/tickets/ticket-detail-drawer.d.ts.map +1 -1
  108. package/dist/components/tickets/ticket-live-provider.d.ts +58 -0
  109. package/dist/components/tickets/ticket-live-provider.d.ts.map +1 -0
  110. package/dist/components/tickets/types.d.ts +58 -11
  111. package/dist/components/tickets/types.d.ts.map +1 -1
  112. package/dist/components/ui/button/button.d.ts +6 -5
  113. package/dist/components/ui/button/button.d.ts.map +1 -1
  114. package/dist/components/ui/file-manager/index.cjs +50 -50
  115. package/dist/components/ui/file-manager/index.js +1 -1
  116. package/dist/components/ui/index.cjs +5 -5
  117. package/dist/components/ui/index.js +4 -4
  118. package/dist/contexts/chat-runtime-context.d.ts +8 -0
  119. package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
  120. package/dist/contexts/index.cjs +2 -2
  121. package/dist/contexts/index.js +1 -1
  122. package/dist/hooks/index.cjs +3 -3
  123. package/dist/hooks/index.js +2 -2
  124. package/dist/hooks/ui/use-header-height.d.ts.map +1 -1
  125. package/dist/index.cjs +11 -5
  126. package/dist/index.cjs.map +1 -1
  127. package/dist/index.js +10 -4
  128. package/dist/types/navigation.d.ts +10 -0
  129. package/dist/types/navigation.d.ts.map +1 -1
  130. package/dist/utils/index.cjs +171 -0
  131. package/dist/utils/index.cjs.map +1 -1
  132. package/dist/utils/index.d.ts +1 -0
  133. package/dist/utils/index.d.ts.map +1 -1
  134. package/dist/utils/index.js +171 -1
  135. package/dist/utils/index.js.map +1 -1
  136. package/dist/utils/sse-subscription.d.ts +60 -0
  137. package/dist/utils/sse-subscription.d.ts.map +1 -0
  138. package/package.json +1 -1
  139. package/src/components/features/time-tracker/time-tracker-header-button.tsx +1 -1
  140. package/src/components/navigation/app-header.tsx +36 -20
  141. package/src/components/navigation/header-button.tsx +18 -2
  142. package/src/components/navigation/header-mingo-button.tsx +2 -2
  143. package/src/components/navigation/header-skeleton.tsx +4 -4
  144. package/src/components/navigation/header.tsx +41 -21
  145. package/src/components/navigation/index.ts +4 -0
  146. package/src/components/navigation/mingo-ai-button.tsx +23 -22
  147. package/src/components/navigation/mobile-burger-menu.tsx +3 -3
  148. package/src/components/navigation/navigation-sidebar-item.tsx +2 -3
  149. package/src/components/navigation/ticket-alerts-button.tsx +61 -0
  150. package/src/components/navigation/top-navigation.tsx +17 -1
  151. package/src/components/navigation/unread-dot.tsx +34 -0
  152. package/src/components/tickets/.types.md +1 -2
  153. package/src/components/tickets/help-center-card.tsx +16 -0
  154. package/src/components/tickets/help-center-list.tsx +6 -6
  155. package/src/components/tickets/hooks/.use-ticket-engagements.md +3 -12
  156. package/src/components/tickets/hooks/.use-tickets-list.md +0 -2
  157. package/src/components/tickets/hooks/use-ticket-actions.ts +9 -0
  158. package/src/components/tickets/hooks/use-ticket-engagements.ts +4 -13
  159. package/src/components/tickets/hooks/use-tickets-list.ts +2 -13
  160. package/src/components/tickets/index.ts +10 -0
  161. package/src/components/tickets/ticket-detail-drawer.tsx +45 -13
  162. package/src/components/tickets/ticket-live-provider.tsx +474 -0
  163. package/src/components/tickets/types.ts +62 -11
  164. package/src/components/ui/button/button.tsx +165 -133
  165. package/src/contexts/chat-runtime-context.tsx +8 -0
  166. package/src/hooks/ui/use-header-height.ts +3 -1
  167. package/src/stories/TopNavigation.stories.tsx +20 -1
  168. package/src/types/navigation.ts +10 -0
  169. package/src/utils/index.ts +12 -0
  170. package/src/utils/sse-subscription.ts +276 -0
  171. package/dist/chunk-3T4SMJM5.cjs.map +0 -1
  172. package/dist/chunk-6GT557KN.js.map +0 -1
  173. package/dist/chunk-DMXB3VE2.cjs.map +0 -1
  174. package/dist/chunk-F7AD5CUU.js.map +0 -1
  175. package/dist/chunk-FFXNRJ5S.cjs.map +0 -1
  176. package/dist/chunk-HR6F6IP4.js.map +0 -1
  177. package/dist/chunk-JGXAVNZQ.js.map +0 -1
  178. package/dist/chunk-L5CI57RM.cjs.map +0 -1
  179. package/dist/chunk-LILQJ3UZ.cjs.map +0 -1
  180. package/dist/chunk-WHJGRRK2.js.map +0 -1
  181. /package/dist/{chunk-SFJRMBJC.js.map → chunk-5SVNA7IU.js.map} +0 -0
  182. /package/dist/{chunk-OAWFYZZT.js.map → chunk-7DOSAJDK.js.map} +0 -0
  183. /package/dist/{chunk-BZF63JXP.js.map → chunk-CRJVO4KO.js.map} +0 -0
  184. /package/dist/{chunk-VO2ZONMR.js.map → chunk-FNTFWP2A.js.map} +0 -0
  185. /package/dist/{chunk-VCPTU4IW.js.map → chunk-HWER6KGU.js.map} +0 -0
  186. /package/dist/{chunk-4SR2QHMB.js.map → chunk-ITVWX5HX.js.map} +0 -0
  187. /package/dist/{chunk-FTOVZ7O2.js.map → chunk-NPO6CHCR.js.map} +0 -0
  188. /package/dist/{chunk-XV5SXALY.js.map → chunk-NT5FX7GZ.js.map} +0 -0
  189. /package/dist/{chunk-CLQEBWV7.js.map → chunk-QE55ROZW.js.map} +0 -0
  190. /package/dist/{chunk-G43CEISB.js.map → chunk-TA3VJ2QO.js.map} +0 -0
  191. /package/dist/{chunk-VYEDTVIZ.js.map → chunk-XJPMTJAR.js.map} +0 -0
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Fetch-based SSE subscription primitive.
3
+ *
4
+ * Why not `EventSource`: cross-origin embedders authenticate via the
5
+ * `embedAuthedFetch` adapter's HEADERS (OpenFrame bearer), which
6
+ * `EventSource` cannot carry. This reads `response.body` from a normal
7
+ * authed fetch and parses standard `event:`/`data:` frames.
8
+ *
9
+ * Lifecycle (never-terminating by design):
10
+ * - Infinite reconnects with capped exponential backoff (~30s max +
11
+ * jitter), reset on a successful open. Do NOT copy the terminating
12
+ * `maxRetries → exhausted` model of the hub's server-side
13
+ * `RealtimeRetryManager` — a long outage must self-heal when the
14
+ * backend returns, because there is NO polling fallback behind this.
15
+ * - Liveness by silence: the server keepalives every ~15s; any bytes
16
+ * (INCLUDING `: keepalive` comment lines — they reset the timer
17
+ * before frame parsing drops them) count as life. Silence beyond
18
+ * `silenceTimeoutMs` (default 45s = 3× keepalive) aborts + reconnects.
19
+ * - Terminal responses (NO retry): any 4xx except 408/429 — the
20
+ * `x-block-layer` header, when readable, is logged for attribution
21
+ * only, never used as a retry gate (proxies may strip it); and
22
+ * 204 → a distinct `no-stream` status (the caller decides when to
23
+ * try again). Backoff-retry is for transport errors, 408/429, and 5xx.
24
+ *
25
+ * THE common SSE client for lib + hosts — exported from the `utils`
26
+ * barrel. Consumers: `TicketLiveProvider` (lib) and the hub's
27
+ * workflow/invocation stream hooks (which replaced the hub's old
28
+ * EventSource-based `sse-client.ts`). Finite streams (workflows,
29
+ * invocations) end with a terminal event and a server-side close —
30
+ * their handlers MUST call `close()` on the terminal event, or the
31
+ * never-terminating reconnect loop re-opens the finished stream.
32
+ */
33
+ /** Transport-level status. Server-sent `status` FRAMES (`subscribed` /
34
+ * `retrying` / `reconnect_failed`) arrive via `onEvent('status', …)` —
35
+ * callers derive their "connected" notion from those, not from these. */
36
+ export type SseTransportStatus = 'connecting' | 'open' | 'reconnecting' | 'no-stream' | 'terminal' | 'closed';
37
+ export interface SseSubscriptionOptions {
38
+ url: string;
39
+ /** Defaults to `embedAuthedFetch` (adapter headers + credentials). */
40
+ fetchImpl?: (url: string, init?: RequestInit) => Promise<Response>;
41
+ /** Called per parsed frame: `eventName` from `event:` (default
42
+ * 'message'), `data` JSON-parsed when possible (raw string otherwise). */
43
+ onEvent: (eventName: string, data: unknown) => void;
44
+ onStatusChange?: (status: SseTransportStatus) => void;
45
+ /** Silence threshold before the connection is presumed dead. MUST be
46
+ * ≥2.5× the server keepalive cadence or jitter causes false-disconnect
47
+ * churn (each one costs a full server invocation). */
48
+ silenceTimeoutMs?: number;
49
+ maxBackoffMs?: number;
50
+ initialBackoffMs?: number;
51
+ }
52
+ export interface SseSubscription {
53
+ /** Permanently stop — no further reconnects, no callbacks. */
54
+ close: () => void;
55
+ /** Drop the current connection (if any) and reconnect immediately,
56
+ * resetting backoff. No-op after `close()`. */
57
+ reconnectNow: () => void;
58
+ }
59
+ export declare function createSseSubscription(options: SseSubscriptionOptions): SseSubscription;
60
+ //# sourceMappingURL=sse-subscription.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse-subscription.d.ts","sourceRoot":"","sources":["../../src/utils/sse-subscription.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAIH;;0EAE0E;AAC1E,MAAM,MAAM,kBAAkB,GAC1B,YAAY,GACZ,MAAM,GACN,cAAc,GACd,WAAW,GACX,UAAU,GACV,QAAQ,CAAA;AAEZ,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,MAAM,CAAA;IACX,sEAAsE;IACtE,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAClE;+EAC2E;IAC3E,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACnD,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAA;IACrD;;2DAEuD;IACvD,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,8DAA8D;IAC9D,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB;oDACgD;IAChD,YAAY,EAAE,MAAM,IAAI,CAAA;CACzB;AAMD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,eAAe,CAyMtF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.503",
3
+ "version": "0.0.504-snapshot.20260801181709",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -93,7 +93,7 @@ export function TimeTrackerHeaderButton({ className, disabled }: TimeTrackerHead
93
93
  <>
94
94
  <ClockHistoryIcon
95
95
  className={cn(
96
- 'h-4 w-4 md:h-6 md:w-6',
96
+ 'h-6 w-6',
97
97
  isPaused ? 'text-ods-text-secondary' : isActive && 'text-ods-accent',
98
98
  )}
99
99
  />
@@ -11,6 +11,7 @@ import { BellIcon } from '../icons-v2-generated/interface/bell-icon';
11
11
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, SquareAvatar } from '../ui';
12
12
  import { HeaderButton } from './header-button';
13
13
  import { HeaderGlobalSearch } from './header-global-search';
14
+ import { TicketAlertsButton } from './ticket-alerts-button';
14
15
  import { HeaderMingoButton } from './header-mingo-button';
15
16
  import { HeaderOrganizationFilter } from './header-organization-filter';
16
17
  import { TopNavigation } from './top-navigation';
@@ -24,6 +25,14 @@ export interface AppHeaderProps {
24
25
  onOrgChange?: (id: string) => void;
25
26
  showNotifications?: boolean;
26
27
  unreadCount?: number;
28
+ /** Render the support-ticket alerts cell (`TicketAlertsButton`).
29
+ * Requires wrapping the app in `<TicketLiveProvider>` — without it
30
+ * the cell renders nothing. */
31
+ showTicketAlerts?: boolean;
32
+ /** Navigation target for the tickets surface (e.g. '/help-center/tickets'). */
33
+ ticketAlertsHref?: string;
34
+ /** Host navigation for the tickets cell (router push). Wins over href. */
35
+ onTicketAlerts?: () => void;
27
36
  /** Render the time-tracker button + popup. Requires wrapping the app in `<TimeTrackerProvider>`. */
28
37
  showTimeTracker?: boolean;
29
38
  /** Render the "Mingo AI" launcher button (drawer-style trigger for an
@@ -110,6 +119,9 @@ export const AppHeader = React.memo(function AppHeader({
110
119
  onOrgChange,
111
120
  showNotifications,
112
121
  unreadCount = 0,
122
+ showTicketAlerts = false,
123
+ ticketAlertsHref,
124
+ onTicketAlerts,
113
125
  showTimeTracker = false,
114
126
  showMingoAI = false,
115
127
  onMingoAI,
@@ -145,6 +157,7 @@ export const AppHeader = React.memo(function AppHeader({
145
157
  showOrganizations && 'icon-lg',
146
158
  showTimeTracker && 'icon',
147
159
  showNotifications && 'icon',
160
+ showTicketAlerts && 'icon',
148
161
  showUser && 'icon-md',
149
162
  showMingoAI && 'wide',
150
163
  ].filter(Boolean) as HeaderLoadingCell[])
@@ -169,7 +182,7 @@ export const AppHeader = React.memo(function AppHeader({
169
182
  <HeaderButton
170
183
  onClick={onToggleMobileMenu}
171
184
  isActive={isMobileMenuOpen}
172
- icon={isMobileMenuOpen ? <XmarkIcon className="w-4 h-4" /> : <Menu01Icon className="w-4 h-4" />}
185
+ icon={isMobileMenuOpen ? <XmarkIcon className="w-6 h-6" /> : <Menu01Icon className="w-6 h-6" />}
173
186
  aria-label={isMobileMenuOpen ? 'Close menu' : 'Open menu'}
174
187
  aria-expanded={isMobileMenuOpen}
175
188
  className="border-r border-ods-border"
@@ -195,7 +208,7 @@ export const AppHeader = React.memo(function AppHeader({
195
208
  {/* Mobile: Search button */}
196
209
  {showSearch && (
197
210
  <HeaderButton
198
- icon={<SearchIcon className="w-4 h-4 md:w-6 md:h-6" />}
211
+ icon={<SearchIcon className="w-6 h-6" />}
199
212
  aria-label="Search"
200
213
  className={cn('md:hidden', cellDivider, dimmedClass)}
201
214
  disabled={disabled}
@@ -224,6 +237,17 @@ export const AppHeader = React.memo(function AppHeader({
224
237
  />
225
238
  )}
226
239
 
240
+ {/* Support-ticket alerts (Help Center) — renders nothing when
241
+ no <TicketLiveProvider> is mounted. */}
242
+ {showTicketAlerts && (
243
+ <TicketAlertsButton
244
+ href={ticketAlertsHref}
245
+ onClick={onTicketAlerts}
246
+ disabled={disabled}
247
+ className={cn(cellDivider, dimmedClass)}
248
+ />
249
+ )}
250
+
227
251
  {isMdUp && showUser && (
228
252
  <DropdownMenu>
229
253
  <DropdownMenuTrigger asChild disabled={disabled}>
@@ -322,18 +346,10 @@ function NotificationsHeaderButton({ fallbackUnreadCount, disabled, dimmedClass
322
346
 
323
347
  return (
324
348
  <HeaderButton
325
- icon={
326
- isActive ? (
327
- <XmarkIcon className="w-4 h-4 md:w-6 md:h-6" />
328
- ) : (
329
- <div className="relative w-4 h-4 md:w-6 md:h-6">
330
- <BellIcon className="w-full h-full" />
331
- {hasUnread && (
332
- <span className="absolute top-0 right-0 bg-ods-warning rounded-full w-1.5 h-1.5 md:w-2 md:h-2" />
333
- )}
334
- </div>
335
- )
336
- }
349
+ icon={isActive ? <XmarkIcon className="w-6 h-6" /> : <BellIcon className="w-6 h-6" />}
350
+ // Shared dot primitive on the cell — same markup every indicator
351
+ // cell renders (see HeaderButton.showUnreadDot / <UnreadDot>).
352
+ showUnreadDot={!isActive && hasUnread}
337
353
  aria-label={isActive ? 'Close notifications' : 'Notifications'}
338
354
  onClick={onClick}
339
355
  isActive={isActive}
@@ -357,11 +373,11 @@ function HeaderCellSkeleton({ className }: { className?: string }) {
357
373
  return (
358
374
  <div
359
375
  className={cn(
360
- 'flex h-full w-12 shrink-0 items-center justify-center border-l border-ods-border md:w-14',
376
+ 'flex h-full w-14 shrink-0 items-center justify-center border-l border-ods-border',
361
377
  className,
362
378
  )}
363
379
  >
364
- <div className="h-4 w-4 animate-pulse rounded bg-ods-border md:h-6 md:w-6" />
380
+ <div className="h-6 w-6 animate-pulse rounded bg-ods-border" />
365
381
  </div>
366
382
  );
367
383
  }
@@ -376,8 +392,8 @@ function HeaderCellSkeleton({ className }: { className?: string }) {
376
392
  */
377
393
  function HeaderWideCellSkeleton() {
378
394
  return (
379
- <div className="flex h-full w-12 shrink-0 items-center justify-center gap-2 border-l border-ods-border md:w-[140px] md:px-4">
380
- <div className="h-4 w-4 shrink-0 animate-pulse rounded bg-ods-border md:h-6 md:w-6" />
395
+ <div className="flex h-full w-14 shrink-0 items-center justify-center gap-2 border-l border-ods-border md:w-[140px] md:px-4">
396
+ <div className="h-6 w-6 shrink-0 animate-pulse rounded bg-ods-border" />
381
397
  <div className="hidden h-5 animate-pulse rounded bg-ods-border md:block md:w-[72px]" />
382
398
  </div>
383
399
  );
@@ -411,8 +427,8 @@ function AppHeaderSkeleton({
411
427
  aria-busy="true"
412
428
  leading={
413
429
  // Burger cell: mobile only, in CSS.
414
- <div className="flex h-full w-12 shrink-0 items-center justify-center border-r border-ods-border md:hidden">
415
- <div className="h-4 w-4 animate-pulse rounded bg-ods-border" />
430
+ <div className="flex h-full w-14 shrink-0 items-center justify-center border-r border-ods-border md:hidden">
431
+ <div className="h-6 w-6 animate-pulse rounded bg-ods-border" />
416
432
  </div>
417
433
  }
418
434
  logo={
@@ -2,12 +2,17 @@
2
2
 
3
3
  import React from 'react'
4
4
  import { cn } from '../../utils/cn'
5
+ import { UnreadDot } from './unread-dot'
5
6
 
6
7
  export interface HeaderButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
7
8
  /** Whether the button is in active/pressed state */
8
9
  isActive?: boolean
9
10
  /** Icon to display in the button */
10
11
  icon: React.ReactNode
12
+ /** Render the shared `UnreadDot` at the icon's top-right (the
13
+ * notifications-bell treatment, promoted to the cell primitive so
14
+ * indicator cells never copy the dot markup). */
15
+ showUnreadDot?: boolean
11
16
  /** Additional class names */
12
17
  className?: string
13
18
  }
@@ -15,6 +20,7 @@ export interface HeaderButtonProps extends React.ButtonHTMLAttributes<HTMLButton
15
20
  export function HeaderButton({
16
21
  isActive = false,
17
22
  icon,
23
+ showUnreadDot = false,
18
24
  className,
19
25
  ...props
20
26
  }: HeaderButtonProps) {
@@ -23,7 +29,10 @@ export function HeaderButton({
23
29
  className={cn(
24
30
  "flex items-center justify-center shrink-0",
25
31
  "transition-colors duration-200",
26
- "w-12 h-full md:w-14",
32
+ // Square cell width follows the TopNavigation size (56px small /
33
+ // 72px big) via the shell's --top-nav-cell var; 56px fallback for
34
+ // standalone use.
35
+ "w-[var(--top-nav-cell,3.5rem)] h-full",
27
36
  isActive
28
37
  ? "text-ods-text-primary bg-ods-bg-active"
29
38
  : // Transparent at rest so the cell inherits the bar's background
@@ -33,7 +42,14 @@ export function HeaderButton({
33
42
  )}
34
43
  {...props}
35
44
  >
36
- {icon}
45
+ {showUnreadDot ? (
46
+ <span className="relative inline-flex">
47
+ {icon}
48
+ <UnreadDot />
49
+ </span>
50
+ ) : (
51
+ icon
52
+ )}
37
53
  </button>
38
54
  )
39
55
  }
@@ -57,7 +57,7 @@ export function HeaderMingoButton({
57
57
  // Mobile, drawer open: the icon-only button doubles as the close
58
58
  // affordance, so swap the Mingo logo for an X. Match the other header
59
59
  // icons (e.g. notifications): secondary color, w-4 h-4 → md:w-6 h-6.
60
- <XmarkIcon className="w-4 h-4 md:w-6 md:h-6 shrink-0 text-ods-text-secondary" />
60
+ <XmarkIcon className="w-6 h-6 shrink-0 text-ods-text-secondary" />
61
61
  ) : (
62
62
  // Outer frame follows the button's text color (currentColor); the eyes
63
63
  // and corner block are ODS cyan.
@@ -65,7 +65,7 @@ export function HeaderMingoButton({
65
65
  color="currentColor"
66
66
  eyesColor="var(--ods-flamingo-cyan-base)"
67
67
  cornerColor="var(--ods-flamingo-cyan-base)"
68
- className="w-4 h-4 md:w-6 md:h-6 shrink-0"
68
+ className="w-6 h-6 shrink-0"
69
69
  />
70
70
  )}
71
71
  {!iconOnly && (
@@ -21,20 +21,20 @@ export function HeaderSkeleton({ config }: HeaderSkeletonProps) {
21
21
  <div className="sticky top-0 z-[50] w-full">
22
22
  <header
23
23
  className={cn(
24
- 'flex h-12 w-full items-center border-b border-t border-ods-border md:h-14 md:border-t-0',
24
+ 'flex h-[72px] w-full items-center border-b border-t border-ods-border md:border-t-0',
25
25
  config?.backgroundColor || 'bg-ods-card',
26
26
  config?.className,
27
27
  )}
28
28
  >
29
29
  {/* Leading cells: admin toggle / burger */}
30
30
  {showLeftActions && (
31
- <div className="flex h-full w-12 items-center justify-center border-r border-ods-border md:w-14">
31
+ <div className="flex h-full w-[72px] items-center justify-center border-r border-ods-border">
32
32
  <div className="h-6 w-6 animate-pulse rounded bg-ods-border" />
33
33
  </div>
34
34
  )}
35
35
  {showMobileMenu && (
36
- <div className="flex h-full w-12 items-center justify-center border-r border-ods-border md:w-14 lg:hidden">
37
- <div className="h-4 w-4 animate-pulse rounded bg-ods-border md:h-6 md:w-6" />
36
+ <div className="flex h-full w-[72px] items-center justify-center border-r border-ods-border lg:hidden">
37
+ <div className="h-6 w-6 animate-pulse rounded bg-ods-border" />
38
38
  </div>
39
39
  )}
40
40
 
@@ -8,6 +8,7 @@ import { Menu01Icon, XmarkIcon } from '../icons-v2-generated';
8
8
  import { Button } from '../ui/button';
9
9
  import { HeaderButton } from './header-button';
10
10
  import { MingoAiButton } from './mingo-ai-button';
11
+ import { TicketAlertsButton } from './ticket-alerts-button';
11
12
  import { MOBILE_NAV_PANEL_ID } from './mobile-nav-panel';
12
13
  import { TopNavigation } from './top-navigation';
13
14
 
@@ -135,11 +136,12 @@ export function Header({ config, platform }: HeaderProps) {
135
136
  [item.id]: !prev[item.id],
136
137
  }));
137
138
  }}
138
- size="small-legacy"
139
+ size="default"
140
+ font="regular"
139
141
  className={cn(
140
- // Top-level nav links: h3 bold per the ODS top-navigation spec
141
- // (Figma 2797-5978); dropdown children stay on the h6 step.
142
- 'text-h3 font-bold tracking-[-0.36px]',
142
+ // Top-level nav links: the default-size button with the h4
143
+ // (DM Sans 500) `font="regular"` label Figma 133-740, added
144
+ // for navigation; dropdown children stay on the h6 step.
143
145
  item.isActive && 'bg-ods-bg-hover', // Active items get subtle gray background
144
146
  isOpen && 'bg-ods-bg-hover', // Open dropdowns get gray background
145
147
  item.className,
@@ -227,9 +229,9 @@ export function Header({ config, platform }: HeaderProps) {
227
229
  onClick={item.onClick} // Only for non-navigation actions
228
230
  leftIcon={item.icon}
229
231
  rightIcon={item.badge}
230
- size="small-legacy"
232
+ size="default"
233
+ font="regular"
231
234
  className={cn(
232
- 'text-h3 font-bold tracking-[-0.36px]',
233
235
  'hover:bg-ods-bg-hover focus:bg-ods-bg-hover',
234
236
  'whitespace-nowrap',
235
237
  'text-ods-text-primary', // All items use primary text color
@@ -251,9 +253,9 @@ export function Header({ config, platform }: HeaderProps) {
251
253
  onClick={item.onClick}
252
254
  leftIcon={item.icon}
253
255
  rightIcon={item.badge}
254
- size="small-legacy"
256
+ size="default"
257
+ font="regular"
255
258
  className={cn(
256
- 'text-h3 font-bold tracking-[-0.36px]',
257
259
  'hover:bg-ods-bg-hover focus:bg-ods-bg-hover',
258
260
  'whitespace-nowrap',
259
261
  'text-ods-text-primary', // All items use primary text color
@@ -301,15 +303,16 @@ export function Header({ config, platform }: HeaderProps) {
301
303
  style={config.style}
302
304
  backgroundClassName={config.backgroundColor}
303
305
  centerBreakpoint="lg"
306
+ size="big"
304
307
  leading={
305
308
  <>
306
309
  {/* Length-guarded: platform configs often pass `left: []`, and an
307
310
  empty array is truthy — without the guard the cell would render
308
311
  as a bare divider + padding. */}
312
+ {/* No padding on the wrapper: leading cells (HeaderButton-based
313
+ admin toggles) size themselves to the bar's square cell. */}
309
314
  {!!config.actions?.left?.length && (
310
- <div className="flex h-full items-center border-r border-ods-border px-[var(--spacing-system-xs)]">
311
- {config.actions.left}
312
- </div>
315
+ <div className="flex h-full items-center border-r border-ods-border">{config.actions.left}</div>
313
316
  )}
314
317
  {/* Mobile/tablet menu toggle — a leading cell per the ODS spec
315
318
  (banded with the nav breakpoint `lg` so the desktop nav and the
@@ -328,8 +331,8 @@ export function Header({ config, platform }: HeaderProps) {
328
331
  aria-controls={config.mobile?.isOpen ? MOBILE_NAV_PANEL_ID : undefined}
329
332
  icon={
330
333
  config.mobile?.isOpen
331
- ? config.mobile?.closeIcon || <XmarkIcon className="w-4 h-4 md:w-6 md:h-6" />
332
- : config.mobile?.menuIcon || <Menu01Icon className="w-4 h-4 md:w-6 md:h-6" />
334
+ ? config.mobile?.closeIcon || <XmarkIcon className="w-6 h-6" />
335
+ : config.mobile?.menuIcon || <Menu01Icon className="w-6 h-6" />
333
336
  }
334
337
  />
335
338
  )}
@@ -340,7 +343,11 @@ export function Header({ config, platform }: HeaderProps) {
340
343
  {config.logo.element}
341
344
  </Link>
342
345
  }
343
- logoClassName={hasLeftCells ? 'lg:pl-[var(--spacing-system-l)]' : undefined}
346
+ // Logo inset for the hub sites: 12px on mobile (the shell's base p-m),
347
+ // 16px on tablet (mf token; the shell default is pl-l = 24px there),
348
+ // desktop unchanged — pl-xxl, collapsed to pl-l when an always-visible
349
+ // leading cell sits before the logo.
350
+ logoClassName={cn('md:pl-[var(--spacing-system-mf)]', hasLeftCells && 'lg:pl-[var(--spacing-system-l)]')}
344
351
  center={
345
352
  hasNav ? (
346
353
  <nav
@@ -378,13 +385,26 @@ export function Header({ config, platform }: HeaderProps) {
378
385
  }
379
386
  ctaClassName="gap-3"
380
387
  sideActions={
381
- config.mingo?.enabled ? (
382
- <MingoAiButton
383
- source={config.mingo.source}
384
- icon={config.mingo.icon}
385
- label={config.mingo.label}
386
- className={config.mingo.className}
387
- />
388
+ config.tickets || config.mingo?.enabled ? (
389
+ <>
390
+ {/* Support-ticket alerts cell — before Mingo, flush cell row.
391
+ Renders nothing unless the host mounted <TicketLiveProvider>. */}
392
+ {config.tickets && (
393
+ <TicketAlertsButton
394
+ href={config.tickets.href}
395
+ onClick={config.tickets.onClick}
396
+ className="border-l border-ods-border"
397
+ />
398
+ )}
399
+ {config.mingo?.enabled && (
400
+ <MingoAiButton
401
+ source={config.mingo.source}
402
+ icon={config.mingo.icon}
403
+ label={config.mingo.label}
404
+ className={config.mingo.className}
405
+ />
406
+ )}
407
+ </>
388
408
  ) : undefined
389
409
  }
390
410
  />
@@ -49,6 +49,10 @@ export type { MobileBurgerMenuProps } from './mobile-burger-menu'
49
49
 
50
50
  export { HeaderButton } from './header-button'
51
51
  export type { HeaderButtonProps } from './header-button'
52
+ export { UnreadDot } from './unread-dot'
53
+ export type { UnreadDotProps } from './unread-dot'
54
+ export { TicketAlertsButton } from './ticket-alerts-button'
55
+ export type { TicketAlertsButtonProps } from './ticket-alerts-button'
52
56
 
53
57
  export { TopNavigation } from './top-navigation'
54
58
  export type { TopNavigationProps, TopNavigationCenterBreakpoint } from './top-navigation'
@@ -1,20 +1,20 @@
1
- 'use client'
1
+ 'use client';
2
2
 
3
- import React from 'react'
4
- import { MingoIcon } from '../icons'
5
- import { cn } from '../../utils'
3
+ import React from 'react';
4
+ import { cn } from '../../utils';
5
+ import { MingoIcon } from '../icons';
6
6
 
7
7
  export interface MingoAiButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
8
- source?: string
8
+ source?: string;
9
9
  /** The platform's Mingo identity glyph — pass the SAME server-configured
10
10
  * icon the chat panel renders (host-side: `EntityIcon` fed by the admin
11
11
  * `assistantIcon`), so the launcher and the panel can never diverge.
12
12
  * Falls back to the packaged Mingo mark when the server has none. */
13
- icon?: React.ReactNode
13
+ icon?: React.ReactNode;
14
14
  /** The launcher's wordmark + aria-label — pass the server-configured
15
15
  * assistant name (same `assistantName` the chat panel shows) so the
16
16
  * launcher never hardcodes an identity the admin has renamed. */
17
- label?: string
17
+ label?: string;
18
18
  }
19
19
 
20
20
  /**
@@ -32,7 +32,7 @@ export interface MingoAiButtonProps extends React.ButtonHTMLAttributes<HTMLButto
32
32
  * icon-only collapse that `Button` cannot express (same precedent as
33
33
  * `header-mingo-button.tsx`).
34
34
  */
35
- const MINGO_ACCENT = 'var(--ods-flamingo-cyan-base)'
35
+ const MINGO_ACCENT = 'var(--ods-flamingo-cyan-base)';
36
36
 
37
37
  export function MingoAiButton({ source, icon, label = 'Mingo AI', className, onClick, ...props }: MingoAiButtonProps) {
38
38
  return (
@@ -40,12 +40,12 @@ export function MingoAiButton({ source, icon, label = 'Mingo AI', className, onC
40
40
  {...props}
41
41
  type="button"
42
42
  aria-label={label}
43
- onClick={(e) => {
43
+ onClick={e => {
44
44
  // Coalesce to '' so a source-less mount still matches EmbeddableChat's
45
45
  // own `runtime.source ?? ''` comparison (undefined !== '' would make
46
46
  // the panel silently ignore the event).
47
- window.dispatchEvent(new CustomEvent('ask-ai:open', { detail: { source: source ?? '' } }))
48
- onClick?.(e)
47
+ window.dispatchEvent(new CustomEvent('ask-ai:open', { detail: { source: source ?? '' } }));
48
+ onClick?.(e);
49
49
  }}
50
50
  className={cn(
51
51
  // Unified ODS top-navigation cell (Figma 2797-6808 desktop /
@@ -58,11 +58,11 @@ export function MingoAiButton({ source, icon, label = 'Mingo AI', className, onC
58
58
  className,
59
59
  )}
60
60
  >
61
- {/* Inner box = 1:1 twin of the header CTA's `size="small"` Button
62
- geometry (rounded-md, h-6 md:h-8) so the animated ring traces
63
- exactly where a normal small-button border would sit inside the
64
- 48/56px cell. */}
65
- <span className="relative flex h-6 items-center gap-[var(--spacing-system-xsf)] rounded-md px-[var(--spacing-system-xsf)] md:h-8">
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
66
  {/* AI edge light (Apple-Intelligence-style): a rotating accent-
67
67
  gradient arc clipped to a 3px ring on the box outline via CSS
68
68
  mask (.mingo-edge-frame) — NO opaque cover, so the launcher
@@ -92,13 +92,14 @@ export function MingoAiButton({ source, icon, label = 'Mingo AI', className, onC
92
92
  />
93
93
  )}
94
94
  {/* Wordmark collapses below md — the mobile cell is icon-only per
95
- spec. `text-code` (sentence-case mono caption, matching the small
96
- CTA next to it) by product decision — the Figma wordmark is h3
97
- bold; flagged for the designer. */}
98
- <span className="relative hidden whitespace-nowrap text-code text-ods-text-primary md:inline">{label}</span>
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-h3 text-ods-text-primary md:inline">{label}</span>
99
100
  </span>
100
101
  </button>
101
- )
102
+ );
102
103
  }
103
104
 
104
- export default MingoAiButton
105
+ export default MingoAiButton;
@@ -9,8 +9,8 @@ import { Logout02Icon, PenEditIcon, UserSearchIcon } from '../icons-v2-generated
9
9
  import { Button, SquareAvatar } from '../ui'
10
10
  import { OVERLAY_BACKDROP_CLASS } from '../ui/drawer'
11
11
 
12
- // Header height constant (h-12 = 48px)
13
- const HEADER_HEIGHT = 48
12
+ // Header height constant — the unified top-navigation `small` bar (56px on all screens)
13
+ const HEADER_HEIGHT = 56
14
14
 
15
15
  export interface MobileBurgerMenuProps {
16
16
  /** Whether the menu is open */
@@ -188,7 +188,7 @@ export const MobileBurgerMenu = React.memo(function MobileBurgerMenu({
188
188
  fallback={user.userName}
189
189
  size="lg"
190
190
  variant="round"
191
- className="w-12 h-12 shrink-0"
191
+ className="w-14 h-14 shrink-0"
192
192
  />
193
193
 
194
194
  {/* User Info */}
@@ -4,6 +4,7 @@ import { cloneElement, memo, type ReactElement, type ReactNode } from 'react'
4
4
  import Link from '../../embed-shims/next-link'
5
5
  import { NavigationSidebarItem } from '../../types/navigation'
6
6
  import { cn } from '../../utils'
7
+ import { UnreadDot } from './unread-dot'
7
8
 
8
9
  interface IconProps {
9
10
  color?: string
@@ -82,9 +83,7 @@ export const NavigationSidebarItemButton = memo(function NavigationSidebarItemBu
82
83
  {cloneElement(item.icon as ReactElement<IconProps>, {
83
84
  color: isActive && !disabled ? "text-ods-accent" : "text-ods-text-secondary",
84
85
  })}
85
- {hasUnread && !showLabel && (
86
- <span className="absolute top-0 right-0 bg-ods-warning rounded-full w-2 h-2" />
87
- )}
86
+ {hasUnread && !showLabel && <UnreadDot size="fixed" />}
88
87
  </div>
89
88
 
90
89
  <span
@@ -0,0 +1,61 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * TicketAlertsButton — the unified header affordance for support-ticket
5
+ * unread indication. Mounted by BOTH header shells (hub `Header` via
6
+ * `HeaderConfig.tickets`, console `AppHeader` via `showTicketAlerts`).
7
+ *
8
+ * Design note: the console's NATS-backed notifications bell covers
9
+ * OpenFrame-internal events; Help Center replies come from the HubSpot
10
+ * mirror domain and must ALSO surface on the hub, which mounts no
11
+ * notifications system — so the affordance both hosts can share is this
12
+ * dedicated cell, not the bell. (Optional follow-up, out of scope:
13
+ * bridge ticket events into the console bell as well.)
14
+ *
15
+ * Renders nothing when no `TicketLiveProvider` is mounted (host didn't
16
+ * opt into ticket realtime) — the cell can be composed unconditionally.
17
+ * Unread state comes exclusively from the provider's single summary map.
18
+ */
19
+
20
+ import React from 'react'
21
+ import { cn } from '../../utils/cn'
22
+ import { LifeBuoyIcon } from '../icons-v2-generated/interface/life-buoy-icon'
23
+ import { useOptionalTicketLive } from '../tickets/ticket-live-provider'
24
+ import { HeaderButton } from './header-button'
25
+
26
+ export interface TicketAlertsButtonProps {
27
+ /** Navigation target for the host's tickets surface (hub `/tickets`,
28
+ * console `/help-center/tickets`). Used when `onClick` is absent. */
29
+ href?: string
30
+ /** Host-provided navigation (e.g. router push). Wins over `href`. */
31
+ onClick?: () => void
32
+ disabled?: boolean
33
+ className?: string
34
+ }
35
+
36
+ export function TicketAlertsButton({ href, onClick, disabled, className }: TicketAlertsButtonProps) {
37
+ const live = useOptionalTicketLive()
38
+ // No provider (host didn't opt in) or signed-out viewer → no cell.
39
+ if (!live || !live.authed) return null
40
+
41
+ const handleClick =
42
+ onClick ??
43
+ (href
44
+ ? () => {
45
+ window.location.assign(href)
46
+ }
47
+ : undefined)
48
+
49
+ return (
50
+ <HeaderButton
51
+ icon={<LifeBuoyIcon className="w-6 h-6" />}
52
+ showUnreadDot={live.unreadTotal > 0}
53
+ aria-label={live.unreadTotal > 0 ? `Support tickets (${live.unreadTotal} unread)` : 'Support tickets'}
54
+ onClick={handleClick}
55
+ disabled={disabled || !handleClick}
56
+ className={cn(className)}
57
+ />
58
+ )
59
+ }
60
+
61
+ export default TicketAlertsButton