@flamingo-stack/openframe-frontend-core 0.0.203 → 0.0.204-snapshot.20260523020255

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 (70) hide show
  1. package/dist/chunk-27APPAJN.cjs +24 -0
  2. package/dist/chunk-27APPAJN.cjs.map +1 -0
  3. package/dist/{chunk-XGL5FKIK.js → chunk-D6BNDYZK.js} +114 -4
  4. package/dist/chunk-D6BNDYZK.js.map +1 -0
  5. package/dist/chunk-MJNXIEV2.js +24 -0
  6. package/dist/chunk-MJNXIEV2.js.map +1 -0
  7. package/dist/{chunk-25LVV26X.cjs → chunk-NLXY4QZE.cjs} +114 -4
  8. package/dist/chunk-NLXY4QZE.cjs.map +1 -0
  9. package/dist/{chunk-E6Q6UGDK.js → chunk-OFCRX24Y.js} +383 -569
  10. package/dist/chunk-OFCRX24Y.js.map +1 -0
  11. package/dist/{chunk-3YH2M76N.cjs → chunk-RDXOQWBY.cjs} +556 -742
  12. package/dist/chunk-RDXOQWBY.cjs.map +1 -0
  13. package/dist/components/announcement-bar.d.ts.map +1 -1
  14. package/dist/components/features/index.cjs +4 -3
  15. package/dist/components/features/index.cjs.map +1 -1
  16. package/dist/components/features/index.js +3 -2
  17. package/dist/components/index.cjs +4 -3
  18. package/dist/components/index.cjs.map +1 -1
  19. package/dist/components/index.js +3 -2
  20. package/dist/components/navigation/index.cjs +4 -3
  21. package/dist/components/navigation/index.cjs.map +1 -1
  22. package/dist/components/navigation/index.js +3 -2
  23. package/dist/components/navigation/navigation-sidebar.d.ts.map +1 -1
  24. package/dist/components/shared/product-release/product-release-card-skeleton.d.ts +1 -1
  25. package/dist/components/shared/product-release/product-release-card-skeleton.d.ts.map +1 -1
  26. package/dist/components/shared/product-release/product-release-card.d.ts +19 -12
  27. package/dist/components/shared/product-release/product-release-card.d.ts.map +1 -1
  28. package/dist/components/ui/index.cjs +4 -3
  29. package/dist/components/ui/index.cjs.map +1 -1
  30. package/dist/components/ui/index.js +3 -2
  31. package/dist/contexts/endpoints-runtime-context.d.ts +28 -0
  32. package/dist/contexts/endpoints-runtime-context.d.ts.map +1 -0
  33. package/dist/contexts/index.cjs +13 -0
  34. package/dist/contexts/index.cjs.map +1 -0
  35. package/dist/contexts/index.d.ts +24 -0
  36. package/dist/contexts/index.d.ts.map +1 -0
  37. package/dist/contexts/index.js +13 -0
  38. package/dist/contexts/index.js.map +1 -0
  39. package/dist/hooks/index.cjs +5 -2
  40. package/dist/hooks/index.cjs.map +1 -1
  41. package/dist/hooks/index.d.ts +1 -0
  42. package/dist/hooks/index.d.ts.map +1 -1
  43. package/dist/hooks/index.js +4 -1
  44. package/dist/hooks/use-access-code-integration.d.ts +48 -0
  45. package/dist/hooks/use-access-code-integration.d.ts.map +1 -0
  46. package/dist/hooks/use-contact-submission.d.ts.map +1 -1
  47. package/dist/index.cjs +4 -3
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.js +7 -6
  50. package/dist/utils/access-code-client.d.ts +21 -37
  51. package/dist/utils/access-code-client.d.ts.map +1 -1
  52. package/dist/utils/index.cjs +10 -51
  53. package/dist/utils/index.cjs.map +1 -1
  54. package/dist/utils/index.js +11 -47
  55. package/dist/utils/index.js.map +1 -1
  56. package/package.json +7 -1
  57. package/src/components/announcement-bar.tsx +25 -3
  58. package/src/components/navigation/navigation-sidebar.tsx +3 -1
  59. package/src/components/shared/product-release/product-release-card-skeleton.tsx +66 -70
  60. package/src/components/shared/product-release/product-release-card.tsx +194 -242
  61. package/src/contexts/endpoints-runtime-context.tsx +68 -0
  62. package/src/contexts/index.ts +29 -0
  63. package/src/hooks/index.ts +6 -0
  64. package/src/hooks/use-access-code-integration.ts +107 -0
  65. package/src/hooks/use-contact-submission.ts +6 -2
  66. package/src/utils/access-code-client.ts +32 -75
  67. package/dist/chunk-25LVV26X.cjs.map +0 -1
  68. package/dist/chunk-3YH2M76N.cjs.map +0 -1
  69. package/dist/chunk-E6Q6UGDK.js.map +0 -1
  70. package/dist/chunk-XGL5FKIK.js.map +0 -1
