@flamingo-stack/openframe-frontend-core 0.0.415 → 0.0.416-snapshot.20260712013500

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 (112) hide show
  1. package/dist/{chunk-LAGZ56MO.cjs → chunk-2IJX5XDG.cjs} +11 -11
  2. package/dist/{chunk-LAGZ56MO.cjs.map → chunk-2IJX5XDG.cjs.map} +1 -1
  3. package/dist/{chunk-24IQYYNK.cjs → chunk-3R7XBEUB.cjs} +26 -26
  4. package/dist/{chunk-24IQYYNK.cjs.map → chunk-3R7XBEUB.cjs.map} +1 -1
  5. package/dist/{chunk-VENX4SF4.cjs → chunk-42KMUJFK.cjs} +26 -26
  6. package/dist/{chunk-VENX4SF4.cjs.map → chunk-42KMUJFK.cjs.map} +1 -1
  7. package/dist/{chunk-SUOPU3OE.cjs → chunk-53LGRFM2.cjs} +444 -369
  8. package/dist/chunk-53LGRFM2.cjs.map +1 -0
  9. package/dist/{chunk-47FXB4DR.js → chunk-6OZPER3T.js} +2 -2
  10. package/dist/{chunk-CCEQDC7F.js → chunk-BIRJP6QA.js} +1471 -1396
  11. package/dist/chunk-BIRJP6QA.js.map +1 -0
  12. package/dist/{chunk-MHOKOUKE.js → chunk-E3AB36EG.js} +4 -4
  13. package/dist/{chunk-NM4WUXPO.cjs → chunk-F3U6OH3K.cjs} +31 -31
  14. package/dist/{chunk-NM4WUXPO.cjs.map → chunk-F3U6OH3K.cjs.map} +1 -1
  15. package/dist/{chunk-XAVAHIZR.cjs → chunk-F4HV6IBG.cjs} +62 -62
  16. package/dist/{chunk-XAVAHIZR.cjs.map → chunk-F4HV6IBG.cjs.map} +1 -1
  17. package/dist/{chunk-57IHEHW3.cjs → chunk-GGTNMXGZ.cjs} +5 -5
  18. package/dist/{chunk-57IHEHW3.cjs.map → chunk-GGTNMXGZ.cjs.map} +1 -1
  19. package/dist/{chunk-ND4AYD6P.cjs → chunk-H3HZMHN3.cjs} +37 -37
  20. package/dist/{chunk-ND4AYD6P.cjs.map → chunk-H3HZMHN3.cjs.map} +1 -1
  21. package/dist/{chunk-F5US4NUC.js → chunk-IEAQBLZI.js} +4 -4
  22. package/dist/{chunk-2MEM6QO4.cjs → chunk-KGYUVJJ5.cjs} +15 -15
  23. package/dist/{chunk-2MEM6QO4.cjs.map → chunk-KGYUVJJ5.cjs.map} +1 -1
  24. package/dist/{chunk-XUWZHO32.js → chunk-MHUT2NR4.js} +2 -2
  25. package/dist/{chunk-ZE5CY2H2.cjs → chunk-MOOMQTZ5.cjs} +9 -9
  26. package/dist/{chunk-ZE5CY2H2.cjs.map → chunk-MOOMQTZ5.cjs.map} +1 -1
  27. package/dist/{chunk-I6LSGR3V.js → chunk-P7FJIB7V.js} +2 -2
  28. package/dist/{chunk-NG6F7TM4.cjs → chunk-S52UK6KQ.cjs} +7 -7
  29. package/dist/{chunk-NG6F7TM4.cjs.map → chunk-S52UK6KQ.cjs.map} +1 -1
  30. package/dist/{chunk-EB4USXFD.js → chunk-SIF5WN7S.js} +4 -4
  31. package/dist/{chunk-OTUNYR26.cjs → chunk-T6VFIDS4.cjs} +59 -59
  32. package/dist/{chunk-OTUNYR26.cjs.map → chunk-T6VFIDS4.cjs.map} +1 -1
  33. package/dist/{chunk-YQBAYZZV.js → chunk-UL5RGRWZ.js} +2 -2
  34. package/dist/{chunk-P7NC3H2Y.js → chunk-WTJ4AOVR.js} +2 -2
  35. package/dist/{chunk-VVAAUQKZ.js → chunk-Y37BRIBI.js} +4 -4
  36. package/dist/{chunk-2HBGX2VN.js → chunk-Y4ZQTUBT.js} +2 -2
  37. package/dist/{chunk-IKWWXX4V.js → chunk-YX2YP4YM.js} +2 -2
  38. package/dist/components/announcement-bar.d.ts.map +1 -1
  39. package/dist/components/case-studies/index.cjs +8 -8
  40. package/dist/components/case-studies/index.js +2 -2
  41. package/dist/components/chat/chat-panel-header-mobile.d.ts +8 -8
  42. package/dist/components/chat/chat-panel-header-mobile.d.ts.map +1 -1
  43. package/dist/components/chat/index.cjs +2 -2
  44. package/dist/components/chat/index.js +1 -1
  45. package/dist/components/chat/utils/is-cross-origin-url.d.ts.map +1 -1
  46. package/dist/components/contact/index.cjs +3 -3
  47. package/dist/components/contact/index.js +2 -2
  48. package/dist/components/docs/index.cjs +5 -5
  49. package/dist/components/docs/index.js +4 -4
  50. package/dist/components/embeds/index.cjs +3 -3
  51. package/dist/components/embeds/index.js +2 -2
  52. package/dist/components/faq/index.cjs +3 -3
  53. package/dist/components/faq/index.js +2 -2
  54. package/dist/components/features/index.cjs +2 -2
  55. package/dist/components/features/index.js +1 -1
  56. package/dist/components/help-center-pages/index.cjs +20 -20
  57. package/dist/components/help-center-pages/index.js +11 -11
  58. package/dist/components/index.cjs +131 -129
  59. package/dist/components/index.cjs.map +1 -1
  60. package/dist/components/index.js +12 -10
  61. package/dist/components/index.js.map +1 -1
  62. package/dist/components/navigation/header.d.ts.map +1 -1
  63. package/dist/components/navigation/index.cjs +4 -2
  64. package/dist/components/navigation/index.cjs.map +1 -1
  65. package/dist/components/navigation/index.d.ts +2 -0
  66. package/dist/components/navigation/index.d.ts.map +1 -1
  67. package/dist/components/navigation/index.js +3 -1
  68. package/dist/components/navigation/mingo-ai-button.d.ts +16 -0
  69. package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -0
  70. package/dist/components/onboarding-guides/index.cjs +5 -5
  71. package/dist/components/onboarding-guides/index.js +4 -4
  72. package/dist/components/related-content/index.cjs +3 -3
  73. package/dist/components/related-content/index.js +2 -2
  74. package/dist/components/tickets/index.cjs +5 -5
  75. package/dist/components/tickets/index.js +4 -4
  76. package/dist/components/ui/index.cjs +2 -2
  77. package/dist/components/ui/index.js +1 -1
  78. package/dist/index.cjs +4 -2
  79. package/dist/index.cjs.map +1 -1
  80. package/dist/index.js +3 -1
  81. package/dist/types/navigation.d.ts +12 -0
  82. package/dist/types/navigation.d.ts.map +1 -1
  83. package/dist/utils/index.cjs +1 -0
  84. package/dist/utils/index.cjs.map +1 -1
  85. package/dist/utils/index.js +1 -0
  86. package/dist/utils/index.js.map +1 -1
  87. package/package.json +1 -1
  88. package/src/components/announcement-bar.tsx +5 -1
  89. package/src/components/chat/chat-panel-header-mobile.tsx +21 -8
  90. package/src/components/chat/embeddable-chat.tsx +13 -0
  91. package/src/components/chat/utils/__tests__/is-cross-origin-url.test.ts +43 -0
  92. package/src/components/chat/utils/is-cross-origin-url.ts +4 -0
  93. package/src/components/navigation/header.tsx +25 -10
  94. package/src/components/navigation/index.ts +3 -0
  95. package/src/components/navigation/mingo-ai-button.tsx +98 -0
  96. package/src/stories/Header.stories.tsx +28 -0
  97. package/src/stories/MingoAiButton.stories.tsx +46 -0
  98. package/src/styles/chat-animations.css +54 -0
  99. package/src/types/navigation.ts +12 -0
  100. package/dist/chunk-CCEQDC7F.js.map +0 -1
  101. package/dist/chunk-SUOPU3OE.cjs.map +0 -1
  102. /package/dist/{chunk-47FXB4DR.js.map → chunk-6OZPER3T.js.map} +0 -0
  103. /package/dist/{chunk-MHOKOUKE.js.map → chunk-E3AB36EG.js.map} +0 -0
  104. /package/dist/{chunk-F5US4NUC.js.map → chunk-IEAQBLZI.js.map} +0 -0
  105. /package/dist/{chunk-XUWZHO32.js.map → chunk-MHUT2NR4.js.map} +0 -0
  106. /package/dist/{chunk-I6LSGR3V.js.map → chunk-P7FJIB7V.js.map} +0 -0
  107. /package/dist/{chunk-EB4USXFD.js.map → chunk-SIF5WN7S.js.map} +0 -0
  108. /package/dist/{chunk-YQBAYZZV.js.map → chunk-UL5RGRWZ.js.map} +0 -0
  109. /package/dist/{chunk-P7NC3H2Y.js.map → chunk-WTJ4AOVR.js.map} +0 -0
  110. /package/dist/{chunk-VVAAUQKZ.js.map → chunk-Y37BRIBI.js.map} +0 -0
  111. /package/dist/{chunk-2HBGX2VN.js.map → chunk-Y4ZQTUBT.js.map} +0 -0
  112. /package/dist/{chunk-IKWWXX4V.js.map → chunk-YX2YP4YM.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.415",
3
+ "version": "0.0.416-snapshot.20260712013500",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -175,7 +175,11 @@ export function AnnouncementBar({
175
175
  role="region"
176
176
  aria-label="Announcement"
177
177
  aria-hidden={!expanded}
178
- data-announcement-bar
178
+ // The marker means "I am the PAGE-CHROME bar" — `useHeaderHeight` sums
179
+ // the first match with the header to offset fixed panels (e.g. the
180
+ // sliding admin sidebar). Admin PREVIEW instances must NOT carry it, or
181
+ // a preview card inflates that offset (phantom gap above the drawer).
182
+ {...(previewMode ? {} : { 'data-announcement-bar': true })}
179
183
  className={`relative w-full grid transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none ${previewMode ? '' : 'z-50'} ${className ?? ''}`}
180
184
  style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}
181
185
  >
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { Chevron02LeftIcon, ClockHistoryIcon } from '../icons-v2-generated'
3
+ import { Chevron02LeftIcon, ClockHistoryIcon, XmarkIcon } from '../icons-v2-generated'
4
4
  import { Button } from '../ui/button'
5
5
  import { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu'
6
6
  import { cn } from '../../utils/cn'
@@ -19,14 +19,14 @@ export interface ChatPanelHeaderMobileProps extends ChatPanelHeaderProps {
19
19
  * bottom-aligns under a tall header).
20
20
  * - Title: `text-h2` (vs the desktop `h3`/`h4`), truncated.
21
21
  * - Actions: 44px outlined icon buttons (`Button variant="outline"
22
- * size="icon"`) — a back chevron (when applicable) and a single trailing
23
- * `⋯` menu carrying the view-dependent actions. There is no Close action
24
- * (the drawer is dismissed via its backdrop / the back chevron); when no
25
- * action applies the `⋯` trigger is hidden entirely.
22
+ * size="icon"`) — a back chevron (when applicable), a trailing `⋯` menu
23
+ * carrying the view-dependent actions (hidden when no action applies),
24
+ * and an X Close. The X is REQUIRED here: on mobile the drawer is
25
+ * full-screen (`w-screen`), so there is no visible backdrop to tap —
26
+ * without it the panel cannot be dismissed at all.
26
27
  *
27
- * Same prop contract as `ChatPanelHeader` (`onClose` is accepted for parity but
28
- * unused here); the orchestrator renders this on mobile and the desktop bar on
29
- * `md+`.
28
+ * Same prop contract as `ChatPanelHeader`; the orchestrator renders this on
29
+ * mobile and the desktop bar on `md+`.
30
30
  */
31
31
  export function ChatPanelHeaderMobile({
32
32
  showBack = false,
@@ -34,6 +34,7 @@ export function ChatPanelHeaderMobile({
34
34
  backAriaLabel = 'Back',
35
35
  isArchivedView = false,
36
36
  onBack,
37
+ onClose,
37
38
  onRestore,
38
39
  onRename,
39
40
  onArchive,
@@ -87,6 +88,18 @@ export function ChatPanelHeaderMobile({
87
88
  triggerClassName="shrink-0 bg-ods-card border-ods-border hover:bg-ods-bg-hover flex items-center justify-center focus-visible:ring-0"
88
89
  />
89
90
  )}
91
+
92
+ {onClose && (
93
+ <Button
94
+ variant="outline"
95
+ size="icon"
96
+ aria-label="Close chat"
97
+ onClick={onClose}
98
+ className="shrink-0"
99
+ >
100
+ <XmarkIcon />
101
+ </Button>
102
+ )}
90
103
  </div>
91
104
  </div>
92
105
  </div>
@@ -1520,6 +1520,19 @@ function EmbeddableChatInner({
1520
1520
  return () => window.removeEventListener('ask-ai:open-with-ref', handler)
1521
1521
  }, [source, discussRef, setIsOpen])
1522
1522
 
1523
+ // Listen for plain "open chat" events (no row context). Fired by the
1524
+ // header MingoAiButton. Same strict source filter as `ask-ai:open-with-ref`
1525
+ // above: events without a matching source are ignored.
1526
+ useEffect(() => {
1527
+ const handler = (e: Event) => {
1528
+ const detail = (e as CustomEvent<{ source?: string }>).detail
1529
+ if (!detail || detail.source !== source) return
1530
+ setIsOpen(true)
1531
+ }
1532
+ window.addEventListener('ask-ai:open', handler)
1533
+ return () => window.removeEventListener('ask-ai:open', handler)
1534
+ }, [source, setIsOpen])
1535
+
1523
1536
  const hasMessages = messages.length > 0
1524
1537
  // First dialog page in flight and nothing cached yet — we don't yet know if
1525
1538
  // the user is new or returning, so the Mingo empty state shows a skeleton
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { isCrossOriginUrl } from '../is-cross-origin-url'
3
+ import { decideNewTab } from '../decide-new-tab'
4
+
5
+ describe('isCrossOriginUrl', () => {
6
+ it('treats falsy values as same-origin', () => {
7
+ expect(isCrossOriginUrl(null)).toBe(false)
8
+ expect(isCrossOriginUrl(undefined)).toBe(false)
9
+ expect(isCrossOriginUrl('')).toBe(false)
10
+ })
11
+
12
+ it('treats same-document references as same-origin', () => {
13
+ // Regression: bare-hash anchors ("More coming in Next Generations" on
14
+ // /pricing) were classified cross-origin and opened in a new tab.
15
+ expect(isCrossOriginUrl('#next-generations')).toBe(false)
16
+ expect(isCrossOriginUrl('?tab=pricing')).toBe(false)
17
+ })
18
+
19
+ it('treats rooted relative paths as same-origin', () => {
20
+ expect(isCrossOriginUrl('/support')).toBe(false)
21
+ expect(isCrossOriginUrl('/roadmap#top')).toBe(false)
22
+ })
23
+
24
+ it('treats explicit-host URLs as cross-origin', () => {
25
+ expect(isCrossOriginUrl('https://flamingo.run/support')).toBe(true)
26
+ expect(isCrossOriginUrl('http://example.com')).toBe(true)
27
+ expect(isCrossOriginUrl('//cdn.example.com/file.js')).toBe(true)
28
+ })
29
+ })
30
+
31
+ describe('decideNewTab fallback (no targetPlatform)', () => {
32
+ it('keeps same-page hash CTAs in the same tab', () => {
33
+ expect(
34
+ decideNewTab({ href: '#next-generations', currentSource: 'flamingo' }),
35
+ ).toBe(false)
36
+ })
37
+
38
+ it('still opens explicit-host URLs in a new tab', () => {
39
+ expect(
40
+ decideNewTab({ href: 'https://example.com', currentSource: 'flamingo' }),
41
+ ).toBe(true)
42
+ })
43
+ })
@@ -20,6 +20,10 @@
20
20
  */
21
21
  export function isCrossOriginUrl(url: string | null | undefined): boolean {
22
22
  if (!url) return false
23
+ // Same-document references — a bare fragment (`#anchor`) or query
24
+ // (`?tab=x`) resolves against the current page and can never leave
25
+ // the origin.
26
+ if (url.startsWith('#') || url.startsWith('?')) return false
23
27
  // Relative URL — by definition same origin.
24
28
  if (url.startsWith('/') && !url.startsWith('//')) return false
25
29
  // Anything else — absolute URL (http(s)://) or protocol-relative (//)
@@ -7,6 +7,7 @@ import { cn } from '../../utils'
7
7
  import { Button } from '../ui/button'
8
8
  import { Menu01Icon } from '../icons-v2-generated'
9
9
  import { MOBILE_NAV_PANEL_ID } from './mobile-nav-panel'
10
+ import { MingoAiButton } from './mingo-ai-button'
10
11
 
11
12
  export interface HeaderProps {
12
13
  config: HeaderConfig
@@ -273,9 +274,12 @@ export function Header({ config, platform }: HeaderProps) {
273
274
  >
274
275
  <header
275
276
  className={cn(
276
- "w-full flex items-center justify-between",
277
+ // 72px = unified-header spec height (Figma 4033-90260); the right
278
+ // cluster self-stretches so the Mingo launcher can sit flush.
279
+ "w-full h-[72px] flex items-center justify-between",
277
280
  "border-b border-ods-border backdrop-blur-sm",
278
- "px-6 py-3",
281
+ "pl-6",
282
+ !config.mingo?.enabled && "pr-6",
279
283
  // Background color (configurable via backgroundColor prop)
280
284
  config.backgroundColor || "bg-ods-card",
281
285
  config.className
@@ -297,13 +301,13 @@ export function Header({ config, platform }: HeaderProps) {
297
301
 
298
302
  {/* Center: Navigation */}
299
303
  {config.navigation && config.navigation.items.length > 0 && (
300
- <nav
304
+ <nav
301
305
  className={cn(
302
- "hidden md:flex items-center gap-2",
306
+ "hidden lg:flex items-center gap-2",
303
307
  config.navigation.position === 'center' && "absolute left-1/2 transform -translate-x-1/2",
304
308
  config.navigation.position === 'right' && "ml-auto mr-4"
305
309
  )}
306
- role="navigation"
310
+ role="navigation"
307
311
  aria-label="Main navigation"
308
312
  >
309
313
  {config.navigation.items.map(renderNavigationItem)}
@@ -311,20 +315,27 @@ export function Header({ config, platform }: HeaderProps) {
311
315
  )}
312
316
 
313
317
  {/* Right: Actions */}
314
- <div className="flex items-center justify-end gap-4 flex-shrink-0">
315
- {/* Desktop Actions */}
318
+ <div className="flex items-center justify-end gap-3 flex-shrink-0 self-stretch">
319
+ {/* Desktop Actions — banded with the nav/burger breakpoint (lg) so the
320
+ desktop right-cluster and the mobile toggle never co-show. */}
316
321
  {config.actions?.right && (
317
- <div className="hidden md:flex items-center gap-4">
322
+ <div className="hidden lg:flex items-center gap-3">
318
323
  {config.actions.right}
319
324
  </div>
320
325
  )}
321
326
 
327
+ {config.actions?.persistent && config.actions.persistent.length > 0 && (
328
+ <div className="flex items-center">
329
+ {config.actions.persistent}
330
+ </div>
331
+ )}
332
+
322
333
  {/* Mobile Menu Toggle */}
323
334
  {config.mobile && config.mobile.enabled && (
324
335
  <Button
325
- variant="transparent"
336
+ variant="outline"
326
337
  size="icon"
327
- className="flex md:hidden"
338
+ className="flex lg:hidden"
328
339
  onClick={() => {
329
340
  config.mobile?.onToggle?.()
330
341
  }}
@@ -336,6 +347,10 @@ export function Header({ config, platform }: HeaderProps) {
336
347
  leftIcon={config.mobile?.menuIcon || <Menu01Icon />}
337
348
  />
338
349
  )}
350
+
351
+ {config.mingo?.enabled && (
352
+ <MingoAiButton source={config.mingo.source} icon={config.mingo.icon} label={config.mingo.label} className={config.mingo.className} />
353
+ )}
339
354
  </div>
340
355
  </header>
341
356
  </div>
@@ -4,6 +4,9 @@
4
4
  export { Header } from './header'
5
5
  export type { HeaderConfig, HeaderProps } from './header'
6
6
 
7
+ export { MingoAiButton } from './mingo-ai-button'
8
+ export type { MingoAiButtonProps } from './mingo-ai-button'
9
+
7
10
  export { ClientOnlyHeader } from './client-only-header'
8
11
  export type { ClientOnlyHeaderProps } from './client-only-header'
9
12
 
@@ -0,0 +1,98 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import { MingoIcon } from '../icons'
5
+ import { cn } from '../../utils'
6
+
7
+ export interface MingoAiButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
8
+ source?: string
9
+ /** The platform's Mingo identity glyph — pass the SAME server-configured
10
+ * icon the chat panel renders (host-side: `EntityIcon` fed by the admin
11
+ * `assistantIcon`), so the launcher and the panel can never diverge.
12
+ * Falls back to the packaged Mingo mark when the server has none. */
13
+ icon?: React.ReactNode
14
+ /** The launcher's wordmark + aria-label — pass the server-configured
15
+ * assistant name (same `assistantName` the chat panel shows) so the
16
+ * launcher never hardcodes an identity the admin has renamed. */
17
+ label?: string
18
+ }
19
+
20
+ /**
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.
25
+ *
26
+ * Distinct from `header-mingo-button.tsx` (`HeaderMingoButton`), the
27
+ * dashboard/AppHeader controlled toggle; different surface and contract, do
28
+ * not merge them.
29
+ *
30
+ * Deliberately a raw `<button>` rather than the ui-kit `Button`: it needs
31
+ * full-height flush layout, an absolutely-positioned glow child, and icon-only
32
+ * collapse that `Button` cannot express (same precedent as
33
+ * `header-mingo-button.tsx`).
34
+ */
35
+ const MINGO_ACCENT = 'var(--ods-flamingo-cyan-base)'
36
+
37
+ export function MingoAiButton({ source, icon, label = 'Mingo AI', className, onClick, ...props }: MingoAiButtonProps) {
38
+ return (
39
+ <button
40
+ {...props}
41
+ type="button"
42
+ aria-label={label}
43
+ 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 ?? '' } }))
48
+ onClick?.(e)
49
+ }}
50
+ className={cn(
51
+ // `--spacing-system-l` is itself responsive (16px, 24px from 800px):
52
+ // the compact mobile padding keeps the 375px header row
53
+ // (logo + CTA + burger + Mingo) inside one viewport per Figma
54
+ // 3924-35639 (mobile) / 4033-90260 (desktop).
55
+ 'group/mingo relative flex h-full items-center border-l border-ods-border bg-ods-card px-[var(--spacing-system-l)] transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent',
56
+ className,
57
+ )}
58
+ >
59
+ {/* Inner box = 1:1 twin of the ui-kit Button geometry the header CTA
60
+ uses (rounded-md, h-10 md:h-12, px m) so the animated ring traces
61
+ EXACTLY where a normal button border would sit. */}
62
+ <span className="relative flex h-10 items-center gap-[var(--spacing-system-s)] rounded-md px-[var(--spacing-system-m)] md:h-12">
63
+ {/* AI edge light (Apple-Intelligence-style): a rotating accent-
64
+ gradient arc revealed as a 3px ring on the Button-shaped outline,
65
+ platform-tinted via the accent token. */}
66
+ <span aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden rounded-md">
67
+ <span className="mingo-edge" />
68
+ </span>
69
+ {/* Interior cover: reveals the rotating gradient ONLY as the ring
70
+ band; radius = rounded-md minus the 3px band so the inner curve
71
+ stays concentric. Must match the button surface (bg-ods-card). */}
72
+ <span aria-hidden="true" className="pointer-events-none absolute inset-[3px] rounded-[3px] bg-ods-card" />
73
+ {/* One-shot light-streak shimmer on hover, clipped to the box. */}
74
+ <span aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden rounded-md">
75
+ <span
76
+ className="mingo-shimmer absolute inset-y-0 left-0 w-1/2"
77
+ style={{ background: 'linear-gradient(105deg, transparent, color-mix(in srgb, var(--ods-system-greys-white) 12%, transparent), transparent)' }}
78
+ />
79
+ </span>
80
+ {icon ? (
81
+ <span className="relative inline-flex size-6 shrink-0 items-center justify-center">{icon}</span>
82
+ ) : (
83
+ <MingoIcon
84
+ color="currentColor"
85
+ eyesColor={MINGO_ACCENT}
86
+ cornerColor={MINGO_ACCENT}
87
+ className="relative h-6 w-6 shrink-0 text-ods-text-primary"
88
+ />
89
+ )}
90
+ <span className="relative hidden whitespace-nowrap text-h3 font-bold tracking-[-0.36px] text-ods-text-primary md:inline">
91
+ {label}
92
+ </span>
93
+ </span>
94
+ </button>
95
+ )
96
+ }
97
+
98
+ export default MingoAiButton
@@ -197,3 +197,31 @@ export const FixedHeader: Story = {
197
197
  } satisfies HeaderConfig,
198
198
  },
199
199
  };
200
+
201
+ /**
202
+ * Header with the Mingo AI launcher enabled via `config.mingo`. `MingoAiButton`
203
+ * renders after the mobile toggle, so below `lg` the right cluster reads
204
+ * `Try for Free | hamburger | Mingo AI`; at `lg+` the hamburger is hidden and it
205
+ * collapses to `Try for Free | Mingo AI`, with Mingo full-height and flush to the
206
+ * right edge (its left border acts as the divider). Resize under 800px to see the
207
+ * Mingo label collapse to icon-only. Clicking it dispatches an `ask-ai:open` event.
208
+ */
209
+ export const WithMingoAi: Story = {
210
+ args: {
211
+ config: {
212
+ logo: { element: flamingoLogo, href: '/' },
213
+ navigation: baseNavigation,
214
+ actions: {
215
+ persistent: [
216
+ <Button key="cta" variant="accent" href="/signup">
217
+ Try for Free
218
+ </Button>,
219
+ ],
220
+ },
221
+ autoHide: false,
222
+ mobile: { enabled: true, onToggle: fn() },
223
+ mingo: { enabled: true, source: 'flamingo' },
224
+ className: 'px-0',
225
+ } satisfies HeaderConfig,
226
+ },
227
+ };
@@ -0,0 +1,46 @@
1
+ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
2
+ import { MingoAiButton } from '../components/navigation/mingo-ai-button'
3
+
4
+ /**
5
+ * Marketing-header Mingo AI launcher. Full-height and flush inside the 72px
6
+ * unified header: the decorator reproduces that context so the left border
7
+ * divider and traveling accent edge light render as they do in `Header` (`config.mingo`).
8
+ * The "Mingo AI" label hides below `md` (icon-only, see IconOnly).
9
+ */
10
+ const meta = {
11
+ title: 'Navigation/MingoAiButton',
12
+ component: MingoAiButton,
13
+ parameters: {
14
+ layout: 'centered',
15
+ },
16
+ tags: ['autodocs'],
17
+ argTypes: {
18
+ source: { control: 'text' },
19
+ },
20
+ decorators: [
21
+ (Story) => (
22
+ <div className="flex h-[72px] items-center justify-end border-b border-ods-border bg-ods-card w-[360px]">
23
+ <Story />
24
+ </div>
25
+ ),
26
+ ],
27
+ } satisfies Meta<typeof MingoAiButton>
28
+
29
+ export default meta
30
+ type Story = StoryObj<typeof meta>
31
+
32
+ export const Default: Story = {
33
+ args: {
34
+ source: 'flamingo',
35
+ },
36
+ }
37
+
38
+ /** Below `md` the wordmark is hidden and only the Mingo icon shows. */
39
+ export const IconOnly: Story = {
40
+ args: {
41
+ source: 'flamingo',
42
+ },
43
+ parameters: {
44
+ viewport: { defaultViewport: 'mobile1' },
45
+ },
46
+ }
@@ -63,3 +63,57 @@
63
63
  .mingo-chat-content[data-state="open"] { animation-name: mingo-chat-slide-in-right; }
64
64
  .mingo-chat-content[data-state="closed"] { animation-name: mingo-chat-slide-out-right; }
65
65
  }
