@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.
Files changed (159) hide show
  1. package/dist/{chunk-LHR7MBUY.js → chunk-2VZQJIVA.js} +48 -13
  2. package/dist/chunk-2VZQJIVA.js.map +1 -0
  3. package/dist/{chunk-GYKDSPC4.cjs → chunk-3DC6MHW6.cjs} +31 -31
  4. package/dist/{chunk-GYKDSPC4.cjs.map → chunk-3DC6MHW6.cjs.map} +1 -1
  5. package/dist/{chunk-LZL6P7SN.cjs → chunk-4MXBBBHB.cjs} +108 -11
  6. package/dist/chunk-4MXBBBHB.cjs.map +1 -0
  7. package/dist/{chunk-HMFBMVJC.js → chunk-6GBA6YJN.js} +2 -2
  8. package/dist/{chunk-SGJBUL2Y.cjs → chunk-7VMEOFEA.cjs} +45 -2
  9. package/dist/chunk-7VMEOFEA.cjs.map +1 -0
  10. package/dist/{chunk-UGFSMA7K.cjs → chunk-AIQ4DGO7.cjs} +77 -42
  11. package/dist/chunk-AIQ4DGO7.cjs.map +1 -0
  12. package/dist/{chunk-6TB24AUH.js → chunk-CBIQDPHK.js} +155 -236
  13. package/dist/chunk-CBIQDPHK.js.map +1 -0
  14. package/dist/{chunk-6LK4VEXE.cjs → chunk-EB76VWM4.cjs} +118 -63
  15. package/dist/chunk-EB76VWM4.cjs.map +1 -0
  16. package/dist/{chunk-RCW2IL4K.cjs → chunk-FBH73RRM.cjs} +1254 -1335
  17. package/dist/chunk-FBH73RRM.cjs.map +1 -0
  18. package/dist/{chunk-4HHYUEMD.js → chunk-IJHORDN5.js} +100 -3
  19. package/dist/chunk-IJHORDN5.js.map +1 -0
  20. package/dist/{chunk-MKVKVJQQ.js → chunk-J3QMPOS6.js} +5 -4
  21. package/dist/chunk-J3QMPOS6.js.map +1 -0
  22. package/dist/{chunk-CVGZJBHT.cjs → chunk-LCYDTEEK.cjs} +19 -19
  23. package/dist/{chunk-CVGZJBHT.cjs.map → chunk-LCYDTEEK.cjs.map} +1 -1
  24. package/dist/{chunk-2FCXJTOV.js → chunk-LDEXTWMK.js} +10 -2
  25. package/dist/chunk-LDEXTWMK.js.map +1 -0
  26. package/dist/{chunk-Z4IBEIWD.cjs → chunk-LGLPNWS6.cjs} +3 -3
  27. package/dist/{chunk-Z4IBEIWD.cjs.map → chunk-LGLPNWS6.cjs.map} +1 -1
  28. package/dist/{chunk-TU26IZPO.cjs → chunk-N5UOJC3V.cjs} +10 -2
  29. package/dist/chunk-N5UOJC3V.cjs.map +1 -0
  30. package/dist/{chunk-LKI3YCAX.js → chunk-NAOTZAKM.js} +62 -7
  31. package/dist/chunk-NAOTZAKM.js.map +1 -0
  32. package/dist/{chunk-2FAX6LZB.cjs → chunk-NNDLCSGR.cjs} +30 -30
  33. package/dist/{chunk-2FAX6LZB.cjs.map → chunk-NNDLCSGR.cjs.map} +1 -1
  34. package/dist/{chunk-VAUI3CND.js → chunk-NR535XIY.js} +44 -1
  35. package/dist/chunk-NR535XIY.js.map +1 -0
  36. package/dist/{chunk-D524LJYO.js → chunk-RUMSS73U.js} +4 -4
  37. package/dist/{chunk-265KKBV2.js → chunk-V4IIBNTA.js} +3 -3
  38. package/dist/chunk-V4IIBNTA.js.map +1 -0
  39. package/dist/{chunk-EFIEQ4XD.js → chunk-YMZZ5THD.js} +4 -4
  40. package/dist/{chunk-3TDLFCCS.cjs → chunk-ZJP2BB3E.cjs} +6 -5
  41. package/dist/chunk-ZJP2BB3E.cjs.map +1 -0
  42. package/dist/components/chat/entity-cards/entity-author-card.d.ts +13 -9
  43. package/dist/components/chat/entity-cards/entity-author-card.d.ts.map +1 -1
  44. package/dist/components/chat/index.cjs +9 -7
  45. package/dist/components/chat/index.cjs.map +1 -1
  46. package/dist/components/chat/index.d.ts +1 -0
  47. package/dist/components/chat/index.d.ts.map +1 -1
  48. package/dist/components/chat/index.js +8 -6
  49. package/dist/components/chat/types/entities/onboarding-guide.d.ts +2 -7
  50. package/dist/components/chat/types/entities/onboarding-guide.d.ts.map +1 -1
  51. package/dist/components/contact/index.cjs +8 -8
  52. package/dist/components/contact/index.js +7 -7
  53. package/dist/components/faq/faq-section.d.ts.map +1 -1
  54. package/dist/components/faq/index.cjs +3 -3
  55. package/dist/components/faq/index.js +2 -2
  56. package/dist/components/features/index.cjs +7 -7
  57. package/dist/components/features/index.js +6 -6
  58. package/dist/components/icons/index.cjs +2 -2
  59. package/dist/components/icons/index.js +1 -1
  60. package/dist/components/index.cjs +168 -155
  61. package/dist/components/index.cjs.map +1 -1
  62. package/dist/components/index.d.ts +1 -0
  63. package/dist/components/index.d.ts.map +1 -1
  64. package/dist/components/index.js +58 -45
  65. package/dist/components/index.js.map +1 -1
  66. package/dist/components/navigation/index.cjs +7 -7
  67. package/dist/components/navigation/index.js +6 -6
  68. package/dist/components/onboarding-guides/index.cjs +43 -27
  69. package/dist/components/onboarding-guides/index.cjs.map +1 -1
  70. package/dist/components/onboarding-guides/index.js +20 -4
  71. package/dist/components/onboarding-guides/index.js.map +1 -1
  72. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts +11 -1
  73. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  74. package/dist/components/related-content/index.cjs +10 -8
  75. package/dist/components/related-content/index.cjs.map +1 -1
  76. package/dist/components/related-content/index.d.ts +1 -1
  77. package/dist/components/related-content/index.d.ts.map +1 -1
  78. package/dist/components/related-content/index.js +9 -7
  79. package/dist/components/related-content/related-content-section.d.ts +16 -1
  80. package/dist/components/related-content/related-content-section.d.ts.map +1 -1
  81. package/dist/components/shared/article-author-byline.d.ts +58 -0
  82. package/dist/components/shared/article-author-byline.d.ts.map +1 -0
  83. package/dist/components/shared/product-release/release-detail-page.d.ts +4 -1
  84. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  85. package/dist/components/social-icon-row.d.ts +19 -4
  86. package/dist/components/social-icon-row.d.ts.map +1 -1
  87. package/dist/components/tickets/index.cjs +80 -80
  88. package/dist/components/tickets/index.js +8 -8
  89. package/dist/components/ui/button/button.d.ts +1 -1
  90. package/dist/components/ui/button/button.d.ts.map +1 -1
  91. package/dist/components/ui/file-manager/index.cjs +20 -20
  92. package/dist/components/ui/file-manager/index.js +1 -1
  93. package/dist/components/ui/index.cjs +9 -7
  94. package/dist/components/ui/index.cjs.map +1 -1
  95. package/dist/components/ui/index.js +8 -6
  96. package/dist/hooks/index.cjs +3 -3
  97. package/dist/hooks/index.js +2 -2
  98. package/dist/index.cjs +14 -10
  99. package/dist/index.cjs.map +1 -1
  100. package/dist/index.js +20 -16
  101. package/dist/types/blog.d.ts +7 -0
  102. package/dist/types/blog.d.ts.map +1 -1
  103. package/dist/types/entity-author.d.ts +21 -0
  104. package/dist/types/entity-author.d.ts.map +1 -0
  105. package/dist/types/index.cjs.map +1 -1
  106. package/dist/types/index.d.ts +1 -0
  107. package/dist/types/index.d.ts.map +1 -1
  108. package/dist/types/index.js.map +1 -1
  109. package/dist/types/product-release.d.ts +2 -7
  110. package/dist/types/product-release.d.ts.map +1 -1
  111. package/dist/utils/common.d.ts +9 -0
  112. package/dist/utils/common.d.ts.map +1 -1
  113. package/dist/utils/content-href.d.ts.map +1 -1
  114. package/dist/utils/index.cjs +12 -3
  115. package/dist/utils/index.cjs.map +1 -1
  116. package/dist/utils/index.d.ts +1 -1
  117. package/dist/utils/index.d.ts.map +1 -1
  118. package/dist/utils/index.js +12 -4
  119. package/dist/utils/index.js.map +1 -1
  120. package/package.json +1 -1
  121. package/src/components/chat/entity-cards/entity-author-card.tsx +30 -11
  122. package/src/components/chat/index.ts +1 -0
  123. package/src/components/chat/types/entities/onboarding-guide.ts +3 -7
  124. package/src/components/faq/faq-section.tsx +4 -1
  125. package/src/components/icons/x-logo.tsx +2 -2
  126. package/src/components/index.ts +5 -0
  127. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +27 -0
  128. package/src/components/related-content/index.ts +1 -0
  129. package/src/components/related-content/related-content-section.tsx +84 -8
  130. package/src/components/shared/article-author-byline.tsx +187 -0
  131. package/src/components/shared/product-release/release-detail-page.tsx +18 -21
  132. package/src/components/social-icon-row.tsx +68 -22
  133. package/src/components/ui/button/button.tsx +13 -0
  134. package/src/types/blog.ts +9 -1
  135. package/src/types/entity-author.ts +20 -0
  136. package/src/types/index.ts +1 -0
  137. package/src/types/product-release.ts +2 -7
  138. package/src/utils/__tests__/content-href.test.ts +3 -0
  139. package/src/utils/common.ts +12 -0
  140. package/src/utils/content-href.ts +5 -0
  141. package/src/utils/index.ts +1 -1
  142. package/dist/chunk-265KKBV2.js.map +0 -1
  143. package/dist/chunk-2FCXJTOV.js.map +0 -1
  144. package/dist/chunk-3TDLFCCS.cjs.map +0 -1
  145. package/dist/chunk-4HHYUEMD.js.map +0 -1
  146. package/dist/chunk-6LK4VEXE.cjs.map +0 -1
  147. package/dist/chunk-6TB24AUH.js.map +0 -1
  148. package/dist/chunk-LHR7MBUY.js.map +0 -1
  149. package/dist/chunk-LKI3YCAX.js.map +0 -1
  150. package/dist/chunk-LZL6P7SN.cjs.map +0 -1
  151. package/dist/chunk-MKVKVJQQ.js.map +0 -1
  152. package/dist/chunk-RCW2IL4K.cjs.map +0 -1
  153. package/dist/chunk-SGJBUL2Y.cjs.map +0 -1
  154. package/dist/chunk-TU26IZPO.cjs.map +0 -1
  155. package/dist/chunk-UGFSMA7K.cjs.map +0 -1
  156. package/dist/chunk-VAUI3CND.js.map +0 -1
  157. /package/dist/{chunk-HMFBMVJC.js.map → chunk-6GBA6YJN.js.map} +0 -0
  158. /package/dist/{chunk-D524LJYO.js.map → chunk-RUMSS73U.js.map} +0 -0
  159. /package/dist/{chunk-EFIEQ4XD.js.map → chunk-YMZZ5THD.js.map} +0 -0