@@ -7,7 +7,6 @@ import { StatusBadge } from '../../ui/status-badge'
7
7
  import { SquareAvatar } from '../../ui/square-avatar'
8
8
  import {
9
9
  AlertTriangle,
10
- ChevronRight,
11
10
  Eye,
12
11
  Package,
13
12
  Play,
@@ -18,21 +17,28 @@ import {
18
17
  import { cn } from '../../../utils/cn'
19
18
 
20
19
  /**
21
- * Card density:
22
- * - `default`: full /releases page row (vertical title + description on the
23
- * left, version + date column on the right).
24
- * - `sm`: compact horizontal layout (~80px tall) sized for inline rendering
25
- * inside chat messages and other tight surfaces. Drops `<h3>` (block-only,
20
+ * Card density. Two variants, both actively used across openframe + flamingo
21
+ * apps and the related-content rail:
22
+ *
23
+ * - `lg`: rich large card used everywhere a product release is the focal
24
+ * item openframe's `/releases` catalog row, flamingo's DevCenter tab,
25
+ * investor-update related-content section. Three zones — hero (16:9 cover
26
+ * + version pill + title + summary), changelog stats strip (icons +
27
+ * counts), metadata grid footer (Type · Status · Released · Author). The
28
+ * grid mirrors the hub's `<EntityAuthorCard>` byte-for-byte (see lg
29
+ * branch comment).
30
+ *
31
+ * - `sm`: compact horizontal layout (~80px tall) for inline rendering inside
32
+ * chat messages and other tight surfaces. Drops `<h3>` (block-only,
26
33
  * illegal inside markdown `<p>`) for `<span>` text, swaps the outer
27
34
  * `InteractiveCard` for a `<span>`-anchored link, and collapses to:
28
35
  * 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).
36
+ *
37
+ * A previous `default` variant (vertical title+description / version+date
38
+ * column) was deleted in the 2026-05 DRY pass it had a single consumer
39
+ * (the hub's RelatedContentSection) that has since moved to `lg`.
34
40
  */
35
- export type ProductReleaseCardSize = 'default' | 'sm' | 'catalog'
41
+ export type ProductReleaseCardSize = 'lg' | 'sm'
36
42
 
37
43
  /**
38
44
  * Minimal structural `<a>` prop bundle the consumer composes (typically
@@ -80,7 +86,7 @@ export interface ProductReleaseCardProps {
80
86
  anchorProps?: ProductReleaseCardAnchorProps
81
87
  /** Additional CSS classes */
82
88
  className?: string
83
- /** Card density. Defaults to `'default'`. */
89
+ /** Card density. Defaults to `'lg'` (the canonical large card). */
84
90
  size?: ProductReleaseCardSize
85
91
 
86
92
  // ─── Catalog-only props (ignored by `default` / `sm` branches) ─────────
@@ -125,7 +131,7 @@ export function ProductReleaseCard({
125
131
  onClick,
126
132
  anchorProps,
127
133
  className,
128
- size = 'default',
134
+ size = 'lg',
129
135
  coverImage,
130
136
  hasVideoCover,
131
137
  releaseType,
@@ -135,7 +141,7 @@ export function ProductReleaseCard({
135
141
  author,
136
142
  changelogCounts,
137
143
  }: ProductReleaseCardProps) {
138
- // ----- CATALOG branch (rich /releases catalog row) -------------------------
144
+ // ----- LG branch (rich /releases catalog row + related-content rail) -----
139
145
  // The card has THREE zones:
140
146
  // 1. Hero — cover image LEFT, version pill + title + summary RIGHT.
141
147
  // 2. Changelog strip — icons + counts (hidden when total === 0).
@@ -145,11 +151,11 @@ export function ProductReleaseCard({
145
151
  // author-cell shapes, byte-for-byte). The OSS lib has zero hub
146
152
  // coupling by design; we cannot import the hub's
147
153
  // <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') {
154
+ // documented for the COMPACT_CARD_* string set in the chat-card
155
+ // file. If the hub's <EntityAuthorCard> visual changes (cell
156
+ // padding, divider styles, avatar size, etc.), update this branch
157
+ // in lockstep.
158
+ if (size === 'lg') {
153
159
  const totalChangelog =
154
160
  (changelogCounts?.features ?? 0) +
155
161
  (changelogCounts?.fixes ?? 0) +
@@ -157,46 +163,60 @@ export function ProductReleaseCard({
157
163
  (changelogCounts?.breaking ?? 0)
158
164
 
159
165
  // 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.
166
+ // EntityAuthorCard composition. ALWAYS render all 3 value cells
167
+ // (Type / Status / Released) missing values render as a plain
168
+ // em-dash + label so the grid keeps a fixed 4-cell shape (matching
169
+ // the skeleton). The Author cell is also always rendered below
170
+ // (effectiveAuthor falls back to a placeholder shape). This is
171
+ // load-to-resolve baseline parity: any conditional cell would
172
+ // introduce a reflow when the skeleton resolves.
173
+ //
174
+ // Plan note: em-dash placeholders read as plain text (NOT a colored
175
+ // StatusBadge for the Type cell) so empty badges don't look broken
176
+ // next to populated badges.
163
177
  type ValueCell = {
164
178
  value: string
165
179
  label: string
166
180
  uppercase: boolean
167
181
  colorScheme?: 'error' | 'cyan' | 'success' | 'warning'
168
182
  }
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'
183
+ const valueCells: ValueCell[] = [
184
+ releaseType && releaseTypeBadgeColor
185
+ ? {
186
+ value: releaseType.toUpperCase(),
187
+ label: 'Type',
188
+ uppercase: true,
189
+ colorScheme: releaseTypeBadgeColor,
190
+ }
191
+ : { value: '—', label: 'Type', uppercase: false },
192
+ releaseStatus
193
+ ? {
194
+ value: releaseStatus.toUpperCase(),
195
+ label: 'Status',
196
+ uppercase: true,
197
+ }
198
+ : { value: '—', label: 'Status', uppercase: false },
199
+ formattedDate
200
+ ? {
201
+ value: formattedDate,
202
+ label: 'Released',
203
+ uppercase: false,
204
+ }
205
+ : { value: '—', label: 'Released', uppercase: false },
206
+ ]
207
+ // EMPTY_AUTHOR_PLACEHOLDER shape mirrors the hub's
208
+ // EMPTY_AUTHOR_PLACEHOLDER constant exported from
209
+ // components/shared/entity-author-card.tsx (hub can't be imported
210
+ // here; the two are kept in lockstep per the inline-duplication
211
+ // policy documented in the catalog branch comment above).
212
+ const effectiveAuthor = author?.full_name
213
+ ? author
214
+ : { full_name: '—', avatar_url: null, job_title: 'Unknown' }
215
+ // Fixed 4-cell grid (Type / Status / Released / Author) so the
216
+ // skeleton's shape matches the loaded card exactly. The earlier
217
+ // dynamic `gridColsClass` ternary collapsed missing cells and
218
+ // caused 28-56px reflow on resolve.
219
+ const gridColsClass = 'md:grid-cols-4'
200
220
  const dividerClass = 'border-b md:border-b-0 md:border-r border-ods-border'
201
221
 
202
222
  const frameClass = cn(
@@ -242,107 +262,130 @@ export function ProductReleaseCard({
242
262
  v{version}
243
263
  </span>
244
264
  </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}
265
+ {/* Title reserve a fixed 2-line height so cards with
266
+ 1-line titles don't shrink and the catalog skeleton-to-
267
+ content transition is shift-free. Mirrors the
268
+ onboarding-guide catalog card. */}
269
+ <div className="min-h-[60px] md:min-h-[72px] flex items-start mb-3">
270
+ <h3 className="font-['Azeret_Mono'] font-semibold text-xl md:text-2xl text-ods-text-primary leading-tight line-clamp-2">
271
+ {title}
272
+ </h3>
273
+ </div>
274
+ {/* Summary — fixed 3-line height. `line-clamp-3` caps long
275
+ summaries at 3 lines; `min-h` reserves the same vertical
276
+ space when content is shorter, so the catalog grid stays
277
+ row-consistent regardless of per-card content length.
278
+ Heights derived from text-sm md:text-base × leading-relaxed
279
+ (1.625): 14×1.625×3 ≈ 68 px mobile, 16×1.625×3 ≈ 78 px desktop. */}
280
+ <div className="min-h-[68px] md:min-h-[78px]">
281
+ <p className="font-['DM_Sans'] text-sm md:text-base text-ods-text-secondary leading-relaxed line-clamp-3">
282
+ {summary ?? ''}
251
283
  </p>
252
- )}
284
+ </div>
253
285
  </div>
254
286
  </div>
255
287
 
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
- )}
288
+ {/* CHANGELOG STRIP — ALWAYS rendered so the skeleton's
289
+ always-on changelog placeholder matches the loaded shape
290
+ (zero reflow on resolve). When `totalChangelog === 0`, an
291
+ empty-state line takes the same vertical space as the
292
+ populated row. */}
293
+ <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">
294
+ {totalChangelog > 0 && changelogCounts ? (
295
+ <>
296
+ {changelogCounts.features > 0 && (
297
+ <span className="inline-flex items-center gap-1.5">
298
+ <Sparkles className="w-3.5 h-3.5" />
299
+ {changelogCounts.features} {changelogCounts.features === 1 ? 'feature' : 'features'}
300
+ </span>
301
+ )}
302
+ {changelogCounts.fixes > 0 && (
303
+ <span className="inline-flex items-center gap-1.5">
304
+ <Wrench className="w-3.5 h-3.5" />
305
+ {changelogCounts.fixes} {changelogCounts.fixes === 1 ? 'fix' : 'fixes'}
306
+ </span>
307
+ )}
308
+ {changelogCounts.improvements > 0 && (
309
+ <span className="inline-flex items-center gap-1.5">
310
+ <TrendingUp className="w-3.5 h-3.5" />
311
+ {changelogCounts.improvements} {changelogCounts.improvements === 1 ? 'improvement' : 'improvements'}
312
+ </span>
313
+ )}
314
+ {changelogCounts.breaking > 0 && (
315
+ <span className="inline-flex items-center gap-1.5 text-[var(--ods-attention-yellow-warning)]">
316
+ <AlertTriangle className="w-3.5 h-3.5" />
317
+ {changelogCounts.breaking} breaking
318
+ </span>
319
+ )}
320
+ </>
321
+ ) : (
322
+ <span className="text-sm text-ods-text-secondary">No changelog entries yet</span>
323
+ )}
324
+ </div>
285
325
 
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'}
326
+ {/* METADATA GRID FOOTER — fixed 4-cell shape (Type / Status /
327
+ Released / Author) so the skeleton mirrors the loaded card
328
+ exactly. Empty value cells render em-dash + label (plain
329
+ text, no colored badge — em-dash badges read as broken next
330
+ to populated ones); the Author cell falls back to the
331
+ EMPTY_AUTHOR_PLACEHOLDER shape declared above. */}
332
+ <div
333
+ className={cn(
334
+ 'grid grid-cols-1',
335
+ gridColsClass,
336
+ 'border border-ods-border rounded-md overflow-hidden w-full',
337
+ )}
338
+ >
339
+ {valueCells.map((cell, i) => (
340
+ <div
341
+ key={`${cell.label}-${i}`}
342
+ className={cn('bg-ods-card p-4 flex flex-col gap-3', dividerClass)}
343
+ >
344
+ <div className="flex flex-col gap-0">
345
+ {cell.colorScheme ? (
346
+ <StatusBadge
347
+ text={cell.value}
348
+ variant="card"
349
+ colorScheme={cell.colorScheme}
350
+ singleLine
351
+ className="self-start"
352
+ />
353
+ ) : (
354
+ <p
355
+ className={cn(
356
+ 'text-h4',
357
+ // Em-dash placeholder reads as secondary text;
358
+ // populated values stay primary.
359
+ cell.value === '' ? 'text-ods-text-secondary' : 'text-ods-text-primary',
360
+ )}
361
+ >
362
+ {cell.uppercase ? cell.value.toLocaleUpperCase() : cell.value}
340
363
  </p>
341
- </div>
364
+ )}
365
+ <p className="font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary">
366
+ {cell.label}
367
+ </p>
342
368
  </div>
343
- )}
369
+ </div>
370
+ ))}
371
+ <div className="bg-ods-card p-4 flex items-center gap-3">
372
+ <SquareAvatar
373
+ src={effectiveAuthor.avatar_url ?? undefined}
374
+ alt={effectiveAuthor.full_name}
375
+ fallback={effectiveAuthor.full_name.charAt(0).toUpperCase()}
376
+ size="md"
377
+ variant="round"
378
+ />
379
+ <div className="flex flex-col gap-0 flex-1 min-w-0">
380
+ <p className="text-h3 tracking-[-0.36px] text-ods-text-primary truncate">
381
+ {effectiveAuthor.full_name}
382
+ </p>
383
+ <p className="font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary">
384
+ {effectiveAuthor.job_title || 'Author'}
385
+ </p>
386
+ </div>
344
387
  </div>
345
- )}
388
+ </div>
346
389
 
347
390
  {typeof viewCount === 'number' && viewCount > 0 && (
348
391
  <div className="flex items-center gap-1.5 text-xs text-ods-text-secondary">
@@ -519,99 +562,8 @@ export function ProductReleaseCard({
519
562
  )
520
563
  }
521
564
 
522
- // ----- DEFAULT branch (existing /releases card layout) -----------------
523
- // When `anchorProps` is supplied, the card behaves like every other
524
- // entity card on the related-content rail — real `<a>` with the
525
- // consumer's `useNavLink` bundle (cross-origin new tab, same-origin
526
- // → soft RSC nav, modifier-click → browser default). The
527
- // `<InteractiveCard onClick>` form remains the back-compat path for
528
- // the public `/releases` tab caller which still routes via
529
- // `router.push()` directly.
530
- if (anchorProps) {
531
- return (
532
- <a
533
- {...anchorProps}
534
- className={cn(
535
- 'bg-ods-card border border-ods-border rounded-[6px]',
536
- 'flex flex-col md:flex-row',
537
- 'items-start md:items-center',
538
- 'gap-3 md:gap-4',
539
- 'p-4 no-underline',
540
- 'transition-colors hover:border-ods-text-secondary/40',
541
- 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent focus-visible:ring-offset-1 focus-visible:ring-offset-ods-card',
542
- className,
543
- )}
544
- >
545
- {/* Left column - content */}
546
- <div className="flex-1 w-full md:w-auto min-w-0 flex flex-col justify-center gap-2">
547
- <div className="min-h-[48px] flex items-center">
548
- <h3 className="text-h3 text-ods-text-primary tracking-[-0.36px] line-clamp-2" title={title}>
549
- {title}
550
- </h3>
551
- </div>
552
- <p className="text-h4 text-ods-text-secondary line-clamp-3" title={summary || ' '}>
553
- {summary || ' '}
554
- </p>
555
- </div>
556
- <div className="flex items-center gap-2 w-full md:w-auto justify-start md:justify-end shrink-0">
557
- <div className="w-[200px] flex flex-col justify-center gap-2">
558
- <p className="text-h3 text-ods-text-primary tracking-[-0.36px] truncate">
559
- {version}
560
- </p>
561
- <p className="font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary truncate">
562
- {formattedDate}
563
- </p>
564
- </div>
565
- <div className="flex items-center justify-center p-3 shrink-0">
566
- <ChevronRight className="h-6 w-6 text-ods-text-primary" />
567
- </div>
568
- </div>
569
- </a>
570
- )
571
- }
572
- return (
573
- <InteractiveCard
574
- clickable={true}
575
- onClick={onClick}
576
- className={cn(
577
- 'bg-ods-card border border-ods-border rounded-[6px]',
578
- 'flex flex-col md:flex-row',
579
- 'items-start md:items-center',
580
- 'gap-3 md:gap-4',
581
- 'p-4',
582
- className
583
- )}
584
- >
585
- {/* Left column - content */}
586
- <div className="flex-1 w-full md:w-auto min-w-0 flex flex-col justify-center gap-2">
587
- <div className="min-h-[48px] flex items-center">
588
- <h3 className="text-h3 text-ods-text-primary tracking-[-0.36px] line-clamp-2" title={title}>
589
- {title}
590
- </h3>
591
- </div>
592
- <p className="text-h4 text-ods-text-secondary line-clamp-3" title={summary || ' '}>
593
- {summary || ' '}
594
- </p>
595
- </div>
596
-
597
- {/* Right column - version + date */}
598
- <div
599
- className="flex items-center gap-2 w-full md:w-auto justify-start md:justify-end shrink-0"
600
- onClick={(e) => e.stopPropagation()}
601
- >
602
- <div className="w-[200px] flex flex-col justify-center gap-2">
603
- <p className="text-h3 text-ods-text-primary tracking-[-0.36px] truncate">
604
- {version}
605
- </p>
606
- <p className="font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary truncate">
607
- {formattedDate}
608
- </p>
609
- </div>
610
- {/* Icon column */}
611
- <div className="flex items-center justify-center p-3 shrink-0">
612
- <ChevronRight className="h-6 w-6 text-ods-text-primary" />
613
- </div>
614
- </div>
615
- </InteractiveCard>
616
- )
565
+ // Unreachable `size` is typed `'lg' | 'sm'` and both branches return
566
+ // above. Kept as a defensive throw so a future variant addition that
567
+ // forgets to return doesn't silently render `undefined`.
568
+ throw new Error(`ProductReleaseCard: unsupported size '${size as string}'`)
617
569
  }
@@ -0,0 +1,68 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Endpoints runtime — sibling of ChatRuntime. Carries the API path
5
+ * literals consumed by oss-lib components/hooks/utils so a host
6
+ * application can override them (e.g. when running behind a reverse
7
+ * proxy as `user1.openframe.ai` → `/api/mingo-guide/*`).
8
+ *
9
+ * The hub mounts `<HubRuntimeProvider>` at root with the
10
+ * canonical hub paths; an embedded app mounts its own provider with
11
+ * remapped paths. The pattern mirrors ChatRuntimeContext exactly:
12
+ *
13
+ * - `useEndpointsRuntime()` returns null when no provider is mounted.
14
+ * For optional consumers that should gracefully no-op without one.
15
+ * - `useRequiredEndpointsRuntime()` throws on missing provider — for
16
+ * hooks/components that cannot function without endpoints.
17
+ *
18
+ * IMPORTANT for embedders: memoize the value passed to
19
+ * `<EndpointsRuntimeContext.Provider value={...}>` (e.g. React.useMemo).
20
+ * Reference changes invalidate downstream effect dependency arrays and
21
+ * trigger unnecessary re-fetches.
22
+ */
23
+
24
+ import { createContext, useContext } from 'react'
25
+
26
+ export interface EndpointsRuntime {
27
+ /** GET active announcement (used by `<AnnouncementBar>` polling). */
28
+ announcementsUrl: string
29
+ accessCode: {
30
+ /** POST validate access code. */
31
+ validateUrl: string
32
+ /** POST consume / redeem access code after registration. */
33
+ consumeUrl: string
34
+ }
35
+ /** POST contact-form submission. */
36
+ contactUrl: string
37
+ }
38
+
39
+ export const EndpointsRuntimeContext = createContext<EndpointsRuntime | null>(null)
40
+
41
+ /**
42
+ * Optional read — returns null when no provider is mounted. Use for
43
+ * surfaces that should silently skip the fetch (e.g. announcement
44
+ * polling on a page rendered outside the provider tree).
45
+ */
46
+ export function useEndpointsRuntime(): EndpointsRuntime | null {
47
+ return useContext(EndpointsRuntimeContext)
48
+ }
49
+
50
+ /**
51
+ * Strict variant — throws on missing provider. Use for consumers that
52
+ * cannot function without an endpoint (form submission, code
53
+ * validation). In tests/Storybook, wrap with the hub's
54
+ * `<HubRuntimeProvider>` or a stub
55
+ * `<EndpointsRuntimeContext.Provider value={mockedEndpoints}>`.
56
+ */
57
+ export function useRequiredEndpointsRuntime(): EndpointsRuntime {
58
+ const v = useContext(EndpointsRuntimeContext)
59
+ if (!v) {
60
+ throw new Error(
61
+ '[endpoints-runtime] hook called outside an <EndpointsRuntimeContext.Provider>. ' +
62
+ 'Hub: mount <HubRuntimeProvider> in your providers tree. ' +
63
+ 'Embedded app: mount your own provider with proxied URLs at the tree root. ' +
64
+ 'Tests/Storybook: wrap render() in <EndpointsRuntimeContext.Provider value={mocked}>.',
65
+ )
66
+ }
67
+ return v
68
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Runtime contexts for embeddable oss-lib components.
3
+ *
4
+ * BUNDLE-SPLIT CONVENTION — read before adding new files here:
5
+ * Every module in `src/contexts/` calls `createContext()` at module
6
+ * top level. The tsup config builds this directory under the CLIENT
7
+ * bundle (with `"use client"` banner). Importing anything from here
8
+ * into a SERVER-bundled module (`utils/index.ts` and its `export *`
9
+ * transitive closure) crashes SSR with
10
+ * `createContext is not a function`.
11
+ *
12
+ * Rule of thumb when adding a runtime-aware helper:
13
+ * - Pure functions that take endpoints/runtime as args → `utils/`.
14
+ * - React hooks that READ the runtime via useContext → `hooks/`
15
+ * (with `'use client'` at the top of the file).
16
+ * - Never re-export from `utils/*` anything that pulls
17
+ * `src/contexts/*` transitively.
18
+ *
19
+ * The split between `utils/access-code-client.ts` (pure) and
20
+ * `hooks/use-access-code-integration.ts` (runtime-aware) is the
21
+ * reference example.
22
+ */
23
+
24
+ export {
25
+ EndpointsRuntimeContext,
26
+ useEndpointsRuntime,
27
+ useRequiredEndpointsRuntime,
28
+ type EndpointsRuntime,
29
+ } from './endpoints-runtime-context'
@@ -18,3 +18,9 @@ export * from './nats/use-nats-client'
18
18
 
19
19
  // Viewport / lazy-mount primitive (shared IO singleton)
20
20
  export * from './use-near-viewport'
21
+
22
+ // Access code integration — pairs with the standalone helpers in
23
+ // `utils/access-code-client`. Lives in `hooks/` so the createContext
24
+ // pulled in via EndpointsRuntimeContext doesn't end up in the
25
+ // server-safe utils bundle.
26
+ export * from './use-access-code-integration'