66
+
67
+ /* =============================================================================
68
+ * Mingo AI header button — the 2026 AI edge-light pattern (Apple Intelligence
69
+ * glow-edge / Gemini gradient language): a thin accent-gradient arc TRAVELS
70
+ * around the button's border. No blurry blobs. Transform-only rotation on an
71
+ * oversized conic layer (compositor-friendly); an inset cover reveals only
72
+ * the edge. Reduced motion: rotation stops → a static gradient edge (the
73
+ * colorful state survives, only motion is removed). Hover adds a one-shot
74
+ * light-streak shimmer.
75
+ * ---------------------------------------------------------------------------*/
76
+ @keyframes mingo-edge-spin {
77
+ from { transform: translate(-50%, -50%) rotate(0deg); }
78
+ to { transform: translate(-50%, -50%) rotate(360deg); }
79
+ }
80
+
81
+ .mingo-edge {
82
+ position: absolute;
83
+ left: 50%;
84
+ top: 50%;
85
+ width: 300%;
86
+ aspect-ratio: 1;
87
+ transform: translate(-50%, -50%);
88
+ /* Single luminous accent arc with a soft tail — platform-tinted via the
89
+ RAW --color-* accent var (the --ods-* alias does not exist in raw CSS). */
90
+ background: conic-gradient(
91
+ from 0deg,
92
+ transparent 0deg,
93
+ color-mix(in srgb, var(--color-accent-primary) 40%, transparent) 50deg,
94
+ var(--color-accent-primary) 90deg,
95
+ color-mix(in srgb, var(--color-accent-primary) 45%, white) 105deg,
96
+ transparent 160deg
97
+ );
98
+ animation: mingo-edge-spin 4s linear infinite;
99
+ }
100
+
101
+ @keyframes mingo-shimmer {
102
+ from { transform: translateX(-160%) skewX(-18deg); }
103
+ to { transform: translateX(260%) skewX(-18deg); }
104
+ }
105
+
106
+ .mingo-shimmer {
107
+ opacity: 0;
108
+ transform: translateX(-160%) skewX(-18deg);
109
+ }
110
+
111
+ .group\/mingo:hover .mingo-shimmer {
112
+ opacity: 1;
113
+ animation: mingo-shimmer 0.9s ease-out;
114
+ }
115
+
116
+ @media (prefers-reduced-motion: reduce) {
117
+ .mingo-edge { animation: none; }
118
+ .group\/mingo:hover .mingo-shimmer { animation: none; opacity: 0; }
119
+ }
@@ -36,6 +36,7 @@ export interface HeaderConfig {
36
36
  actions?: {
37
37
  left?: React.ReactNode[]
38
38
  right?: React.ReactNode[]
39
+ persistent?: React.ReactNode[]
39
40
  }
40
41
  mobile?: {
41
42
  enabled: boolean
@@ -44,6 +45,17 @@ export interface HeaderConfig {
44
45
  onToggle?: () => void
45
46
  isOpen?: boolean
46
47
  }
48
+ mingo?: {
49
+ enabled?: boolean
50
+ source?: string
51
+ className?: string
52
+ /** Server-configured Mingo identity glyph (same EntityIcon the chat
53
+ * panel renders); omit to use the packaged fallback mark. */
54
+ icon?: React.ReactNode
55
+ /** Server-configured assistant name for the wordmark/aria-label; omit
56
+ * for the default "Mingo AI". */
57
+ label?: string
58
+ }
47
59
  className?: string
48
60
  style?: React.CSSProperties
49
61
  autoHide?: boolean