@@ -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 { SquareAvatar } from '../../ui/square-avatar';
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
- const author = release.author as { avatar_url?: string; full_name?: string } | undefined;
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
- <div className="bg-ods-card p-4 flex items-center gap-3">
295
- <SquareAvatar
296
- src={author?.avatar_url || ''}
297
- alt={author?.full_name || 'Author'}
298
- fallback={nameInitials(author?.full_name || 'Unknown', '')}
299
- size="md"
300
- variant="round"
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,30 @@
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
- interface SocialLink {
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
+ /** Quiet metadata row for page-level identity/share slots: 32px ghost
23
+ * icon buttons (size="icon-sm", 16px glyphs), gap-2, w-fit container,
24
+ * variant defaulting to "transparent" (an explicit `variant` still wins).
25
+ * Default false: 44/48px buttons stretching across the container —
26
+ * the original card-width behavior (TMCG member cards, footers). */
27
+ compact?: boolean;
16
28
  }
17
29
 
18
30
  const defaultLinks: SocialLink[] = [
@@ -57,32 +69,66 @@ function renderSocialIcon(platform: string) {
57
69
  case 'facebook':
58
70
  case 'fb':
59
71
  return <FacebookIcon className="w-5 h-5" />;
72
+ case 'copy':
73
+ // CopyIcon's default fill is grey and would mismatch its row-mates —
74
+ // force the themed foreground via the ODS token (white on the dark
75
+ // theme, tracking the theme unlike the literal the reddit/slack cases
76
+ // still carry).
77
+ return <CopyIcon className="w-5 h-5" color="var(--ods-text-primary)" />;
60
78
  default:
61
79
  return <GlobeIcon className="w-5 h-5" />;
62
80
  }
63
81
  }
