@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.
Files changed (115) hide show
  1. package/dist/{chunk-WPFLV4QJ.js → chunk-4BGDD6CU.js} +4 -4
  2. package/dist/{chunk-LIRXKEOE.cjs → chunk-7M24CJRH.cjs} +15 -15
  3. package/dist/{chunk-LIRXKEOE.cjs.map → chunk-7M24CJRH.cjs.map} +1 -1
  4. package/dist/{chunk-5ICPQAUI.cjs → chunk-CDRNCBLS.cjs} +59 -59
  5. package/dist/{chunk-5ICPQAUI.cjs.map → chunk-CDRNCBLS.cjs.map} +1 -1
  6. package/dist/{chunk-FG5ALORQ.js → chunk-DZU3FCOQ.js} +2 -2
  7. package/dist/{chunk-J2J36PEN.cjs → chunk-E5AIN5SS.cjs} +31 -31
  8. package/dist/{chunk-J2J36PEN.cjs.map → chunk-E5AIN5SS.cjs.map} +1 -1
  9. package/dist/{chunk-O6S6Q52W.cjs → chunk-FFL3D5LX.cjs} +26 -26
  10. package/dist/{chunk-O6S6Q52W.cjs.map → chunk-FFL3D5LX.cjs.map} +1 -1
  11. package/dist/{chunk-VML66FJQ.cjs → chunk-FP62TQ3A.cjs} +9 -9
  12. package/dist/{chunk-VML66FJQ.cjs.map → chunk-FP62TQ3A.cjs.map} +1 -1
  13. package/dist/{chunk-QHRNQTW5.js → chunk-GANUPKHV.js} +2 -2
  14. package/dist/{chunk-LVDI7R7T.cjs → chunk-J6MC4SY7.cjs} +37 -37
  15. package/dist/{chunk-LVDI7R7T.cjs.map → chunk-J6MC4SY7.cjs.map} +1 -1
  16. package/dist/{chunk-U5LU56JE.js → chunk-KDPQGHQ2.js} +2 -2
  17. package/dist/{chunk-56J7RLMM.js → chunk-KXOSLXL2.js} +1667 -1602
  18. package/dist/chunk-KXOSLXL2.js.map +1 -0
  19. package/dist/{chunk-OFFYASUY.cjs → chunk-L5GMHETR.cjs} +26 -26
  20. package/dist/{chunk-OFFYASUY.cjs.map → chunk-L5GMHETR.cjs.map} +1 -1
  21. package/dist/{chunk-P3ITKTUE.js → chunk-OQFXEWW2.js} +2 -2
  22. package/dist/{chunk-2XAAFAQT.cjs → chunk-P77L6MPJ.cjs} +11 -11
  23. package/dist/{chunk-2XAAFAQT.cjs.map → chunk-P77L6MPJ.cjs.map} +1 -1
  24. package/dist/{chunk-4J3CB4KO.js → chunk-PBAFZETL.js} +2 -2
  25. package/dist/{chunk-PI5BMKV3.cjs → chunk-QHGGQZFK.cjs} +62 -62
  26. package/dist/{chunk-PI5BMKV3.cjs.map → chunk-QHGGQZFK.cjs.map} +1 -1
  27. package/dist/{chunk-M4SCYPCR.cjs → chunk-QRHAIJMG.cjs} +7 -7
  28. package/dist/{chunk-M4SCYPCR.cjs.map → chunk-QRHAIJMG.cjs.map} +1 -1
  29. package/dist/{chunk-4MLOMMDX.cjs → chunk-UCZHGV4E.cjs} +635 -570
  30. package/dist/chunk-UCZHGV4E.cjs.map +1 -0
  31. package/dist/{chunk-SZWMJIH3.js → chunk-VCHK2B6W.js} +2 -2
  32. package/dist/{chunk-5RHJVPL7.js → chunk-VUUIYBIH.js} +4 -4
  33. package/dist/{chunk-4AWRPTKE.js → chunk-WUM5BTDY.js} +4 -4
  34. package/dist/{chunk-BTIRYGDT.js → chunk-YQ4B7LCE.js} +4 -4
  35. package/dist/{chunk-WKWZ7LVC.js → chunk-ZP4QMJX4.js} +2 -2
  36. package/dist/{chunk-DY6AJNVK.cjs → chunk-ZVCODNW7.cjs} +5 -5
  37. package/dist/{chunk-DY6AJNVK.cjs.map → chunk-ZVCODNW7.cjs.map} +1 -1
  38. package/dist/components/case-studies/index.cjs +8 -8
  39. package/dist/components/case-studies/index.js +2 -2
  40. package/dist/components/chat/index.cjs +2 -2
  41. package/dist/components/chat/index.js +1 -1
  42. package/dist/components/contact/index.cjs +3 -3
  43. package/dist/components/contact/index.js +2 -2
  44. package/dist/components/docs/index.cjs +5 -5
  45. package/dist/components/docs/index.js +4 -4
  46. package/dist/components/embeds/index.cjs +3 -3
  47. package/dist/components/embeds/index.js +2 -2
  48. package/dist/components/faq/index.cjs +3 -3
  49. package/dist/components/faq/index.js +2 -2
  50. package/dist/components/features/index.cjs +2 -2
  51. package/dist/components/features/index.js +1 -1
  52. package/dist/components/features/time-tracker/time-tracker-header-button.d.ts.map +1 -1
  53. package/dist/components/help-center-pages/index.cjs +20 -20
  54. package/dist/components/help-center-pages/index.js +11 -11
  55. package/dist/components/index.cjs +132 -130
  56. package/dist/components/index.cjs.map +1 -1
  57. package/dist/components/index.js +11 -9
  58. package/dist/components/index.js.map +1 -1
  59. package/dist/components/navigation/app-header.d.ts.map +1 -1
  60. package/dist/components/navigation/header-button.d.ts.map +1 -1
  61. package/dist/components/navigation/header-global-search.d.ts.map +1 -1
  62. package/dist/components/navigation/header-mingo-button.d.ts.map +1 -1
  63. package/dist/components/navigation/header-organization-filter.d.ts.map +1 -1
  64. package/dist/components/navigation/header-skeleton.d.ts +6 -0
  65. package/dist/components/navigation/header-skeleton.d.ts.map +1 -1
  66. package/dist/components/navigation/header.d.ts.map +1 -1
  67. package/dist/components/navigation/index.cjs +4 -2
  68. package/dist/components/navigation/index.cjs.map +1 -1
  69. package/dist/components/navigation/index.d.ts +2 -0
  70. package/dist/components/navigation/index.d.ts.map +1 -1
  71. package/dist/components/navigation/index.js +3 -1
  72. package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
  73. package/dist/components/navigation/mobile-nav-panel.d.ts.map +1 -1
  74. package/dist/components/navigation/top-navigation.d.ts +55 -0
  75. package/dist/components/navigation/top-navigation.d.ts.map +1 -0
  76. package/dist/components/onboarding-guides/index.cjs +5 -5
  77. package/dist/components/onboarding-guides/index.js +4 -4
  78. package/dist/components/related-content/index.cjs +3 -3
  79. package/dist/components/related-content/index.js +2 -2
  80. package/dist/components/tickets/index.cjs +5 -5
  81. package/dist/components/tickets/index.js +4 -4
  82. package/dist/components/ui/index.cjs +2 -2
  83. package/dist/components/ui/index.js +1 -1
  84. package/dist/index.cjs +4 -2
  85. package/dist/index.cjs.map +1 -1
  86. package/dist/index.js +3 -1
  87. package/package.json +1 -1
  88. package/src/components/features/time-tracker/time-tracker-header-button.tsx +42 -38
  89. package/src/components/navigation/app-header.tsx +190 -198
  90. package/src/components/navigation/header-button.tsx +3 -1
  91. package/src/components/navigation/header-global-search.tsx +3 -1
  92. package/src/components/navigation/header-mingo-button.tsx +3 -1
  93. package/src/components/navigation/header-organization-filter.tsx +2 -1
  94. package/src/components/navigation/header-skeleton.tsx +57 -54
  95. package/src/components/navigation/header.tsx +231 -208
  96. package/src/components/navigation/index.ts +3 -0
  97. package/src/components/navigation/mingo-ai-button.tsx +24 -19
  98. package/src/components/navigation/mobile-nav-panel.tsx +6 -3
  99. package/src/components/navigation/top-navigation.tsx +128 -0
  100. package/src/stories/AppHeader.stories.tsx +148 -0
  101. package/src/stories/Header.stories.tsx +18 -23
  102. package/src/stories/TopNavigation.stories.tsx +150 -0
  103. package/dist/chunk-4MLOMMDX.cjs.map +0 -1
  104. package/dist/chunk-56J7RLMM.js.map +0 -1
  105. /package/dist/{chunk-WPFLV4QJ.js.map → chunk-4BGDD6CU.js.map} +0 -0
  106. /package/dist/{chunk-FG5ALORQ.js.map → chunk-DZU3FCOQ.js.map} +0 -0
  107. /package/dist/{chunk-QHRNQTW5.js.map → chunk-GANUPKHV.js.map} +0 -0
  108. /package/dist/{chunk-U5LU56JE.js.map → chunk-KDPQGHQ2.js.map} +0 -0
  109. /package/dist/{chunk-P3ITKTUE.js.map → chunk-OQFXEWW2.js.map} +0 -0
  110. /package/dist/{chunk-4J3CB4KO.js.map → chunk-PBAFZETL.js.map} +0 -0
  111. /package/dist/{chunk-SZWMJIH3.js.map → chunk-VCHK2B6W.js.map} +0 -0
  112. /package/dist/{chunk-5RHJVPL7.js.map → chunk-VUUIYBIH.js.map} +0 -0
  113. /package/dist/{chunk-4AWRPTKE.js.map → chunk-WUM5BTDY.js.map} +0 -0
  114. /package/dist/{chunk-BTIRYGDT.js.map → chunk-YQ4B7LCE.js.map} +0 -0
  115. /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 glow child, and icon-only
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
- // `--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-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 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">
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 Button-shaped outline
65
- via CSS mask (.mingo-edge-frame) — NO opaque cover, so the
66
- launcher inherits whatever background sits behind it. Platform-
67
- tinted via the accent token. */}
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={{ background: 'linear-gradient(105deg, transparent, color-mix(in srgb, var(--ods-system-greys-white) 12%, transparent), transparent)' }}
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
- <span className="relative hidden whitespace-nowrap text-h3 font-bold tracking-[-0.36px] text-ods-text-primary md:inline">
89
- {label}
90
- </span>
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
- "right-2 left-2 md:left-auto",
108
- "md:right-6 md:w-[400px] md:max-w-[calc(100vw-3rem)]",
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
- 'Top navigation header with dropdown menus. 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`.',
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
- (Story) => (
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: (args) => {
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: (args) => {
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
- 'header nav a[href]'
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
- header DOM (visible to crawlers regardless of dropdown state):
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`. `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.
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
+ };