@flamingo-stack/openframe-frontend-core 0.0.493 → 0.0.494
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-WPFLV4QJ.js → chunk-4BGDD6CU.js} +4 -4
- package/dist/{chunk-LIRXKEOE.cjs → chunk-7M24CJRH.cjs} +15 -15
- package/dist/{chunk-LIRXKEOE.cjs.map → chunk-7M24CJRH.cjs.map} +1 -1
- package/dist/{chunk-5ICPQAUI.cjs → chunk-CDRNCBLS.cjs} +59 -59
- package/dist/{chunk-5ICPQAUI.cjs.map → chunk-CDRNCBLS.cjs.map} +1 -1
- package/dist/{chunk-FG5ALORQ.js → chunk-DZU3FCOQ.js} +2 -2
- package/dist/{chunk-J2J36PEN.cjs → chunk-E5AIN5SS.cjs} +31 -31
- package/dist/{chunk-J2J36PEN.cjs.map → chunk-E5AIN5SS.cjs.map} +1 -1
- package/dist/{chunk-O6S6Q52W.cjs → chunk-FFL3D5LX.cjs} +26 -26
- package/dist/{chunk-O6S6Q52W.cjs.map → chunk-FFL3D5LX.cjs.map} +1 -1
- package/dist/{chunk-VML66FJQ.cjs → chunk-FP62TQ3A.cjs} +9 -9
- package/dist/{chunk-VML66FJQ.cjs.map → chunk-FP62TQ3A.cjs.map} +1 -1
- package/dist/{chunk-QHRNQTW5.js → chunk-GANUPKHV.js} +2 -2
- package/dist/{chunk-LVDI7R7T.cjs → chunk-J6MC4SY7.cjs} +37 -37
- package/dist/{chunk-LVDI7R7T.cjs.map → chunk-J6MC4SY7.cjs.map} +1 -1
- package/dist/{chunk-U5LU56JE.js → chunk-KDPQGHQ2.js} +2 -2
- package/dist/{chunk-56J7RLMM.js → chunk-KXOSLXL2.js} +1667 -1602
- package/dist/chunk-KXOSLXL2.js.map +1 -0
- package/dist/{chunk-OFFYASUY.cjs → chunk-L5GMHETR.cjs} +26 -26
- package/dist/{chunk-OFFYASUY.cjs.map → chunk-L5GMHETR.cjs.map} +1 -1
- package/dist/{chunk-P3ITKTUE.js → chunk-OQFXEWW2.js} +2 -2
- package/dist/{chunk-2XAAFAQT.cjs → chunk-P77L6MPJ.cjs} +11 -11
- package/dist/{chunk-2XAAFAQT.cjs.map → chunk-P77L6MPJ.cjs.map} +1 -1
- package/dist/{chunk-4J3CB4KO.js → chunk-PBAFZETL.js} +2 -2
- package/dist/{chunk-PI5BMKV3.cjs → chunk-QHGGQZFK.cjs} +62 -62
- package/dist/{chunk-PI5BMKV3.cjs.map → chunk-QHGGQZFK.cjs.map} +1 -1
- package/dist/{chunk-M4SCYPCR.cjs → chunk-QRHAIJMG.cjs} +7 -7
- package/dist/{chunk-M4SCYPCR.cjs.map → chunk-QRHAIJMG.cjs.map} +1 -1
- package/dist/{chunk-4MLOMMDX.cjs → chunk-UCZHGV4E.cjs} +635 -570
- package/dist/chunk-UCZHGV4E.cjs.map +1 -0
- package/dist/{chunk-SZWMJIH3.js → chunk-VCHK2B6W.js} +2 -2
- package/dist/{chunk-5RHJVPL7.js → chunk-VUUIYBIH.js} +4 -4
- package/dist/{chunk-4AWRPTKE.js → chunk-WUM5BTDY.js} +4 -4
- package/dist/{chunk-BTIRYGDT.js → chunk-YQ4B7LCE.js} +4 -4
- package/dist/{chunk-WKWZ7LVC.js → chunk-ZP4QMJX4.js} +2 -2
- package/dist/{chunk-DY6AJNVK.cjs → chunk-ZVCODNW7.cjs} +5 -5
- package/dist/{chunk-DY6AJNVK.cjs.map → chunk-ZVCODNW7.cjs.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/index.cjs +2 -2
- package/dist/components/chat/index.js +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/features/time-tracker/time-tracker-header-button.d.ts.map +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 +132 -130
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +11 -9
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/app-header.d.ts.map +1 -1
- package/dist/components/navigation/header-button.d.ts.map +1 -1
- package/dist/components/navigation/header-global-search.d.ts.map +1 -1
- package/dist/components/navigation/header-mingo-button.d.ts.map +1 -1
- package/dist/components/navigation/header-organization-filter.d.ts.map +1 -1
- package/dist/components/navigation/header-skeleton.d.ts +6 -0
- package/dist/components/navigation/header-skeleton.d.ts.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.map +1 -1
- package/dist/components/navigation/mobile-nav-panel.d.ts.map +1 -1
- package/dist/components/navigation/top-navigation.d.ts +55 -0
- package/dist/components/navigation/top-navigation.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/package.json +1 -1
- package/src/components/features/time-tracker/time-tracker-header-button.tsx +42 -38
- package/src/components/navigation/app-header.tsx +190 -198
- package/src/components/navigation/header-button.tsx +3 -1
- package/src/components/navigation/header-global-search.tsx +3 -1
- package/src/components/navigation/header-mingo-button.tsx +3 -1
- package/src/components/navigation/header-organization-filter.tsx +2 -1
- package/src/components/navigation/header-skeleton.tsx +57 -54
- package/src/components/navigation/header.tsx +231 -208
- package/src/components/navigation/index.ts +3 -0
- package/src/components/navigation/mingo-ai-button.tsx +24 -19
- package/src/components/navigation/mobile-nav-panel.tsx +6 -3
- package/src/components/navigation/top-navigation.tsx +128 -0
- package/src/stories/AppHeader.stories.tsx +148 -0
- package/src/stories/Header.stories.tsx +18 -23
- package/src/stories/TopNavigation.stories.tsx +150 -0
- package/dist/chunk-4MLOMMDX.cjs.map +0 -1
- package/dist/chunk-56J7RLMM.js.map +0 -1
- /package/dist/{chunk-WPFLV4QJ.js.map → chunk-4BGDD6CU.js.map} +0 -0
- /package/dist/{chunk-FG5ALORQ.js.map → chunk-DZU3FCOQ.js.map} +0 -0
- /package/dist/{chunk-QHRNQTW5.js.map → chunk-GANUPKHV.js.map} +0 -0
- /package/dist/{chunk-U5LU56JE.js.map → chunk-KDPQGHQ2.js.map} +0 -0
- /package/dist/{chunk-P3ITKTUE.js.map → chunk-OQFXEWW2.js.map} +0 -0
- /package/dist/{chunk-4J3CB4KO.js.map → chunk-PBAFZETL.js.map} +0 -0
- /package/dist/{chunk-SZWMJIH3.js.map → chunk-VCHK2B6W.js.map} +0 -0
- /package/dist/{chunk-5RHJVPL7.js.map → chunk-VUUIYBIH.js.map} +0 -0
- /package/dist/{chunk-4AWRPTKE.js.map → chunk-WUM5BTDY.js.map} +0 -0
- /package/dist/{chunk-BTIRYGDT.js.map → chunk-YQ4B7LCE.js.map} +0 -0
- /package/dist/{chunk-WKWZ7LVC.js.map → chunk-ZP4QMJX4.js.map} +0 -0
|
@@ -28,8 +28,8 @@ export interface MingoAiButtonProps extends React.ButtonHTMLAttributes<HTMLButto
|
|
|
28
28
|
* not merge them.
|
|
29
29
|
*
|
|
30
30
|
* Deliberately a raw `<button>` rather than the ui-kit `Button`: it needs
|
|
31
|
-
* full-height flush layout, an absolutely-positioned
|
|
32
|
-
* collapse that `Button` cannot express (same precedent as
|
|
31
|
+
* full-height flush cell layout, an absolutely-positioned animated ring, and
|
|
32
|
+
* icon-only collapse that `Button` cannot express (same precedent as
|
|
33
33
|
* `header-mingo-button.tsx`).
|
|
34
34
|
*/
|
|
35
35
|
const MINGO_ACCENT = 'var(--ods-flamingo-cyan-base)'
|
|
@@ -48,23 +48,23 @@ export function MingoAiButton({ source, icon, label = 'Mingo AI', className, onC
|
|
|
48
48
|
onClick?.(e)
|
|
49
49
|
}}
|
|
50
50
|
className={cn(
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
'group/mingo relative flex h-full items-center border-l border-ods-border bg-transparent px-[var(--spacing-system-l)] transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent',
|
|
51
|
+
// Unified ODS top-navigation cell (Figma 2797-6808 desktop /
|
|
52
|
+
// 2797-7275 mobile): full-height cell with a leading divider,
|
|
53
|
+
// transparent at rest so it inherits the bar's background.
|
|
54
|
+
'group/mingo relative flex h-full shrink-0 items-center border-l border-ods-border bg-transparent px-[var(--spacing-system-m)] transition-colors duration-200 hover:bg-ods-bg-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent',
|
|
56
55
|
className,
|
|
57
56
|
)}
|
|
58
57
|
>
|
|
59
|
-
{/* Inner box = 1:1 twin of the
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
{/* Inner box = 1:1 twin of the header CTA's `size="small"` Button
|
|
59
|
+
geometry (rounded-md, h-6 md:h-8) so the animated ring traces
|
|
60
|
+
exactly where a normal small-button border would sit inside the
|
|
61
|
+
48/56px cell. */}
|
|
62
|
+
<span className="relative flex h-6 items-center gap-[var(--spacing-system-xsf)] rounded-md px-[var(--spacing-system-xsf)] md:h-8">
|
|
63
63
|
{/* AI edge light (Apple-Intelligence-style): a rotating accent-
|
|
64
|
-
gradient arc clipped to a 3px ring on the
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
gradient arc clipped to a 3px ring on the box outline via CSS
|
|
65
|
+
mask (.mingo-edge-frame) — NO opaque cover, so the launcher
|
|
66
|
+
inherits whatever background sits behind it. Platform-tinted via
|
|
67
|
+
the accent token. */}
|
|
68
68
|
<span aria-hidden="true" className="mingo-edge-frame pointer-events-none absolute inset-0 rounded-md">
|
|
69
69
|
<span className="mingo-edge" />
|
|
70
70
|
</span>
|
|
@@ -72,7 +72,10 @@ export function MingoAiButton({ source, icon, label = 'Mingo AI', className, onC
|
|
|
72
72
|
<span aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden rounded-md">
|
|
73
73
|
<span
|
|
74
74
|
className="mingo-shimmer absolute inset-y-0 left-0 w-1/2"
|
|
75
|
-
style={{
|
|
75
|
+
style={{
|
|
76
|
+
background:
|
|
77
|
+
'linear-gradient(105deg, transparent, color-mix(in srgb, var(--ods-system-greys-white) 12%, transparent), transparent)',
|
|
78
|
+
}}
|
|
76
79
|
/>
|
|
77
80
|
</span>
|
|
78
81
|
{icon ? (
|
|
@@ -85,9 +88,11 @@ export function MingoAiButton({ source, icon, label = 'Mingo AI', className, onC
|
|
|
85
88
|
className="relative h-6 w-6 shrink-0 text-ods-text-primary"
|
|
86
89
|
/>
|
|
87
90
|
)}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
{/* Wordmark collapses below md — the mobile cell is icon-only per
|
|
92
|
+
spec. `text-code` (sentence-case mono caption, matching the small
|
|
93
|
+
CTA next to it) by product decision — the Figma wordmark is h3
|
|
94
|
+
bold; flagged for the designer. */}
|
|
95
|
+
<span className="relative hidden whitespace-nowrap text-code text-ods-text-primary md:inline">{label}</span>
|
|
91
96
|
</span>
|
|
92
97
|
</button>
|
|
93
98
|
)
|
|
@@ -103,9 +103,12 @@ export function MobileNavPanel({ isOpen, config }: MobileNavPanelProps) {
|
|
|
103
103
|
className={cn(
|
|
104
104
|
"fixed z-[9999] rounded-lg shadow-xl",
|
|
105
105
|
config.className ? "" : "bg-ods-card border border-ods-border",
|
|
106
|
-
// Responsive positioning and sizing
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
// Responsive positioning and sizing — LEFT-anchored from md, under
|
|
107
|
+
// the burger cell that sits at the left edge of the unified
|
|
108
|
+
// top-navigation (it used to be right-anchored when the burger
|
|
109
|
+
// lived on the right).
|
|
110
|
+
"right-2 left-2 md:right-auto",
|
|
111
|
+
"md:left-6 md:w-[400px] md:max-w-[calc(100vw-3rem)]",
|
|
109
112
|
// Small-viewport (svh) sizing anchored below the measured header —
|
|
110
113
|
// 100vh overflowed under mobile browser chrome, leaving the bottom
|
|
111
114
|
// entries and footer unreachable while the body was scroll-locked.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { cn } from '../../utils/cn';
|
|
5
|
+
|
|
6
|
+
export type TopNavigationCenterBreakpoint = 'md' | 'lg';
|
|
7
|
+
|
|
8
|
+
// Literal class maps — Tailwind's scanner needs the full class strings.
|
|
9
|
+
const CENTER_VISIBILITY: Record<TopNavigationCenterBreakpoint, string> = {
|
|
10
|
+
md: 'hidden md:flex',
|
|
11
|
+
lg: 'hidden lg:flex',
|
|
12
|
+
};
|
|
13
|
+
const LOGO_GROW: Record<TopNavigationCenterBreakpoint, string> = {
|
|
14
|
+
md: 'flex-1 md:flex-none',
|
|
15
|
+
lg: 'flex-1 lg:flex-none',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface TopNavigationProps extends React.HTMLAttributes<HTMLElement> {
|
|
19
|
+
/** Leading cells (burger toggle, admin-sidebar toggle). Rendered raw, first
|
|
20
|
+
* in the row — cells carry their own trailing divider
|
|
21
|
+
* (`border-r border-ods-border`), matching the ODS spec where dividers
|
|
22
|
+
* belong to the cells, not the bar. */
|
|
23
|
+
leading?: React.ReactNode;
|
|
24
|
+
/** Logo zone content. Grows (`flex-1`) below `centerBreakpoint`, shrinks to
|
|
25
|
+
* content above it. Wrap in a `<Link>` on the consumer side if clickable. */
|
|
26
|
+
logo?: React.ReactNode;
|
|
27
|
+
/** Extra classes for the logo zone. Defaults follow the ODS spec paddings:
|
|
28
|
+
* `p-m` on mobile, `pl-l` (24px) from md, `pl-xxl` (80px) from lg — the
|
|
29
|
+
* desktop inset mirrors the leading menu-cell width so centered nav links
|
|
30
|
+
* sit visually centered (Figma 2797-6808 / 2805-6194). */
|
|
31
|
+
logoClassName?: string;
|
|
32
|
+
/** Center zone (marketing nav links, console global search). Hidden below
|
|
33
|
+
* `centerBreakpoint`; above it, it is the `flex-1` region that pushes the
|
|
34
|
+
* CTA and side actions to the right edge. Rendered even when empty so the
|
|
35
|
+
* bar always has its spacer. */
|
|
36
|
+
center?: React.ReactNode;
|
|
37
|
+
centerClassName?: string;
|
|
38
|
+
/** Breakpoint at which the center zone appears (and the logo zone stops
|
|
39
|
+
* growing). ODS spec default is `lg` (nav links are desktop-only);
|
|
40
|
+
* the console header uses `md` for its global search. */
|
|
41
|
+
centerBreakpoint?: TopNavigationCenterBreakpoint;
|
|
42
|
+
/** CTA zone — right-aligned, padded, no dividers (per ODS spec). */
|
|
43
|
+
cta?: React.ReactNode;
|
|
44
|
+
ctaClassName?: string;
|
|
45
|
+
/** Trailing action cells (time tracker, notifications, avatar, Mingo).
|
|
46
|
+
* Rendered raw — each cell carries its own leading divider
|
|
47
|
+
* (`border-l border-ods-border`). */
|
|
48
|
+
sideActions?: React.ReactNode;
|
|
49
|
+
/** Opaque ODS background class. Defaults to `bg-ods-card`. Keep it opaque —
|
|
50
|
+
* translucent backgrounds under a sticky bar need a deliberate
|
|
51
|
+
* backdrop-blur treatment (see the note in `header.tsx`). */
|
|
52
|
+
backgroundClassName?: string;
|
|
53
|
+
/** Top border on mobile (the ODS spec bar shows it below md). Default true. */
|
|
54
|
+
mobileTopBorder?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const hasContent = (node: React.ReactNode): boolean => node !== null && node !== undefined && node !== false;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Unified ODS top-navigation bar (Figma `[UPD] top-navigation`, node
|
|
61
|
+
* 2797-5978): the single 48px/56px (mobile / md+) cell-based header shell
|
|
62
|
+
* shared by the console `AppHeader` and the marketing `Header` across all
|
|
63
|
+
* platforms.
|
|
64
|
+
*
|
|
65
|
+
* The shell owns the bar geometry only — height, borders, background, and the
|
|
66
|
+
* zone layout `[leading][logo][center][cta][sideActions]`. What goes inside
|
|
67
|
+
* the zones (and each cell's divider) is the consumer's job, so platforms can
|
|
68
|
+
* differ in background and in which elements they mount without forking the
|
|
69
|
+
* bar itself.
|
|
70
|
+
*/
|
|
71
|
+
export function TopNavigation({
|
|
72
|
+
leading,
|
|
73
|
+
logo,
|
|
74
|
+
logoClassName,
|
|
75
|
+
center,
|
|
76
|
+
centerClassName,
|
|
77
|
+
centerBreakpoint = 'lg',
|
|
78
|
+
cta,
|
|
79
|
+
ctaClassName,
|
|
80
|
+
sideActions,
|
|
81
|
+
backgroundClassName,
|
|
82
|
+
mobileTopBorder = true,
|
|
83
|
+
className,
|
|
84
|
+
children,
|
|
85
|
+
...props
|
|
86
|
+
}: TopNavigationProps) {
|
|
87
|
+
return (
|
|
88
|
+
<header
|
|
89
|
+
className={cn(
|
|
90
|
+
'flex h-12 w-full items-center border-b border-ods-border md:h-14',
|
|
91
|
+
mobileTopBorder && 'border-t md:border-t-0',
|
|
92
|
+
backgroundClassName ?? 'bg-ods-card',
|
|
93
|
+
className,
|
|
94
|
+
)}
|
|
95
|
+
{...props}
|
|
96
|
+
>
|
|
97
|
+
{leading}
|
|
98
|
+
{hasContent(logo) && (
|
|
99
|
+
<div
|
|
100
|
+
className={cn(
|
|
101
|
+
'flex h-full min-w-0 items-center',
|
|
102
|
+
LOGO_GROW[centerBreakpoint],
|
|
103
|
+
'p-[var(--spacing-system-m)] md:py-0 md:pr-0 md:pl-[var(--spacing-system-l)] lg:pl-[var(--spacing-system-xxl)]',
|
|
104
|
+
logoClassName,
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
{logo}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
<div
|
|
111
|
+
className={cn(
|
|
112
|
+
CENTER_VISIBILITY[centerBreakpoint],
|
|
113
|
+
'h-full min-w-0 flex-1 items-center justify-center',
|
|
114
|
+
centerClassName,
|
|
115
|
+
)}
|
|
116
|
+
>
|
|
117
|
+
{center}
|
|
118
|
+
</div>
|
|
119
|
+
{hasContent(cta) && (
|
|
120
|
+
<div className={cn('flex h-full shrink-0 items-center px-[var(--spacing-system-m)]', ctaClassName)}>{cta}</div>
|
|
121
|
+
)}
|
|
122
|
+
{sideActions}
|
|
123
|
+
{children}
|
|
124
|
+
</header>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default TopNavigation;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { fn } from 'storybook/test';
|
|
3
|
+
|
|
4
|
+
import { TimeTrackerProvider } from '../components/features/time-tracker';
|
|
5
|
+
import { AppHeader } from '../components/navigation/app-header';
|
|
6
|
+
|
|
7
|
+
const trackerCallbacks = {
|
|
8
|
+
onSelectedTicketChange: fn(),
|
|
9
|
+
onNotesChange: fn(),
|
|
10
|
+
onStart: fn(),
|
|
11
|
+
onPause: fn(),
|
|
12
|
+
onResume: fn(),
|
|
13
|
+
onCancel: fn(),
|
|
14
|
+
onSubmit: fn(),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const meta = {
|
|
18
|
+
title: 'Navigation/AppHeader',
|
|
19
|
+
component: AppHeader,
|
|
20
|
+
parameters: {
|
|
21
|
+
layout: 'fullscreen',
|
|
22
|
+
docs: {
|
|
23
|
+
description: {
|
|
24
|
+
component:
|
|
25
|
+
'Console top bar built on the unified `TopNavigation` shell (Figma 2797-5978): 48px mobile / 56px md+, ' +
|
|
26
|
+
'cell model with per-cell dividers. Cells left→right: burger + logo (mobile only), global search (md+), ' +
|
|
27
|
+
'organizations filter (lg+), time tracker, notifications, user menu (md+), Mingo AI. ' +
|
|
28
|
+
'Resize the viewport across 800px / 1280px to see the responsive arrangement.',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
tags: ['autodocs'],
|
|
33
|
+
args: {
|
|
34
|
+
isMobileMenuOpen: false,
|
|
35
|
+
onToggleMobileMenu: fn(),
|
|
36
|
+
},
|
|
37
|
+
decorators: [
|
|
38
|
+
Story => (
|
|
39
|
+
<div style={{ minHeight: '50vh', backgroundColor: 'var(--ods-bg)' }}>
|
|
40
|
+
<Story />
|
|
41
|
+
</div>
|
|
42
|
+
),
|
|
43
|
+
],
|
|
44
|
+
} satisfies Meta<typeof AppHeader>;
|
|
45
|
+
|
|
46
|
+
export default meta;
|
|
47
|
+
type Story = StoryObj<typeof meta>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Everything the console mounts: search, organizations filter, notifications,
|
|
51
|
+
* user menu and the Mingo AI launcher.
|
|
52
|
+
*/
|
|
53
|
+
export const Default: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
showSearch: true,
|
|
56
|
+
onSearch: fn(),
|
|
57
|
+
showOrganizations: true,
|
|
58
|
+
organizations: [
|
|
59
|
+
{ id: '1', name: 'Acme Corp' },
|
|
60
|
+
{ id: '2', name: 'Globex' },
|
|
61
|
+
],
|
|
62
|
+
selectedOrgId: '1',
|
|
63
|
+
onOrgChange: fn(),
|
|
64
|
+
showNotifications: true,
|
|
65
|
+
unreadCount: 3,
|
|
66
|
+
showUser: true,
|
|
67
|
+
userName: 'Jane Doe',
|
|
68
|
+
userEmail: 'jane@example.com',
|
|
69
|
+
onProfile: fn(),
|
|
70
|
+
onLogout: fn(),
|
|
71
|
+
showMingoAI: true,
|
|
72
|
+
onMingoAI: fn(),
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Minimal bar — no search or organizations, so the center zone is just the
|
|
78
|
+
* spacer pushing the action cells right (the current OpenFrame FE setup).
|
|
79
|
+
*/
|
|
80
|
+
export const Minimal: Story = {
|
|
81
|
+
args: {
|
|
82
|
+
showNotifications: true,
|
|
83
|
+
showUser: true,
|
|
84
|
+
userName: 'Jane Doe',
|
|
85
|
+
userEmail: 'jane@example.com',
|
|
86
|
+
showMingoAI: true,
|
|
87
|
+
onMingoAI: fn(),
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Idle time tracker: a square icon cell, same footprint as the other cells.
|
|
93
|
+
*/
|
|
94
|
+
export const WithTimeTrackerIdle: Story = {
|
|
95
|
+
args: {
|
|
96
|
+
...Minimal.args,
|
|
97
|
+
showTimeTracker: true,
|
|
98
|
+
},
|
|
99
|
+
render: args => (
|
|
100
|
+
<TimeTrackerProvider
|
|
101
|
+
status="ready"
|
|
102
|
+
ticketOptions={[]}
|
|
103
|
+
selectedTicketId={null}
|
|
104
|
+
notes=""
|
|
105
|
+
lastEntries={[]}
|
|
106
|
+
{...trackerCallbacks}
|
|
107
|
+
>
|
|
108
|
+
<AppHeader {...args} />
|
|
109
|
+
</TimeTrackerProvider>
|
|
110
|
+
),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Active time tracker per the ODS spec: on mobile the cell keeps its square
|
|
115
|
+
* icon-only footprint (accent icon, no digits); from md up it becomes a fixed
|
|
116
|
+
* 144px cell with the live mono clock.
|
|
117
|
+
*/
|
|
118
|
+
export const WithTimeTrackerActive: Story = {
|
|
119
|
+
args: {
|
|
120
|
+
...Minimal.args,
|
|
121
|
+
showTimeTracker: true,
|
|
122
|
+
},
|
|
123
|
+
render: args => (
|
|
124
|
+
<TimeTrackerProvider
|
|
125
|
+
status="tracking"
|
|
126
|
+
runningSince={Date.now() - 4_000}
|
|
127
|
+
accumulatedMs={0}
|
|
128
|
+
ticketOptions={[]}
|
|
129
|
+
selectedTicketId={null}
|
|
130
|
+
notes=""
|
|
131
|
+
lastEntries={[]}
|
|
132
|
+
{...trackerCallbacks}
|
|
133
|
+
>
|
|
134
|
+
<AppHeader {...args} />
|
|
135
|
+
</TimeTrackerProvider>
|
|
136
|
+
),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Disabled (subscription-lock) state: every cell is dimmed and inert except
|
|
141
|
+
* the mobile burger toggle.
|
|
142
|
+
*/
|
|
143
|
+
export const Disabled: Story = {
|
|
144
|
+
args: {
|
|
145
|
+
...Default.args,
|
|
146
|
+
disabled: true,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
@@ -10,9 +10,7 @@ import type { HeaderConfig } from '../types/navigation';
|
|
|
10
10
|
const flamingoLogo = (
|
|
11
11
|
<div className="flex items-center gap-2">
|
|
12
12
|
<FlamingoLogo className="h-8 w-8" />
|
|
13
|
-
<span className="font-['DM_Sans'] font-bold text-[18px] text-ods-text-primary">
|
|
14
|
-
Flamingo
|
|
15
|
-
</span>
|
|
13
|
+
<span className="font-['DM_Sans'] font-bold text-[18px] text-ods-text-primary">Flamingo</span>
|
|
16
14
|
</div>
|
|
17
15
|
);
|
|
18
16
|
|
|
@@ -62,13 +60,15 @@ const meta = {
|
|
|
62
60
|
docs: {
|
|
63
61
|
description: {
|
|
64
62
|
component:
|
|
65
|
-
'
|
|
63
|
+
'Marketing/site header built on the unified `TopNavigation` shell (Figma `[UPD] top-navigation`, node 2797-5978): ' +
|
|
64
|
+
'48px mobile / 56px md+ bar, cell model, burger as a leading cell below `lg`, nav links centered from `lg`. ' +
|
|
65
|
+
'Dropdown children are **always rendered in the DOM** — toggled via `visibility`/`opacity` — so their `<a href>` links are present in the initial HTML for SEO crawlers (no more "orphan" pages). Hidden anchors are automatically excluded from tab order and the accessibility tree via `visibility: hidden` + `aria-hidden`.',
|
|
66
66
|
},
|
|
67
67
|
},
|
|
68
68
|
},
|
|
69
69
|
tags: ['autodocs'],
|
|
70
70
|
decorators: [
|
|
71
|
-
|
|
71
|
+
Story => (
|
|
72
72
|
<div style={{ minHeight: '100vh', backgroundColor: 'var(--ods-bg)' }}>
|
|
73
73
|
<Story />
|
|
74
74
|
<div style={{ padding: '2rem', color: 'var(--ods-text-primary)' }}>
|
|
@@ -114,12 +114,10 @@ export const DropdownOpen: Story = {
|
|
|
114
114
|
mobile: { enabled: true, onToggle: fn() },
|
|
115
115
|
} satisfies HeaderConfig,
|
|
116
116
|
},
|
|
117
|
-
render:
|
|
117
|
+
render: args => {
|
|
118
118
|
useEffect(() => {
|
|
119
119
|
// Simulate user click on the first dropdown trigger after mount
|
|
120
|
-
const trigger = document.querySelector<HTMLButtonElement>(
|
|
121
|
-
'nav button'
|
|
122
|
-
);
|
|
120
|
+
const trigger = document.querySelector<HTMLButtonElement>('nav button');
|
|
123
121
|
trigger?.click();
|
|
124
122
|
}, []);
|
|
125
123
|
return <Header {...args} />;
|
|
@@ -142,15 +140,13 @@ export const SeoLinksAlwaysInDom: Story = {
|
|
|
142
140
|
mobile: { enabled: true, onToggle: fn() },
|
|
143
141
|
} satisfies HeaderConfig,
|
|
144
142
|
},
|
|
145
|
-
render:
|
|
143
|
+
render: args => {
|
|
146
144
|
const [linkHrefs, setLinkHrefs] = useState<string[]>([]);
|
|
147
145
|
|
|
148
146
|
useEffect(() => {
|
|
149
147
|
const collect = () => {
|
|
150
|
-
const anchors = document.querySelectorAll<HTMLAnchorElement>(
|
|
151
|
-
|
|
152
|
-
);
|
|
153
|
-
setLinkHrefs(Array.from(anchors, (a) => a.getAttribute('href') || ''));
|
|
148
|
+
const anchors = document.querySelectorAll<HTMLAnchorElement>('header nav a[href]');
|
|
149
|
+
setLinkHrefs(Array.from(anchors, a => a.getAttribute('href') || ''));
|
|
154
150
|
};
|
|
155
151
|
collect();
|
|
156
152
|
const interval = window.setInterval(collect, 500);
|
|
@@ -169,8 +165,8 @@ export const SeoLinksAlwaysInDom: Story = {
|
|
|
169
165
|
}}
|
|
170
166
|
>
|
|
171
167
|
<p style={{ marginBottom: 12 }}>
|
|
172
|
-
<strong>{linkHrefs.length}</strong> anchor(s) currently in the
|
|
173
|
-
|
|
168
|
+
<strong>{linkHrefs.length}</strong> anchor(s) currently in the header DOM (visible to crawlers regardless of
|
|
169
|
+
dropdown state):
|
|
174
170
|
</p>
|
|
175
171
|
<ul style={{ lineHeight: 1.8 }}>
|
|
176
172
|
{linkHrefs.map((href, i) => (
|
|
@@ -199,12 +195,12 @@ export const FixedHeader: Story = {
|
|
|
199
195
|
};
|
|
200
196
|
|
|
201
197
|
/**
|
|
202
|
-
* Header with the Mingo AI launcher enabled via `config.mingo`.
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
198
|
+
* Header with the Mingo AI launcher enabled via `config.mingo`. Per the unified
|
|
199
|
+
* top-navigation spec the burger is a **leading** cell (left edge, below `lg`),
|
|
200
|
+
* so the bar reads `hamburger | logo … Try for Free | Mingo AI`, with Mingo
|
|
201
|
+
* full-height and flush to the right edge (its left border acts as the
|
|
202
|
+
* divider). Resize under 800px to see the Mingo label collapse to icon-only.
|
|
203
|
+
* Clicking it dispatches an `ask-ai:open` event.
|
|
208
204
|
*/
|
|
209
205
|
export const WithMingoAi: Story = {
|
|
210
206
|
args: {
|
|
@@ -221,7 +217,6 @@ export const WithMingoAi: Story = {
|
|
|
221
217
|
autoHide: false,
|
|
222
218
|
mobile: { enabled: true, onToggle: fn() },
|
|
223
219
|
mingo: { enabled: true, source: 'flamingo' },
|
|
224
|
-
className: 'px-0',
|
|
225
220
|
} satisfies HeaderConfig,
|
|
226
221
|
},
|
|
227
222
|
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { fn } from 'storybook/test';
|
|
3
|
+
|
|
4
|
+
import { OpenFrameLogo, OpenFrameText } from '../components/icons';
|
|
5
|
+
import { ClockHistoryIcon } from '../components/icons-v2-generated/date-and-time/clock-history-icon';
|
|
6
|
+
import { BellIcon } from '../components/icons-v2-generated/interface/bell-icon';
|
|
7
|
+
import { HeaderButton } from '../components/navigation/header-button';
|
|
8
|
+
import { HeaderMingoButton } from '../components/navigation/header-mingo-button';
|
|
9
|
+
import { TopNavigation } from '../components/navigation/top-navigation';
|
|
10
|
+
import { Button } from '../components/ui/button';
|
|
11
|
+
|
|
12
|
+
const logo = (
|
|
13
|
+
<>
|
|
14
|
+
<OpenFrameLogo
|
|
15
|
+
className="h-6 w-6 shrink-0"
|
|
16
|
+
upperPathColor="var(--color-text-primary)"
|
|
17
|
+
lowerPathColor="var(--color-accent-primary)"
|
|
18
|
+
/>
|
|
19
|
+
<OpenFrameText textColor="var(--color-text-primary)" className="h-4" />
|
|
20
|
+
</>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const navLinks = (
|
|
24
|
+
<nav className="flex items-center gap-2" aria-label="Main navigation">
|
|
25
|
+
{['OpenFrame', 'OpenMSP', 'Resources', 'Pricing'].map(label => (
|
|
26
|
+
<Button key={label} variant="transparent" size="small-legacy" className="text-h3 font-bold tracking-[-0.36px]">
|
|
27
|
+
{label}
|
|
28
|
+
</Button>
|
|
29
|
+
))}
|
|
30
|
+
</nav>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const burgerCell = (
|
|
34
|
+
<HeaderButton
|
|
35
|
+
className="border-r border-ods-border lg:hidden"
|
|
36
|
+
aria-label="Open menu"
|
|
37
|
+
onClick={fn()}
|
|
38
|
+
icon={
|
|
39
|
+
<svg className="h-4 w-4 md:h-6 md:w-6" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
40
|
+
<path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
|
41
|
+
</svg>
|
|
42
|
+
}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const sideActionCells = (
|
|
47
|
+
<>
|
|
48
|
+
<HeaderButton
|
|
49
|
+
className="border-l border-ods-border"
|
|
50
|
+
aria-label="Time tracker"
|
|
51
|
+
icon={<ClockHistoryIcon className="h-4 w-4 md:h-6 md:w-6" />}
|
|
52
|
+
/>
|
|
53
|
+
<HeaderButton
|
|
54
|
+
className="border-l border-ods-border"
|
|
55
|
+
aria-label="Notifications"
|
|
56
|
+
icon={<BellIcon className="h-4 w-4 md:h-6 md:w-6" />}
|
|
57
|
+
/>
|
|
58
|
+
<HeaderMingoButton className="border-l border-ods-border" onClick={fn()} />
|
|
59
|
+
</>
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const meta = {
|
|
63
|
+
title: 'Navigation/TopNavigation',
|
|
64
|
+
component: TopNavigation,
|
|
65
|
+
parameters: {
|
|
66
|
+
layout: 'fullscreen',
|
|
67
|
+
docs: {
|
|
68
|
+
description: {
|
|
69
|
+
component:
|
|
70
|
+
'Unified ODS top-navigation shell (Figma `[UPD] top-navigation`, node 2797-5978). ' +
|
|
71
|
+
'Owns the bar geometry only — 48px mobile / 56px md+ height, top/bottom borders, background — and the zone layout ' +
|
|
72
|
+
'`[leading][logo][center][cta][sideActions]`. Dividers belong to the cells (`border-l` / `border-r`), not the bar. ' +
|
|
73
|
+
'Consumers: the console `AppHeader` (centerBreakpoint `md`, global search in the center zone) and the marketing ' +
|
|
74
|
+
'`Header` (centerBreakpoint `lg`, nav links in the center zone). Resize the viewport to see the tablet burger and ' +
|
|
75
|
+
'the mobile arrangement.',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
tags: ['autodocs'],
|
|
80
|
+
argTypes: {
|
|
81
|
+
centerBreakpoint: { control: 'radio', options: ['md', 'lg'] },
|
|
82
|
+
backgroundClassName: { control: 'text' },
|
|
83
|
+
mobileTopBorder: { control: 'boolean' },
|
|
84
|
+
},
|
|
85
|
+
decorators: [
|
|
86
|
+
Story => (
|
|
87
|
+
<div style={{ minHeight: '50vh', backgroundColor: 'var(--ods-bg)' }}>
|
|
88
|
+
<Story />
|
|
89
|
+
</div>
|
|
90
|
+
),
|
|
91
|
+
],
|
|
92
|
+
} satisfies Meta<typeof TopNavigation>;
|
|
93
|
+
|
|
94
|
+
export default meta;
|
|
95
|
+
type Story = StoryObj<typeof meta>;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The full marketing arrangement per the Figma spec: burger (below `lg`),
|
|
99
|
+
* logo (grows below `lg`), centered nav links (from `lg`), CTA and the
|
|
100
|
+
* side-action cells. Resize across 800px / 1280px to see all three device
|
|
101
|
+
* variants.
|
|
102
|
+
*/
|
|
103
|
+
export const MarketingFull: Story = {
|
|
104
|
+
args: {
|
|
105
|
+
leading: burgerCell,
|
|
106
|
+
logo,
|
|
107
|
+
logoClassName: 'gap-2',
|
|
108
|
+
center: navLinks,
|
|
109
|
+
cta: (
|
|
110
|
+
<Button variant="accent" size="small" onClick={fn()}>
|
|
111
|
+
Try for Free
|
|
112
|
+
</Button>
|
|
113
|
+
),
|
|
114
|
+
sideActions: sideActionCells,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Console-style arrangement: no logo/nav (the app sidebar owns branding),
|
|
120
|
+
* `centerBreakpoint="md"` so the center zone (here just the spacer) starts at
|
|
121
|
+
* 800px, side-action cells flush right. This is the shape `AppHeader` renders.
|
|
122
|
+
*/
|
|
123
|
+
export const ConsoleShape: Story = {
|
|
124
|
+
args: {
|
|
125
|
+
centerBreakpoint: 'md',
|
|
126
|
+
sideActions: sideActionCells,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Per-platform background override — platforms may run the bar on `bg-ods-bg`
|
|
132
|
+
* instead of the default `bg-ods-card`. Keep the override opaque.
|
|
133
|
+
*/
|
|
134
|
+
export const CustomBackground: Story = {
|
|
135
|
+
args: {
|
|
136
|
+
...MarketingFull.args,
|
|
137
|
+
backgroundClassName: 'bg-ods-bg',
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Logo-only bar (e.g. the `openframe` platform in the hub): every optional
|
|
143
|
+
* zone omitted — the logo zone grows and nothing else renders.
|
|
144
|
+
*/
|
|
145
|
+
export const LogoOnly: Story = {
|
|
146
|
+
args: {
|
|
147
|
+
logo,
|
|
148
|
+
logoClassName: 'gap-2',
|
|
149
|
+
},
|
|
150
|
+
};
|