64
82
 
65
- export function SocialIconRow({ className = '', links = defaultLinks, variant = 'outline' }: SocialIconRowProps) {
83
+ export function SocialIconRow({ className = '', links = defaultLinks, variant, compact = false }: SocialIconRowProps) {
84
+ // ── Compact design rationale ──────────────────────────────────────────
85
+ // Page-level identity/share rows read as METADATA, not CTAs. The major
86
+ // design systems converge on one recipe for this slot: a ~32px ghost icon
87
+ // button with a ~16px glyph, tight 8px gap, transparent at rest, subtle
88
+ // background state-layer on hover (Carbon "ghost" sm, Primer "invisible"
89
+ // medium, shadcn ghost+icon-sm — all 32px; Material 3 "standard" icon
90
+ // button = state-layer hover). Author headers on content platforms
91
+ // (Medium / dev.to / Substack) use the same quiet treatment. Hence
92
+ // compact: size="icon-sm" + variant defaulting to "transparent" — the
93
+ // hover affordance comes from the bg state layer (hover:bg-ods-bg-hover
94
+ // inside the variant) because the brand icons carry fixed fills, not
95
+ // currentColor. An explicit `variant` prop still wins (e.g. outline
96
+ // chips). Non-compact keeps the legacy outline default + full-width
97
+ // stretch untouched.
98
+ const resolvedVariant = variant !== undefined ? variant : (compact ? 'transparent' : 'outline');
66
99
  return (
67
- <div className={`flex flex-row gap-3 w-full ${className}`}>
68
- {links.map((link, index) => (
69
- <Button
70
- key={index}
71
- asChild
72
- variant={variant}
73
- size="icon"
74
- className="flex-1"
75
- >
76
- <a
100
+ <div className={`flex flex-row ${compact ? 'gap-2 w-fit' : 'gap-3 w-full'} ${className}`}>
101
+ {links.map((link, index) => {
102
+ const ariaLabel = link.label || link.platform;
103
+ return link.onClick ? (
104
+ <Button
105
+ key={index}
106
+ type="button"
107
+ variant={resolvedVariant}
108
+ size={compact ? 'icon-sm' : 'icon'}
109
+ className={compact ? undefined : 'flex-1'}
110
+ aria-label={ariaLabel}
111
+ onClick={link.onClick}
112
+ >
113
+ {renderSocialIcon(link.platform)}
114
+ </Button>
115
+ ) : (
116
+ // Props-based linking — Button renders the anchor itself
117
+ // (openInNewTab carries target="_blank" + rel="noopener noreferrer");
118
+ // no asChild/<a> nesting.
119
+ <Button
120
+ key={index}
121
+ variant={resolvedVariant}
122
+ size={compact ? 'icon-sm' : 'icon'}
123
+ className={compact ? undefined : 'flex-1'}
77
124
  href={link.href}
78
- target="_blank"
79
- rel="noopener noreferrer"
80
- aria-label={link.label || link.platform}
125
+ openInNewTab
126
+ aria-label={ariaLabel}
81
127
  >
82
128
  {renderSocialIcon(link.platform)}
83
- </a>
84
- </Button>
85
- ))}
129
+ </Button>
130
+ );
131
+ })}
86
132
  </div>
87
133
  );
88
- }
134
+ }
@@ -31,6 +31,12 @@ const buttonVariants = cva(
31
31
  small: "p-[var(--spacing-system-xs)] text-h5 h-6 md:h-8",
32
32
  "small-legacy": "py-[var(--spacing-system-xs)] px-[var(--spacing-system-m)] h-10 text-[14px] font-bold", // Temporary alias for "small" to avoid breaking changes in AnnouncementBar's CTA button; will be removed in the future
33
33
  icon: "p-[var(--spacing-system-sf)] h-11 w-11 md:h-12 md:w-12 [&_svg]:h-4 [&_svg]:w-4 md:[&_svg]:h-6 md:[&_svg]:w-6",
34
+ // Quiet 32px icon target with a 16px glyph, fixed across breakpoints
35
+ // (Carbon ghost sm / Primer medium / shadcn icon-sm all pin 32px;
36
+ // ≥ the 24px WCAG 2.5.8 target floor). For icon actions that read as
37
+ // metadata rather than CTAs — author-page social rows, share rows.
38
+ // Pair with variant="transparent" for the ghost treatment.
39
+ "icon-sm": "p-[var(--spacing-system-xxs)] h-8 w-8 [&_svg]:h-4 [&_svg]:w-4",
34
40
  },
