@flamingo-stack/openframe-frontend-core 0.0.303-snapshot.20260622160827 → 0.0.303

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 (116) hide show
  1. package/dist/{chunk-RNF2E736.cjs → chunk-3GYV6RP7.cjs} +10 -11
  2. package/dist/chunk-3GYV6RP7.cjs.map +1 -0
  3. package/dist/{chunk-BMNGBMSN.cjs → chunk-4JK3ZFMD.cjs} +26 -26
  4. package/dist/{chunk-BMNGBMSN.cjs.map → chunk-4JK3ZFMD.cjs.map} +1 -1
  5. package/dist/{chunk-A25ZI7HO.cjs → chunk-6D53QFBF.cjs} +12 -12
  6. package/dist/{chunk-A25ZI7HO.cjs.map → chunk-6D53QFBF.cjs.map} +1 -1
  7. package/dist/{chunk-INZOAK77.js → chunk-FG4XA6NA.js} +2 -2
  8. package/dist/{chunk-X5N6ANEO.js → chunk-JEHWEKWA.js} +3 -4
  9. package/dist/{chunk-X5N6ANEO.js.map → chunk-JEHWEKWA.js.map} +1 -1
  10. package/dist/{chunk-7KTSRZI4.cjs → chunk-KZGSVTRP.cjs} +543 -475
  11. package/dist/chunk-KZGSVTRP.cjs.map +1 -0
  12. package/dist/{chunk-UAJAJFI6.cjs → chunk-LTDGGKOW.cjs} +50 -44
  13. package/dist/chunk-LTDGGKOW.cjs.map +1 -0
  14. package/dist/{chunk-YV73VRRY.js → chunk-OFLTDHC2.js} +2 -2
  15. package/dist/{chunk-JO6EUJGU.js → chunk-OZSU6S6U.js} +27 -21
  16. package/dist/chunk-OZSU6S6U.js.map +1 -0
  17. package/dist/{chunk-2X4HTRQ4.js → chunk-R2NVJUOB.js} +22 -10
  18. package/dist/chunk-R2NVJUOB.js.map +1 -0
  19. package/dist/{chunk-Y2D2RJQX.js → chunk-SLRDPGGS.js} +2762 -2694
  20. package/dist/chunk-SLRDPGGS.js.map +1 -0
  21. package/dist/{chunk-O4TIFKDG.cjs → chunk-SYTHAQRP.cjs} +7 -7
  22. package/dist/{chunk-O4TIFKDG.cjs.map → chunk-SYTHAQRP.cjs.map} +1 -1
  23. package/dist/{chunk-45DC5AJC.js → chunk-UMYWG5C3.js} +2 -2
  24. package/dist/{chunk-MZRNARMO.cjs → chunk-UNWVMS3E.cjs} +37 -37
  25. package/dist/{chunk-MZRNARMO.cjs.map → chunk-UNWVMS3E.cjs.map} +1 -1
  26. package/dist/{chunk-DOYOOBP4.js → chunk-VAH46QQO.js} +2 -2
  27. package/dist/{chunk-FVLEE7YZ.cjs → chunk-YKP7UXWT.cjs} +35 -23
  28. package/dist/chunk-YKP7UXWT.cjs.map +1 -0
  29. package/dist/{chunk-Z7322A4A.cjs → chunk-ZCXABON3.cjs} +5 -5
  30. package/dist/{chunk-Z7322A4A.cjs.map → chunk-ZCXABON3.cjs.map} +1 -1
  31. package/dist/{chunk-ZXIM2DJM.js → chunk-ZRSS67EY.js} +2 -2
  32. package/dist/components/case-studies/index.cjs +8 -8
  33. package/dist/components/case-studies/index.js +2 -2
  34. package/dist/components/chat/hooks/use-chat-identity.d.ts +1 -7
  35. package/dist/components/chat/hooks/use-chat-identity.d.ts.map +1 -1
  36. package/dist/components/chat/hooks/use-empty-state-config.d.ts.map +1 -1
  37. package/dist/components/chat/hooks/use-slash-commands.d.ts.map +1 -1
  38. package/dist/components/chat/index.cjs +2 -2
  39. package/dist/components/chat/index.js +1 -1
  40. package/dist/components/contact/index.cjs +3 -3
  41. package/dist/components/contact/index.js +2 -2
  42. package/dist/components/docs/doc-viewer.d.ts +4 -3
  43. package/dist/components/docs/doc-viewer.d.ts.map +1 -1
  44. package/dist/components/docs/index.cjs +5 -5
  45. package/dist/components/docs/index.js +4 -4
  46. package/dist/components/docs/use-docs-resolve-link.d.ts.map +1 -1
  47. package/dist/components/docs/use-document-tree.d.ts.map +1 -1
  48. package/dist/components/embeds/index.cjs +3 -3
  49. package/dist/components/embeds/index.js +2 -2
  50. package/dist/components/faq/index.cjs +3 -3
  51. package/dist/components/faq/index.js +2 -2
  52. package/dist/components/features/index.cjs +2 -2
  53. package/dist/components/features/index.js +1 -1
  54. package/dist/components/index.cjs +178 -172
  55. package/dist/components/index.cjs.map +1 -1
  56. package/dist/components/index.js +14 -8
  57. package/dist/components/index.js.map +1 -1
  58. package/dist/components/layout/page-header.d.ts +78 -0
  59. package/dist/components/layout/page-header.d.ts.map +1 -0
  60. package/dist/components/layout/page-layout.d.ts +10 -1
  61. package/dist/components/layout/page-layout.d.ts.map +1 -1
  62. package/dist/components/layout/page-with-header.d.ts +67 -0
  63. package/dist/components/layout/page-with-header.d.ts.map +1 -0
  64. package/dist/components/layout/title-block.d.ts +17 -1
  65. package/dist/components/layout/title-block.d.ts.map +1 -1
  66. package/dist/components/navigation/index.cjs +2 -2
  67. package/dist/components/navigation/index.js +1 -1
  68. package/dist/components/onboarding-guides/index.cjs +23 -23
  69. package/dist/components/onboarding-guides/index.js +3 -3
  70. package/dist/components/related-content/index.cjs +3 -3
  71. package/dist/components/related-content/index.js +2 -2
  72. package/dist/components/shared/dev-section/dev-section-page.d.ts +9 -0
  73. package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -1
  74. package/dist/components/shared/dev-section/dev-section-view.d.ts.map +1 -1
  75. package/dist/components/shared/dev-section/index.d.ts +1 -1
  76. package/dist/components/shared/dev-section/index.d.ts.map +1 -1
  77. package/dist/components/shared/doc-search/use-doc-search.d.ts.map +1 -1
  78. package/dist/components/tickets/index.cjs +60 -60
  79. package/dist/components/tickets/index.js +3 -3
  80. package/dist/components/ui/index.cjs +6 -2
  81. package/dist/components/ui/index.cjs.map +1 -1
  82. package/dist/components/ui/index.d.ts +1 -0
  83. package/dist/components/ui/index.d.ts.map +1 -1
  84. package/dist/components/ui/index.js +5 -1
  85. package/dist/index.cjs +6 -2
  86. package/dist/index.cjs.map +1 -1
  87. package/dist/index.js +5 -1
  88. package/package.json +1 -1
  89. package/src/components/chat/embeddable-chat.tsx +3 -25
  90. package/src/components/chat/hooks/use-chat-identity.ts +2 -13
  91. package/src/components/chat/hooks/use-empty-state-config.ts +16 -30
  92. package/src/components/chat/hooks/use-slash-commands.ts +8 -24
  93. package/src/components/docs/doc-viewer.tsx +22 -25
  94. package/src/components/docs/use-docs-resolve-link.ts +1 -2
  95. package/src/components/docs/use-document-tree.ts +2 -3
  96. package/src/components/layout/page-header.tsx +182 -0
  97. package/src/components/layout/page-layout.tsx +14 -28
  98. package/src/components/layout/page-with-header.tsx +110 -0
  99. package/src/components/layout/title-block.tsx +40 -86
  100. package/src/components/shared/dev-section/dev-section-page.tsx +9 -1
  101. package/src/components/shared/dev-section/dev-section-view.tsx +14 -9
  102. package/src/components/shared/dev-section/index.ts +1 -1
  103. package/src/components/shared/doc-search/use-doc-search.ts +1 -2
  104. package/src/components/ui/index.ts +1 -0
  105. package/dist/chunk-2X4HTRQ4.js.map +0 -1
  106. package/dist/chunk-7KTSRZI4.cjs.map +0 -1
  107. package/dist/chunk-FVLEE7YZ.cjs.map +0 -1
  108. package/dist/chunk-JO6EUJGU.js.map +0 -1
  109. package/dist/chunk-RNF2E736.cjs.map +0 -1
  110. package/dist/chunk-UAJAJFI6.cjs.map +0 -1
  111. package/dist/chunk-Y2D2RJQX.js.map +0 -1
  112. /package/dist/{chunk-INZOAK77.js.map → chunk-FG4XA6NA.js.map} +0 -0
  113. /package/dist/{chunk-YV73VRRY.js.map → chunk-OFLTDHC2.js.map} +0 -0
  114. /package/dist/{chunk-45DC5AJC.js.map → chunk-UMYWG5C3.js.map} +0 -0
  115. /package/dist/{chunk-DOYOOBP4.js.map → chunk-VAH46QQO.js.map} +0 -0
  116. /package/dist/{chunk-ZXIM2DJM.js.map → chunk-ZRSS67EY.js.map} +0 -0
