@flamingo-stack/openframe-frontend-core 0.0.197 → 0.0.199-snapshot.20260520170030

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 (73) hide show
  1. package/dist/chunk-4CWSZPXH.cjs.map +1 -1
  2. package/dist/{chunk-IMDXOVYD.cjs → chunk-C3M6R6JH.cjs} +528 -286
  3. package/dist/chunk-C3M6R6JH.cjs.map +1 -0
  4. package/dist/chunk-OII2IERE.cjs.map +1 -1
  5. package/dist/chunk-UC43NICZ.cjs.map +1 -1
  6. package/dist/chunk-V2FNIPZJ.cjs.map +1 -1
  7. package/dist/chunk-VJTFBYVG.cjs.map +1 -1
  8. package/dist/chunk-VRHGVLSL.cjs.map +1 -1
  9. package/dist/chunk-WZW7C7TF.cjs.map +1 -1
  10. package/dist/chunk-XQFFGR6U.cjs.map +1 -1
  11. package/dist/{chunk-GIQZAYY5.js → chunk-ZOM75JOY.js} +398 -156
  12. package/dist/chunk-ZOM75JOY.js.map +1 -0
  13. package/dist/components/chart.d.ts +14 -7
  14. package/dist/components/chart.d.ts.map +1 -1
  15. package/dist/components/chat/chat-input.d.ts.map +1 -1
  16. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  17. package/dist/components/chat/chat-message-skeleton.d.ts.map +1 -1
  18. package/dist/components/chat/types/component.types.d.ts +4 -0
  19. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  20. package/dist/components/features/index.cjs +2 -2
  21. package/dist/components/features/index.cjs.map +1 -1
  22. package/dist/components/features/index.js +1 -1
  23. package/dist/components/icons/index.cjs.map +1 -1
  24. package/dist/components/icons-v2-generated/index.cjs.map +1 -1
  25. package/dist/components/index.cjs +2 -2
  26. package/dist/components/index.cjs.map +1 -1
  27. package/dist/components/index.js +1 -1
  28. package/dist/components/navigation/index.cjs +2 -2
  29. package/dist/components/navigation/index.cjs.map +1 -1
  30. package/dist/components/navigation/index.js +1 -1
  31. package/dist/components/platform/ScriptArguments.d.ts.map +1 -1
  32. package/dist/components/resizable.d.ts +1 -1
  33. package/dist/components/shared/product-release/product-release-card-skeleton.d.ts +1 -1
  34. package/dist/components/shared/product-release/product-release-card-skeleton.d.ts.map +1 -1
  35. package/dist/components/shared/product-release/product-release-card.d.ts +38 -2
  36. package/dist/components/shared/product-release/product-release-card.d.ts.map +1 -1
  37. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  38. package/dist/components/toast/index.cjs.map +1 -1
  39. package/dist/components/ui/file-manager/index.cjs.map +1 -1
  40. package/dist/components/ui/index.cjs +2 -2
  41. package/dist/components/ui/index.cjs.map +1 -1
  42. package/dist/components/ui/index.js +1 -1
  43. package/dist/components/ui/release-changelog-section.d.ts +7 -1
  44. package/dist/components/ui/release-changelog-section.d.ts.map +1 -1
  45. package/dist/hooks/index.cjs.map +1 -1
  46. package/dist/index.cjs +2 -2
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.js +1 -1
  49. package/dist/tailwind.config.cjs +10 -7
  50. package/dist/tailwind.config.cjs.map +1 -1
  51. package/dist/tailwind.config.js +10 -7
  52. package/dist/tailwind.config.js.map +1 -1
  53. package/dist/types/index.cjs.map +1 -1
  54. package/dist/types/index.js.map +1 -1
  55. package/dist/types/product-release.d.ts +12 -0
  56. package/dist/types/product-release.d.ts.map +1 -1
  57. package/package.json +1 -1
  58. package/src/components/chat/.chat-input.md +0 -1
  59. package/src/components/chat/chat-container.tsx +3 -3
  60. package/src/components/chat/chat-input.tsx +38 -51
  61. package/src/components/chat/chat-message-enhanced.tsx +21 -22
  62. package/src/components/chat/chat-message-list.tsx +6 -6
  63. package/src/components/chat/chat-message-skeleton.tsx +9 -10
  64. package/src/components/chat/types/component.types.ts +4 -0
  65. package/src/components/platform/ScriptArguments.tsx +7 -6
  66. package/src/components/shared/product-release/product-release-card-skeleton.tsx +69 -1
  67. package/src/components/shared/product-release/product-release-card.tsx +334 -4
  68. package/src/components/shared/product-release/release-detail-page.tsx +8 -2
  69. package/src/components/ui/release-changelog-section.tsx +29 -2
  70. package/src/types/product-release.ts +12 -0
  71. package/tailwind.config.ts +4 -0
  72. package/dist/chunk-GIQZAYY5.js.map +0 -1
  73. package/dist/chunk-IMDXOVYD.cjs.map +0 -1
