@flamingo-stack/openframe-frontend-core 0.0.255 → 0.0.256-snapshot.20260612014656
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-LHR7MBUY.js → chunk-2VZQJIVA.js} +48 -13
- package/dist/chunk-2VZQJIVA.js.map +1 -0
- package/dist/{chunk-GYKDSPC4.cjs → chunk-3DC6MHW6.cjs} +31 -31
- package/dist/{chunk-GYKDSPC4.cjs.map → chunk-3DC6MHW6.cjs.map} +1 -1
- package/dist/{chunk-LZL6P7SN.cjs → chunk-4MXBBBHB.cjs} +108 -11
- package/dist/chunk-4MXBBBHB.cjs.map +1 -0
- package/dist/{chunk-HMFBMVJC.js → chunk-6GBA6YJN.js} +2 -2
- package/dist/{chunk-SGJBUL2Y.cjs → chunk-7VMEOFEA.cjs} +45 -2
- package/dist/chunk-7VMEOFEA.cjs.map +1 -0
- package/dist/{chunk-UGFSMA7K.cjs → chunk-AIQ4DGO7.cjs} +77 -42
- package/dist/chunk-AIQ4DGO7.cjs.map +1 -0
- package/dist/{chunk-6TB24AUH.js → chunk-CBIQDPHK.js} +155 -236
- package/dist/chunk-CBIQDPHK.js.map +1 -0
- package/dist/{chunk-6LK4VEXE.cjs → chunk-EB76VWM4.cjs} +118 -63
- package/dist/chunk-EB76VWM4.cjs.map +1 -0
- package/dist/{chunk-RCW2IL4K.cjs → chunk-FBH73RRM.cjs} +1254 -1335
- package/dist/chunk-FBH73RRM.cjs.map +1 -0
- package/dist/{chunk-4HHYUEMD.js → chunk-IJHORDN5.js} +100 -3
- package/dist/chunk-IJHORDN5.js.map +1 -0
- package/dist/{chunk-MKVKVJQQ.js → chunk-J3QMPOS6.js} +5 -4
- package/dist/chunk-J3QMPOS6.js.map +1 -0
- package/dist/{chunk-CVGZJBHT.cjs → chunk-LCYDTEEK.cjs} +19 -19
- package/dist/{chunk-CVGZJBHT.cjs.map → chunk-LCYDTEEK.cjs.map} +1 -1
- package/dist/{chunk-2FCXJTOV.js → chunk-LDEXTWMK.js} +10 -2
- package/dist/chunk-LDEXTWMK.js.map +1 -0
- package/dist/{chunk-Z4IBEIWD.cjs → chunk-LGLPNWS6.cjs} +3 -3
- package/dist/{chunk-Z4IBEIWD.cjs.map → chunk-LGLPNWS6.cjs.map} +1 -1
- package/dist/{chunk-TU26IZPO.cjs → chunk-N5UOJC3V.cjs} +10 -2
- package/dist/chunk-N5UOJC3V.cjs.map +1 -0
- package/dist/{chunk-LKI3YCAX.js → chunk-NAOTZAKM.js} +62 -7
- package/dist/chunk-NAOTZAKM.js.map +1 -0
- package/dist/{chunk-2FAX6LZB.cjs → chunk-NNDLCSGR.cjs} +30 -30
- package/dist/{chunk-2FAX6LZB.cjs.map → chunk-NNDLCSGR.cjs.map} +1 -1
- package/dist/{chunk-VAUI3CND.js → chunk-NR535XIY.js} +44 -1
- package/dist/chunk-NR535XIY.js.map +1 -0
- package/dist/{chunk-D524LJYO.js → chunk-RUMSS73U.js} +4 -4
- package/dist/{chunk-265KKBV2.js → chunk-V4IIBNTA.js} +3 -3
- package/dist/chunk-V4IIBNTA.js.map +1 -0
- package/dist/{chunk-EFIEQ4XD.js → chunk-YMZZ5THD.js} +4 -4
- package/dist/{chunk-3TDLFCCS.cjs → chunk-ZJP2BB3E.cjs} +6 -5
- package/dist/chunk-ZJP2BB3E.cjs.map +1 -0
- package/dist/components/chat/entity-cards/entity-author-card.d.ts +13 -9
- package/dist/components/chat/entity-cards/entity-author-card.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +9 -7
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +8 -6
- package/dist/components/chat/types/entities/onboarding-guide.d.ts +2 -7
- package/dist/components/chat/types/entities/onboarding-guide.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +8 -8
- package/dist/components/contact/index.js +7 -7
- package/dist/components/faq/faq-section.d.ts.map +1 -1
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +7 -7
- package/dist/components/features/index.js +6 -6
- package/dist/components/icons/index.cjs +2 -2
- package/dist/components/icons/index.js +1 -1
- package/dist/components/index.cjs +168 -155
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +58 -45
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +7 -7
- package/dist/components/navigation/index.js +6 -6
- package/dist/components/onboarding-guides/index.cjs +43 -27
- package/dist/components/onboarding-guides/index.cjs.map +1 -1
- package/dist/components/onboarding-guides/index.js +20 -4
- package/dist/components/onboarding-guides/index.js.map +1 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts +11 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +10 -8
- package/dist/components/related-content/index.cjs.map +1 -1
- package/dist/components/related-content/index.d.ts +1 -1
- package/dist/components/related-content/index.d.ts.map +1 -1
- package/dist/components/related-content/index.js +9 -7
- package/dist/components/related-content/related-content-section.d.ts +16 -1
- package/dist/components/related-content/related-content-section.d.ts.map +1 -1
- package/dist/components/shared/article-author-byline.d.ts +58 -0
- package/dist/components/shared/article-author-byline.d.ts.map +1 -0
- package/dist/components/shared/product-release/release-detail-page.d.ts +4 -1
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/social-icon-row.d.ts +19 -4
- package/dist/components/social-icon-row.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +80 -80
- package/dist/components/tickets/index.js +8 -8
- package/dist/components/ui/button/button.d.ts +1 -1
- package/dist/components/ui/button/button.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +20 -20
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/index.cjs +9 -7
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +8 -6
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/index.cjs +14 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +20 -16
- package/dist/types/blog.d.ts +7 -0
- package/dist/types/blog.d.ts.map +1 -1
- package/dist/types/entity-author.d.ts +21 -0
- package/dist/types/entity-author.d.ts.map +1 -0
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/product-release.d.ts +2 -7
- package/dist/types/product-release.d.ts.map +1 -1
- package/dist/utils/common.d.ts +9 -0
- package/dist/utils/common.d.ts.map +1 -1
- package/dist/utils/content-href.d.ts.map +1 -1
- package/dist/utils/index.cjs +12 -3
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +12 -4
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/entity-cards/entity-author-card.tsx +30 -11
- package/src/components/chat/index.ts +1 -0
- package/src/components/chat/types/entities/onboarding-guide.ts +3 -7
- package/src/components/faq/faq-section.tsx +4 -1
- package/src/components/icons/x-logo.tsx +2 -2
- package/src/components/index.ts +5 -0
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +27 -0
- package/src/components/related-content/index.ts +1 -0
- package/src/components/related-content/related-content-section.tsx +84 -8
- package/src/components/shared/article-author-byline.tsx +187 -0
- package/src/components/shared/product-release/release-detail-page.tsx +18 -21
- package/src/components/social-icon-row.tsx +68 -22
- package/src/components/ui/button/button.tsx +13 -0
- package/src/types/blog.ts +9 -1
- package/src/types/entity-author.ts +20 -0
- package/src/types/index.ts +1 -0
- package/src/types/product-release.ts +2 -7
- package/src/utils/__tests__/content-href.test.ts +3 -0
- package/src/utils/common.ts +12 -0
- package/src/utils/content-href.ts +5 -0
- package/src/utils/index.ts +1 -1
- package/dist/chunk-265KKBV2.js.map +0 -1
- package/dist/chunk-2FCXJTOV.js.map +0 -1
- package/dist/chunk-3TDLFCCS.cjs.map +0 -1
- package/dist/chunk-4HHYUEMD.js.map +0 -1
- package/dist/chunk-6LK4VEXE.cjs.map +0 -1
- package/dist/chunk-6TB24AUH.js.map +0 -1
- package/dist/chunk-LHR7MBUY.js.map +0 -1
- package/dist/chunk-LKI3YCAX.js.map +0 -1
- package/dist/chunk-LZL6P7SN.cjs.map +0 -1
- package/dist/chunk-MKVKVJQQ.js.map +0 -1
- package/dist/chunk-RCW2IL4K.cjs.map +0 -1
- package/dist/chunk-SGJBUL2Y.cjs.map +0 -1
- package/dist/chunk-TU26IZPO.cjs.map +0 -1
- package/dist/chunk-UGFSMA7K.cjs.map +0 -1
- package/dist/chunk-VAUI3CND.js.map +0 -1
- /package/dist/{chunk-HMFBMVJC.js.map → chunk-6GBA6YJN.js.map} +0 -0
- /package/dist/{chunk-D524LJYO.js.map → chunk-RUMSS73U.js.map} +0 -0
- /package/dist/{chunk-EFIEQ4XD.js.map → chunk-YMZZ5THD.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import Link from '../../../embed-shims/next-link'
|
|
2
|
+
import type { EntityAuthor } from '../../../types/entity-author'
|
|
1
3
|
import { SquareAvatar } from '../../ui/square-avatar'
|
|
2
4
|
import { formatDate, nameInitials } from '../../../utils/format'
|
|
3
5
|
|
|
@@ -23,8 +25,10 @@ import { formatDate, nameInitials } from '../../../utils/format'
|
|
|
23
25
|
* Renders null when `author` is null/empty unless `renderEmptyAuthor` is set.
|
|
24
26
|
*/
|
|
25
27
|
/**
|
|
26
|
-
* Documented empty-author placeholder
|
|
27
|
-
*
|
|
28
|
+
* Documented empty-author placeholder for CHAT CARD surfaces ("—" name,
|
|
29
|
+
* "Unknown" role). The release detail page deliberately keeps its legacy
|
|
30
|
+
* empty rendering instead ("Unknown Author"/"Author" via a null-field stub
|
|
31
|
+
* — see release-detail-page.tsx) — two documented empty styles, one cell.
|
|
28
32
|
*/
|
|
29
33
|
export const EMPTY_AUTHOR_PLACEHOLDER = {
|
|
30
34
|
full_name: '—',
|
|
@@ -33,14 +37,14 @@ export const EMPTY_AUTHOR_PLACEHOLDER = {
|
|
|
33
37
|
} as const
|
|
34
38
|
|
|
35
39
|
export interface EntityAuthorCardProps {
|
|
36
|
-
author:
|
|
37
|
-
full_name: string | null
|
|
38
|
-
avatar_url: string | null
|
|
39
|
-
job_title: string | null
|
|
40
|
-
} | null | undefined
|
|
40
|
+
author: EntityAuthor | null | undefined
|
|
41
41
|
/** Role label rendered under the name. Defaults to "Author". Override
|
|
42
42
|
* to e.g. "Presenter" / "Contributor" if semantics differ. */
|
|
43
43
|
roleLabel?: string
|
|
44
|
+
/** Link target for the author name (e.g. the public author page). The
|
|
45
|
+
* HOST computes it (route availability is host knowledge) — absent ⇒
|
|
46
|
+
* plain text, exactly the prior render. */
|
|
47
|
+
authorHref?: string
|
|
44
48
|
/** Optional publication date. When provided, the component renders as a
|
|
45
49
|
* 2-cell grid (Published | Author). When omitted, only the Author cell
|
|
46
50
|
* renders inside the same bordered container. */
|
|
@@ -102,13 +106,19 @@ export function EntityMetadataValueCell({
|
|
|
102
106
|
export function EntityMetadataAuthorCell({
|
|
103
107
|
author,
|
|
104
108
|
roleLabel = 'Author',
|
|
109
|
+
authorHref,
|
|
105
110
|
className,
|
|
106
111
|
}: {
|
|
107
112
|
author: NonNullable<EntityAuthorCardProps['author']>
|
|
108
113
|
roleLabel?: string
|
|
114
|
+
authorHref?: string
|
|
109
115
|
className?: string
|
|
110
116
|
}) {
|
|
111
|
-
|
|
117
|
+
// Trimmed real name gates the LINK — a placeholder/unknown label must
|
|
118
|
+
// never render as a clickable author (an authorHref passed alongside an
|
|
119
|
+
// empty name would link "Unknown Author").
|
|
120
|
+
const trimmedName = typeof author.full_name === 'string' ? author.full_name.trim() : ''
|
|
121
|
+
const fullName = trimmedName || 'Unknown Author'
|
|
112
122
|
return (
|
|
113
123
|
<div className={`bg-ods-card p-4 flex items-center gap-3 ${className ?? ''}`}>
|
|
114
124
|
<SquareAvatar
|
|
@@ -119,8 +129,16 @@ export function EntityMetadataAuthorCell({
|
|
|
119
129
|
variant="round"
|
|
120
130
|
/>
|
|
121
131
|
<div className="flex flex-col gap-0 flex-1 min-w-0">
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
{/* title = full-name tooltip on truncation (carried over from the
|
|
133
|
+
release page's cell when it adopted this shared one). */}
|
|
134
|
+
<p className="text-h3 tracking-[-0.36px] text-ods-text-primary truncate" title={fullName}>
|
|
135
|
+
{authorHref && trimmedName ? (
|
|
136
|
+
<Link href={authorHref} className="hover:text-ods-accent transition-colors">
|
|
137
|
+
{fullName}
|
|
138
|
+
</Link>
|
|
139
|
+
) : (
|
|
140
|
+
fullName
|
|
141
|
+
)}
|
|
124
142
|
</p>
|
|
125
143
|
<p className="font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary">
|
|
126
144
|
{author.job_title || roleLabel}
|
|
@@ -133,6 +151,7 @@ export function EntityMetadataAuthorCell({
|
|
|
133
151
|
export function EntityAuthorCard({
|
|
134
152
|
author,
|
|
135
153
|
roleLabel = 'Author',
|
|
154
|
+
authorHref,
|
|
136
155
|
publishedAt,
|
|
137
156
|
publishedLabel = 'Published',
|
|
138
157
|
extraCells,
|
|
@@ -187,7 +206,7 @@ export function EntityAuthorCard({
|
|
|
187
206
|
className={dividerClass}
|
|
188
207
|
/>
|
|
189
208
|
)}
|
|
190
|
-
<EntityMetadataAuthorCell author={effectiveAuthor} roleLabel={roleLabel} />
|
|
209
|
+
<EntityMetadataAuthorCell author={effectiveAuthor} roleLabel={roleLabel} authorHref={authorHref} />
|
|
191
210
|
</div>
|
|
192
211
|
)
|
|
193
212
|
}
|
|
@@ -16,6 +16,7 @@ export * from './chat-message-row'
|
|
|
16
16
|
export * from './chat-message-skeleton'
|
|
17
17
|
|
|
18
18
|
export * from './chat-quick-action'
|
|
19
|
+
export * from './chat-quick-action-row'
|
|
19
20
|
export * from './chat-ticket-list'
|
|
20
21
|
export * from './chat-typing-indicator'
|
|
21
22
|
export * from './tool-execution-display'
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { EntityAuthor } from '../../../../types/entity-author';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Onboarding Guide entity — wire shape for the chat onboarding-guide
|
|
3
5
|
* card + the `/api/onboarding-guides` reads.
|
|
@@ -52,13 +54,7 @@ export interface OnboardingGuide {
|
|
|
52
54
|
status: string;
|
|
53
55
|
published_at: string | null;
|
|
54
56
|
author_id: string | null;
|
|
55
|
-
author?:
|
|
56
|
-
id: string;
|
|
57
|
-
full_name: string;
|
|
58
|
-
avatar_url: string | null;
|
|
59
|
-
job_title: string | null;
|
|
60
|
-
email?: string | null;
|
|
61
|
-
};
|
|
57
|
+
author?: EntityAuthor;
|
|
62
58
|
custom_instructions: string | null;
|
|
63
59
|
config: Record<string, unknown> | null;
|
|
64
60
|
ai_effort_score: number | null;
|
|
@@ -5,6 +5,7 @@ import type { Faq } from '../../types/faq'
|
|
|
5
5
|
import { FaqAccordion, type FaqItem } from '../faq-accordion'
|
|
6
6
|
import { useSelfFetch } from '../../hooks/use-self-fetch'
|
|
7
7
|
import { buildSuggestionUrl } from '../../utils/suggestion-url'
|
|
8
|
+
import { serializeJsonLd } from '../../utils/common'
|
|
8
9
|
import { buildFaqJsonLdFromFaqs, type FaqSchemaOptions } from './json-ld'
|
|
9
10
|
|
|
10
11
|
export interface FaqSectionProps {
|
|
@@ -202,7 +203,9 @@ export function FaqSection({
|
|
|
202
203
|
<script
|
|
203
204
|
type="application/ld+json"
|
|
204
205
|
// eslint-disable-next-line react/no-danger
|
|
205
|
-
|
|
206
|
+
// serializeJsonLd, NOT raw JSON.stringify — FAQ answers are
|
|
207
|
+
// admin-entered; an embedded "</script>" must not break the tag.
|
|
208
|
+
dangerouslySetInnerHTML={{ __html: serializeJsonLd(schema) }}
|
|
206
209
|
/>
|
|
207
210
|
)}
|
|
208
211
|
</>
|
|
@@ -16,8 +16,8 @@ export function XLogo({ className = '', color = 'white', ...props }: XLogoProps)
|
|
|
16
16
|
className={className}
|
|
17
17
|
{...props}
|
|
18
18
|
>
|
|
19
|
-
<path d="M9.75292 12.6562C10.1923 12.2169 10.9044 12.2169 11.3437 12.6562C11.7831 13.0956 11.7831 13.8077 11.3437 14.2471L3.7954 21.7954C3.35606 22.2348 2.64392 22.2348 2.20458 21.7954C1.76524 21.3561 1.76524 20.6439 2.20458 20.2046L9.75292 12.6562ZM20.2046 2.20459C20.6439 1.76525 21.3561 1.76525 21.7954 2.20459C22.2347 2.64393 22.2347 3.35607 21.7954 3.79541L14.2471 11.3438C13.8077 11.7831 13.0956 11.7831 12.6562 11.3438C12.2169 10.9044 12.2169 10.1923 12.6562 9.75293L20.2046 2.20459Z" fill=
|
|
20
|
-
<path d="M7.9995 1.875L8.13426 1.88232C8.444 1.91951 8.72702 2.08473 8.9121 2.34082L21.9126 20.3408C22.1599 20.6833 22.1943 21.1365 22.0019 21.5127C21.8096 21.8887 21.4223 22.125 21 22.125H16.0005C15.6393 22.125 15.2994 21.9519 15.0879 21.6592L2.08739 3.65918C1.84004 3.31669 1.8057 2.86346 1.99803 2.4873C2.19042 2.11133 2.57763 1.875 2.99999 1.875H7.9995ZM16.5747 19.875H18.7983L7.42528 4.125H5.20165L16.5747 19.875Z" fill=
|
|
19
|
+
<path d="M9.75292 12.6562C10.1923 12.2169 10.9044 12.2169 11.3437 12.6562C11.7831 13.0956 11.7831 13.8077 11.3437 14.2471L3.7954 21.7954C3.35606 22.2348 2.64392 22.2348 2.20458 21.7954C1.76524 21.3561 1.76524 20.6439 2.20458 20.2046L9.75292 12.6562ZM20.2046 2.20459C20.6439 1.76525 21.3561 1.76525 21.7954 2.20459C22.2347 2.64393 22.2347 3.35607 21.7954 3.79541L14.2471 11.3438C13.8077 11.7831 13.0956 11.7831 12.6562 11.3438C12.2169 10.9044 12.2169 10.1923 12.6562 9.75293L20.2046 2.20459Z" fill={color} />
|
|
20
|
+
<path d="M7.9995 1.875L8.13426 1.88232C8.444 1.91951 8.72702 2.08473 8.9121 2.34082L21.9126 20.3408C22.1599 20.6833 22.1943 21.1365 22.0019 21.5127C21.8096 21.8887 21.4223 22.125 21 22.125H16.0005C15.6393 22.125 15.2994 21.9519 15.0879 21.6592L2.08739 3.65918C1.84004 3.31669 1.8057 2.86346 1.99803 2.4873C2.19042 2.11133 2.57763 1.875 2.99999 1.875H7.9995ZM16.5747 19.875H18.7983L7.42528 4.125H5.20165L16.5747 19.875Z" fill={color} />
|
|
21
21
|
</svg>
|
|
22
22
|
);
|
|
23
23
|
}
|
package/src/components/index.ts
CHANGED
|
@@ -90,5 +90,10 @@ export * from './shared/legal-document'
|
|
|
90
90
|
// Detail Page Skeleton
|
|
91
91
|
export { DetailPageSkeleton, type DetailPageSkeletonProps } from './shared/detail-page-skeleton'
|
|
92
92
|
|
|
93
|
+
// Author byline card (end-of-article author description) — embeddable via
|
|
94
|
+
// embed-shims + optional-runtime avatar proxying; the hub wraps it to inject
|
|
95
|
+
// its platform-aware fallbackBio default.
|
|
96
|
+
export { ArticleAuthorByline, type ArticleAuthorBylineProps } from './shared/article-author-byline'
|
|
97
|
+
|
|
93
98
|
// Priority UI components that exist in main components directory
|
|
94
99
|
// Note: These are re-exported from ./ui already, no need to duplicate
|
|
@@ -32,6 +32,7 @@ import { getCaptionsUrl } from '../features/captions-url'
|
|
|
32
32
|
import { SimpleMarkdownRenderer } from '../ui/simple-markdown-renderer'
|
|
33
33
|
import { EntityTagBadges } from '../features/entity-tag-badges'
|
|
34
34
|
import { LoadError } from '../ui/error-state'
|
|
35
|
+
import { ArticleAuthorByline } from '../shared/article-author-byline'
|
|
35
36
|
import { EntityAuthorCard } from '../chat/entity-cards/entity-author-card'
|
|
36
37
|
import { OnboardingGuideCard } from '../chat/entity-cards/onboarding-guide-card'
|
|
37
38
|
import { useChatRuntime } from '../../contexts/chat-runtime-context'
|
|
@@ -50,6 +51,16 @@ export interface OnboardingGuideDetailViewProps {
|
|
|
50
51
|
* e.g. `(s) => \`/content/api/onboarding-guides/${s}\``. */
|
|
51
52
|
guideEndpoint?: (slug: string) => string
|
|
52
53
|
related?: OnboardingGuide[]
|
|
54
|
+
/** Link target for the author name in the metadata grid — the host
|
|
55
|
+
* computes it (public author page; absent ⇒ plain text). */
|
|
56
|
+
authorHref?: string
|
|
57
|
+
/** Bio paragraph for the end-of-article author byline. Defaults to the
|
|
58
|
+
* guide payload's `author.about` (hubs that hydrate it can omit this). */
|
|
59
|
+
authorBio?: string | null
|
|
60
|
+
/** Byline fallback paragraph when the bio is empty — the host passes its
|
|
61
|
+
* platform-aware copy (the lib has no config awareness). Absent ⇒ the
|
|
62
|
+
* byline renders nothing below the name when the bio is empty. */
|
|
63
|
+
fallbackBio?: string | null
|
|
53
64
|
/** Optional markdown renderer override. Defaults to lib
|
|
54
65
|
* `<SimpleMarkdownRenderer>`. */
|
|
55
66
|
MarkdownRenderer?: ComponentType<{ content: string }>
|
|
@@ -65,6 +76,9 @@ export interface OnboardingGuideDetailViewProps {
|
|
|
65
76
|
}
|
|
66
77
|
|
|
67
78
|
export function OnboardingGuideDetailView({
|
|
79
|
+
authorHref,
|
|
80
|
+
authorBio,
|
|
81
|
+
fallbackBio,
|
|
68
82
|
initialData,
|
|
69
83
|
slug,
|
|
70
84
|
guideEndpoint,
|
|
@@ -139,6 +153,7 @@ export function OnboardingGuideDetailView({
|
|
|
139
153
|
{/* Metadata grid — Section · Step | Published | Author. */}
|
|
140
154
|
<EntityAuthorCard
|
|
141
155
|
author={guide.author}
|
|
156
|
+
authorHref={authorHref}
|
|
142
157
|
publishedAt={guide.published_at}
|
|
143
158
|
extraCells={[
|
|
144
159
|
{
|
|
@@ -185,6 +200,18 @@ export function OnboardingGuideDetailView({
|
|
|
185
200
|
/>
|
|
186
201
|
)}
|
|
187
202
|
|
|
203
|
+
{/* End-of-article author byline (avatar + linked name + bio) — the
|
|
204
|
+
author DESCRIPTION block every article-shaped detail page renders.
|
|
205
|
+
Hidden when the guide has no author (the byline returns null). */}
|
|
206
|
+
<ArticleAuthorByline
|
|
207
|
+
author={guide.author?.full_name ?? null}
|
|
208
|
+
avatar={guide.author?.avatar_url}
|
|
209
|
+
jobTitle={guide.author?.job_title}
|
|
210
|
+
bio={authorBio ?? guide.author?.about}
|
|
211
|
+
href={authorHref}
|
|
212
|
+
fallbackBio={fallbackBio}
|
|
213
|
+
/>
|
|
214
|
+
|
|
188
215
|
{/* Related — same-section, ordered by step. */}
|
|
189
216
|
{related.length > 0 && (
|
|
190
217
|
<div className="space-y-4 pt-8 border-t border-ods-border">
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
* content type, add it BOTH there and here (cards + skeleton + list URL).
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
|
-
import React, { useMemo } from 'react';
|
|
45
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
46
46
|
import {
|
|
47
47
|
CONTENT_REF_GROUPS,
|
|
48
48
|
getContentRefLabelOrTitleCase,
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
} from '../../utils/content-ref-groups';
|
|
52
52
|
import type { ContentRef, ContentRefWithReason } from '../../types/content-ref';
|
|
53
53
|
import { useSelfFetch } from '../../hooks/use-self-fetch';
|
|
54
|
+
import { Pagination } from '../pagination';
|
|
54
55
|
import { extractItems, extractItemId } from '../../utils/extract-items';
|
|
55
56
|
import { buildListUrl as libBuildListUrl, canonicalContentRefType } from '../../utils/list-url';
|
|
56
57
|
import { buildSuggestionUrl } from '../../utils/suggestion-url';
|
|
@@ -342,6 +343,16 @@ function resolveGroupConfig(type: string): ContentRefGroupConfig {
|
|
|
342
343
|
};
|
|
343
344
|
}
|
|
344
345
|
|
|
346
|
+
/** Items per page within one type group. Groups larger than this paginate
|
|
347
|
+
* with the standard Pagination control (NO nested scrolling — a bounded
|
|
348
|
+
* scrollbox inside the page traps wheel events and hides the sections
|
|
349
|
+
* below it). MUST stay at or above the largest suggestion fill
|
|
350
|
+
* (RELATED_SAME_TYPE_COUNT in the hub's lib/constants/suggestions.ts) so
|
|
351
|
+
* current rails never paginate — only genuinely big groups (author pages)
|
|
352
|
+
* do. Exported through the subpath barrel for the hub's module-load
|
|
353
|
+
* assertion of that relation (entity-suggestion-sections.tsx). */
|
|
354
|
+
export const GROUP_PAGE_SIZE = 12;
|
|
355
|
+
|
|
345
356
|
function ContentGroup({
|
|
346
357
|
type,
|
|
347
358
|
refs,
|
|
@@ -371,11 +382,38 @@ function ContentGroup({
|
|
|
371
382
|
const isListLayout = config.layout === 'list';
|
|
372
383
|
const cardSize = config.gridSize;
|
|
373
384
|
|
|
385
|
+
// Per-group pagination for big groups (author pages): GROUP_PAGE_SIZE items
|
|
386
|
+
// per page with the standard Pagination control below the group. Client-side
|
|
387
|
+
// slicing — useGroupItems already fetched every row in one batched call, so
|
|
388
|
+
// page flips are instant. Hooks live above every early return (file
|
|
389
|
+
// convention). Page is clamped so a shrinking refs array (suggestion
|
|
390
|
+
// refetch) can never strand the view past the last page, and RESET when the
|
|
391
|
+
// ref set actually changes (shrink→grow must not return to a stale page).
|
|
392
|
+
const [page, setPage] = useState(1);
|
|
393
|
+
const refsKey = refs.map((r) => r.id).join('|');
|
|
394
|
+
const prevRefsKeyRef = useRef(refsKey);
|
|
395
|
+
useEffect(() => {
|
|
396
|
+
if (prevRefsKeyRef.current !== refsKey) {
|
|
397
|
+
prevRefsKeyRef.current = refsKey;
|
|
398
|
+
setPage(1);
|
|
399
|
+
}
|
|
400
|
+
}, [refsKey]);
|
|
401
|
+
const totalGroupPages = Math.max(1, Math.ceil(refs.length / GROUP_PAGE_SIZE));
|
|
402
|
+
const safePage = Math.min(page, totalGroupPages);
|
|
403
|
+
const visibleGroupRefs =
|
|
404
|
+
refs.length > GROUP_PAGE_SIZE
|
|
405
|
+
? refs.slice((safePage - 1) * GROUP_PAGE_SIZE, safePage * GROUP_PAGE_SIZE)
|
|
406
|
+
: refs;
|
|
407
|
+
const groupPagination =
|
|
408
|
+
totalGroupPages > 1 ? (
|
|
409
|
+
<Pagination currentPage={safePage} totalPages={totalGroupPages} onPageChange={setPage} />
|
|
410
|
+
) : null;
|
|
411
|
+
|
|
374
412
|
// Skeleton gate: `isLoading && !items` — SSR HTML and the client's first
|
|
375
413
|
// paint render identical skeletons (useSelfFetch starts isLoading=true on
|
|
376
414
|
// both sides), and once items exist they are never replaced by skeletons.
|
|
377
415
|
if (isLoading && !items) {
|
|
378
|
-
const skeletons =
|
|
416
|
+
const skeletons = visibleGroupRefs.map((r) => (
|
|
379
417
|
<div key={r.id}>{renderSkeletonForType(type, cardSize, adminCampaignCard)}</div>
|
|
380
418
|
));
|
|
381
419
|
return (
|
|
@@ -404,7 +442,7 @@ function ContentGroup({
|
|
|
404
442
|
(items as any[]).map((it) => [extractItemId(type, it) ?? String((it as any)?.id), it]),
|
|
405
443
|
);
|
|
406
444
|
|
|
407
|
-
const cards =
|
|
445
|
+
const cards = visibleGroupRefs
|
|
408
446
|
.map((contentRef) => {
|
|
409
447
|
const itemId = String(contentRef.id);
|
|
410
448
|
const item = itemById.get(itemId);
|
|
@@ -436,7 +474,22 @@ function ContentGroup({
|
|
|
436
474
|
})
|
|
437
475
|
.filter(Boolean);
|
|
438
476
|
|
|
439
|
-
if (cards.length === 0)
|
|
477
|
+
if (cards.length === 0) {
|
|
478
|
+
// Current PAGE resolved zero cards (rows deleted between the ref fetch
|
|
479
|
+
// and the group fetch, or a stricter list-API gate dropped them). When a
|
|
480
|
+
// pager exists the user must keep the controls to navigate back —
|
|
481
|
+
// dropping the whole group would strand them. A genuinely empty group
|
|
482
|
+
// (no pager) still vanishes with its heading.
|
|
483
|
+
if (groupPagination) {
|
|
484
|
+
return (
|
|
485
|
+
<div className="space-y-4">
|
|
486
|
+
{heading}
|
|
487
|
+
{groupPagination}
|
|
488
|
+
</div>
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
440
493
|
|
|
441
494
|
return (
|
|
442
495
|
<div className="space-y-4">
|
|
@@ -446,6 +499,7 @@ function ContentGroup({
|
|
|
446
499
|
) : (
|
|
447
500
|
<div className={gridClassFor(columns)}>{cards}</div>
|
|
448
501
|
)}
|
|
502
|
+
{groupPagination}
|
|
449
503
|
</div>
|
|
450
504
|
);
|
|
451
505
|
}
|
|
@@ -467,6 +521,12 @@ export interface RelatedContentSectionProps {
|
|
|
467
521
|
* `contentRefs` is provided. */
|
|
468
522
|
entityType?: string;
|
|
469
523
|
entityId?: number | string;
|
|
524
|
+
/** AUTHOR mode: self-fetch ALL published content authored by this profile
|
|
525
|
+
* from `{apiBaseUrl}/api/related-content?authorId=…` (grouped per type,
|
|
526
|
+
* endless within each group). Ignored when `contentRefs` is provided;
|
|
527
|
+
* takes precedence over the entityType/entityId suggestion scope.
|
|
528
|
+
* SSR-hydrate via `initialItems`, same as suggestion mode. */
|
|
529
|
+
authorId?: string;
|
|
470
530
|
/** Maps to the suggestion API's `count` param — the PER-TYPE fill target
|
|
471
531
|
* for every candidate type EXCEPT the host's own. Absent → param not sent
|
|
472
532
|
* (server default applies). */
|
|
@@ -532,6 +592,7 @@ export function RelatedContentSection({
|
|
|
532
592
|
contentRefs,
|
|
533
593
|
entityType,
|
|
534
594
|
entityId,
|
|
595
|
+
authorId,
|
|
535
596
|
minResults,
|
|
536
597
|
sameTypeMinResults,
|
|
537
598
|
includeTypes,
|
|
@@ -555,8 +616,24 @@ export function RelatedContentSection({
|
|
|
555
616
|
// entirely (an empty-string `types=` param would be dropped by the URL
|
|
556
617
|
// builder and read server-side as "all candidates") AND ignore SSR refs.
|
|
557
618
|
const suggestionsDisabled = includeTypes?.length === 0;
|
|
619
|
+
// Shared type-filter params — one spelling for both fetch modes so a future
|
|
620
|
+
// normalization (trim/dedupe) can't diverge between them.
|
|
621
|
+
const typeFilterParams = {
|
|
622
|
+
types: includeTypes !== undefined ? includeTypes.join(',') : undefined,
|
|
623
|
+
excludeTypes: excludeTypes && excludeTypes.length > 0 ? excludeTypes.join(',') : undefined,
|
|
624
|
+
};
|
|
625
|
+
// AUTHOR mode beats suggestion mode: when `authorId` is set the rail lists
|
|
626
|
+
// everything that profile authored (the server returns ALL, no count).
|
|
627
|
+
const authorUrl =
|
|
628
|
+
contentRefs === undefined && authorId && !suggestionsDisabled
|
|
629
|
+
? buildSuggestionUrl('/api/related-content', {
|
|
630
|
+
apiBaseUrl,
|
|
631
|
+
extraParams: { authorId, ...typeFilterParams },
|
|
632
|
+
})
|
|
633
|
+
: null;
|
|
558
634
|
const suggestUrl =
|
|
559
|
-
|
|
635
|
+
authorUrl ??
|
|
636
|
+
(contentRefs === undefined &&
|
|
560
637
|
entityType &&
|
|
561
638
|
entityId !== undefined &&
|
|
562
639
|
entityId !== null &&
|
|
@@ -569,11 +646,10 @@ export function RelatedContentSection({
|
|
|
569
646
|
count: minResults,
|
|
570
647
|
extraParams: {
|
|
571
648
|
sameTypeCount: sameTypeMinResults !== undefined ? String(sameTypeMinResults) : undefined,
|
|
572
|
-
|
|
573
|
-
excludeTypes: excludeTypes && excludeTypes.length > 0 ? excludeTypes.join(',') : undefined,
|
|
649
|
+
...typeFilterParams,
|
|
574
650
|
},
|
|
575
651
|
})
|
|
576
|
-
: null;
|
|
652
|
+
: null);
|
|
577
653
|
// Memoize the initialData wrapper — useSelfFetch re-syncs on [initialData],
|
|
578
654
|
// and a fresh per-render object would loop setState under re-rendering
|
|
579
655
|
// parents (the latent FaqSection bug, fixed there in the same change).
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared "author byline" card used by article-shaped detail pages
|
|
5
|
+
* (blog post, product release, onboarding guide, investor update).
|
|
6
|
+
*
|
|
7
|
+
* MOVED from the hub (`components/shared/article-author-byline.tsx`) so any
|
|
8
|
+
* consuming app can embed it; the hub keeps a thin wrapper that pre-binds its
|
|
9
|
+
* host-specific injections (the platform-aware `fallbackBio` default derived
|
|
10
|
+
* from `getAppConfig()`).
|
|
11
|
+
*
|
|
12
|
+
* Embed-readiness contract:
|
|
13
|
+
* - `Link` / `Image` render through the embed-shims (plain `<a>` / `<img>`
|
|
14
|
+
* in non-Next hosts; the real Next primitives once the host registers
|
|
15
|
+
* them at app init).
|
|
16
|
+
* - The avatar is proxied through the OPTIONAL ambient `ChatRuntime`
|
|
17
|
+
* (`endpoints.imageProxyUrlPrefix`, same config `useProxiedImageUrl`
|
|
18
|
+
* reads) — when no runtime is mounted the raw URL renders as-is, so the
|
|
19
|
+
* component never throws outside a provider. Hosts can also inject
|
|
20
|
+
* `proxyImageUrl` to bypass the runtime entirely.
|
|
21
|
+
* - `fallbackBio` is a PLAIN prop (no app-config import): the lib has no
|
|
22
|
+
* platform awareness; pass copy or leave it absent to render nothing.
|
|
23
|
+
*
|
|
24
|
+
* Render order: avatar → name + job title + date → bio (when present).
|
|
25
|
+
* Returns null when `author` is empty (no card rendered).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import React from 'react'
|
|
29
|
+
import { Calendar, User } from 'lucide-react'
|
|
30
|
+
import Image from '../../embed-shims/next-image'
|
|
31
|
+
import Link from '../../embed-shims/next-link'
|
|
32
|
+
import { cn } from '../../utils/cn'
|
|
33
|
+
import { formatBioText } from '../../utils/format'
|
|
34
|
+
import { getProxiedImageUrl } from '../../utils/image-proxy'
|
|
35
|
+
import { useChatRuntime } from '../../contexts/chat-runtime-context'
|
|
36
|
+
|
|
37
|
+
export interface ArticleAuthorBylineProps {
|
|
38
|
+
/** Author display name. Required — block is hidden when null/empty. */
|
|
39
|
+
author: string | null
|
|
40
|
+
/** Avatar URL. Falls back to a placeholder when null. */
|
|
41
|
+
avatar?: string | null
|
|
42
|
+
/** Optional bio paragraph rendered below the name. */
|
|
43
|
+
bio?: string | null
|
|
44
|
+
/** Optional job title rendered immediately under the name. */
|
|
45
|
+
jobTitle?: string | null
|
|
46
|
+
/** Optional published date (ISO string or Date). Rendered next to the name. */
|
|
47
|
+
publishedAt?: string | Date | null
|
|
48
|
+
/** Optional link target for the author name (e.g. the author page). */
|
|
49
|
+
href?: string | null
|
|
50
|
+
/**
|
|
51
|
+
* Fallback paragraph when `bio` is empty. Plain copy — the lib has no
|
|
52
|
+
* platform/config awareness, so hosts that want a branded default
|
|
53
|
+
* ("Contributing author on the {platform} platform") pass it explicitly
|
|
54
|
+
* (the hub wrapper derives it from `getAppConfig()`). Absent/null ⇒
|
|
55
|
+
* nothing renders below the name when `bio` is empty.
|
|
56
|
+
*/
|
|
57
|
+
fallbackBio?: string | null
|
|
58
|
+
/** Avatar size variant. `md` = 56px (default), `lg` = 64px. */
|
|
59
|
+
size?: 'md' | 'lg'
|
|
60
|
+
/**
|
|
61
|
+
* Host-injected avatar-URL mapper. Wins over the ambient-runtime proxy
|
|
62
|
+
* resolution. Use when the host proxies images outside the `ChatRuntime`
|
|
63
|
+
* config (e.g. a bespoke CDN rewrite).
|
|
64
|
+
*/
|
|
65
|
+
proxyImageUrl?: (url: string) => string
|
|
66
|
+
className?: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatDate(value: string | Date): string {
|
|
70
|
+
const d = typeof value === 'string' ? new Date(value) : value
|
|
71
|
+
if (Number.isNaN(d.getTime())) return ''
|
|
72
|
+
// `timeZone: 'UTC'` keeps the SSR (server = UTC) and client (user's local tz)
|
|
73
|
+
// renders identical — without it a published_at near a midnight boundary
|
|
74
|
+
// formats to a different day on each side, triggering a React #418 hydration
|
|
75
|
+
// text mismatch. Matches the convention in blog-metadata.tsx / investor-update.
|
|
76
|
+
return d.toLocaleDateString('en-US', {
|
|
77
|
+
month: 'long',
|
|
78
|
+
day: 'numeric',
|
|
79
|
+
year: 'numeric',
|
|
80
|
+
timeZone: 'UTC',
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function ArticleAuthorByline({
|
|
85
|
+
author,
|
|
86
|
+
avatar,
|
|
87
|
+
bio,
|
|
88
|
+
jobTitle,
|
|
89
|
+
publishedAt,
|
|
90
|
+
href,
|
|
91
|
+
fallbackBio,
|
|
92
|
+
size = 'md',
|
|
93
|
+
proxyImageUrl,
|
|
94
|
+
className,
|
|
95
|
+
}: ArticleAuthorBylineProps) {
|
|
96
|
+
// Optional runtime — `useChatRuntime` returns null outside a provider, so
|
|
97
|
+
// the byline works in bare embeds (raw avatar URL) and proxies whenever the
|
|
98
|
+
// host mounted a runtime with `imageProxyUrlPrefix` (the hub always does).
|
|
99
|
+
const runtime = useChatRuntime()
|
|
100
|
+
if (!author) return null
|
|
101
|
+
|
|
102
|
+
const proxiedAvatar = avatar
|
|
103
|
+
? proxyImageUrl
|
|
104
|
+
? proxyImageUrl(avatar)
|
|
105
|
+
: (getProxiedImageUrl(avatar, {
|
|
106
|
+
proxyPrefix: runtime?.endpoints.imageProxyUrlPrefix,
|
|
107
|
+
skipDomains: runtime?.endpoints.imageProxySkipDomains,
|
|
108
|
+
directHttps: true,
|
|
109
|
+
}) ?? avatar)
|
|
110
|
+
: ''
|
|
111
|
+
|
|
112
|
+
// Class-driven sizing (md = 56px, lg = 64px). The numeric pair feeds only
|
|
113
|
+
// the Image element's intrinsic width/height attributes (required by the
|
|
114
|
+
// Next image shim) — layout comes from the classes.
|
|
115
|
+
const avatarSizeClass = size === 'lg' ? 'w-16 h-16' : 'w-14 h-14'
|
|
116
|
+
const avatarIconClass = size === 'lg' ? 'w-8 h-8' : 'w-7 h-7'
|
|
117
|
+
const avatarDim = size === 'lg' ? 64 : 56
|
|
118
|
+
const formattedBio = formatBioText(bio ?? null)
|
|
119
|
+
const dateLabel = publishedAt ? formatDate(publishedAt) : ''
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div
|
|
123
|
+
className={cn(
|
|
124
|
+
'bg-ods-card border border-ods-border rounded-lg p-6',
|
|
125
|
+
'flex flex-col md:flex-row gap-4 items-start',
|
|
126
|
+
className,
|
|
127
|
+
)}
|
|
128
|
+
>
|
|
129
|
+
{/* Avatar */}
|
|
130
|
+
<div className="flex-shrink-0">
|
|
131
|
+
{avatar ? (
|
|
132
|
+
<Image
|
|
133
|
+
src={proxiedAvatar || avatar}
|
|
134
|
+
alt={author}
|
|
135
|
+
width={avatarDim}
|
|
136
|
+
height={avatarDim}
|
|
137
|
+
className={cn('rounded-full border-2 border-ods-border object-cover', avatarSizeClass)}
|
|
138
|
+
/>
|
|
139
|
+
) : (
|
|
140
|
+
<div
|
|
141
|
+
className={cn(
|
|
142
|
+
'rounded-full border-2 border-ods-border bg-ods-bg flex items-center justify-center',
|
|
143
|
+
avatarSizeClass,
|
|
144
|
+
)}
|
|
145
|
+
>
|
|
146
|
+
<User className={cn('text-ods-text-secondary', avatarIconClass)} />
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/* Name + meta + bio */}
|
|
152
|
+
<div className="flex-1 min-w-0">
|
|
153
|
+
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
|
154
|
+
<h3 className="text-h5 text-ods-text-primary">
|
|
155
|
+
{href ? (
|
|
156
|
+
<Link href={href} className="hover:text-ods-accent transition-colors">
|
|
157
|
+
{author}
|
|
158
|
+
</Link>
|
|
159
|
+
) : (
|
|
160
|
+
author
|
|
161
|
+
)}
|
|
162
|
+
</h3>
|
|
163
|
+
{dateLabel && (
|
|
164
|
+
<span className="inline-flex items-center gap-1 font-body text-body-sm text-ods-text-secondary">
|
|
165
|
+
<Calendar className="h-3 w-3" />
|
|
166
|
+
{dateLabel}
|
|
167
|
+
</span>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
{jobTitle && (
|
|
171
|
+
<p className="font-body text-body-sm text-ods-text-secondary mt-0.5">
|
|
172
|
+
{jobTitle}
|
|
173
|
+
</p>
|
|
174
|
+
)}
|
|
175
|
+
{formattedBio ? (
|
|
176
|
+
<p className="font-body text-body-md text-ods-text-secondary leading-relaxed mt-2">
|
|
177
|
+
{formattedBio}
|
|
178
|
+
</p>
|
|
179
|
+
) : fallbackBio ? (
|
|
180
|
+
<p className="font-body text-body-md text-ods-text-secondary italic mt-2">
|
|
181
|
+
{fallbackBio}
|
|
182
|
+
</p>
|
|
183
|
+
) : null}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
)
|
|
187
|
+
}
|