@@ -62,25 +62,14 @@ export async function fetchSlashCommands(
62
62
  signal: AbortSignal | undefined,
63
63
  commandsUrl: string,
64
64
  ): Promise<SlashCommandSummary[]> {
65
- try {
66
- const url = new URL(commandsUrl, window.location.origin);
67
- if (prefix) url.searchParams.set("q", prefix);
68
- // `headers: {}` opts out of the default `Content-Type: application/json`
69
- // this is a bare GET with no body, so no content-type is needed.
70
- const res = await embedAuthedFetch(url.toString(), { signal, headers: {} });
71
- if (!res.ok) return [];
72
- const data = (await res.json()) as { commands?: SlashCommandSummary[] };
73
- return data.commands ?? [];
74
- } catch (err) {
75
- // Cancellation (unmount / dep change) MUST propagate so react-query treats
76
- // it as cancelled, not as a successful empty result. Every OTHER failure
77
- // (network down, proxy reject, non-JSON body) degrades to "no commands" so
78
- // a flaky commands endpoint can NEVER break the chat — the autocomplete /
79
- // onboarding list just renders empty.
80
- if ((err as Error)?.name === "AbortError") throw err;
81
- console.warn("[chat] slash-commands fetch failed, showing none:", err);
82
- return [];
83
- }
65
+ const url = new URL(commandsUrl, window.location.origin);
66
+ if (prefix) url.searchParams.set("q", prefix);
67
+ // `headers: {}` opts out of the default `Content-Type: application/json`
68
+ // this is a bare GET with no body, so no content-type is needed.
69
+ const res = await embedAuthedFetch(url.toString(), { signal, headers: {} });
70
+ if (!res.ok) return [];
71
+ const data = (await res.json()) as { commands?: SlashCommandSummary[] };
72
+ return data.commands ?? [];
84
73
  }
