@flamingo-stack/openframe-frontend-core 0.0.255 → 0.0.256
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-GYKDSPC4.cjs → chunk-3DC6MHW6.cjs} +31 -31
- package/dist/{chunk-GYKDSPC4.cjs.map → chunk-3DC6MHW6.cjs.map} +1 -1
- package/dist/{chunk-EFIEQ4XD.js → chunk-46DHPQY6.js} +4 -4
- package/dist/{chunk-RCW2IL4K.cjs → chunk-6F5CGBVT.cjs} +692 -728
- package/dist/chunk-6F5CGBVT.cjs.map +1 -0
- package/dist/{chunk-HMFBMVJC.js → chunk-6GBA6YJN.js} +2 -2
- package/dist/{chunk-4HHYUEMD.js → chunk-7RLV3U5C.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-CMWRZECW.cjs} +77 -42
- package/dist/chunk-CMWRZECW.cjs.map +1 -0
- package/dist/{chunk-D524LJYO.js → chunk-HCMVLEC3.js} +4 -4
- package/dist/{chunk-LZL6P7SN.cjs → chunk-IG4QRNYZ.cjs} +10 -10
- package/dist/{chunk-LZL6P7SN.cjs.map → chunk-IG4QRNYZ.cjs.map} +1 -1
- package/dist/{chunk-CVGZJBHT.cjs → chunk-ISALYOFX.cjs} +19 -19
- package/dist/{chunk-CVGZJBHT.cjs.map → chunk-ISALYOFX.cjs.map} +1 -1
- package/dist/{chunk-MKVKVJQQ.js → chunk-J3QMPOS6.js} +5 -4
- package/dist/chunk-J3QMPOS6.js.map +1 -0
- package/dist/{chunk-LHR7MBUY.js → chunk-JK25REDL.js} +48 -13
- package/dist/chunk-JK25REDL.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-VAUI3CND.js → chunk-NR535XIY.js} +44 -1
- package/dist/chunk-NR535XIY.js.map +1 -0
- package/dist/{chunk-2FCXJTOV.js → chunk-QXSRQVRO.js} +3 -1
- package/dist/chunk-QXSRQVRO.js.map +1 -0
- package/dist/{chunk-6TB24AUH.js → chunk-S7IX7YU4.js} +154 -190
- package/dist/chunk-S7IX7YU4.js.map +1 -0
- package/dist/{chunk-6LK4VEXE.cjs → chunk-TSZBJVYK.cjs} +35 -31
- package/dist/chunk-TSZBJVYK.cjs.map +1 -0
- package/dist/{chunk-TU26IZPO.cjs → chunk-U6UNAC32.cjs} +3 -1
- package/dist/chunk-U6UNAC32.cjs.map +1 -0
- package/dist/{chunk-265KKBV2.js → chunk-V4IIBNTA.js} +3 -3
- package/dist/chunk-V4IIBNTA.js.map +1 -0
- package/dist/{chunk-2FAX6LZB.cjs → chunk-V4Q5PIBV.cjs} +30 -30
- package/dist/{chunk-2FAX6LZB.cjs.map → chunk-V4Q5PIBV.cjs.map} +1 -1
- package/dist/{chunk-LKI3YCAX.js → chunk-V5L4D454.js} +10 -6
- package/dist/chunk-V5L4D454.js.map +1 -0
- 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 +166 -156
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +54 -44
- 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 +25 -23
- package/dist/components/onboarding-guides/index.cjs.map +1 -1
- package/dist/components/onboarding-guides/index.js +6 -4
- package/dist/components/onboarding-guides/index.js.map +1 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts +4 -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/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 +17 -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.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 +12 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +16 -12
- package/dist/types/blog.d.ts +7 -0
- package/dist/types/blog.d.ts.map +1 -1
- package/dist/types/entity-author.d.ts +18 -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/index.cjs +6 -2
- 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 +6 -3
- 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/onboarding-guides/onboarding-guide-detail-view.tsx +5 -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/product-release/release-detail-page.tsx +18 -21
- package/src/components/social-icon-row.tsx +51 -22
- package/src/components/ui/button/button.tsx +7 -0
- package/src/types/blog.ts +9 -1
- package/src/types/entity-author.ts +17 -0
- package/src/types/index.ts +1 -0
- package/src/types/product-release.ts +2 -7
- package/src/utils/common.ts +12 -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-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-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-EFIEQ4XD.js.map → chunk-46DHPQY6.js.map} +0 -0
- /package/dist/{chunk-HMFBMVJC.js.map → chunk-6GBA6YJN.js.map} +0 -0
- /package/dist/{chunk-4HHYUEMD.js.map → chunk-7RLV3U5C.js.map} +0 -0
- /package/dist/{chunk-D524LJYO.js.map → chunk-HCMVLEC3.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
|
}
|
|
@@ -50,6 +50,9 @@ export interface OnboardingGuideDetailViewProps {
|
|
|
50
50
|
* e.g. `(s) => \`/content/api/onboarding-guides/${s}\``. */
|
|
51
51
|
guideEndpoint?: (slug: string) => string
|
|
52
52
|
related?: OnboardingGuide[]
|
|
53
|
+
/** Link target for the author name in the metadata grid — the host
|
|
54
|
+
* computes it (public author page; absent ⇒ plain text). */
|
|
55
|
+
authorHref?: string
|
|
53
56
|
/** Optional markdown renderer override. Defaults to lib
|
|
54
57
|
* `<SimpleMarkdownRenderer>`. */
|
|
55
58
|
MarkdownRenderer?: ComponentType<{ content: string }>
|
|
@@ -65,6 +68,7 @@ export interface OnboardingGuideDetailViewProps {
|
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
export function OnboardingGuideDetailView({
|
|
71
|
+
authorHref,
|
|
68
72
|
initialData,
|
|
69
73
|
slug,
|
|
70
74
|
guideEndpoint,
|
|
@@ -139,6 +143,7 @@ export function OnboardingGuideDetailView({
|
|
|
139
143
|
{/* Metadata grid — Section · Step | Published | Author. */}
|
|
140
144
|
<EntityAuthorCard
|
|
141
145
|
author={guide.author}
|
|
146
|
+
authorHref={authorHref}
|
|
142
147
|
publishedAt={guide.published_at}
|
|
143
148
|
extraCells={[
|
|
144
149
|
{
|
|
@@ -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).
|
|
@@ -11,12 +11,12 @@ import { PageShell } from '../../layout/article-detail-layout';
|
|
|
11
11
|
import { BackButton } from '../../layout/back-button';
|
|
12
12
|
import { ReleaseChangelogSection } from '../../ui/release-changelog-section';
|
|
13
13
|
import { EntityTagBadges } from '../../features/entity-tag-badges';
|
|
14
|
-
import {
|
|
14
|
+
import { EntityMetadataAuthorCell } from '../../chat/entity-cards/entity-author-card';
|
|
15
|
+
import type { EntityAuthor } from '../../../types/entity-author';
|
|
15
16
|
import { ImageGalleryModal } from '../../ui/image-gallery-modal';
|
|
16
17
|
import { GitHubIcon } from '../../icons/github-icon';
|
|
17
18
|
import { AlertTriangle, ExternalLink, BookMarked, Sparkles, TrendingUp, Wrench } from 'lucide-react';
|
|
18
19
|
import { formatReleaseDate } from '../../../utils/date-formatters';
|
|
19
|
-
import { nameInitials } from '../../../utils/format';
|
|
20
20
|
import { Video } from '../../features/video';
|
|
21
21
|
import { DetailPageSkeleton } from '../detail-page-skeleton';
|
|
22
22
|
import type { ChangelogEntry } from '../../../types/product-release';
|
|
@@ -103,6 +103,9 @@ export interface ReleaseDetailPageProps {
|
|
|
103
103
|
* `LegalDocumentPage`. Pass `false` to hide. Default
|
|
104
104
|
* `{ label: 'Back to home', href: '/' }`. */
|
|
105
105
|
backButton?: { label?: string; href?: string } | false;
|
|
106
|
+
/** Link target for the author name in the metadata grid — the host
|
|
107
|
+
* computes it (public author page; absent ⇒ plain text). */
|
|
108
|
+
authorHref?: string;
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
// Default simple markdown renderer (just renders as text)
|
|
@@ -111,6 +114,7 @@ function DefaultMarkdownRenderer({ content }: MarkdownRendererProps) {
|
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
export function ReleaseDetailPage({
|
|
117
|
+
authorHref,
|
|
114
118
|
slug,
|
|
115
119
|
initialData,
|
|
116
120
|
useRelease,
|
|
@@ -207,7 +211,9 @@ export function ReleaseDetailPage({
|
|
|
207
211
|
const releaseType = release.release_type as string;
|
|
208
212
|
const releaseStatus = release.release_status as string;
|
|
209
213
|
const releaseMedia = release.release_media as Array<{ id?: string; media_type: string; media_url: string; title?: string }> | undefined;
|
|
210
|
-
|
|
214
|
+
// Field-cast per this file's loose-release idiom (release_type etc. above)
|
|
215
|
+
// — but to the SHARED EntityAuthor, never an inline shadow author shape.
|
|
216
|
+
const author = release.author as EntityAuthor | undefined;
|
|
211
217
|
const githubReleases = release.github_releases as Array<{ id: string; github_release_url: string }> | undefined;
|
|
212
218
|
const knowledgeBaseLinks = release.knowledge_base_links as Array<{ id?: string; kb_article_path: string }> | string[] | undefined;
|
|
213
219
|
const migrationGuideUrl = release.migration_guide_url as string | undefined;
|
|
@@ -290,24 +296,15 @@ export function ReleaseDetailPage({
|
|
|
290
296
|
</div>
|
|
291
297
|
</div>
|
|
292
298
|
|
|
293
|
-
{/* Author
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
<div className="flex flex-col gap-0 flex-1 min-w-0">
|
|
303
|
-
<p className="text-h3 tracking-[-0.36px] text-ods-text-primary truncate" title={author?.full_name || 'Unknown Author'}>
|
|
304
|
-
{author?.full_name || 'Unknown Author'}
|
|
305
|
-
</p>
|
|
306
|
-
<p className="font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary">
|
|
307
|
-
Author
|
|
308
|
-
</p>
|
|
309
|
-
</div>
|
|
310
|
-
</div>
|
|
299
|
+
{/* Author — the shared metadata-grid author cell (it was extracted
|
|
300
|
+
FROM this page; rendering the export instead of an inline copy
|
|
301
|
+
keeps the two in lockstep). The stub author preserves the legacy
|
|
302
|
+
"Unknown Author" rendering for author-less releases; the
|
|
303
|
+
job_title-over-roleLabel rule matches the guide detail page. */}
|
|
304
|
+
<EntityMetadataAuthorCell
|
|
305
|
+
author={author ?? { full_name: null, avatar_url: null }}
|
|
306
|
+
authorHref={author?.full_name ? authorHref : undefined}
|
|
307
|
+
/>
|
|
311
308
|
</div>
|
|
312
309
|
|
|
313
310
|
{/* Image Gallery - Horizontal Scrolling */}
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import { Button } from './ui/button';
|
|
4
|
-
import { GitHubIcon, RedditIcon, XLogo, LinkedInIcon, LumaIcon, WhatsAppIcon, GlobeIcon, MessageCircleIcon, TelegramIcon, YouTubeIcon, InstagramIcon, FacebookIcon, SlackIcon } from './icons';
|
|
4
|
+
import { GitHubIcon, RedditIcon, XLogo, LinkedInIcon, LumaIcon, WhatsAppIcon, GlobeIcon, MessageCircleIcon, TelegramIcon, YouTubeIcon, InstagramIcon, FacebookIcon, SlackIcon, CopyIcon } from './icons';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/** Exactly ONE of `href` (anchor, target _blank) or `onClick` (action
|
|
7
|
+
* button — share popups via window.open inside the click gesture,
|
|
8
|
+
* copy-to-clipboard) — the discriminated union makes a dead no-action
|
|
9
|
+
* entry unrepresentable. */
|
|
10
|
+
type SocialLink = {
|
|
7
11
|
platform: string;
|
|
8
|
-
href: string;
|
|
9
12
|
label?: string;
|
|
10
|
-
}
|
|
13
|
+
} & (
|
|
14
|
+
| { href: string; onClick?: never }
|
|
15
|
+
| { onClick: () => void; href?: never }
|
|
16
|
+
);
|
|
11
17
|
|
|
12
18
|
interface SocialIconRowProps {
|
|
13
19
|
className?: string;
|
|
14
20
|
links?: SocialLink[];
|
|
15
21
|
variant?: "accent" | "outline" | "transparent" | "destructive" | null | undefined;
|
|
22
|
+
/** Natural icon-button sizing (w-fit container, no flex-1 stretch) for
|
|
23
|
+
* page-level rows. Default false: buttons stretch across the container —
|
|
24
|
+
* the original card-width behavior (TMCG member cards, footers). */
|
|
25
|
+
compact?: boolean;
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
const defaultLinks: SocialLink[] = [
|
|
@@ -57,32 +67,51 @@ function renderSocialIcon(platform: string) {
|
|
|
57
67
|
case 'facebook':
|
|
58
68
|
case 'fb':
|
|
59
69
|
return <FacebookIcon className="w-5 h-5" />;
|
|
70
|
+
case 'copy':
|
|
71
|
+
// CopyIcon's default fill is grey and would mismatch its row-mates —
|
|
72
|
+
// force the themed foreground via the ODS token (white on the dark
|
|
73
|
+
// theme, tracking the theme unlike the literal the reddit/slack cases
|
|
74
|
+
// still carry).
|
|
75
|
+
return <CopyIcon className="w-5 h-5" color="var(--ods-text-primary)" />;
|
|
60
76
|
default:
|
|
61
77
|
return <GlobeIcon className="w-5 h-5" />;
|
|
62
78
|
}
|
|
63
79
|
}
|
|
64
80
|
|
|
65
|
-
export function SocialIconRow({ className = '', links = defaultLinks, variant = 'outline' }: SocialIconRowProps) {
|
|
81
|
+
export function SocialIconRow({ className = '', links = defaultLinks, variant = 'outline', compact = false }: SocialIconRowProps) {
|
|
66
82
|
return (
|
|
67
|
-
<div className={`flex flex-row gap-3 w-full ${className}`}>
|
|
68
|
-
{links.map((link, index) =>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
<div className={`flex flex-row gap-3 ${compact ? 'w-fit' : 'w-full'} ${className}`}>
|
|
84
|
+
{links.map((link, index) => {
|
|
85
|
+
const ariaLabel = link.label || link.platform;
|
|
86
|
+
return link.onClick ? (
|
|
87
|
+
<Button
|
|
88
|
+
key={index}
|
|
89
|
+
type="button"
|
|
90
|
+
variant={variant}
|
|
91
|
+
size="icon"
|
|
92
|
+
className={compact ? undefined : 'flex-1'}
|
|
93
|
+
aria-label={ariaLabel}
|
|
94
|
+
onClick={link.onClick}
|
|
95
|
+
>
|
|
96
|
+
{renderSocialIcon(link.platform)}
|
|
97
|
+
</Button>
|
|
98
|
+
) : (
|
|
99
|
+
// Props-based linking — Button renders the anchor itself
|
|
100
|
+
// (openInNewTab carries target="_blank" + rel="noopener noreferrer");
|
|
101
|
+
// no asChild/<a> nesting.
|
|
102
|
+
<Button
|
|
103
|
+
key={index}
|
|
104
|
+
variant={variant}
|
|
105
|
+
size="icon"
|
|
106
|
+
className={compact ? undefined : 'flex-1'}
|
|
77
107
|
href={link.href}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
aria-label={link.label || link.platform}
|
|
108
|
+
openInNewTab
|
|
109
|
+
aria-label={ariaLabel}
|
|
81
110
|
>
|
|
82
111
|
{renderSocialIcon(link.platform)}
|
|
83
|
-
</
|
|
84
|
-
|
|
85
|
-
)
|
|
112
|
+
</Button>
|
|
113
|
+
);
|
|
114
|
+
})}
|
|
86
115
|
</div>
|
|
87
116
|
);
|
|
88
|
-
}
|
|
117
|
+
}
|
|
@@ -235,6 +235,10 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
|
|
235
235
|
prefetch={prefetch}
|
|
236
236
|
target={splitAnchor.target}
|
|
237
237
|
rel={splitAnchor.rel}
|
|
238
|
+
// The Link branches don't spread {...props} (button attrs don't
|
|
239
|
+
// belong on an anchor), but the accessible name MUST survive —
|
|
240
|
+
// icon-only link buttons have no text content.
|
|
241
|
+
aria-label={props['aria-label']}
|
|
238
242
|
aria-disabled={isDisabled || undefined}
|
|
239
243
|
tabIndex={isDisabled ? -1 : undefined}
|
|
240
244
|
className={cn(shellClasses, isDisabled && "pointer-events-none")}
|
|
@@ -300,6 +304,9 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
|
|
300
304
|
prefetch={prefetch}
|
|
301
305
|
target={anchor.target}
|
|
302
306
|
rel={anchor.rel}
|
|
307
|
+
// See the splitAnchor branch — keep the accessible name on the
|
|
308
|
+
// anchor render (icon-only link buttons have no text content).
|
|
309
|
+
aria-label={props['aria-label']}
|
|
303
310
|
aria-disabled={isDisabled || undefined}
|
|
304
311
|
tabIndex={isDisabled ? -1 : undefined}
|
|
305
312
|
className={cn(classes, isDisabled && "pointer-events-none")}
|
package/src/types/blog.ts
CHANGED
|
@@ -4,12 +4,20 @@ import type { UserProfile } from "./user"
|
|
|
4
4
|
export type Platform = string;
|
|
5
5
|
export type BlogStatus = 'draft' | 'ai_drafted' | 'published' | 'ai_published' | 'scheduled' | 'archived';
|
|
6
6
|
|
|
7
|
-
// Author interface for blog posts
|
|
7
|
+
// Author interface for blog posts (camelCase — the blog contract's own
|
|
8
|
+
// shape; detail-row authors elsewhere use the shared EntityAuthor).
|
|
8
9
|
export interface BlogAuthor {
|
|
9
10
|
id: number;
|
|
10
11
|
name?: string;
|
|
11
12
|
avatar?: string;
|
|
12
13
|
bio?: string;
|
|
14
|
+
jobTitle?: string;
|
|
15
|
+
/** Public author-page slug — present only when the hub's public-author
|
|
16
|
+
* gate passes (is_real profiles only). */
|
|
17
|
+
slug?: string;
|
|
18
|
+
/** Author's LinkedIn from profile_social_links — Article schema sameAs
|
|
19
|
+
* + the author.url fallback for slug-less authors. */
|
|
20
|
+
linkedinUrl?: string;
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
// Database Models
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hydrated content author — THE shared author shape attached to detail rows
|
|
3
|
+
* by the hub's `hydrateAuthor` (product releases, onboarding guides) and the
|
|
4
|
+
* per-entity author joins. One type for every author surface: metadata-grid
|
|
5
|
+
* author cells, bylines, author-page hrefs.
|
|
6
|
+
*/
|
|
7
|
+
export interface EntityAuthor {
|
|
8
|
+
id?: string
|
|
9
|
+
full_name: string | null
|
|
10
|
+
avatar_url: string | null
|
|
11
|
+
job_title?: string | null
|
|
12
|
+
/** Public author-page slug — present only when the hub's public-author
|
|
13
|
+
* gate passes (`is_real` profiles only). Feeds /authors/[slug] links. */
|
|
14
|
+
slug?: string
|
|
15
|
+
/** Present only on server/admin payloads — public DALs strip it. */
|
|
16
|
+
email?: string | null
|
|
17
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Following the blog.ts pattern for consistency
|
|
3
3
|
|
|
4
4
|
import type { PlatformRecord } from './platform'
|
|
5
|
+
import type { EntityAuthor } from './entity-author'
|
|
5
6
|
import type { BlogTag, TagAssoc } from './blog'
|
|
6
7
|
import type { VideoTeaser, TranscriptWord, SpeakerMapping } from './video-processing'
|
|
7
8
|
|
|
@@ -128,13 +129,7 @@ export interface ProductRelease {
|
|
|
128
129
|
* `hydrateAuthor` in the hub DAL. Optional so admin form + every existing
|
|
129
130
|
* consumer that doesn't read author stay backward-compatible.
|
|
130
131
|
*/
|
|
131
|
-
author?:
|
|
132
|
-
id: string
|
|
133
|
-
full_name: string
|
|
134
|
-
avatar_url: string | null
|
|
135
|
-
job_title: string | null
|
|
136
|
-
email?: string | null
|
|
137
|
-
}
|
|
132
|
+
author?: EntityAuthor
|
|
138
133
|
|
|
139
134
|
// Timestamps
|
|
140
135
|
created_at: string
|