@flamingo-stack/openframe-frontend-core 0.0.199 → 0.0.200-snapshot.20260520171313
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-V2FNIPZJ.cjs → chunk-3B43AHYE.cjs} +2 -2
- package/dist/{chunk-TMD44IKJ.js.map → chunk-3B43AHYE.cjs.map} +1 -1
- package/dist/{chunk-TMD44IKJ.js → chunk-55HF462A.js} +2 -2
- package/dist/chunk-55HF462A.js.map +1 -0
- package/dist/{chunk-5URU5DHE.js → chunk-CSW5GYBU.js} +1005 -793
- package/dist/chunk-CSW5GYBU.js.map +1 -0
- package/dist/{chunk-332L6IO7.cjs → chunk-UCY537V4.cjs} +782 -570
- package/dist/chunk-UCY537V4.cjs.map +1 -0
- package/dist/components/features/index.cjs +3 -5
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.d.ts +0 -1
- package/dist/components/features/index.d.ts.map +1 -1
- package/dist/components/features/index.js +2 -4
- package/dist/components/index.cjs +3 -3
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +4 -4
- package/dist/components/layout/title-block.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/shared/product-release/product-release-card-skeleton.d.ts +1 -1
- package/dist/components/shared/product-release/product-release-card-skeleton.d.ts.map +1 -1
- package/dist/components/shared/product-release/product-release-card.d.ts +38 -2
- package/dist/components/shared/product-release/product-release-card.d.ts.map +1 -1
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/ui/entity-image.d.ts +9 -0
- package/dist/components/ui/entity-image.d.ts.map +1 -0
- package/dist/components/ui/file-manager/index.cjs +50 -50
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/index.cjs +5 -3
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +4 -2
- package/dist/components/ui/organization-card.d.ts.map +1 -1
- package/dist/components/ui/release-changelog-section.d.ts +7 -1
- package/dist/components/ui/release-changelog-section.d.ts.map +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -4
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/product-release.d.ts +12 -0
- package/dist/types/product-release.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/features/index.ts +0 -1
- package/src/components/layout/title-block.tsx +6 -30
- package/src/components/shared/product-release/product-release-card-skeleton.tsx +69 -1
- package/src/components/shared/product-release/product-release-card.tsx +334 -4
- package/src/components/shared/product-release/release-detail-page.tsx +8 -2
- package/src/components/ui/button/button.tsx +1 -1
- package/src/components/ui/checkbox-block.tsx +13 -13
- package/src/components/ui/entity-image.tsx +56 -0
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/organization-card.tsx +4 -8
- package/src/components/ui/release-changelog-section.tsx +29 -2
- package/src/stories/CheckboxBlock.stories.tsx +1 -3
- package/src/stories/OrganizationCard.stories.tsx +14 -0
- package/src/types/product-release.ts +12 -0
- package/dist/chunk-332L6IO7.cjs.map +0 -1
- package/dist/chunk-5URU5DHE.js.map +0 -1
- package/dist/chunk-V2FNIPZJ.cjs.map +0 -1
- package/dist/components/features/organization-icon.d.ts +0 -80
- package/dist/components/features/organization-icon.d.ts.map +0 -1
- package/src/components/features/organization-icon.tsx +0 -175
|
@@ -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 {
|
|
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
|
-
|
|
124
|
-
|
|
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
|
|
|
@@ -27,7 +27,7 @@ const buttonVariants = cva(
|
|
|
27
27
|
destructive: buttonSurfaceClasses.destructive,
|
|
28
28
|
},
|
|
29
29
|
size: {
|
|
30
|
-
default: "py-[var(--spacing-system-sf)] px-[var(--spacing-system-m)] text-h3 h-12",
|
|
30
|
+
default: "py-[var(--spacing-system-sf)] px-[var(--spacing-system-m)] text-h3 md:h-12 h-10",
|
|
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",
|
|
@@ -28,14 +28,15 @@ const CheckboxBlock = React.forwardRef<
|
|
|
28
28
|
<label
|
|
29
29
|
htmlFor={id}
|
|
30
30
|
className={cn(
|
|
31
|
-
"flex items-center gap-[var(--spacing-system-s)] rounded-
|
|
31
|
+
"flex items-center gap-[var(--spacing-system-s)] rounded-md ring-1 ring-inset w-full",
|
|
32
32
|
"p-[var(--spacing-system-sf)]",
|
|
33
|
-
!description && "h-
|
|
34
|
-
"
|
|
33
|
+
!description && "min-h-[44px] md:min-h-[48px]",
|
|
34
|
+
description && "min-h-[60px] md:min-h-[64px]",
|
|
35
|
+
"bg-ods-card ring-ods-border",
|
|
35
36
|
"cursor-pointer transition-colors duration-200",
|
|
36
|
-
"hover:
|
|
37
|
-
disabled && "opacity-50 cursor-not-allowed hover:
|
|
38
|
-
error && "
|
|
37
|
+
"hover:ring-ods-accent/30",
|
|
38
|
+
disabled && "opacity-50 cursor-not-allowed hover:ring-ods-border",
|
|
39
|
+
error && "ring-ods-error",
|
|
39
40
|
)}
|
|
40
41
|
>
|
|
41
42
|
<CheckboxPrimitive.Root
|
|
@@ -46,7 +47,7 @@ const CheckboxBlock = React.forwardRef<
|
|
|
46
47
|
onCheckedChange={onCheckedChange}
|
|
47
48
|
disabled={disabled}
|
|
48
49
|
className={cn(
|
|
49
|
-
"h-6 w-6 shrink-0",
|
|
50
|
+
"h-4 w-4 md:h-6 md:w-6 shrink-0",
|
|
50
51
|
"rounded-[6px] border-2",
|
|
51
52
|
error ? "border-ods-error" : "border-[var(--color-border-strong)]",
|
|
52
53
|
"bg-ods-card",
|
|
@@ -58,21 +59,20 @@ const CheckboxBlock = React.forwardRef<
|
|
|
58
59
|
<CheckboxPrimitive.Indicator
|
|
59
60
|
className="flex items-center justify-center text-ods-text-on-accent"
|
|
60
61
|
>
|
|
61
|
-
<CheckboxCheckmarkIcon
|
|
62
|
+
<CheckboxCheckmarkIcon className="w-2 h-2 md:w-2.5 md:h-2.5" />
|
|
62
63
|
</CheckboxPrimitive.Indicator>
|
|
63
64
|
</CheckboxPrimitive.Root>
|
|
64
65
|
<div className="flex flex-1 flex-col justify-center min-w-0">
|
|
65
66
|
<span className={cn(
|
|
66
|
-
"text-h4",
|
|
67
|
-
"text-ods-text-primary select-none"
|
|
68
|
-
!description && "truncate"
|
|
67
|
+
"text-h4 !leading-5 md:!leading-6",
|
|
68
|
+
"text-ods-text-primary select-none break-words"
|
|
69
69
|
)}>
|
|
70
70
|
{label}
|
|
71
71
|
</span>
|
|
72
72
|
{description && (
|
|
73
73
|
<span className={cn(
|
|
74
|
-
"text-h6",
|
|
75
|
-
"text-ods-text-secondary select-none"
|
|
74
|
+
"text-h6 !leading-4",
|
|
75
|
+
"text-ods-text-secondary select-none break-words"
|
|
76
76
|
)}>
|
|
77
77
|
{description}
|
|
78
78
|
</span>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { cn } from '../../utils/cn'
|
|
5
|
+
|
|
6
|
+
function getInitials(name?: string): string {
|
|
7
|
+
if (!name) return ''
|
|
8
|
+
const words = name.trim().split(/\s+/)
|
|
9
|
+
if (words.length === 1) return words[0].charAt(0).toUpperCase()
|
|
10
|
+
return (words[0].charAt(0) + words[words.length - 1].charAt(0)).toUpperCase()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface EntityImageProps {
|
|
14
|
+
src?: string | null
|
|
15
|
+
alt?: string
|
|
16
|
+
/** Overrides the initials source. Defaults to `alt`. */
|
|
17
|
+
fallbackText?: string
|
|
18
|
+
className?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function EntityImage({ src, alt, fallbackText, className }: EntityImageProps) {
|
|
22
|
+
const [imageFailed, setImageFailed] = React.useState(false)
|
|
23
|
+
|
|
24
|
+
React.useEffect(() => {
|
|
25
|
+
setImageFailed(false)
|
|
26
|
+
}, [src])
|
|
27
|
+
|
|
28
|
+
const showFallback = imageFailed || !src
|
|
29
|
+
const initials = getInitials(fallbackText ?? alt)
|
|
30
|
+
|
|
31
|
+
if (showFallback) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
aria-label={alt}
|
|
35
|
+
className={cn(
|
|
36
|
+
'size-[52px] md:size-[60px] shrink-0 rounded-md border border-ods-border bg-ods-bg flex items-center justify-center text-ods-text-secondary text-h4 select-none',
|
|
37
|
+
className,
|
|
38
|
+
)}
|
|
39
|
+
>
|
|
40
|
+
{initials || '?'}
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<img
|
|
47
|
+
src={src ?? undefined}
|
|
48
|
+
alt={alt ?? ''}
|
|
49
|
+
onError={() => setImageFailed(true)}
|
|
50
|
+
className={cn(
|
|
51
|
+
'size-[52px] md:size-[60px] shrink-0 rounded-md border border-ods-border object-cover',
|
|
52
|
+
className,
|
|
53
|
+
)}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -72,6 +72,7 @@ export { CheckIcon, CheckCircleIcon as LucideCheckCircleIcon, XIcon as LucideXIc
|
|
|
72
72
|
export * from './dashboard-info-card'
|
|
73
73
|
export * from './device-card'
|
|
74
74
|
export * from './device-card-compact'
|
|
75
|
+
export * from './entity-image'
|
|
75
76
|
export * from './feature-card'
|
|
76
77
|
export * from './feature-list'
|
|
77
78
|
export { FloatingTooltip } from './floating-tooltip'
|
|
@@ -4,7 +4,7 @@ import React from "react"
|
|
|
4
4
|
import Link from "next/link"
|
|
5
5
|
import { Monitor } from "lucide-react"
|
|
6
6
|
import { cn } from "../../utils/cn"
|
|
7
|
-
import {
|
|
7
|
+
import { EntityImage } from "./entity-image"
|
|
8
8
|
|
|
9
9
|
export interface Organization {
|
|
10
10
|
id: string
|
|
@@ -98,13 +98,9 @@ export function OrganizationCard({
|
|
|
98
98
|
|
|
99
99
|
{/* Header */}
|
|
100
100
|
<div className="flex items-start gap-3 w-full">
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
size="xl"
|
|
105
|
-
backgroundStyle="dark"
|
|
106
|
-
showBackground={true}
|
|
107
|
-
className="w-[60px] h-[60px]"
|
|
101
|
+
<EntityImage
|
|
102
|
+
src={fetchedImageUrl || organization.imageUrl}
|
|
103
|
+
alt={organization.name}
|
|
108
104
|
/>
|
|
109
105
|
|
|
110
106
|
<div className="flex-1 flex flex-col justify-center py-2 min-w-0">
|
|
@@ -14,6 +14,12 @@ interface ReleaseChangelogSectionProps {
|
|
|
14
14
|
collapsible?: boolean;
|
|
15
15
|
/** Initial collapsed state (only used when collapsible=true). Defaults to true (collapsed). */
|
|
16
16
|
defaultCollapsed?: boolean;
|
|
17
|
+
/** Optional lucide icon rendered inline before the title text. Matches the
|
|
18
|
+
* catalog card's changelog-strip icons (Sparkles for Features, Wrench for
|
|
19
|
+
* Fixes, TrendingUp for Improvements, AlertTriangle for Breaking) — same
|
|
20
|
+
* visual taxonomy across catalog and detail. Inherits the title's color
|
|
21
|
+
* (secondary for normal sections, red for breaking). */
|
|
22
|
+
icon?: React.ReactNode;
|
|
17
23
|
SimpleMarkdownRenderer: React.ComponentType<{ content: string }>;
|
|
18
24
|
}
|
|
19
25
|
|
|
@@ -24,6 +30,7 @@ export function ReleaseChangelogSection({
|
|
|
24
30
|
hideTitle = false,
|
|
25
31
|
collapsible = false,
|
|
26
32
|
defaultCollapsed = true,
|
|
33
|
+
icon,
|
|
27
34
|
SimpleMarkdownRenderer
|
|
28
35
|
}: ReleaseChangelogSectionProps) {
|
|
29
36
|
const [collapsed, setCollapsed] = useState(collapsible ? defaultCollapsed : false);
|
|
@@ -42,6 +49,7 @@ export function ReleaseChangelogSection({
|
|
|
42
49
|
className="flex items-center justify-between w-full cursor-pointer"
|
|
43
50
|
>
|
|
44
51
|
<h2 className={`flex items-center gap-2 text-2xl font-bold ${isBreaking ? 'text-red-500' : 'text-ods-text-primary'}`}>
|
|
52
|
+
{icon}
|
|
45
53
|
{title}
|
|
46
54
|
<Badge variant="secondary" className="ml-2">{entries.length}</Badge>
|
|
47
55
|
</h2>
|
|
@@ -53,6 +61,7 @@ export function ReleaseChangelogSection({
|
|
|
53
61
|
</button>
|
|
54
62
|
) : (
|
|
55
63
|
<h2 className={`flex items-center gap-2 text-2xl font-bold ${isBreaking ? 'text-red-500' : 'text-ods-text-primary'}`}>
|
|
64
|
+
{icon}
|
|
56
65
|
{title}
|
|
57
66
|
<Badge variant="secondary" className="ml-2">{entries.length}</Badge>
|
|
58
67
|
</h2>
|
|
@@ -62,9 +71,27 @@ export function ReleaseChangelogSection({
|
|
|
62
71
|
<ul className="space-y-6">
|
|
63
72
|
{entries.map((entry, index) => (
|
|
64
73
|
<li key={index} className="border-l-2 border-ods-border pl-4 ml-0">
|
|
65
|
-
|
|
74
|
+
{/* Entry title — `text-h3` is body family + BOLD weight (per
|
|
75
|
+
ODS tokens: `--font-h3-weight: var(--font-weight-bold)`)
|
|
76
|
+
at 14/18px responsive. Same body size as the description
|
|
77
|
+
below, distinguished by weight — clean visual hierarchy
|
|
78
|
+
without inflating the body scale. */}
|
|
79
|
+
<p className="text-h3 text-ods-text-primary mb-2">{entry.title}</p>
|
|
66
80
|
{entry.description && (
|
|
67
|
-
|
|
81
|
+
/* Entry description — body text matches the main release
|
|
82
|
+
summary (release-detail-page.tsx:321) at the SAME 14/18px
|
|
83
|
+
responsive `text-h4` scale. The `SimpleMarkdownRenderer`
|
|
84
|
+
forces its own `<p>` typography
|
|
85
|
+
(`text-[16px] md:text-[18px] lg:text-[20px]`) which
|
|
86
|
+
overrides the wrapper's `text-h4` on `lg+` viewports and
|
|
87
|
+
inflates the changelog body to 20px — larger than the
|
|
88
|
+
main summary AND larger than the entry title.
|
|
89
|
+
The `[&_p]:!` overrides pin every descendant `<p>` back
|
|
90
|
+
to the h4 responsive tokens (`var(--font-size-h4-body)`
|
|
91
|
+
+ `var(--font-line-space-h4-body)`) — same variables
|
|
92
|
+
`text-h4` itself uses, so the responsive breakpoints
|
|
93
|
+
stay aligned with the rest of the page. */
|
|
94
|
+
<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">
|
|
68
95
|
<SimpleMarkdownRenderer content={entry.description} />
|
|
69
96
|
</div>
|
|
70
97
|
)}
|