85
74
 
86
75
  /**
@@ -155,11 +144,6 @@ export function useSlashCommandRegistry(
155
144
  enabled: options?.enabled ?? true,
156
145
  staleTime: Infinity,
157
146
  gcTime: Infinity,
158
- // The commands registry is non-critical chrome — a failure degrades to an
159
- // empty registry (handled in `fetchSlashCommands`). Don't retry: settle to
160
- // the neutral empty state immediately so a flaky endpoint never holds the
161
- // welcome UI in a loading spinner.
162
- retry: false,
163
147
  });
164
148
  return {
165
149
  commands: query.data ?? [],
@@ -2,6 +2,7 @@
2
2
 
3
3
  import React, { useMemo } from "react"
4
4
  import { MultiLevelNavigation, MobileNavigationDropdown } from "../navigation/multi-level-navigation"
5
+ import { PageHeader } from "../layout/page-header"
5
6
  import { PageLayout } from "../layout/page-layout"
6
7
  import { PageShell } from "../layout/article-detail-layout"
7
8
  import { useRouter } from "../../embed-shims/next-navigation"
@@ -58,9 +59,10 @@ export interface DocViewerProps {
58
59
  */
59
60
  chatSource: string
60
61
 
61
- /** Page title — rendered as the inline hero `<h1>` (same DOM
62
- * `<DevSectionView>`'s hero uses) so the doc-viewer chrome matches the
63
- * dev-section pages. ReactNode is intentionally not supported here —
62
+ /** Page title — rendered via the shared `<PageHeader>` primitive
63
+ * so the doc-viewer chrome matches every other lib page
64
+ * (DevSectionPage / LegalDocumentPage / OnboardingGuideDetailView)
65
+ * pixel-for-pixel. ReactNode is intentionally not supported here —
64
66
  * every consumer renders the same typography. */
65
67
  title?: string
