@flamingo-stack/openframe-frontend-core 0.0.337 → 0.0.338-snapshot.20260629235958

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 (121) hide show
  1. package/dist/{chunk-6XIB47I2.cjs → chunk-4DXRKAV6.cjs} +5 -5
  2. package/dist/{chunk-6XIB47I2.cjs.map → chunk-4DXRKAV6.cjs.map} +1 -1
  3. package/dist/{chunk-XB5LZ2YF.js → chunk-5A5SLX2F.js} +2 -2
  4. package/dist/{chunk-SKK3C6RE.cjs → chunk-6YWPFPKC.cjs} +25 -25
  5. package/dist/{chunk-SKK3C6RE.cjs.map → chunk-6YWPFPKC.cjs.map} +1 -1
  6. package/dist/{chunk-6G4NIF4Y.js → chunk-742ZCG3D.js} +4 -4
  7. package/dist/{chunk-6OA77Y55.cjs → chunk-7OPVJDX6.cjs} +15 -15
  8. package/dist/{chunk-6OA77Y55.cjs.map → chunk-7OPVJDX6.cjs.map} +1 -1
  9. package/dist/{chunk-TXVO2E5C.cjs → chunk-D5BRTQEA.cjs} +26 -26
  10. package/dist/{chunk-TXVO2E5C.cjs.map → chunk-D5BRTQEA.cjs.map} +1 -1
  11. package/dist/{chunk-HKAOAKPW.cjs → chunk-DKK3VRRM.cjs} +12 -12
  12. package/dist/{chunk-HKAOAKPW.cjs.map → chunk-DKK3VRRM.cjs.map} +1 -1
  13. package/dist/{chunk-EX5IVHLS.js → chunk-DLXHC6JS.js} +2 -2
  14. package/dist/{chunk-OKPYF6AI.js → chunk-DUJINSKJ.js} +2 -2
  15. package/dist/{chunk-LF2BC2Z6.cjs → chunk-FFNTLQZR.cjs} +60 -60
  16. package/dist/chunk-FFNTLQZR.cjs.map +1 -0
  17. package/dist/{chunk-75I2PDDE.cjs → chunk-GBE2R7R4.cjs} +7 -7
  18. package/dist/{chunk-75I2PDDE.cjs.map → chunk-GBE2R7R4.cjs.map} +1 -1
  19. package/dist/{chunk-AAPECG2D.js → chunk-GEJQRRD2.js} +8 -8
  20. package/dist/chunk-GEJQRRD2.js.map +1 -0
  21. package/dist/{chunk-X5DAKNSR.js → chunk-HHS3CJ7G.js} +2 -2
  22. package/dist/{chunk-KNTKGT4X.js → chunk-I54DGTLP.js} +3171 -3165
  23. package/dist/chunk-I54DGTLP.js.map +1 -0
  24. package/dist/{chunk-IZ72DTGG.js → chunk-I7NP2ZAJ.js} +4 -4
  25. package/dist/{chunk-43LYMONR.js → chunk-IK22LSYP.js} +4 -4
  26. package/dist/{chunk-7JZFRSMF.cjs → chunk-J344NBBP.cjs} +37 -37
  27. package/dist/{chunk-7JZFRSMF.cjs.map → chunk-J344NBBP.cjs.map} +1 -1
  28. package/dist/{chunk-WQZPEEFC.js → chunk-J7VJHE6O.js} +2 -2
  29. package/dist/{chunk-3YN5ZRHG.cjs → chunk-JOLEA7LT.cjs} +31 -31
  30. package/dist/{chunk-3YN5ZRHG.cjs.map → chunk-JOLEA7LT.cjs.map} +1 -1
  31. package/dist/{chunk-SZJLJWWO.cjs → chunk-KPR563UC.cjs} +62 -62
  32. package/dist/{chunk-SZJLJWWO.cjs.map → chunk-KPR563UC.cjs.map} +1 -1
  33. package/dist/{chunk-A3AMVJDJ.cjs → chunk-OJJCWMLT.cjs} +9 -9
  34. package/dist/{chunk-A3AMVJDJ.cjs.map → chunk-OJJCWMLT.cjs.map} +1 -1
  35. package/dist/{chunk-ASMOZBD6.cjs → chunk-PNWORHOR.cjs} +401 -395
  36. package/dist/chunk-PNWORHOR.cjs.map +1 -0
  37. package/dist/{chunk-3B6JQUCN.js → chunk-S5B3ZX6U.js} +2 -2
  38. package/dist/{chunk-F77UFU5A.js → chunk-YX7RIC4Z.js} +2 -2
  39. package/dist/components/case-studies/index.cjs +8 -8
  40. package/dist/components/case-studies/index.js +2 -2
  41. package/dist/components/chat/index.cjs +2 -2
  42. package/dist/components/chat/index.js +1 -1
  43. package/dist/components/contact/index.cjs +3 -3
  44. package/dist/components/contact/index.js +2 -2
  45. package/dist/components/docs/index.cjs +5 -5
  46. package/dist/components/docs/index.js +4 -4
  47. package/dist/components/embeds/index.cjs +3 -3
  48. package/dist/components/embeds/index.js +2 -2
  49. package/dist/components/faq/index.cjs +3 -3
  50. package/dist/components/faq/index.js +2 -2
  51. package/dist/components/features/entity-summary-editor.d.ts.map +1 -1
  52. package/dist/components/features/highlight-video-combined-section.d.ts.map +1 -1
  53. package/dist/components/features/highlight-video-preview.d.ts.map +1 -1
  54. package/dist/components/features/highlight-video-section.d.ts.map +1 -1
  55. package/dist/components/features/index.cjs +2 -2
  56. package/dist/components/features/index.js +1 -1
  57. package/dist/components/features/seo-editor-preview.d.ts.map +1 -1
  58. package/dist/components/features/transcript-summary-editor.d.ts.map +1 -1
  59. package/dist/components/features/video-source-selector.d.ts.map +1 -1
  60. package/dist/components/help-center-pages/index.cjs +20 -20
  61. package/dist/components/help-center-pages/index.js +11 -11
  62. package/dist/components/index.cjs +123 -121
  63. package/dist/components/index.cjs.map +1 -1
  64. package/dist/components/index.js +11 -9
  65. package/dist/components/index.js.map +1 -1
  66. package/dist/components/layout/page-layout.d.ts +6 -1
  67. package/dist/components/layout/page-layout.d.ts.map +1 -1
  68. package/dist/components/layout/title-block.d.ts +8 -1
  69. package/dist/components/layout/title-block.d.ts.map +1 -1
  70. package/dist/components/navigation/index.cjs +2 -2
  71. package/dist/components/navigation/index.js +1 -1
  72. package/dist/components/onboarding-guides/index.cjs +5 -5
  73. package/dist/components/onboarding-guides/index.js +4 -4
  74. package/dist/components/related-content/index.cjs +3 -3
  75. package/dist/components/related-content/index.js +2 -2
  76. package/dist/components/tickets/index.cjs +5 -5
  77. package/dist/components/tickets/index.js +4 -4
  78. package/dist/components/ui/ai-generated-badge.d.ts +10 -0
  79. package/dist/components/ui/ai-generated-badge.d.ts.map +1 -0
  80. package/dist/components/ui/index.cjs +4 -2
  81. package/dist/components/ui/index.cjs.map +1 -1
  82. package/dist/components/ui/index.d.ts +1 -0
  83. package/dist/components/ui/index.d.ts.map +1 -1
  84. package/dist/components/ui/index.js +3 -1
  85. package/dist/components/ui/info-card.d.ts.map +1 -1
  86. package/dist/components/ui/release-changelog-section.d.ts +2 -2
  87. package/dist/components/ui/release-changelog-section.d.ts.map +1 -1
  88. package/dist/components/ui/table/table-skeleton.d.ts.map +1 -1
  89. package/dist/index.cjs +4 -2
  90. package/dist/index.cjs.map +1 -1
  91. package/dist/index.js +3 -1
  92. package/package.json +1 -1
  93. package/src/components/features/entity-summary-editor.tsx +2 -4
  94. package/src/components/features/highlight-video-combined-section.tsx +2 -4
  95. package/src/components/features/highlight-video-preview.tsx +2 -4
  96. package/src/components/features/highlight-video-section.tsx +2 -4
  97. package/src/components/features/seo-editor-preview.tsx +4 -12
  98. package/src/components/features/transcript-summary-editor.tsx +3 -8
  99. package/src/components/features/video-source-selector.tsx +2 -4
  100. package/src/components/layout/page-layout.tsx +10 -1
  101. package/src/components/layout/title-block.tsx +56 -5
  102. package/src/components/shared/product-release/release-detail-page.tsx +4 -4
  103. package/src/components/ui/ai-generated-badge.tsx +17 -0
  104. package/src/components/ui/index.ts +1 -0
  105. package/src/components/ui/info-card.tsx +55 -41
  106. package/src/components/ui/release-changelog-section.tsx +19 -14
  107. package/src/components/ui/table/table-skeleton.tsx +6 -2
  108. package/dist/chunk-AAPECG2D.js.map +0 -1
  109. package/dist/chunk-ASMOZBD6.cjs.map +0 -1
  110. package/dist/chunk-KNTKGT4X.js.map +0 -1
  111. package/dist/chunk-LF2BC2Z6.cjs.map +0 -1
  112. /package/dist/{chunk-XB5LZ2YF.js.map → chunk-5A5SLX2F.js.map} +0 -0
  113. /package/dist/{chunk-6G4NIF4Y.js.map → chunk-742ZCG3D.js.map} +0 -0
  114. /package/dist/{chunk-EX5IVHLS.js.map → chunk-DLXHC6JS.js.map} +0 -0
  115. /package/dist/{chunk-OKPYF6AI.js.map → chunk-DUJINSKJ.js.map} +0 -0
  116. /package/dist/{chunk-X5DAKNSR.js.map → chunk-HHS3CJ7G.js.map} +0 -0
  117. /package/dist/{chunk-IZ72DTGG.js.map → chunk-I7NP2ZAJ.js.map} +0 -0
  118. /package/dist/{chunk-43LYMONR.js.map → chunk-IK22LSYP.js.map} +0 -0
  119. /package/dist/{chunk-WQZPEEFC.js.map → chunk-J7VJHE6O.js.map} +0 -0
  120. /package/dist/{chunk-3B6JQUCN.js.map → chunk-S5B3ZX6U.js.map} +0 -0
  121. /package/dist/{chunk-F77UFU5A.js.map → chunk-YX7RIC4Z.js.map} +0 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  import React, { useState, useCallback, useEffect } from 'react';