35
41
  fullWidth: {
36
42
  true: "w-full",
@@ -235,6 +241,10 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
235
241
  prefetch={prefetch}
236
242
  target={splitAnchor.target}
237
243
  rel={splitAnchor.rel}
244
+ // The Link branches don't spread {...props} (button attrs don't
245
+ // belong on an anchor), but the accessible name MUST survive —
246
+ // icon-only link buttons have no text content.
247
+ aria-label={props['aria-label']}
238
248
  aria-disabled={isDisabled || undefined}
239
249
  tabIndex={isDisabled ? -1 : undefined}
240
250
  className={cn(shellClasses, isDisabled && "pointer-events-none")}
@@ -300,6 +310,9 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
300
310
  prefetch={prefetch}
301
311
  target={anchor.target}
302
312
  rel={anchor.rel}
313
+ // See the splitAnchor branch — keep the accessible name on the
314
+ // anchor render (icon-only link buttons have no text content).
315
+ aria-label={props['aria-label']}
303
316
  aria-disabled={isDisabled || undefined}
304
317
  tabIndex={isDisabled ? -1 : undefined}
305
318
  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,20 @@
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
+ /** Author bio paragraph (`profiles.about`) — feeds the end-of-article
13
+ * `<ArticleAuthorByline bio>` on every detail page with an author. */
14
+ about?: string | null
15
+ /** Public author-page slug — present only when the hub's public-author
16
+ * gate passes (`is_real` profiles only). Feeds /authors/[slug] links. */
17
+ slug?: string
18
+ /** Present only on server/admin payloads — public DALs strip it. */
19
+ email?: string | null
20
+ }
@@ -16,6 +16,7 @@ export * from './auth'
16
16
 