66
68
  /** Optional icon rendered inline before the title text — same slot
@@ -257,10 +259,15 @@ function DocViewerContent({
257
259
  const resolvedEmptyText = emptyStateText || defaultEmptyText
258
260
 
259
261
  return (
260
- // Render through the shared wrapper chain (PageShell → PageLayout →
261
- // `gap-10 flex-col`). PageLayout owns the back-button row; the inner
262
- // `gap-10` div holds an inline title hero (same DOM `<DevSectionView>`'s
263
- // hero renders) followed by the search bar + content grid.
262
+ // STRUCTURAL UNIFICATION: render through the IDENTICAL wrapper chain
263
+ // `<DevSectionPage>` uses (PageShell → PageLayout → `gap-10 flex-col`),
264
+ // not a hand-rolled custom container with similar-looking spacing.
265
+ // PageLayout owns the back-button row; the inner `gap-10` div +
266
+ // `<PageHeader noTopPadding noBottomMargin>` renders the title section
267
+ // the same way `<DevSectionView>`'s hero does. This is the only way to
268
+ // guarantee /knowledge-base sits at pixel-identical vertical rhythm to
269
+ // /roadmap / /releases / /onboarding-guides — same components, same DOM,
270
+ // not "same CSS classes that look similar on paper."
264
271
  //
265
272
  // `colorPalette` / `className` / `bgStyle` flow through PageShell's
266
273
  // contentClassName + an inner style-passthrough wrapper so legacy
@@ -270,24 +277,14 @@ function DocViewerContent({
270
277
  <div style={{ ...bgStyle, ...containerBgStyle }}>
271
278
  <PageLayout backButton={backCfg ?? undefined}>
272
279
  <div className="w-full flex flex-col gap-10">
273
- {(title || titleIcon || subtitle) && (
274
- <div className="space-y-4">
275
- {(title || titleIcon) && (
276
- <h1 className="text-h1 tracking-[-1.12px] text-ods-text-primary flex items-center gap-3">
277
- {titleIcon}
278
- {title && (
279
- <span>
280
- {title}
281
- {accentDot && <span className="text-ods-accent">.</span>}
282
- </span>
283
- )}
284
- </h1>
285
- )}
286
- <p className="font-['DM_Sans'] font-medium text-[18px] leading-[28px] text-ods-text-secondary max-w-3xl line-clamp-2 min-h-[56px]">
287
- {subtitle || ' '}
288
- </p>
289
- </div>
290
- )}
280
+ <PageHeader
281
+ title={title}
282
+ titleIcon={titleIcon}
283
+ subtitle={subtitle}
284
+ accentDot={accentDot}
285
+ noTopPadding
286
+ noBottomMargin
287
+ />
291
288
 
292
289
  {showAIChat && (
293
290
  <DocSearchBar
@@ -1,7 +1,6 @@
1
1
  import { useCallback } from 'react'
2
2
  import { useChatRuntime } from '../../contexts/chat-runtime-context'
3
3
  import type { ResolveLinkResult } from '../../types/doc-source'
4
- import { contentFetch } from '../../utils/embed-content-fetch'
5
4
 
6
5
  /**
7
6
  * `useDocsResolveLink(sourceId, override?)` — POST `/api/docs/resolve-link`
@@ -31,7 +30,7 @@ export function useDocsResolveLink(
31
30
  return useCallback(
32
31
  async (href: string, currentPath: string): Promise<ResolveLinkResult> => {
33
32
  try {
34
- const response = await contentFetch(resolvedResolveLinkEndpoint, {
33
+ const response = await fetch(resolvedResolveLinkEndpoint, {
35
34
  method: 'POST',
36
35
  headers: { 'Content-Type': 'application/json' },
37
36
  body: JSON.stringify({ link: href, currentPath, source: sourceId }),
@@ -9,7 +9,6 @@ import {
9
9
  DEFAULT_FOLDER_INDEX_FILE,
10
10
  } from '../../utils/doc-tree-nav'
11
11
  import { useDocNavigation } from './doc-navigation-context'
12
- import { contentFetch } from '../../utils/embed-content-fetch'
13
12
  import { scrollElementIntoView } from '../../utils/scroll-into-view'
14
13
  import { navigateSamePageHash, HUB_HEADER_OFFSET_PX } from '../../utils/same-page-hash-nav'
15
14
 
@@ -180,7 +179,7 @@ export function useDocumentTree(
180
179
  setIsLoadingStructure(true)
181
180
  setError(null)
182
181
 
183
- const response = await contentFetch(structureEndpoint)
182
+ const response = await fetch(structureEndpoint)
184
183
 
185
184
  if (!response.ok) {
186
185
  throw new Error('Failed to load documentation structure')
@@ -233,7 +232,7 @@ export function useDocumentTree(
233
232
  // returns early without writing to state. Clearing error here would
234
233
  // briefly flicker the user-visible error message.
235
234
 
236
- const response = await contentFetch(`${contentEndpoint}?path=${encodeURIComponent(path)}`)
235
+ const response = await fetch(`${contentEndpoint}?path=${encodeURIComponent(path)}`)
237
236
 
238
237
  // Request-id guard: between awaits, `lastFetchedPath.current` may have
239
238
  // been bumped by a newer fetch (the structure-arrives auto-select issues
@@ -0,0 +1,182 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import { cn } from '../../utils/cn'
5
+ import { EntityImage } from '../ui/entity-image'
6
+ import { BackButton } from './back-button'
7
+
8
+ /**
9
+ * Page-header primitive — the canonical "back-button + title + subtitle
10
+ * + (optional) image / actions" chrome every lib page uses.
11
+ *
12
+ * Owns the SSOT for the page-header DOM/CSS that the rest of the lib
13
+ * was duplicating: pre-`mb` top padding, h1 typography (`text-h2`), h6
14
+ * subtitle (`text-h6`), the gap between the back button and the title
15
+ * block, and the right-side actions slot. Consumers either render this
16
+ * directly (e.g. `<DocViewer>` / `<DocsHubPage>`) or compose it through
17
+ * the `<TitleBlock>` adapter which adds the `PageActions` /
18
+ * `ActionsMenu` / selector wiring on top.
19
+ *
20
+ * Why this exists: knowledge-hub vs releases sat at different vertical
21
+ * rhythms (px-perfect mismatch on title baseline + subtitle offset)
22
+ * because the docs surface hand-rolled its own chrome instead of going
23
+ * through `TitleBlock`. Centralizing the layout here means every
24
+ * embeddable lib page (DocViewer, DevSectionPage, LegalDocumentPage,
25
+ * OnboardingGuideDetailView) renders pixel-identical title/subtitle/
26
+ * back-button typography + spacing — and a future spacing/typography
27
+ * tweak is one file.
28
+ */
29
+ export interface PageHeaderProps {
30
+ /** Page title (h1). Plain string — ReactNode is intentionally not
31
+ * supported here so every consumer renders the same typography. */
32
+ title?: string
33
+ /** Optional icon rendered inline before the title text (e.g. the
34
+ * rocket emoji on /releases, the docs icon on /knowledge-base).
35
+ * Same `flex items-center gap-3` row as `<DevSectionView>`'s hero. */
36
+ titleIcon?: React.ReactNode
37
+ /** Page subtitle (description paragraph). */
38
+ subtitle?: string
39
+ /**
40
+ * Render a yellow accent dot (`.`) after the title. Mirrors the
41
+ * hub's legacy `<AdminPageHeader accentDot>` flag — now lib-wide so
42
+ * surfaces like `/knowledge-base` keep their existing accent styling
43
+ * after the migration.
44
+ */
45
+ accentDot?: boolean
46
+ /** Optional thumbnail / hero image rendered to the left of the
47
+ * title block. Used by entity-image-style headers (onboarding
48
+ * guides, knowledge-base entries). */
49
+ image?: { src: string; alt?: string }
50
+ /** Back-button shown above the title block. Hidden on mobile (matches
51
+ * the existing TitleBlock + DocViewer behavior). */
52
+ backButton?: { label?: string; onClick: () => void }
53
+ /** Right-side actions slot (action buttons / menu / tab selector).
54
+ * Composed externally (e.g. `<TitleBlock>` builds `PageActions` + menu
55
+ * + selector and passes the result here). */
56
+ actions?: React.ReactNode
57
+ /**
58
+ * Visual variant.
59
+ * - `plain` (default): transparent background, no border.
60
+ * - `card`: card background, border, and padding on mobile only —
61
+ * collapses to plain on md+ (legacy `TitleBlock` variant — kept so
62
+ * surfaces that depend on the card affordance don't regress).
63
+ */
64
+ variant?: 'plain' | 'card'
65
+ /** When the consumer wraps `<PageHeader>` in its OWN spacing container
66
+ * (e.g. `<DevSectionView>`'s `gap-10 flex-col`), the default `mb-l`
67
+ * bottom margin doubles up. Pass `noBottomMargin` to opt out. */
68
+ noBottomMargin?: boolean
69
+ /** Same as `noBottomMargin` for the default `pt-l` top padding. Set
70
+ * this when PageHeader is nested INSIDE another layout that already
71
+ * provides top spacing (e.g. `<DevSectionView>`'s hero, which sits
72
+ * inside `<PageLayout>`'s children flow). */
73
+ noTopPadding?: boolean
74
+ className?: string
75
+ }
76
+
77
+ // Title typography — copied verbatim from <DevSectionView>'s hero h1
78
+ // (`src/components/shared/dev-section/dev-section-view.tsx`). The user-
79
+ // reported "header text not aligned" between /knowledge-base and
80
+ // /releases bottomed out here: DevSectionView rendered text-h1 with
81
+ // tracking-[-1.12px] while this component used text-h2 — visually huge
82
+ // gap. Now both render through the exact same class string. DevSectionView
83
+ // is being refactored in this commit to delegate to <PageHeader> so the
84
+ // shared-component claim is enforced at the code level too.
85
+ const TITLE_CLASS = 'text-h1 tracking-[-1.12px] text-ods-text-primary flex items-center gap-3'
86
+ // Subtitle ALWAYS occupies exactly 2 lines of vertical space.
87
+ // `min-h-[56px]` (= 2 × 28px leading) reserves the row height so a
88
+ // single-line subtitle doesn't shrink the header — page-to-page height
89
+ // stays consistent.
90
+ // `line-clamp-2` caps long copy at 2 lines + ellipsis so wrapping doesn't
91
+ // push the search bar down.
92
+ const SUBTITLE_CLASS = "font-['DM_Sans'] font-medium text-[18px] leading-[28px] text-ods-text-secondary max-w-3xl line-clamp-2 min-h-[56px]"
93
+
94
+ export function PageHeader({
95
+ title,
96
+ titleIcon,
97
+ subtitle,
98
+ accentDot,
99
+ image,
100
+ backButton,
101
+ actions,
102
+ variant = 'plain',
103
+ noBottomMargin = false,
104
+ noTopPadding = false,
105
+ className,
106
+ }: PageHeaderProps) {
107
+ return (
108
+ <div
109
+ className={cn(
110
+ 'flex items-end justify-between gap-[var(--spacing-system-m)]',
111
+ 'md:flex-col md:items-start md:justify-start lg:flex-row lg:items-end lg:justify-between',
112
+ !noTopPadding && 'pt-[var(--spacing-system-l)]',
113
+ variant === 'card'
114
+ ? cn(
115
+ 'bg-ods-card border-b border-ods-border',
116
+ 'px-[var(--spacing-system-l)] pb-[var(--spacing-system-l)]',
117
+ 'md:bg-transparent md:border-b-0',
118
+ 'md:px-0 md:pb-0',
119
+ !noBottomMargin && 'md:mb-[var(--spacing-system-l)]',
120
+ )
121
+ : !noBottomMargin && 'mb-[var(--spacing-system-l)]',
122
+ className,
123
+ )}
124
+ >
125
+ <div className="flex flex-col gap-[var(--spacing-system-xs)] flex-1 min-w-0">
126
+ {backButton && (
127
+ <BackButton
128
+ onClick={backButton.onClick}
129
+ label={backButton.label}
130
+ className="hidden md:inline-flex"
131
+ />
132
+ )}
133
+ {/* Title + subtitle stack. Matches `<DevSectionView>`'s hero
134
+ * exactly: `space-y-4` between h1 and p, `flex items-center
135
+ * gap-3` for icon-inline title row, identical class strings.
136
+ * Image (entity-image-style) prefixes the title row, NOT a
137
+ * separate column with its own vertical rhythm — that's the
138
+ * legacy TitleBlock 2-col layout which broke the title-to-
139
+ * subtitle gap. */}
140
+ {(title || subtitle || image || titleIcon) && (
141
+ <div className="space-y-4">
142
+ {(title || image || titleIcon) && (
143
+ <h1 className={TITLE_CLASS}>
144
+ {image && (
145
+ <EntityImage
146
+ src={image.src}
147
+ alt={image.alt}
148
+ fallbackText={image.alt || title}
149
+ />
150
+ )}
151
+ {titleIcon}
152
+ {title && (
153
+ <span>
154
+ {title}
155
+ {accentDot && <span className="text-ods-accent">.</span>}
156
+ </span>
157
+ )}
158
+ </h1>
159
+ )}
160
+ {(title || titleIcon || image) && (
161
+ // Always render the subtitle <p> when the title block exists,
162
+ // even when `subtitle` is empty — `SUBTITLE_CLASS` reserves
163
+ // `min-h-[56px]` so headers WITH and WITHOUT subtitles share a
164
+ // baseline (the JSDoc on SUBTITLE_CLASS claims "ALWAYS occupies
165
+ // exactly 2 lines"; gating the <p> on truthy `subtitle` broke
166
+ // that contract — pages without subtitle were ~56px shorter).
167
+ // Falsy subtitles render an NBSP placeholder so the empty <p>
168
+ // still takes its reserved height.
169
+ <p className={SUBTITLE_CLASS}>{subtitle || ' '}</p>
170
+ )}
171
+ </div>
172
+ )}
173
+ </div>
174
+
175
+ {actions && (
176
+ <div className="flex gap-2 items-center shrink-0">{actions}</div>
177
+ )}
178
+ </div>
179
+ )
180
+ }
181
+
182
+ export default PageHeader
@@ -1,32 +1,5 @@
1
1
  'use client'