4
4
  import { Upload, Sparkles, X, Video, Loader2 } from 'lucide-react';
5
+ import { AIGeneratedBadge } from '../ui/ai-generated-badge';
5
6
  import { Button } from '../ui/button';
6
7
  import { Input } from '../ui/input';
7
8
  import { Label } from '../ui/label';
@@ -201,10 +202,7 @@ export function VideoSourceSelector({
201
202
  <div className="flex items-center gap-2">
202
203
  <Label>{uploadLabel}</Label>
203
204
  {showAIBadge && isAIGenerated && (
204
- <Badge variant="secondary" className="flex items-center gap-1">
205
- <Sparkles className="h-3 w-3" />
206
- AI Generated
207
- </Badge>
205
+ <AIGeneratedBadge />
208
206
  )}
209
207
  </div>
210
208
  <Button
@@ -58,6 +58,11 @@ export interface PageLayoutProps {
58
58
  /** Title typography size, forwarded to `TitleBlock`. Default `'h2'` (frozen
59
59
  * baseline). Pass `'h1'` for the unified Help Center pages. */
60
60
  titleSize?: 'h1' | 'h2'
61
+ /** Optional node rendered inline next to the title (e.g. a status `Tag`), forwarded to `TitleBlock`. */
62
+ titleAdornment?: React.ReactNode
63
+ /** When true, the title/subtitle render as line-box-accurate skeleton bars (forwarded to
64
+ * `TitleBlock`). Header height stays identical to the loaded state — for page skeletons. */
65
+ loading?: boolean
61
66
  }
62
67
 
63
68
  /**
@@ -80,10 +85,12 @@ export function PageLayout({
80
85
  contentClassName,
81
86
  showHeader = true,
82
87
  titleSize,
88
+ titleAdornment,
89
+ loading,
83
90
  }: PageLayoutProps) {
84
91
  const hasActions = actions && actions.length > 0
85
92
  const needsBottomPadding = hasActions && actionsVariant === 'primary-buttons'
86
- const hasHeader = showHeader && (title || subtitle || image || backButton || hasActions || selector)
93
+ const hasHeader = showHeader && (title || subtitle || image || backButton || hasActions || selector || loading)
87
94
 
88
95
  return (
89
96
  <div className={cn('flex flex-col w-full', className)}>
@@ -99,6 +106,8 @@ export function PageLayout({
99
106
  selector={selector}
100
107
  variant={headerVariant}
101
108
  titleSize={titleSize}
109
+ titleAdornment={titleAdornment}
110
+ loading={loading}
102
111
  />
103
112
  )}
104
113
 
@@ -29,6 +29,15 @@
29
29
  * opt the title typography up to `text-h1` (used by the unified Help Center
30
30
  * pages). This is additive and default-preserving; do NOT change the default or
31
31
  * touch anything else here.
32
+ *
33
+ * SANCTIONED EXCEPTION (2026-06, explicit human sign-off): the OPTIONAL
34
+ * `titleAdornment` and `loading` props. `titleAdornment` renders a node inline
35
+ * after the title (e.g. a status `Tag`). `loading` swaps ONLY the title/subtitle
36
+ * TEXT for line-box-accurate skeleton bars (the surrounding `h1`/`p` and their
37
+ * typography are untouched, so a loading header is pixel-identical in height to
38
+ * the loaded one — used by page skeletons that render through `PageLayout`).
39
+ * Both are additive + default-preserving: omit them and every existing caller is
40
+ * byte-identical. Do NOT change defaults or touch anything else here.
32
41
  * ========================================================================== */
33
42
 
34
43
  import React from 'react'
@@ -70,6 +79,30 @@ export interface TitleBlockProps {
70
79
  * opt the title up to `text-h1` (the unified Help Center pages). Subtitle stays
71
80
  * `text-h6` either way. */
72
81
  titleSize?: 'h1' | 'h2'
82
+ /** Optional node rendered inline, immediately after the title (e.g. a status `Tag`).
83
+ * Additive + default-preserving: omit it and every existing caller is byte-identical. */
84
+ titleAdornment?: React.ReactNode
85
+ /** When true, the title/subtitle TEXT is replaced by line-box-accurate skeleton bars
86
+ * (typography + surrounding markup unchanged, so header height is identical to loaded).
87
+ * Additive + default-preserving: omit it and every existing caller is byte-identical. */
88
+ loading?: boolean
89
+ }
90
+
91
+ /**
92
+ * Inline text skeleton — used only in `loading` mode, placed directly inside the real
93
+ * `h1`/`p`. It is a single `inline-block` bar whose height is intentionally SHORTER than the
94
+ * typography line-height, and it uses the default baseline alignment. That way the element's
95
+ * own line-box STRUT (text-h2 → 40px, text-h6 → 20px) sets the height exactly as it would for
96
+ * real text — the bar fits within the ascent and never inflates the line. So a loading header
97
+ * is pixel-identical in height to the loaded one. Phrasing-valid (`span` only).
98
+ */
99
+ function TitleTextSkeleton({ widthClass, heightClass }: { widthClass: string; heightClass: string }) {
100
+ return (
101
+ <span
102
+ aria-hidden
103
+ className={cn('inline-block max-w-full rounded-md bg-ods-border animate-pulse', widthClass, heightClass)}
104
+ />
105
+ )
73
106
  }
74
107
 
75
108
  export function TitleBlock({
@@ -84,6 +117,8 @@ export function TitleBlock({
84
117
  variant = 'plain',
85
118
  className,
86
119
  titleSize = 'h2',
120
+ titleAdornment,
121
+ loading,
87
122
  }: TitleBlockProps) {
88
123
  const hasActions = actions && actions.length > 0
89
124
  const hasMenuActions = !!menuActions && menuActions.some(g => g.items.length > 0)
@@ -115,7 +150,7 @@ export function TitleBlock({
115
150
  className="hidden md:inline-flex"
116
151
  />
117
152
  )}
118
- {(image || subtitle) ? (
153
+ {(image || subtitle || loading) ? (
119
154
  <div className="flex items-center gap-[var(--spacing-system-m)] min-w-0 w-full">
120
155
  {image && (
121
156
  <EntityImage
@@ -125,16 +160,32 @@ export function TitleBlock({
125
160
  />
126
161
  )}
127
162
  <div className="flex flex-col justify-center min-w-0 flex-1">
128
- {title && (
129
- <h1 className={cn(titleClass, 'text-ods-text-primary truncate')} title={title}>{title}</h1>
163
+ {(loading || title) && (
164
+ titleAdornment ? (
165
+ <div className="flex items-center gap-[var(--spacing-system-m)] min-w-0 w-full">
166
+ <h1 className={cn(titleClass, 'text-ods-text-primary truncate min-w-0')} title={title}>{loading ? <TitleTextSkeleton widthClass="w-48 md:w-72" heightClass="h-4 md:h-6" /> : title}</h1>
167
+ <span className="shrink-0">{titleAdornment}</span>
168
+ </div>
169
+ ) : (
170
+ <h1 className={cn(titleClass, 'text-ods-text-primary truncate')} title={title}>{loading ? <TitleTextSkeleton widthClass="w-48 md:w-72" heightClass="h-4 md:h-6" /> : title}</h1>
171
+ )
130
172
  )}
131
173
  {subtitle && (
132
- <p className="text-h6 text-ods-text-secondary truncate" title={subtitle}>{subtitle}</p>
174
+ <p className="text-h6 text-ods-text-secondary truncate" title={subtitle}>{loading ? <TitleTextSkeleton widthClass="w-28 md:w-36" heightClass="h-2.5 md:h-3" /> : subtitle}</p>
133
175
  )}
134
176
  </div>
135
177
  </div>
136
178
  ) : (
137
- title && <h1 className={cn(titleClass, 'text-ods-text-primary')}>{title}</h1>
179
+ title && (
180
+ titleAdornment ? (
181
+ <div className="flex items-center gap-[var(--spacing-system-m)] min-w-0 w-full">
182
+ <h1 className={cn(titleClass, 'text-ods-text-primary truncate min-w-0')} title={title}>{title}</h1>
183
+ <span className="shrink-0">{titleAdornment}</span>
184
+ </div>
185
+ ) : (
186
+ <h1 className={cn(titleClass, 'text-ods-text-primary')}>{title}</h1>
187
+ )
188
+ )
138
189
  )}
139
190
  </div>
140
191
 
@@ -402,7 +402,7 @@ export function ReleaseDetailPage({
402
402
  isBreaking
403
403
  hideTitle
404
404
  icon={<AlertTriangle className="h-6 w-6" />}
405
- SimpleMarkdownRenderer={MarkdownRenderer}
405
+ MarkdownRenderer={MarkdownRenderer}
406
406
  />
407
407
  {/* Features / Bugs / Improvements use `previewFirst` — same
408
408
  progressive-disclosure pattern as the investor-update detail
@@ -415,21 +415,21 @@ export function ReleaseDetailPage({
415
415
  entries={featuresAdded || []}
416
416
  icon={<Sparkles className="h-6 w-6" />}
417
417
  previewFirst
418
- SimpleMarkdownRenderer={MarkdownRenderer}
418
+ MarkdownRenderer={MarkdownRenderer}
419
419
  />
420
420
  <ReleaseChangelogSection
421
421
  title="Bugs Fixed"
422
422
  entries={bugFixed || []}
423
423
  icon={<Wrench className="h-6 w-6" />}
424
424
  previewFirst
425
- SimpleMarkdownRenderer={MarkdownRenderer}
425
+ MarkdownRenderer={MarkdownRenderer}
426
426
  />
427
427
  <ReleaseChangelogSection
428
428
  title="Improvements"
429
429
  entries={improvements || []}
430
430
  icon={<TrendingUp className="h-6 w-6" />}
431
431
  previewFirst
432
- SimpleMarkdownRenderer={MarkdownRenderer}
432
+ MarkdownRenderer={MarkdownRenderer}
433
433
  />
434
434
 
435
435
  {/* Video Bites Section - Only when VideoDisplaySection is not handling it */}
@@ -0,0 +1,17 @@
1
+ import { Badge } from './badge';
2
+ import { Sparkles } from 'lucide-react';
3
+
4
+ /**
5
+ * Canonical "AI Generated" chip (Sparkles + label). Single source of truth for
6
+ * every AI-generated indicator across the app — replaces the byte-identical
7
+ * inline `<Badge variant="secondary"><Sparkles/> AI Generated</Badge>` that was
8
+ * copy-pasted across the summary / video / SEO editors.
9
+ */
10
+ export function AIGeneratedBadge({ className }: { className?: string }) {
11
+ return (
12
+ <Badge variant="secondary" className={`flex items-center gap-1${className ? ` ${className}` : ''}`}>
13
+ <Sparkles className="h-3 w-3" />
14
+ AI Generated
15
+ </Badge>
16
+ );
17
+ }
@@ -52,6 +52,7 @@ export * from './tab-navigation'
52
52
  // Feedback components
53
53
  export * from './alert'
54
54
  export * from './badge'
55
+ export * from './ai-generated-badge'
55
56
  export * from './progress'
56
57
  export * from './release-changelog-section'
57
58
  export * from './status-badge'
@@ -55,52 +55,66 @@ export function InfoCard({ data, className = '' }: InfoCardProps) {
55
55
  className,
56
56
  )}
57
57
  >
58
- <div className="p-[var(--spacing-system-m)] flex flex-col gap-[var(--spacing-system-s)] items-start w-full">
59
- {data.title && (
60
- <div className="flex items-center gap-[var(--spacing-system-xsf)] self-stretch h-6">
61
- <span className="text-h4 text-ods-text-primary truncate" title={data.title}>
62
- {data.title}
63
- </span>
64
- {data.icon}
65
- </div>
66
- )}
67
-
68
- {/* Subtitle */}
69
- {data.subtitle && (
70
- <div className="text-h4 text-ods-text-secondary truncate self-stretch" title={data.subtitle}>
71
- {data.subtitle}
58
+ {/* Header (title + subtitle) and body (rows + progress) are two groups separated by
59
+ `gap-l`; the title/subtitle stack tightly and the rows use `gap-xs` — matching the ODS
60
+ "Info-card" design. With no title/subtitle, the body is the only group. */}
61
+ <div className="p-[var(--spacing-system-m)] flex flex-col gap-[var(--spacing-system-l)] items-start w-full">
62
+ {(data.title || data.subtitle) && (
63
+ <div className="flex flex-col items-start self-stretch w-full">
64
+ {data.title && (
65
+ <div className="flex items-center gap-[var(--spacing-system-xsf)] self-stretch">
66
+ <span className="text-h4 text-ods-text-primary truncate min-w-0" title={data.title}>
67
+ {data.title}
68
+ </span>
69
+ {data.icon}
70
+ </div>
71
+ )}
72
+ {data.subtitle && (
73
+ <div className="flex items-center gap-[var(--spacing-system-xsf)] self-stretch">
74
+ <span className="text-h4 text-ods-text-secondary truncate min-w-0" title={data.subtitle}>
75
+ {data.subtitle}
76
+ </span>
77
+ {/* Icon lives with the title when present; otherwise it falls to the subtitle so
78
+ subtitle-only cards still render it. */}
79
+ {!data.title && data.icon}
80
+ </div>
81
+ )}
72
82
  </div>
73
83
  )}
74
84
 
75
- {/* Info items */}
76
- {data.items.map((item, index) => {
77
- const values = Array.isArray(item.value) ? item.value : [item.value]
85
+ {(data.items.length > 0 || data.progress) && (
86
+ <div className="flex flex-col gap-[var(--spacing-system-xs)] items-start self-stretch w-full">
87
+ {/* Info items */}
88
+ {data.items.map((item, index) => {
89
+ const values = Array.isArray(item.value) ? item.value : [item.value]
78
90
 
79
- return (
80
- <React.Fragment key={index}>
81
- {values.map((val, valIndex) => (
82
- <InfoCardValueRow
83
- key={`${index}-${valIndex}`}
84
- label={item.label}
85
- value={val}
86
- showLabel={valIndex === 0}
87
- copyable={item.copyable}
88
- icon={valIndex === 0 ? item.icon : undefined}
89
- copyAriaLabel={`Copy ${item.label ?? 'value'} ${valIndex + 1}`}
90
- />
91
- ))}
92
- </React.Fragment>
93
- )
94
- })}
91
+ return (
92
+ <React.Fragment key={index}>
93
+ {values.map((val, valIndex) => (
94
+ <InfoCardValueRow
95
+ key={`${index}-${valIndex}`}
96
+ label={item.label}
97
+ value={val}
98
+ showLabel={valIndex === 0}
99
+ copyable={item.copyable}
100
+ icon={valIndex === 0 ? item.icon : undefined}
101
+ copyAriaLabel={`Copy ${item.label ?? 'value'} ${valIndex + 1}`}
102
+ />
103
+ ))}
104
+ </React.Fragment>
105
+ )
106
+ })}
95
107
 
96
- {/* Progress bar */}
97
- {data.progress && (
98
- <ProgressBar
99
- progress={data.progress.value}
100
- warningThreshold={data.progress.warningThreshold}
101
- criticalThreshold={data.progress.criticalThreshold}
102
- inverted={data.progress.inverted}
103
- />
108
+ {/* Progress bar */}
109
+ {data.progress && (
110
+ <ProgressBar
111
+ progress={data.progress.value}
112
+ warningThreshold={data.progress.warningThreshold}
113
+ criticalThreshold={data.progress.criticalThreshold}
114
+ inverted={data.progress.inverted}
115
+ />
116
+ )}
117
+ </div>
104
118
  )}
105
119
  </div>
106
120
 
@@ -36,7 +36,7 @@ interface ReleaseChangelogSectionProps {
36
36
  * the lib's `RichMarkdownRenderer` so changelog rich-link previews
37
37
  * (YouTube, OG cards, etc.) work out of the box. Hosts that already
38
38
  * wrap with a Supabase-aware preset can keep passing their own. */
39
- SimpleMarkdownRenderer?: React.ComponentType<{ content: string }>;
39
+ MarkdownRenderer?: React.ComponentType<{ content: string }>;
40
40
  }
41
41
 
42
42
  // Collapsed height for the preview-first mode. ~120px shows the first
@@ -52,7 +52,7 @@ export function ReleaseChangelogSection({
52
52
  defaultCollapsed = true,
53
53
  previewFirst = false,
54
54
  icon,
55
- SimpleMarkdownRenderer = RichMarkdownRenderer,
55
+ MarkdownRenderer = RichMarkdownRenderer,
56
56
  }: ReleaseChangelogSectionProps) {
57
57
  const [collapsed, setCollapsed] = useState(collapsible ? defaultCollapsed : false);
58
58
  const [previewExpanded, setPreviewExpanded] = useState(false);
@@ -126,7 +126,7 @@ export function ReleaseChangelogSection({
126
126
  } : {}),
127
127
  }}
128
128
  >
129
- <ChangelogEntryList entries={entries} SimpleMarkdownRenderer={SimpleMarkdownRenderer} />
129
+ <ChangelogEntryList entries={entries} MarkdownRenderer={MarkdownRenderer} />
130
130
  </div>
131
131
  {previewNeedsFade && (
132
132
  <button
@@ -142,7 +142,7 @@ export function ReleaseChangelogSection({
142
142
  )}
143
143
  </div>
144
144
  ) : (
145
- <ChangelogEntryList entries={entries} SimpleMarkdownRenderer={SimpleMarkdownRenderer} />
145
+ <ChangelogEntryList entries={entries} MarkdownRenderer={MarkdownRenderer} />
146
146
  )
147
147
  )}
148
148
  </div>
@@ -156,31 +156,36 @@ export function ReleaseChangelogSection({
156
156
  */
157
157
  function ChangelogEntryList({
158
158
  entries,
159
- SimpleMarkdownRenderer,
159
+ MarkdownRenderer,
160
160
  }: {
161
161
  entries: ChangelogEntry[];
162
- SimpleMarkdownRenderer: React.ComponentType<{ content: string }>;
162
+ MarkdownRenderer: React.ComponentType<{ content: string }>;
163
163
  }) {
164
164
  return (
165
165
  <ul className="space-y-6">
166
166
  {entries.map((entry, index) => (
167
167
  <li key={index} className="border-l-2 border-ods-border pl-4 ml-0">
168
- {/* Entry title — `text-h3` is body family + BOLD weight (per
169
- ODS tokens: `--font-h3-weight: var(--font-weight-bold)`)
170
- at 14/18px responsive. Same body size as the description
171
- below, distinguished by weight clean visual hierarchy
172
- without inflating the body scale. */}
173
- <p className="text-h3 text-ods-text-primary mb-2">{entry.title}</p>
168
+ {/* Entry title — run through the SAME markdown renderer as the
169
+ description so inline markdown (links like `[label](url)`,
170
+ emphasis) renders instead of showing as raw text, then pinned
171
+ back to the `text-h3` body+BOLD scale (per ODS tokens:
172
+ `--font-h3-weight: var(--font-weight-bold)`, 14/18px responsive)
173
+ so plain-text titles look exactly as before. `[&_p]:!my-0`
174
+ strips the renderer's paragraph margins; `mb-2` keeps the gap to
175
+ the description. */}
176
+ <div className="text-h3 text-ods-text-primary mb-2 [&_p]:!text-[length:var(--font-size-h3-body)] [&_p]:!leading-[var(--font-line-space-h3-body)] [&_p]:!font-bold [&_p]:!my-0 [&_p+p]:!mt-2">
177
+ <MarkdownRenderer content={entry.title} />
178
+ </div>
174
179
  {entry.description && (
175
180
  /* Entry description — body text matches the main release
176
181
  summary at the SAME 14/18px responsive `text-h4` scale.
177
- The `SimpleMarkdownRenderer` forces its own `<p>` typography
182
+ The `MarkdownRenderer` forces its own `<p>` typography
178
183
  which would override `text-h4` on `lg+` viewports and
179
184
  inflate the changelog body to 20px. The `[&_p]:!` overrides
180
185
  pin every descendant `<p>` back to the h4 responsive tokens
181
186
  so the breakpoints stay aligned with the rest of the page. */
182
187
  <div className="text-h4 text-ods-text-primary [&_p]:!text-[length:var(--font-size-h4-body)] [&_p]:!leading-[var(--font-line-space-h4-body)] [&_p]:!font-medium">
183
- <SimpleMarkdownRenderer content={entry.description} />
188
+ <MarkdownRenderer content={entry.description} />
184
189
  </div>
185
190
  )}
186
191
  </li>
@@ -16,6 +16,10 @@ export function TableCardSkeleton({
16
16
  className,
17
17
  rowClassName
18
18
  }: TableCardSkeletonProps) {
19
+ // The multi-line placeholder belongs to the primary text column — the wide,
20
+ // GROWING one (`flex-1` / `flex-[n]`), not necessarily the first column, which
21
+ // may be a narrow leading action/icon column. Falls back to the first column.
22
+ const primaryKey = (columns.find((c) => /flex-(1|\[)/.test(c.width || '')) ?? columns[0])?.key
19
23
  return (
20
24
  <>
21
25
  {Array.from({ length: rows }).map((_, index) => (
@@ -41,8 +45,8 @@ export function TableCardSkeleton({
41
45
  )}
42
46
  >
43
47
  <div className="h-5 bg-ods-bg-surface rounded w-3/4 mb-1" />
44
- {/* Add second line for some columns to simulate multi-line content */}
45
- {index % 2 === 0 && column.key === columns[0].key && (
48
+ {/* second line simulates multi-line content on the primary column */}
49
+ {index % 2 === 0 && column.key === primaryKey && (
46
50
  <div className="h-4 bg-ods-bg-surface rounded w-1/2 opacity-60" />
47
51
  )}
48
52
  </div>