@flamingo-stack/openframe-frontend-core 0.0.396-snapshot.20260709212121 → 0.0.396

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 (120) hide show
  1. package/dist/{chunk-IVXOR2IA.js → chunk-2PTEK3D6.js} +2 -2
  2. package/dist/{chunk-DB6CUWTL.cjs → chunk-6DGQPKVA.cjs} +26 -26
  3. package/dist/{chunk-DB6CUWTL.cjs.map → chunk-6DGQPKVA.cjs.map} +1 -1
  4. package/dist/{chunk-OAENYOKC.cjs → chunk-AODSKL5I.cjs} +9 -9
  5. package/dist/{chunk-OAENYOKC.cjs.map → chunk-AODSKL5I.cjs.map} +1 -1
  6. package/dist/{chunk-M6NPDNVB.cjs → chunk-CCV6Z3CL.cjs} +26 -26
  7. package/dist/{chunk-M6NPDNVB.cjs.map → chunk-CCV6Z3CL.cjs.map} +1 -1
  8. package/dist/{chunk-JNU3YPLP.js → chunk-EKYE2VJU.js} +4 -4
  9. package/dist/{chunk-J6TAK6HE.js → chunk-FAUC7EA3.js} +4 -4
  10. package/dist/{chunk-2OOZASQE.js → chunk-GA3R77TB.js} +2 -2
  11. package/dist/{chunk-VTR4QDL2.js → chunk-H4WIS6RJ.js} +2 -2
  12. package/dist/{chunk-HPQKX5IW.cjs → chunk-HEH47UTC.cjs} +15 -15
  13. package/dist/{chunk-HPQKX5IW.cjs.map → chunk-HEH47UTC.cjs.map} +1 -1
  14. package/dist/{chunk-XNAYZHPK.cjs → chunk-HNXZFKDQ.cjs} +62 -62
  15. package/dist/{chunk-XNAYZHPK.cjs.map → chunk-HNXZFKDQ.cjs.map} +1 -1
  16. package/dist/{chunk-T5F54MN4.cjs → chunk-HWJUWOXK.cjs} +12 -12
  17. package/dist/{chunk-T5F54MN4.cjs.map → chunk-HWJUWOXK.cjs.map} +1 -1
  18. package/dist/{chunk-K5FRWPWF.js → chunk-J2UHW4UM.js} +2 -2
  19. package/dist/{chunk-55SNCMFS.cjs → chunk-KRMGVMIO.cjs} +31 -31
  20. package/dist/{chunk-55SNCMFS.cjs.map → chunk-KRMGVMIO.cjs.map} +1 -1
  21. package/dist/{chunk-Z6PAD2UN.cjs → chunk-NAFW3LKI.cjs} +7 -7
  22. package/dist/{chunk-Z6PAD2UN.cjs.map → chunk-NAFW3LKI.cjs.map} +1 -1
  23. package/dist/{chunk-TTHTO5H5.js → chunk-NFRHBR7W.js} +4 -4
  24. package/dist/{chunk-3E4BDAHJ.js → chunk-OP3H7VB2.js} +2 -2
  25. package/dist/{chunk-ZFK4HHRX.js → chunk-PQXUY6SB.js} +2 -2
  26. package/dist/{chunk-43C5ECNT.cjs → chunk-RUYZ5QMK.cjs} +37 -37
  27. package/dist/{chunk-43C5ECNT.cjs.map → chunk-RUYZ5QMK.cjs.map} +1 -1
  28. package/dist/{chunk-D4F7ZLJG.js → chunk-STEM5CPV.js} +4 -4
  29. package/dist/{chunk-CK5UZLIN.js → chunk-SV7IJPIH.js} +5000 -5313
  30. package/dist/chunk-SV7IJPIH.js.map +1 -0
  31. package/dist/{chunk-WCDM226S.js → chunk-TIC3UE6L.js} +2 -2
  32. package/dist/{chunk-BB7TZ6KS.cjs → chunk-VSPNWVIE.cjs} +59 -59
  33. package/dist/{chunk-BB7TZ6KS.cjs.map → chunk-VSPNWVIE.cjs.map} +1 -1
  34. package/dist/{chunk-SBWTMJPQ.cjs → chunk-XQILJ7FF.cjs} +5 -5
  35. package/dist/{chunk-SBWTMJPQ.cjs.map → chunk-XQILJ7FF.cjs.map} +1 -1
  36. package/dist/{chunk-ZUKBC2ZX.cjs → chunk-Y36KVLBX.cjs} +798 -1111
  37. package/dist/chunk-Y36KVLBX.cjs.map +1 -0
  38. package/dist/components/case-studies/index.cjs +8 -8
  39. package/dist/components/case-studies/index.js +2 -2
  40. package/dist/components/chat/entity-cards/blog-card.d.ts +3 -6
  41. package/dist/components/chat/entity-cards/blog-card.d.ts.map +1 -1
  42. package/dist/components/chat/entity-cards/case-study-card.d.ts +3 -7
  43. package/dist/components/chat/entity-cards/case-study-card.d.ts.map +1 -1
  44. package/dist/components/chat/entity-cards/customer-interview-card.d.ts +3 -6
  45. package/dist/components/chat/entity-cards/customer-interview-card.d.ts.map +1 -1
  46. package/dist/components/chat/entity-cards/index.d.ts +0 -3
  47. package/dist/components/chat/entity-cards/index.d.ts.map +1 -1
  48. package/dist/components/chat/entity-cards/investor-update-card.d.ts +3 -6
  49. package/dist/components/chat/entity-cards/investor-update-card.d.ts.map +1 -1
  50. package/dist/components/chat/entity-cards/onboarding-guide-card.d.ts +2 -4
  51. package/dist/components/chat/entity-cards/onboarding-guide-card.d.ts.map +1 -1
  52. package/dist/components/chat/entity-cards/program-card.d.ts +5 -17
  53. package/dist/components/chat/entity-cards/program-card.d.ts.map +1 -1
  54. package/dist/components/chat/index.cjs +2 -10
  55. package/dist/components/chat/index.cjs.map +1 -1
  56. package/dist/components/chat/index.js +1 -9
  57. package/dist/components/chat/types/entities/program-types.d.ts +0 -23
  58. package/dist/components/chat/types/entities/program-types.d.ts.map +1 -1
  59. package/dist/components/contact/index.cjs +3 -3
  60. package/dist/components/contact/index.js +2 -2
  61. package/dist/components/docs/index.cjs +5 -5
  62. package/dist/components/docs/index.js +4 -4
  63. package/dist/components/embeds/index.cjs +3 -3
  64. package/dist/components/embeds/index.js +2 -2
  65. package/dist/components/faq/index.cjs +3 -3
  66. package/dist/components/faq/index.js +2 -2
  67. package/dist/components/features/cards-strip.d.ts.map +1 -1
  68. package/dist/components/features/index.cjs +2 -2
  69. package/dist/components/features/index.js +1 -1
  70. package/dist/components/features/video-bites-strip.d.ts.map +1 -1
  71. package/dist/components/help-center-pages/index.cjs +20 -20
  72. package/dist/components/help-center-pages/index.js +11 -11
  73. package/dist/components/index.cjs +120 -128
  74. package/dist/components/index.cjs.map +1 -1
  75. package/dist/components/index.js +9 -17
  76. package/dist/components/index.js.map +1 -1
  77. package/dist/components/navigation/index.cjs +2 -2
  78. package/dist/components/navigation/index.js +1 -1
  79. package/dist/components/onboarding-guides/index.cjs +5 -5
  80. package/dist/components/onboarding-guides/index.js +4 -4
  81. package/dist/components/related-content/index.cjs +3 -3
  82. package/dist/components/related-content/index.js +2 -2
  83. package/dist/components/tickets/index.cjs +5 -5
  84. package/dist/components/tickets/index.js +4 -4
  85. package/dist/components/ui/index.cjs +2 -10
  86. package/dist/components/ui/index.cjs.map +1 -1
  87. package/dist/components/ui/index.js +1 -9
  88. package/dist/index.cjs +2 -10
  89. package/dist/index.cjs.map +1 -1
  90. package/dist/index.js +1 -9
  91. package/package.json +1 -1
  92. package/src/components/chat/entity-cards/blog-card.tsx +7 -40
  93. package/src/components/chat/entity-cards/case-study-card.tsx +6 -32
  94. package/src/components/chat/entity-cards/customer-interview-card.tsx +6 -31
  95. package/src/components/chat/entity-cards/index.ts +0 -18
  96. package/src/components/chat/entity-cards/investor-update-card.tsx +2 -28
  97. package/src/components/chat/entity-cards/onboarding-guide-card.tsx +1 -30
  98. package/src/components/chat/entity-cards/program-card.tsx +17 -69
  99. package/src/components/chat/types/entities/program-types.ts +0 -34
  100. package/src/components/features/cards-strip.tsx +8 -122
  101. package/src/components/features/video-bites-strip.tsx +0 -4
  102. package/dist/chunk-CK5UZLIN.js.map +0 -1
  103. package/dist/chunk-ZUKBC2ZX.cjs.map +0 -1
  104. package/dist/components/chat/entity-cards/entity-portrait-card.d.ts +0 -59
  105. package/dist/components/chat/entity-cards/entity-portrait-card.d.ts.map +0 -1
  106. package/dist/components/chat/entity-cards/use-cover-image-fallback.d.ts +0 -26
  107. package/dist/components/chat/entity-cards/use-cover-image-fallback.d.ts.map +0 -1
  108. package/src/components/chat/entity-cards/entity-portrait-card.tsx +0 -201
  109. package/src/components/chat/entity-cards/use-cover-image-fallback.ts +0 -50
  110. /package/dist/{chunk-IVXOR2IA.js.map → chunk-2PTEK3D6.js.map} +0 -0
  111. /package/dist/{chunk-JNU3YPLP.js.map → chunk-EKYE2VJU.js.map} +0 -0
  112. /package/dist/{chunk-J6TAK6HE.js.map → chunk-FAUC7EA3.js.map} +0 -0
  113. /package/dist/{chunk-2OOZASQE.js.map → chunk-GA3R77TB.js.map} +0 -0
  114. /package/dist/{chunk-VTR4QDL2.js.map → chunk-H4WIS6RJ.js.map} +0 -0
  115. /package/dist/{chunk-K5FRWPWF.js.map → chunk-J2UHW4UM.js.map} +0 -0
  116. /package/dist/{chunk-TTHTO5H5.js.map → chunk-NFRHBR7W.js.map} +0 -0
  117. /package/dist/{chunk-3E4BDAHJ.js.map → chunk-OP3H7VB2.js.map} +0 -0
  118. /package/dist/{chunk-ZFK4HHRX.js.map → chunk-PQXUY6SB.js.map} +0 -0
  119. /package/dist/{chunk-D4F7ZLJG.js.map → chunk-STEM5CPV.js.map} +0 -0
  120. /package/dist/{chunk-WCDM226S.js.map → chunk-TIC3UE6L.js.map} +0 -0