2
2
 
3
- /* ============================================================================
4
- * ⛔️ FROZEN — DO NOT MODIFY (AI agents & contributors, read this first)
5
- * ----------------------------------------------------------------------------
6
- * `PageLayout` and its `TitleBlock` are a FINALIZED, locked component. They are
7
- * the canonical, stable page chrome for OpenFrame surfaces and their visual +
8
- * behavioral contract is intentionally complete. Treat this file as read-only.
9
- *
10
- * Do NOT: change the markup/CSS, swap the title typography (`text-h2`) or
11
- * subtitle (`text-h6`), re-architect this to delegate to another primitive,
12
- * add/rename props, or "unify"/"refactor"/"simplify" it. Do NOT restyle to
13
- * match some other surface.
14
- *
15
- * Why this rule exists (the incident it prevents): a refactor once re-styled
16
- * this layout (bumped the title to `text-h1`, rerouted it through a new
17
- * `PageHeader`/`PageWithHeader` chain) to "unify" page chrome. That silently
18
- * changed the look of every page rendered through `PageLayout` and had to be
19
- * fully reverted. The current code IS the reverted, correct baseline.
20
- *
21
- * MANY consumers depend on the EXACT current output — not only OpenFrame pages
22
- * but also `DevSectionPage`, `DocViewer`, and the multi-platform hub (through
23
- * its own local `PageWithHeader`). A change here ripples across all of them.
24
- *
25
- * If a new design genuinely needs different chrome: build a SEPARATE new
26
- * component for it. Never mutate this one. If you believe an edit here is
27
- * unavoidable, STOP and get explicit human sign-off first.
28
- * ========================================================================== */
29
-
30
3
  import React from 'react'