@@ -432,8 +432,8 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
432
432
  <div
433
433
  ref={setContentRef}
434
434
  className={cn(
435
- "mx-auto flex w-full max-w-3xl flex-col pb-2 min-w-0",
436
- contentClassName || "px-4",
435
+ "mx-auto flex w-full max-w-ods-content-narrow flex-col pb-[var(--spacing-system-xs)] min-w-0",
436
+ contentClassName ?? "px-[var(--spacing-system-m)]",
437
437
  )}
438
438
  style={{ minHeight: '100%' }}
439
439
  >
@@ -470,8 +470,8 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
470
470
  {showStreamingLoader && (
471
471
  <div
472
472
  className={cn(
473
- "mx-auto w-full max-w-3xl flex items-center gap-1 py-2",
474
- contentClassName || "px-4",
473
+ "mx-auto w-full max-w-ods-content-narrow flex items-center gap-[var(--spacing-system-xxs)] py-[var(--spacing-system-xs)]",
474
+ contentClassName ?? "px-[var(--spacing-system-m)]",
475
475
  )}
476
476
  style={{ color: 'var(--color-text-muted)' }}
477
477
  role="status"
@@ -491,8 +491,8 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
491
491
  {pendingApprovals && pendingApprovals.length > 0 && (
492
492
  <div className={cn(
493
493
  "border-t border-ods-border bg-ods-bg/95 backdrop-blur-sm",
494
- "mx-auto w-full max-w-3xl",
495
- contentClassName || "px-4",
494
+ "mx-auto w-full max-w-ods-content-narrow",
495
+ contentClassName ?? "px-[var(--spacing-system-m)]",
496
496
  )}>
497
497
  <ChatMessageEnhanced
498
498
  role="assistant"
@@ -20,25 +20,24 @@ export function ChatMessageSkeleton({
20
20
  return (
21
21
  <div
22
22
  className={cn(
23
- "flex flex-row items-start gap-4",
23
+ "relative",
24
24
  !isUser && "bg-ods-card/50 rounded-lg px-4 -mx-4",
25
25
  className
26
26
  )}
27
27
  >
28
- {/* Avatar Skeleton - optional */}
29
- {showAvatar && (
28
+ {/* Avatar Skeleton - absolutely positioned outside content area to match
29
+ the real ChatMessageEnhanced hanging-avatar layout. */}
30
+ {showAvatar && !isUser && (
30
31
  <div className={cn(
31
- "flex-shrink-0 mt-1 w-8 h-8 rounded animate-pulse",
32
- isUser
33
- ? "invisible"
34
- : isMingo
32
+ "absolute -left-16 top-[var(--spacing-system-s)] w-12 h-12 rounded-full animate-pulse",
33
+ isMingo
35
34
  ? "bg-gradient-to-br from-cyan-400/30 to-cyan-600/30"
36
35
  : "bg-gradient-to-br from-pink-400/30 to-pink-600/30"
37
36
  )} />
38
37
  )}
39
-
38
+
40
39
  {/* Message Content Skeleton */}
41
- <div className="flex flex-1 flex-col gap-1 min-w-0">
40
+ <div className="flex flex-col gap-1 min-w-0">
42
41
  {/* Name and Timestamp Row Skeleton */}
43
42
  <div className="flex items-center justify-between pr-2">
44
43
  <div className="h-5 w-16 bg-ods-border rounded animate-pulse" />
@@ -92,7 +91,7 @@ export function ChatMessageListSkeleton({
92
91
  className
93
92
  )}
94
93
  >
95
- <div className={cn("mx-auto flex w-full max-w-3xl flex-col pb-2 min-w-0", contentClassName || "px-4")} style={{ minHeight: '100%' }}>
94
+ <div className={cn("mx-auto flex w-full max-w-ods-content-narrow flex-col pb-[var(--spacing-system-xs)] min-w-0", contentClassName ?? "px-[var(--spacing-system-m)]")} style={{ minHeight: '100%' }}>
96
95
  <div className="flex-1" />
97
96
  <div className="space-y-6">
98
97
  {messages.map((message) => (
@@ -237,6 +237,10 @@ export interface ChatInputProps extends Omit<TextareaHTMLAttributes<HTMLTextArea
237
237
  onStop?: () => void | Promise<void>
238
238
  sending?: boolean
239
239
  awaitingResponse?: boolean
240
+ /**
241
+ * @deprecated The avatar-offset layout was removed; this prop is accepted
242
+ * for back-compat but silently ignored. Safe to drop from call sites.
243
+ */
240
244
  reserveAvatarOffset?: boolean
241
245
  disabled?: boolean
242
246
  maxRows?: number
@@ -6,12 +6,13 @@
6
6
  * Includes an "Add Script Argument" button to add new entries.
7
7
  */
8
8
 
9
- import { PlusCircle, Trash2 } from 'lucide-react'
9
+ import { Trash2 } from 'lucide-react'
10
10
  import React from 'react'
11
- import { cn } from '../../utils/cn'
11
+ import { cn } from '@/utils'
12
12
  import { Button } from '../ui/button'
13
13
  import { Input } from '../ui/input'
14
14
  import { Label } from '../ui/label'
15
+ import {PlusCircleIcon} from "@/components/icons-v2-generated";
15
16
 
16
17
  /**
17
18
  * Single script argument with key and value
@@ -156,12 +157,12 @@ export const ScriptArguments: React.FC<ScriptArgumentsProps> = ({
156
157
  <div className="flex justify-start">
157
158
  <Button
158
159
  type="button"
159
- variant="transparent"
160
- className="text-ods-text-primary"
160
+ variant="outline"
161
+ size="small"
161
162
  onClick={handleAdd}
162
163
  disabled={disabled}
163
- leftIcon={<PlusCircle className="size-6" />}
164
- noPaddingX
164
+ className="self-start"
165
+ leftIcon={<PlusCircleIcon className="text-ods-text-secondary" />}
165
166
  >
166
167
  {addButtonLabel}
167
168
  </Button>
@@ -8,10 +8,78 @@ export interface ProductReleaseCardSkeletonProps {
8
8
  className?: string
9
9
  /** Card density. Must match the loaded card's `size` prop so the loading
10
10
  * height matches the resolved height (no layout shift on resolve). */
11
- size?: 'default' | 'sm'
11
+ size?: 'default' | 'sm' | 'catalog'
12
12
  }
13
13
 
14
14
  export function ProductReleaseCardSkeleton({ className, size = 'default' }: ProductReleaseCardSkeletonProps) {
15
+ // ----- CATALOG branch — must match ProductReleaseCard size='catalog'.
16
+ // Same outer frame (`bg-ods-system-greys-black border border-ods-border …
17
+ // p-6 gap-4`). Inner: hero (16:9 cover + version pill + title + summary),
18
+ // changelog strip placeholder (always rendered — see note), metadata-grid
19
+ // footer (4 cells via grid). Heights chosen to match the loaded card's
20
+ // rendered metrics so the 5-card slot grid in `ReleasesList` doesn't
21
+ // jump on resolve.
22
+ //
23
+ // Note: the loaded card hides the changelog strip when total === 0
24
+ // (rare — most releases have at least one feature/fix/improvement).
25
+ // The skeleton always renders the placeholder; net effect is a ~28px
26
+ // shrink on empty-changelog releases. Documented tradeoff.
27
+ if (size === 'catalog') {
28
+ return (
29
+ <div
30
+ className={cn(
31
+ 'bg-ods-system-greys-black border border-ods-border rounded-lg overflow-hidden',
32
+ 'flex flex-col p-6 gap-4',
33
+ 'animate-pulse',
34
+ className,
35
+ )}
36
+ >
37
+ {/* HERO */}
38
+ <div className="flex flex-col md:flex-row gap-4 md:gap-6">
39
+ <div className="w-full md:w-[256px] aspect-[16/9] bg-ods-bg rounded-lg flex-shrink-0" />
40
+ <div className="flex-1 min-w-0 flex flex-col">
41
+ {/* Version pill */}
42
+ <div className="h-6 w-20 bg-ods-bg rounded mb-3" />
43
+ {/* Title — 2 lines */}
44
+ <div className="h-7 w-3/4 bg-ods-bg rounded mb-2" />
45
+ <div className="h-7 w-1/2 bg-ods-bg rounded mb-3" />
46
+ {/* Summary — 2 lines */}
47
+ <div className="h-3 w-full bg-ods-bg/60 rounded mb-1" />
48
+ <div className="h-3 w-5/6 bg-ods-bg/60 rounded" />
49
+ </div>
50
+ </div>
51
+
52
+ {/* CHANGELOG strip placeholder — always rendered */}
53
+ <div className="border-t border-ods-border pt-3">
54
+ <div className="h-4 w-2/3 bg-ods-bg/60 rounded" />
55
+ </div>
56
+
57
+ {/* METADATA GRID — 4-cell placeholder */}
58
+ <div className="grid grid-cols-1 md:grid-cols-4 border border-ods-border rounded-md overflow-hidden w-full">
59
+ {[0, 1, 2].map((i) => (
60
+ <div
61
+ key={`cell-${i}`}
62
+ className="bg-ods-card p-4 flex flex-col gap-3 border-b md:border-b-0 md:border-r border-ods-border"
63
+ >
64
+ <div className="flex flex-col gap-2">
65
+ <div className="h-6 w-24 bg-ods-bg rounded" />
66
+ <div className="h-3 w-16 bg-ods-bg/60 rounded" />
67
+ </div>
68
+ </div>
69
+ ))}
70
+ {/* Author cell */}
71
+ <div className="bg-ods-card p-4 flex items-center gap-3">
72
+ <div className="h-10 w-10 rounded-full bg-ods-bg shrink-0" />
73
+ <div className="flex flex-col gap-2 flex-1 min-w-0">
74
+ <div className="h-4 w-3/4 bg-ods-bg rounded" />
75
+ <div className="h-3 w-1/2 bg-ods-bg/60 rounded" />
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ )
81
+ }
82
+
15
83
  // ----- COMPACT branch — must match ProductReleaseCard size='sm' exactly.
16
84
  // Same outer: span + items-start + gap-3 + p-2 + my-1.5 (no border to keep
17
85
  // the skeleton 1px lighter than the resolved card is fine — but we mirror
@@ -1,8 +1,20 @@
1
1
  'use client'
2
2
 
3
3
  import React from 'react'
4
+ import Image from 'next/image'
4
5
  import { InteractiveCard } from '../../ui/interactive-card'
5
- import { ChevronRight, Package } from 'lucide-react'
6
+ import { StatusBadge } from '../../ui/status-badge'
7
+ import { SquareAvatar } from '../../ui/square-avatar'
8
+ import {
9
+ AlertTriangle,
10
+ ChevronRight,
11
+ Eye,
12
+ Package,
13
+ Play,
14
+ Sparkles,
15
+ TrendingUp,
16
+ Wrench,
17
+ } from 'lucide-react'
6
18
  import { cn } from '../../../utils/cn'
7
19
 
8
20
  /**
@@ -14,8 +26,13 @@ import { cn } from '../../../utils/cn'
14
26
  * illegal inside markdown `<p>`) for `<span>` text, swaps the outer
15
27
  * `InteractiveCard` for a `<span>`-anchored link, and collapses to:
16
28
  * 56px icon + 1-line title + 1-line meta (version · date).
29
+ * - `catalog`: rich /releases catalog row. Three zones — hero (16:9 cover +
30
+ * version pill + title + summary), changelog stats strip (icons + counts),
31
+ * metadata grid footer (Type · Status · Released · Author). The grid
32
+ * mirrors the hub's `<EntityAuthorCard>` byte-for-byte (see catalog
33
+ * branch comment).
17
34
  */
18
- export type ProductReleaseCardSize = 'default' | 'sm'
35
+ export type ProductReleaseCardSize = 'default' | 'sm' | 'catalog'
19
36
 
20
37
  /**
21
38
  * Minimal structural `<a>` prop bundle the consumer composes (typically
@@ -65,6 +82,39 @@ export interface ProductReleaseCardProps {
65
82
  className?: string
66
83
  /** Card density. Defaults to `'default'`. */
67
84
  size?: ProductReleaseCardSize
85
+
86
+ // ─── Catalog-only props (ignored by `default` / `sm` branches) ─────────
87
+ /** Cover image URL. Falls back to a neutral `Package`-icon placeholder. */
88
+ coverImage?: string | null
89
+ /** Drives the Play overlay on the cover. Caller sets `true` when the cover
90
+ * came from a `*_video_thumbnail` field. */
91
+ hasVideoCover?: boolean
92
+ /** Release type for the metadata grid's first cell. */
93
+ releaseType?: 'major' | 'minor' | 'patch' | 'beta' | 'alpha'
94
+ /** Release status for the metadata grid's second cell. */
95
+ releaseStatus?: 'alpha' | 'beta' | 'stable' | 'deprecated'
96
+ /** Pre-computed `StatusBadge` colorScheme for the release-type chip. The
97
+ * hub consumer maps `release_type → colorScheme` via its local helper so
98
+ * the OSS card stays mapping-agnostic. */
99
+ releaseTypeBadgeColor?: 'error' | 'cyan' | 'success' | 'warning'
100
+ /** View count for the optional microline below the metadata grid. Hidden
101
+ * when zero or undefined. */
102
+ viewCount?: number
103
+ /** Hydrated author (from the hub DAL's `hydrateAuthor`). When present,
104
+ * renders as the last cell of the metadata grid. */
105
+ author?: {
106
+ full_name: string
107
+ avatar_url: string | null
108
+ job_title: string | null
109
+ }
110
+ /** Per-category counts for the changelog stats strip. The whole strip
111
+ * is hidden when total === 0. */
112
+ changelogCounts?: {
113
+ features: number
114
+ fixes: number
115
+ improvements: number
116
+ breaking: number
117
+ }
68
118
  }
69
119
 
70
120
  export function ProductReleaseCard({
@@ -76,7 +126,265 @@ export function ProductReleaseCard({
76
126
  anchorProps,
77
127
  className,
78
128
  size = 'default',
129
+ coverImage,
130
+ hasVideoCover,
131
+ releaseType,
132
+ releaseStatus,
133
+ releaseTypeBadgeColor,
134
+ viewCount,
135
+ author,
136
+ changelogCounts,
79
137
  }: ProductReleaseCardProps) {
138
+ // ----- CATALOG branch (rich /releases catalog row) -------------------------
139
+ // The card has THREE zones:
140
+ // 1. Hero — cover image LEFT, version pill + title + summary RIGHT.
141
+ // 2. Changelog strip — icons + counts (hidden when total === 0).
142
+ // 3. Metadata grid footer — bordered grid of [Type | Status | Released
143
+ // | Author] cells. This grid INLINES the hub's <EntityAuthorCard>
144
+ // visual treatment by hand (SAME bordered grid with value-cell +
145
+ // author-cell shapes, byte-for-byte). The OSS lib has zero hub
146
+ // coupling by design; we cannot import the hub's
147
+ // <EntityAuthorCard>. This is the SAME inline-duplication policy
148
+ // documented for the COMPACT_CARD_* string set at lines ~104-108.
149
+ // If the hub's <EntityAuthorCard> visual changes (cell padding,
150
+ // divider styles, avatar size, etc.), update this branch in
151
+ // lockstep.
152
+ if (size === 'catalog') {
153
+ const totalChangelog =
154
+ (changelogCounts?.features ?? 0) +
155
+ (changelogCounts?.fixes ?? 0) +
156
+ (changelogCounts?.improvements ?? 0) +
157
+ (changelogCounts?.breaking ?? 0)
158
+
159
+ // Build the metadata-grid cell array — mirrors the hub's
160
+ // EntityAuthorCard composition (extras → date → author). The
161
+ // release-type cell carries a colored chip; other cells are plain
162
+ // value + label.
163
+ type ValueCell = {
164
+ value: string
165
+ label: string
166
+ uppercase: boolean
167
+ colorScheme?: 'error' | 'cyan' | 'success' | 'warning'
168
+ }
169
+ const valueCells: ValueCell[] = []
170
+ if (releaseType && releaseTypeBadgeColor) {
171
+ valueCells.push({
172
+ value: releaseType.toUpperCase(),
173
+ label: 'Type',
174
+ uppercase: true,
175
+ colorScheme: releaseTypeBadgeColor,
176
+ })
177
+ }
178
+ if (releaseStatus) {
179
+ valueCells.push({
180
+ value: releaseStatus.toUpperCase(),
181
+ label: 'Status',
182
+ uppercase: true,
183
+ })
184
+ }
185
+ if (formattedDate) {
186
+ valueCells.push({
187
+ value: formattedDate,
188
+ label: 'Released',
189
+ uppercase: false,
190
+ })
191
+ }
192
+ const hasAuthorCell = !!author?.full_name
193
+ const totalCells = valueCells.length + (hasAuthorCell ? 1 : 0)
194
+ // Tailwind JIT cannot compile dynamic class names — explicit branches:
195
+ const gridColsClass =
196
+ totalCells >= 4 ? 'md:grid-cols-4'
197
+ : totalCells === 3 ? 'md:grid-cols-3'
198
+ : totalCells === 2 ? 'md:grid-cols-2'
199
+ : 'md:grid-cols-1'
200
+ const dividerClass = 'border-b md:border-b-0 md:border-r border-ods-border'
201
+
202
+ const frameClass = cn(
203
+ 'group bg-ods-system-greys-black border border-ods-border rounded-lg overflow-hidden',
204
+ 'flex flex-col p-6 gap-4',
205
+ 'transition-all duration-300 ease-out transform hover:translate-y-[-2px]',
206
+ 'hover:border-ods-accent hover:shadow-lg hover:shadow-ods-accent/[0.08]',
207
+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent focus-visible:ring-offset-2 focus-visible:ring-offset-ods-bg',
208
+ 'no-underline',
209
+ className,
210
+ )
211
+
212
+ const innerLayout = (
213
+ <>
214
+ {/* HERO ZONE — cover LEFT + version pill + title + summary RIGHT */}
215
+ <div className="flex flex-col md:flex-row gap-4 md:gap-6">
216
+ <div className="w-full md:w-[256px] flex-shrink-0">
217
+ <div className="relative rounded-lg overflow-hidden w-full aspect-[16/9] bg-ods-bg">
218
+ {coverImage ? (
219
+ <Image
220
+ src={coverImage}
221
+ alt={title}
222
+ fill
223
+ sizes="(max-width: 768px) 100vw, 256px"
224
+ className="object-cover"
225
+ unoptimized
226
+ />
227
+ ) : (
228
+ <div className="absolute inset-0 flex items-center justify-center text-ods-text-secondary">
229
+ <Package className="w-8 h-8" />
230
+ </div>
231
+ )}
232
+ {hasVideoCover && coverImage && (
233
+ <span className="absolute inset-0 flex items-center justify-center bg-black/30">
234
+ <Play className="w-10 h-10 text-white" fill="white" />
235
+ </span>
236
+ )}
237
+ </div>
238
+ </div>
239
+ <div className="flex-1 min-w-0 flex flex-col">
240
+ <div className="flex items-center gap-3 mb-3">
241
+ <span className="font-mono font-semibold text-lg text-ods-text-primary truncate">
242
+ v{version}
243
+ </span>
244
+ </div>
245
+ <h3 className="font-['Azeret_Mono'] font-semibold text-xl md:text-2xl text-ods-text-primary leading-tight line-clamp-2 mb-3">
246
+ {title}
247
+ </h3>
248
+ {summary && (
249
+ <p className="font-['DM_Sans'] text-sm md:text-base text-ods-text-secondary leading-relaxed line-clamp-4 flex-1">
250
+ {summary}
251
+ </p>
252
+ )}
253
+ </div>
254
+ </div>
255
+
256
+ {/* CHANGELOG STRIP — hidden when total === 0 */}
257
+ {totalChangelog > 0 && changelogCounts && (
258
+ <div className="border-t border-ods-border pt-3 flex flex-wrap items-center gap-x-4 gap-y-1.5 font-['DM_Sans'] text-sm text-ods-text-secondary">
259
+ {changelogCounts.features > 0 && (
260
+ <span className="inline-flex items-center gap-1.5">
261
+ <Sparkles className="w-3.5 h-3.5" />
262
+ {changelogCounts.features} {changelogCounts.features === 1 ? 'feature' : 'features'}
263
+ </span>
264
+ )}
265
+ {changelogCounts.fixes > 0 && (
266
+ <span className="inline-flex items-center gap-1.5">
267
+ <Wrench className="w-3.5 h-3.5" />
268
+ {changelogCounts.fixes} {changelogCounts.fixes === 1 ? 'fix' : 'fixes'}
269
+ </span>
270
+ )}
271
+ {changelogCounts.improvements > 0 && (
272
+ <span className="inline-flex items-center gap-1.5">
273
+ <TrendingUp className="w-3.5 h-3.5" />
274
+ {changelogCounts.improvements} {changelogCounts.improvements === 1 ? 'improvement' : 'improvements'}
275
+ </span>
276
+ )}
277
+ {changelogCounts.breaking > 0 && (
278
+ <span className="inline-flex items-center gap-1.5 text-[var(--ods-attention-yellow-warning)]">
279
+ <AlertTriangle className="w-3.5 h-3.5" />
280
+ {changelogCounts.breaking} breaking
281
+ </span>
282
+ )}
283
+ </div>
284
+ )}
285
+
286
+ {/* METADATA GRID FOOTER — mirrors EntityAuthorCard byte-for-byte */}
287
+ {totalCells > 0 && (
288
+ <div
289
+ className={cn(
290
+ 'grid grid-cols-1',
291
+ gridColsClass,
292
+ 'border border-ods-border rounded-md overflow-hidden w-full',
293
+ )}
294
+ >
295
+ {valueCells.map((cell, i) => (
296
+ <div
297
+ key={`${cell.label}-${i}`}
298
+ className={cn(
299
+ 'bg-ods-card p-4 flex flex-col gap-3',
300
+ // Last value cell skips the trailing divider when no
301
+ // author cell follows; otherwise every value cell gets it.
302
+ (i < valueCells.length - 1 || hasAuthorCell) && dividerClass,
303
+ )}
304
+ >
305
+ <div className="flex flex-col gap-0">
306
+ {cell.colorScheme ? (
307
+ <StatusBadge
308
+ text={cell.value}
309
+ variant="card"
310
+ colorScheme={cell.colorScheme}
311
+ singleLine
312
+ className="self-start"
313
+ />
314
+ ) : (
315
+ <p className="text-h4 text-ods-text-primary">
316
+ {cell.uppercase ? cell.value.toLocaleUpperCase() : cell.value}
317
+ </p>
318
+ )}
319
+ <p className="font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary">
320
+ {cell.label}
321
+ </p>
322
+ </div>
323
+ </div>
324
+ ))}
325
+ {hasAuthorCell && author && (
326
+ <div className="bg-ods-card p-4 flex items-center gap-3">
327
+ <SquareAvatar
328
+ src={author.avatar_url ?? undefined}
329
+ alt={author.full_name}
330
+ fallback={author.full_name.charAt(0).toUpperCase()}
331
+ size="md"
332
+ variant="round"
333
+ />
334
+ <div className="flex flex-col gap-0 flex-1 min-w-0">
335
+ <p className="text-h3 tracking-[-0.36px] text-ods-text-primary truncate">
336
+ {author.full_name}
337
+ </p>
338
+ <p className="font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary">
339
+ {author.job_title || 'Author'}
340
+ </p>
341
+ </div>
342
+ </div>
343
+ )}
344
+ </div>
345
+ )}
346
+
347
+ {typeof viewCount === 'number' && viewCount > 0 && (
348
+ <div className="flex items-center gap-1.5 text-xs text-ods-text-secondary">
349
+ <Eye className="w-3.5 h-3.5" />
350
+ <span>{viewCount.toLocaleString()} views</span>
351
+ </div>
352
+ )}
353
+ </>
354
+ )
355
+
356
+ // Outer-element three-branch decision tree, matching the existing
357
+ // `default` branch precedence at lines ~200-241. PRECEDENCE:
358
+ // anchorProps WINS over onClick.
359
+ if (anchorProps) {
360
+ return (
361
+ <a {...anchorProps} className={frameClass} aria-label={`Open ${title}`}>
362
+ {innerLayout}
363
+ </a>
364
+ )
365
+ }
366
+ if (onClick) {
367
+ return (
368
+ <InteractiveCard clickable onClick={onClick} className={frameClass}>
369
+ {innerLayout}
370
+ </InteractiveCard>
371
+ )
372
+ }
373
+ // Non-interactive fallback — strip the hover lift / accent-border so
374
+ // the cursor doesn't lie about clickability.
375
+ return (
376
+ <div
377
+ className={cn(
378
+ frameClass
379
+ .replace('hover:border-ods-accent', '')
380
+ .replace('hover:translate-y-[-2px]', ''),
381
+ )}
382
+ >
383
+ {innerLayout}
384
+ </div>
385
+ )
386
+ }
387
+
80
388
  // ----- COMPACT branch (chat / tight surfaces) ------------------------------
81
389
  // Outer must be a phrasing-content element (`<a>` or `<span>`) — block
82
390
  // elements like `<div>`/`<h3>` are illegal inside markdown `<p>`, so we
@@ -120,8 +428,30 @@ export function ProductReleaseCard({
120
428
  )
121
429
  const innerChildren = (
122
430
  <>
123
- <span className="flex h-14 w-14 aspect-square shrink-0 self-start items-center justify-center rounded-md bg-ods-bg text-ods-accent">
124
- <Package className="h-5 w-5" />
431
+ {/* 56×56 cover slot. Mirrors BlogCard / ProgramCard / OnboardingGuide
432
+ sm slots — when `coverImage` is set, render the actual image
433
+ (object-contain to keep landscape thumbs visible); fall back to
434
+ the `Package` icon when no cover is provided. Play overlay
435
+ fires only when `hasVideoCover` is true AND a cover image was
436
+ actually supplied (matches the catalog variant's overlay rule). */}
437
+ <span className="relative flex h-14 w-14 aspect-square shrink-0 self-start items-center justify-center overflow-hidden rounded-md bg-ods-bg text-ods-accent">
438
+ {coverImage ? (
439
+ <Image
440
+ src={coverImage}
441
+ alt={title}
442
+ fill
443
+ sizes="56px"
444
+ className="object-contain"
445
+ unoptimized
446
+ />
447
+ ) : (
448
+ <Package className="h-5 w-5" />
449
+ )}
450
+ {hasVideoCover && coverImage && (
451
+ <span className="absolute inset-0 flex items-center justify-center bg-black/30">
452
+ <Play className="h-4 w-4 text-white" fill="white" />
453
+ </span>
454
+ )}
125
455
  </span>
126
456
  {/* Text column structure must mirror the hub's
127
457
  `COMPACT_CARD_TEXT_COL` + `COMPACT_CARD_TITLE_ROW` +
@@ -9,7 +9,7 @@ import { StatusBadge } from '../../ui/status-badge';
9
9
  import { SquareAvatar } from '../../ui/square-avatar';
10
10
  import { ImageGalleryModal } from '../../ui/image-gallery-modal';
11
11
  import { GitHubIcon } from '../../icons/github-icon';
12
- import { AlertTriangle, ExternalLink, BookMarked } from 'lucide-react';
12
+ import { AlertTriangle, ExternalLink, BookMarked, Sparkles, TrendingUp, Wrench } from 'lucide-react';
13
13
  import { formatReleaseDate } from '../../../utils/date-formatters';
14
14
  import { Video } from '../../features/video';
15
15
  import { DetailPageSkeleton } from '../detail-page-skeleton';
@@ -392,27 +392,33 @@ export function ReleaseDetailPage({
392
392
  </Card>
393
393
  )}
394
394
 
395
- {/* Changelog Sections */}
395
+ {/* Changelog Sections — icons match the catalog card's changelog
396
+ strip taxonomy (Sparkles/Wrench/TrendingUp/AlertTriangle) so the
397
+ user sees a consistent visual signature across catalog → detail. */}
396
398
  <ReleaseChangelogSection
397
399
  title="Breaking Changes"
398
400
  entries={breakingChanges || []}
399
401
  isBreaking
400
402
  hideTitle
403
+ icon={<AlertTriangle className="h-6 w-6" />}
401
404
  SimpleMarkdownRenderer={MarkdownRenderer}
402
405
  />
403
406
  <ReleaseChangelogSection
404
407
  title="Features Added"
405
408
  entries={featuresAdded || []}
409
+ icon={<Sparkles className="h-6 w-6" />}
406
410
  SimpleMarkdownRenderer={MarkdownRenderer}
407
411
  />
408
412
  <ReleaseChangelogSection
409
413
  title="Bugs Fixed"
410
414
  entries={bugFixed || []}
415
+ icon={<Wrench className="h-6 w-6" />}
411
416
  SimpleMarkdownRenderer={MarkdownRenderer}
412
417
  />
413
418
  <ReleaseChangelogSection
414
419
  title="Improvements"
415
420
  entries={improvements || []}
421
+ icon={<TrendingUp className="h-6 w-6" />}
416
422
  SimpleMarkdownRenderer={MarkdownRenderer}
417
423
  />
418
424