@@ -1,59 +0,0 @@
1
- /**
2
- * <EntityPortraitCard> — THE portrait (rail/strip) card anatomy, shared by
3
- * EVERY entity card's `size="portrait"` density (case study, blog post,
4
- * customer interview, program, investor update, onboarding guide, …).
5
- *
6
- * One rail = one anatomy (2026 card-UI practice): identical zone boxes across
7
- * all content types —
8
- * media : aspect-[1200/630] slot + content-type chip overlay
9
- * title : fixed 72px zone, 3-line clamp (typography per entity via
10
- * `titleClassName` — e.g. Azeret Mono for programs)
11
- * footer : fixed 60px person zone (author avatar + name + subtitle, with
12
- * the optional bottom-right icon overlay — the case-study MSP
13
- * pattern)
14
- * Card surface, padding (p-6/gap-6) and the hover treatment (accent border +
15
- * accent shadow) are encoded HERE exactly once, so no per-card drift is
16
- * possible. Entity cards only MAP their row onto these props.
17
- *
18
- * Media rule ("our common way"): the REAL cover always wins. Wide covers
19
- * (OG-style 1200×630) fill the slot `object-cover`; a non-wide cover (square
20
- * podcast artwork) renders contained on an `useImageEdgeColor` background —
21
- * the SAME edge-sampled fill the media/news cards use (OGLinkPreview) — never
22
- * a flat filler and never the generic placeholder while a real image exists.
23
- * The cover → placeholder → hide resolution is the shared
24
- * `useCoverImageFallback` chain (one fallback logic for all entity cards).
25
- *
26
- * The content-type chip is the common <StatusBadge> and lives ABOVE the title
27
- * (an eyebrow inside the title zone), never overlaid on the artwork — branded
28
- * covers carry their own marks (Flamingo logo top-left) that an overlay would
29
- * collide with.
30
- */
31
- import React from 'react';
32
- export interface EntityPortraitPerson {
33
- name: string;
34
- avatarUrl?: string | null;
35
- subtitle?: string | null;
36
- /** Small round overlay on the avatar's bottom-right corner (e.g. MSP icon). */
37
- iconOverlayUrl?: string | null;
38
- }
39
- export interface EntityPortraitCardProps {
40
- href: string;
41
- target?: '_blank';
42
- rel?: string;
43
- /** Content-type chip ('Case Study', 'Podcast', …). OMIT to hide the chip —
44
- * single-type rails don't need per-card type identification; only mixed
45
- * rails pass it. */
46
- typeLabel?: string;
47
- imageUrl?: string | null;
48
- /** Branded wide OG fallback — used when `imageUrl` is missing, errors, or
49
- * isn't wide. */
50
- placeholderUrl?: string | null;
51
- imageAlt: string;
52
- title: string;
53
- /** Typography override for the title (entity identity), zone box unchanged. */
54
- titleClassName?: string;
55
- person?: EntityPortraitPerson | null;
56
- className?: string;
57
- }
58
- export declare function EntityPortraitCard({ href, target, rel, typeLabel, imageUrl, placeholderUrl, imageAlt, title, titleClassName, person, className, }: EntityPortraitCardProps): React.JSX.Element;
59
- //# sourceMappingURL=entity-portrait-card.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"entity-portrait-card.d.ts","sourceRoot":"","sources":["../../../../src/components/chat/entity-cards/entity-portrait-card.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAA;AAYlD,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,QAAQ,CAAA;IACjB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;yBAEqB;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;sBACkB;IAClB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAA;IACpC,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,kBAAkB,CAAC,EACjC,IAAI,EACJ,MAAM,EACN,GAAG,EACH,SAAS,EACT,QAAQ,EACR,cAAc,EACd,QAAQ,EACR,KAAK,EACL,cAAc,EACd,MAAM,EACN,SAAS,GACV,EAAE,uBAAuB,qBAmHzB"}
@@ -1,26 +0,0 @@
1
- /**
2
- * THE common cover-image fallback chain for entity cards:
3
- * real cover → branded placeholder → nothing (hide the media element).
4
- *
5
- * One hook so no card hand-rolls its own `imageError` state / `displayImage`
6
- * ternary (they drift: the pre-hook BlogCard chain couldn't recover from a
7
- * FAILING placeholder, EntityPortraitCard duplicated the same logic with
8
- * different state). Cards wire `src` + `onError` and render their own
9
- * empty-state when `src` is null.
10
- *
11
- * Implementation note: state is the SET of candidate URLs that failed, and
12
- * `src` is derived — so a prop change (new cover URL) re-resolves instantly
13
- * with no reset effect, and a URL that failed once isn't retried in a loop.
14
- */
15
- import { type SyntheticEvent } from 'react';
16
- export interface CoverImageFallback {
17
- /** Resolved source: first non-failed of [imageUrl, placeholderUrl], else null. */
18
- src: string | null;
19
- /** Wire to the media element's `onError`. */
20
- onError: () => void;
21
- }
22
- export declare function useCoverImageFallback(imageUrl?: string | null, placeholderUrl?: string | null): CoverImageFallback;
23
- /** Fallback for cards with NO placeholder chain (compact/sm image slots):
24
- * a broken cover simply disappears, leaving the slot's background. */
25
- export declare const hideOnError: (e: SyntheticEvent<HTMLImageElement>) => void;
26
- //# sourceMappingURL=use-cover-image-fallback.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"use-cover-image-fallback.d.ts","sourceRoot":"","sources":["../../../../src/components/chat/entity-cards/use-cover-image-fallback.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAyB,KAAK,cAAc,EAAE,MAAM,OAAO,CAAA;AAElE,MAAM,WAAW,kBAAkB;IACjC,kFAAkF;IAClF,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,EACxB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,GAC7B,kBAAkB,CAcpB;AAED;uEACuE;AACvE,eAAO,MAAM,WAAW,GAAI,GAAG,cAAc,CAAC,gBAAgB,CAAC,SAE9D,CAAA"}
@@ -1,201 +0,0 @@
1
- 'use client'
2
-
3
- /**
4
- * <EntityPortraitCard> — THE portrait (rail/strip) card anatomy, shared by
5
- * EVERY entity card's `size="portrait"` density (case study, blog post,
6
- * customer interview, program, investor update, onboarding guide, …).
7
- *
8
- * One rail = one anatomy (2026 card-UI practice): identical zone boxes across
9
- * all content types —
10
- * media : aspect-[1200/630] slot + content-type chip overlay
11
- * title : fixed 72px zone, 3-line clamp (typography per entity via
12
- * `titleClassName` — e.g. Azeret Mono for programs)
13
- * footer : fixed 60px person zone (author avatar + name + subtitle, with
14
- * the optional bottom-right icon overlay — the case-study MSP
15
- * pattern)
16
- * Card surface, padding (p-6/gap-6) and the hover treatment (accent border +
17
- * accent shadow) are encoded HERE exactly once, so no per-card drift is
18
- * possible. Entity cards only MAP their row onto these props.
19
- *
20
- * Media rule ("our common way"): the REAL cover always wins. Wide covers
21
- * (OG-style 1200×630) fill the slot `object-cover`; a non-wide cover (square
22
- * podcast artwork) renders contained on an `useImageEdgeColor` background —
23
- * the SAME edge-sampled fill the media/news cards use (OGLinkPreview) — never
24
- * a flat filler and never the generic placeholder while a real image exists.
25
- * The cover → placeholder → hide resolution is the shared
26
- * `useCoverImageFallback` chain (one fallback logic for all entity cards).
27
- *
28
- * The content-type chip is the common <StatusBadge> and lives ABOVE the title
29
- * (an eyebrow inside the title zone), never overlaid on the artwork — branded
30
- * covers carry their own marks (Flamingo logo top-left) that an overlay would
31
- * collide with.
32
- */
33
-
34
- import React, { useEffect, useState } from 'react'
35
- import Image from '../../../embed-shims/next-image'
36
- import { Card } from '../../ui/card'
37
- import { StatusBadge } from '../../ui/status-badge'
38
- import { useImageEdgeColor } from '../../../hooks/ui/use-image-edge-color'
39
- import { useCoverImageFallback } from './use-cover-image-fallback'
40
- import { cn } from '../../../utils/cn'
41
-
42
- /** Sources narrower than this are "not wide" → contained on the edge-color
43
- * fill. Wide covers are 1200/630 ≈ 1.9; squares are 1.0; 1.3 splits safely. */
44
- const MIN_WIDE_RATIO = 1.3
45
-
46
- export interface EntityPortraitPerson {
47
- name: string
48
- avatarUrl?: string | null
49
- subtitle?: string | null
50
- /** Small round overlay on the avatar's bottom-right corner (e.g. MSP icon). */
51
- iconOverlayUrl?: string | null
52
- }
53
-
54
- export interface EntityPortraitCardProps {
55
- href: string
56
- target?: '_blank'
57
- rel?: string
58
- /** Content-type chip ('Case Study', 'Podcast', …). OMIT to hide the chip —
59
- * single-type rails don't need per-card type identification; only mixed
60
- * rails pass it. */
61
- typeLabel?: string
62
- imageUrl?: string | null
63
- /** Branded wide OG fallback — used when `imageUrl` is missing, errors, or
64
- * isn't wide. */
65
- placeholderUrl?: string | null
66
- imageAlt: string
67
- title: string
68
- /** Typography override for the title (entity identity), zone box unchanged. */
69
- titleClassName?: string
70
- person?: EntityPortraitPerson | null
71
- className?: string
72
- }
73
-
74
- export function EntityPortraitCard({
75
- href,
76
- target,
77
- rel,
78
- typeLabel,
79
- imageUrl,
80
- placeholderUrl,
81
- imageAlt,
82
- title,
83
- titleClassName,
84
- person,
85
- className,
86
- }: EntityPortraitCardProps) {
87
- const { src, onError: onMediaError } = useCoverImageFallback(imageUrl, placeholderUrl)
88
- // null = unknown (assume wide until the image reports its natural size).
89
- const [isWide, setIsWide] = useState<boolean | null>(null)
90
- useEffect(() => {
91
- // Re-detect whenever the resolved source changes (prop change OR the
92
- // fallback chain advancing to the placeholder).
93
- setIsWide(null)
94
- }, [src])
95
-
96
- const onMediaLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
97
- const img = e.currentTarget
98
- if (img.naturalWidth > 0 && img.naturalHeight > 0) {
99
- setIsWide(img.naturalWidth / img.naturalHeight >= MIN_WIDE_RATIO)
100
- }
101
- }
102
-
103
- // Edge-sampled slot fill for non-wide covers — the SAME hook/fallback the
104
- // media/news cards use (og-link-preview.tsx).
105
- const edgeColor = useImageEdgeColor(isWide === false ? src : null, 'var(--ods-bg-secondary)')
106
-
107
- return (
108
- <a href={href} target={target} rel={rel} className={cn('block h-full', className)} aria-label={`Open ${title}`}>
109
- <Card className="bg-ods-card border border-ods-border hover:border-ods-accent hover:shadow-lg hover:shadow-ods-accent/[0.08] transition-all duration-200 p-6 flex flex-col gap-6 overflow-hidden h-full">
110
- {/* Media zone — the real cover always wins: wide → cover-fill;
111
- non-wide → contained on the edge-color fill (news-card treatment). */}
112
- <div
113
- className="relative w-full aspect-[1200/630] rounded-sm overflow-hidden bg-ods-bg shrink-0 flex items-center justify-center transition-colors duration-300"
114
- style={isWide === false ? { backgroundColor: edgeColor } : undefined}
115
- >
116
- {src && (
117
- <Image
118
- src={src}
119
- alt={imageAlt}
120
- className={cn('w-full h-full', isWide === false ? 'object-contain' : 'object-cover')}
121
- sizes="(min-width: 800px) 400px, 100vw"
122
- fill
123
- unoptimized
124
- onLoad={onMediaLoad}
125
- onError={onMediaError}
126
- />
127
- )}
128
- </div>
129
-
130
- {/* Title zone — fixed box. Mixed rails: common StatusBadge eyebrow +
131
- 2-line title (chip lives here, never on the artwork). Single-type
132
- rails (no typeLabel): centered 3-line title. */}
133
- <div className="h-[72px] flex flex-col justify-center gap-1.5 shrink-0">
134
- {typeLabel && (
135
- <StatusBadge
136
- variant="button"
137
- colorScheme="accentBorder"
138
- singleLine
139
- text={typeLabel}
140
- className="self-start"
141
- />
142
- )}
143
- <h3
144
- className={cn(
145
- 'text-h4 leading-6 text-ods-text-primary break-words',
146
- typeLabel ? 'line-clamp-2' : 'line-clamp-3',
147
- titleClassName,
148
- )}
149
- >
150
- {title}
151
- </h3>
152
- </div>
153
-
154
- {/* Person/footer zone — fixed box (kept even when empty so every card
155
- in a rail shares baselines). */}
156
- <div className="h-[60px] flex items-center shrink-0">
157
- {person && (
158
- <div className="flex items-center gap-3 min-w-0 w-full">
159
- <div className="relative shrink-0 w-12 h-12">
160
- {person.avatarUrl ? (
161
- <Image
162
- src={person.avatarUrl}
163
- alt={person.name}
164
- className="w-12 h-12 rounded-full object-cover bg-ods-bg border border-ods-border"
165
- width={48}
166
- height={48}
167
- unoptimized
168
- />
169
- ) : (
170
- <div className="w-12 h-12 rounded-full bg-ods-bg border border-ods-border flex items-center justify-center">
171
- <span className="text-ods-text-secondary font-medium text-xl">
172
- {person.name.charAt(0).toUpperCase()}
173
- </span>
174
- </div>
175
- )}
176
- {person.iconOverlayUrl && (
177
- <div className="absolute -bottom-1 -right-1 w-6 h-6 rounded-full bg-ods-text-primary ring-1 ring-ods-bg overflow-hidden flex items-center justify-center">
178
- <Image
179
- src={person.iconOverlayUrl}
180
- alt=""
181
- className="w-full h-full object-cover"
182
- width={24}
183
- height={24}
184
- unoptimized
185
- />
186
- </div>
187
- )}
188
- </div>
189
- <div className="flex flex-col min-w-0">
190
- <span className="font-['DM_Sans'] font-bold text-ods-text-primary truncate">{person.name}</span>
191
- {person.subtitle && (
192
- <span className="font-['DM_Sans'] text-sm text-ods-text-secondary truncate">{person.subtitle}</span>
193
- )}
194
- </div>
195
- </div>
196
- )}
197
- </div>
198
- </Card>
199
- </a>
200
- )
201
- }
@@ -1,50 +0,0 @@
1
- 'use client'
2
-
3
- /**
4
- * THE common cover-image fallback chain for entity cards:
5
- * real cover → branded placeholder → nothing (hide the media element).
6
- *
7
- * One hook so no card hand-rolls its own `imageError` state / `displayImage`
8
- * ternary (they drift: the pre-hook BlogCard chain couldn't recover from a
9
- * FAILING placeholder, EntityPortraitCard duplicated the same logic with
10
- * different state). Cards wire `src` + `onError` and render their own
11
- * empty-state when `src` is null.
12
- *
13
- * Implementation note: state is the SET of candidate URLs that failed, and
14
- * `src` is derived — so a prop change (new cover URL) re-resolves instantly
15
- * with no reset effect, and a URL that failed once isn't retried in a loop.
16
- */
17
-
18
- import { useCallback, useState, type SyntheticEvent } from 'react'
19
-
20
- export interface CoverImageFallback {
21
- /** Resolved source: first non-failed of [imageUrl, placeholderUrl], else null. */
22
- src: string | null
23
- /** Wire to the media element's `onError`. */
24
- onError: () => void
25
- }
26
-
27
- export function useCoverImageFallback(
28
- imageUrl?: string | null,
29
- placeholderUrl?: string | null,
30
- ): CoverImageFallback {
31
- const [failed, setFailed] = useState<ReadonlySet<string>>(() => new Set())
32
-
33
- const src =
34
- imageUrl && !failed.has(imageUrl) ? imageUrl
35
- : placeholderUrl && !failed.has(placeholderUrl) ? placeholderUrl
36
- : null
37
-
38
- const onError = useCallback(() => {
39
- if (!src) return
40
- setFailed(prev => new Set(prev).add(src))
41
- }, [src])
42
-
43
- return { src, onError }
44
- }
45
-
46
- /** Fallback for cards with NO placeholder chain (compact/sm image slots):
47
- * a broken cover simply disappears, leaving the slot's background. */
48
- export const hideOnError = (e: SyntheticEvent<HTMLImageElement>) => {
49
- ;(e.currentTarget as HTMLImageElement).style.display = 'none'
50
- }