31
4
  import { cn } from '../../utils/cn'
32
5
  import type { ActionsMenuGroup } from '../ui/actions-menu'
@@ -37,6 +10,11 @@ export interface PageLayoutProps {
37
10
  children: React.ReactNode
38
11
  title?: string
39
12
  subtitle?: string
13
+ /** Inline icon rendered before the title text — forwarded to
14
+ * TitleBlock/PageHeader. Same shape as DevSectionPage's hero icon. */
15
+ titleIcon?: React.ReactNode
16
+ /** Yellow accent dot after the title — forwarded to TitleBlock/PageHeader. */
17
+ accentDot?: boolean
40
18
  image?: { src: string; alt?: string }
41
19
  backButton?: { label?: string; onClick: () => void }
42
20
  actions?: PageActionButton[]
@@ -60,6 +38,8 @@ export function PageLayout({
60
38
  children,
61
39
  title,
62
40
  subtitle,
41
+ titleIcon,
42
+ accentDot,
63
43
  image,
64
44
  backButton,
65
45
  actions,
@@ -73,14 +53,16 @@ export function PageLayout({
73
53
  }: PageLayoutProps) {
74
54
  const hasActions = actions && actions.length > 0
75
55
  const needsBottomPadding = hasActions && actionsVariant === 'primary-buttons'
76
- const hasHeader = showHeader && (title || subtitle || image || backButton || hasActions || selector)
56
+ const hasHeader = showHeader && (title || subtitle || titleIcon || image || backButton || hasActions || selector)
77
57
 
78
58
  return (
79
59
  <div className={cn('flex flex-col w-full', className)}>
80
60
  {hasHeader && (
81
61
  <TitleBlock
82
62
  title={title}
63
+ titleIcon={titleIcon}
83
64
  subtitle={subtitle}
65
+ accentDot={accentDot}
84
66
  image={image}
85
67
  backButton={backButton}
86
68
  actions={actions}
@@ -101,4 +83,8 @@ export function PageLayout({
101
83
  export type { PageActionButton } from '../ui/page-actions'
102
84
  export { TitleBlock } from './title-block'
103
85
  export type { TitleBlockProps } from './title-block'
86
+ export { PageHeader } from './page-header'
87
+ export type { PageHeaderProps } from './page-header'
88
+ export { PageWithHeader } from './page-with-header'
89
+ export type { PageWithHeaderProps } from './page-with-header'
104
90
  export default PageLayout
@@ -0,0 +1,110 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import { useRouter } from '../../embed-shims/next-navigation'
5
+ import { PageShell } from './article-detail-layout'
6
+ import { PageLayout } from './page-layout'
7
+ import { PageHeader, type PageHeaderProps } from './page-header'
8
+
9
+ /**
10
+ * `<PageWithHeader>` — the canonical 4-layer unified-chrome wrapper that
11
+ * every public lib/hub page now uses (Knowledge Hub, Roadmap, FAQs,
12
+ * Authors, Blog, Vendors, Case Studies, Interviews, Investor Updates,
13
+ * /webinars-/podcasts-/events, hub admin-but-public pages, …).
14
+ *
15
+ * Renders the same JSX tree every consumer was hand-rolling individually
16
+ * before this helper landed:
17
+ *
18
+ * PageShell ← bg-ods-bg, max-w-[1920px],
19
+ * page-shell-px/pt/pb gutters
20
+ * PageLayout backButton ← TitleBlock → PageHeader #1 (back-btn row
21
+ * only, pt-l + mb-l)
22
+ * div.w-full.flex-col.gap-10 ← matches DevSectionView's hero container
23
+ * PageHeader title/subtitle ← PageHeader #2 (noTopPadding +
24
+ * titleIcon accentDot noBottomMargin so PageLayout's gap-l owns
25
+ * noTopPadding the spacing between #1 and #2)
26
+ * noBottomMargin
27
+ * children ← page-specific body (search, lists, forms)
28
+ *
29
+ * Why this exists: 15+ pages copy-pasted the EXACT same nesting + the
30
+ * EXACT same `noTopPadding + noBottomMargin + accentDot` triplet on the
31
+ * inner `<PageHeader>`. Forgetting one of those flags collapsed the gap
32
+ * to ~8px (the FAQs-vs-Onboarding-Guides bug). Centralizing the chain
33
+ * here makes the spacing invariant compiler-enforced — consumers can't
34
+ * pick the wrong combination.
35
+ *
36
+ * **Back button is config-driven by default.** Consumers don't have to
37
+ * thread `useRouter()` + `() => router.push('/')` themselves anymore —
38
+ * the helper supplies a sane default (`{ label: 'Back to home', href: '/' }`).
39
+ * Hosts whose `/` lives elsewhere should pass their own `href`. Pass
40
+ * `backButton={false}` to suppress entirely (the lib's own
41
+ * `<DocsHubPage>` does this on the platform-home docs landing).
42
+ */
43
+ export interface PageWithHeaderProps {
44
+ /** Title — passed straight to inner `<PageHeader>`. */
45
+ title?: string
46
+ /** Inline icon rendered before the title (lucide / SVG node). Wrap with
47
+ * `SECTION_HERO_ICON_CLASS` at the call site, or use the lib's
48
+ * `<PageHeader>`'s `titleIcon` ReactNode signature directly. */
49
+ titleIcon?: PageHeaderProps['titleIcon']
50
+ /** Subtitle (1-2 lines, auto-clamped to 2 + min-h-[56px]). */
51
+ subtitle?: string
52
+ /** Render yellow accent dot after the title (default true — matches every
53
+ * unified surface). Pass `false` to opt out. */
54
+ accentDot?: boolean
55
+ /** Back-button config. Defaults to `{ label: 'Back to home', href: '/' }`.
56
+ * Pass `false` to hide entirely (platform-home docs landings). */
57
+ backButton?: { label?: string; href?: string } | false
58
+ /** Optional image (entity-image-style — onboarding guides etc.). */
59
+ image?: PageHeaderProps['image']
60
+ /** Optional actions slot (right side of header). */
61
+ actions?: React.ReactNode
62
+ /** Page body — search bars, lists, forms, sections. Rendered INSIDE the
63
+ * gap-10 flex column so it shares gutters + vertical rhythm with the
64
+ * header. */
65
+ children: React.ReactNode
66
+ /** Extra class applied to the gap-10 content wrapper. */
67
+ contentClassName?: string
68
+ }
69
+
70
+ export function PageWithHeader({
71
+ title,
72
+ titleIcon,
73
+ subtitle,
74
+ accentDot = true,
75
+ backButton,
76
+ image,
77
+ actions,
78
+ children,
79
+ contentClassName,
80
+ }: PageWithHeaderProps) {
81
+ const router = useRouter()
82
+
83
+ const backCfg =
84
+ backButton === false
85
+ ? undefined
86
+ : {
87
+ label: backButton?.label ?? 'Back to home',
88
+ onClick: () => router.push(backButton?.href ?? '/'),
89
+ }
90
+
91
+ return (
92
+ <PageShell>
93
+ <PageLayout backButton={backCfg}>
94
+ <div className={`w-full flex flex-col gap-10${contentClassName ? ` ${contentClassName}` : ''}`}>
95
+ <PageHeader
96
+ title={title}
97
+ titleIcon={titleIcon}
98
+ subtitle={subtitle}
99
+ accentDot={accentDot}
100
+ image={image}
101
+ actions={actions}
102
+ noTopPadding
103
+ noBottomMargin
104
+ />
105
+ {children}
106
+ </div>
107
+ </PageLayout>
108
+ </PageShell>
109
+ )
110
+ }