17
17
  // Content types
18
18
  export * from './blog'
19
+ export * from './entity-author'
19
20
  export * from './case-study'
20
21
  export * from './customer-interview'
21
22
  export * from './customer-interview-ai.types'
@@ -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
@@ -132,6 +132,9 @@ describe('DEFAULT_CONTENT_SUFFIXES', () => {
132
132
  webinar: 'webinars',
133
133
  podcast: 'podcasts',
134
134
  event: 'events',
135
+ // Hub-side home of the segment: AUTHORS_PATH in lib/utils/breadcrumbs.ts
136
+ // (PUBLIC_URL_PATHS.author derives from it).
137
+ author: 'authors',
135
138
  })
136
139
  })
137
140
  })
@@ -49,6 +49,18 @@ export function truncateString(str: string, maxLength: number, suffix = "..."):
49
49
  return str.substring(0, maxLength - suffix.length) + suffix
50
50
  }
51
51
 
52
+ /**
53
+ * Serialize a JSON-LD schema for a `<script type="application/ld+json">`
54
+ * block — THE one home of the escape rule (the hub re-exports this from
55
+ * lib/utils/breadcrumbs). JSON.stringify does NOT HTML-escape: a stored
56
+ * "</script>" inside any admin/user-entered field would terminate the tag
57
+ * early (stored XSS). Every "<" becomes the JSON escape sequence
58
+ * backslash-u003c — still valid JSON, inert in HTML.
59
+ */
60
+ export function serializeJsonLd(schema: unknown): string {
61
+ return JSON.stringify(schema).replace(/</g, "\\u003c")
62
+ }
63
+
52
64
  /**
53
65
  * Deep clone an object
54
66
  * @param obj - Object to clone
@@ -44,6 +44,11 @@ export const DEFAULT_CONTENT_SUFFIXES: Record<string, string> = {
44
44
  webinar: 'webinars',
45
45
  podcast: 'podcasts',
46
46
  event: 'events',
47
+ // Author profile pages (`/authors/<slug>`) — identified by the profile
48
+ // slug. Mirrors the hub's `PUBLIC_URL_PATHS.author`, which derives from
49
+ // `AUTHORS_PATH` in the hub's `lib/utils/breadcrumbs.ts` (the hub-side
50
+ // SSOT for the segment).
51
+ author: 'authors',
47
52
  }
48
53
 
49
54
  /** Input to the unified content-href seam. ONE object covers both callers:
@@ -12,7 +12,7 @@ export { formatDate, formatNumber, formatPrice, formatBytes } from './format'
12
12
  // SVG path constants — re-exported here (server-safe) because icons-v2 has "use client"
13
13
  export { PLAY_ICON_PATH } from '../components/icons-v2-generated/media-playback/play-icon'
14
14
  export { getPlatformAccentColor, getCurrentPlatform, type ColorCategory, HEX_PATTERN } from './ods-color-utils'
15
- export { delay, generateRandomString, truncateString, deepClone, getSlackCommunityJoinUrl } from './common'
15
+ export { delay, generateRandomString, truncateString, deepClone, getSlackCommunityJoinUrl, serializeJsonLd } from './common'
16
16
  export { getBaseUrl } from '../utils/cn'
17
17
 
18
18
  export * from './platform-config'