@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.
- package/dist/{chunk-LAGZ56MO.cjs → chunk-2IJX5XDG.cjs} +11 -11
- package/dist/{chunk-LAGZ56MO.cjs.map → chunk-2IJX5XDG.cjs.map} +1 -1
- package/dist/{chunk-24IQYYNK.cjs → chunk-3R7XBEUB.cjs} +26 -26
- package/dist/{chunk-24IQYYNK.cjs.map → chunk-3R7XBEUB.cjs.map} +1 -1
- package/dist/{chunk-VENX4SF4.cjs → chunk-42KMUJFK.cjs} +26 -26
- package/dist/{chunk-VENX4SF4.cjs.map → chunk-42KMUJFK.cjs.map} +1 -1
- package/dist/{chunk-SUOPU3OE.cjs → chunk-53LGRFM2.cjs} +444 -369
- package/dist/chunk-53LGRFM2.cjs.map +1 -0
- package/dist/{chunk-47FXB4DR.js → chunk-6OZPER3T.js} +2 -2
- package/dist/{chunk-CCEQDC7F.js → chunk-BIRJP6QA.js} +1471 -1396
- package/dist/chunk-BIRJP6QA.js.map +1 -0
- package/dist/{chunk-MHOKOUKE.js → chunk-E3AB36EG.js} +4 -4
- package/dist/{chunk-NM4WUXPO.cjs → chunk-F3U6OH3K.cjs} +31 -31
- package/dist/{chunk-NM4WUXPO.cjs.map → chunk-F3U6OH3K.cjs.map} +1 -1
- package/dist/{chunk-XAVAHIZR.cjs → chunk-F4HV6IBG.cjs} +62 -62
- package/dist/{chunk-XAVAHIZR.cjs.map → chunk-F4HV6IBG.cjs.map} +1 -1
- package/dist/{chunk-57IHEHW3.cjs → chunk-GGTNMXGZ.cjs} +5 -5
- package/dist/{chunk-57IHEHW3.cjs.map → chunk-GGTNMXGZ.cjs.map} +1 -1
- package/dist/{chunk-ND4AYD6P.cjs → chunk-H3HZMHN3.cjs} +37 -37
- package/dist/{chunk-ND4AYD6P.cjs.map → chunk-H3HZMHN3.cjs.map} +1 -1
- package/dist/{chunk-F5US4NUC.js → chunk-IEAQBLZI.js} +4 -4
- package/dist/{chunk-2MEM6QO4.cjs → chunk-KGYUVJJ5.cjs} +15 -15
- package/dist/{chunk-2MEM6QO4.cjs.map → chunk-KGYUVJJ5.cjs.map} +1 -1
- package/dist/{chunk-XUWZHO32.js → chunk-MHUT2NR4.js} +2 -2
- package/dist/{chunk-ZE5CY2H2.cjs → chunk-MOOMQTZ5.cjs} +9 -9
- package/dist/{chunk-ZE5CY2H2.cjs.map → chunk-MOOMQTZ5.cjs.map} +1 -1
- package/dist/{chunk-I6LSGR3V.js → chunk-P7FJIB7V.js} +2 -2
- package/dist/{chunk-NG6F7TM4.cjs → chunk-S52UK6KQ.cjs} +7 -7
- package/dist/{chunk-NG6F7TM4.cjs.map → chunk-S52UK6KQ.cjs.map} +1 -1
- package/dist/{chunk-EB4USXFD.js → chunk-SIF5WN7S.js} +4 -4
- package/dist/{chunk-OTUNYR26.cjs → chunk-T6VFIDS4.cjs} +59 -59
- package/dist/{chunk-OTUNYR26.cjs.map → chunk-T6VFIDS4.cjs.map} +1 -1
- package/dist/{chunk-YQBAYZZV.js → chunk-UL5RGRWZ.js} +2 -2
- package/dist/{chunk-P7NC3H2Y.js → chunk-WTJ4AOVR.js} +2 -2
- package/dist/{chunk-VVAAUQKZ.js → chunk-Y37BRIBI.js} +4 -4
- package/dist/{chunk-2HBGX2VN.js → chunk-Y4ZQTUBT.js} +2 -2
- package/dist/{chunk-IKWWXX4V.js → chunk-YX2YP4YM.js} +2 -2
- package/dist/components/announcement-bar.d.ts.map +1 -1
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/chat-panel-header-mobile.d.ts +8 -8
- package/dist/components/chat/chat-panel-header-mobile.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/chat/utils/is-cross-origin-url.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/help-center-pages/index.cjs +20 -20
- package/dist/components/help-center-pages/index.js +11 -11
- package/dist/components/index.cjs +131 -129
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +12 -10
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/header.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +4 -2
- package/dist/components/navigation/index.cjs.map +1 -1
- package/dist/components/navigation/index.d.ts +2 -0
- package/dist/components/navigation/index.d.ts.map +1 -1
- package/dist/components/navigation/index.js +3 -1
- package/dist/components/navigation/mingo-ai-button.d.ts +16 -0
- package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -0
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +5 -5
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/index.cjs +2 -2
- package/dist/components/ui/index.js +1 -1
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -1
- package/dist/types/navigation.d.ts +12 -0
- package/dist/types/navigation.d.ts.map +1 -1
- package/dist/utils/index.cjs +1 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/announcement-bar.tsx +5 -1
- package/src/components/chat/chat-panel-header-mobile.tsx +21 -8
- package/src/components/chat/embeddable-chat.tsx +13 -0
- package/src/components/chat/utils/__tests__/is-cross-origin-url.test.ts +43 -0
- package/src/components/chat/utils/is-cross-origin-url.ts +4 -0
- package/src/components/navigation/header.tsx +25 -10
- package/src/components/navigation/index.ts +3 -0
- package/src/components/navigation/mingo-ai-button.tsx +98 -0
- package/src/stories/Header.stories.tsx +28 -0
- package/src/stories/MingoAiButton.stories.tsx +46 -0
- package/src/styles/chat-animations.css +54 -0
- package/src/types/navigation.ts +12 -0
- package/dist/chunk-CCEQDC7F.js.map +0 -1
- package/dist/chunk-SUOPU3OE.cjs.map +0 -1
- /package/dist/{chunk-47FXB4DR.js.map → chunk-6OZPER3T.js.map} +0 -0
- /package/dist/{chunk-MHOKOUKE.js.map → chunk-E3AB36EG.js.map} +0 -0
- /package/dist/{chunk-F5US4NUC.js.map → chunk-IEAQBLZI.js.map} +0 -0
- /package/dist/{chunk-XUWZHO32.js.map → chunk-MHUT2NR4.js.map} +0 -0
- /package/dist/{chunk-I6LSGR3V.js.map → chunk-P7FJIB7V.js.map} +0 -0
- /package/dist/{chunk-EB4USXFD.js.map → chunk-SIF5WN7S.js.map} +0 -0
- /package/dist/{chunk-YQBAYZZV.js.map → chunk-UL5RGRWZ.js.map} +0 -0
- /package/dist/{chunk-P7NC3H2Y.js.map → chunk-WTJ4AOVR.js.map} +0 -0
- /package/dist/{chunk-VVAAUQKZ.js.map → chunk-Y37BRIBI.js.map} +0 -0
- /package/dist/{chunk-2HBGX2VN.js.map → chunk-Y4ZQTUBT.js.map} +0 -0
- /package/dist/{chunk-IKWWXX4V.js.map → chunk-YX2YP4YM.js.map} +0 -0
package/package.json
CHANGED
|
@@ -175,7 +175,11 @@ export function AnnouncementBar({
|
|
|
175
175
|
role="region"
|
|
176
176
|
aria-label="Announcement"
|
|
177
177
|
aria-hidden={!expanded}
|
|
178
|
-
|
|
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)
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
|
28
|
-
*
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
|
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-
|
|
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
|
|
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="
|
|
336
|
+
variant="outline"
|
|
326
337
|
size="icon"
|
|
327
|
-
className="flex
|
|
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
|
+
}
|
package/src/types/navigation.ts
CHANGED
|
@@ -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
|