@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.
- package/dist/{chunk-IVXOR2IA.js → chunk-2PTEK3D6.js} +2 -2
- package/dist/{chunk-DB6CUWTL.cjs → chunk-6DGQPKVA.cjs} +26 -26
- package/dist/{chunk-DB6CUWTL.cjs.map → chunk-6DGQPKVA.cjs.map} +1 -1
- package/dist/{chunk-OAENYOKC.cjs → chunk-AODSKL5I.cjs} +9 -9
- package/dist/{chunk-OAENYOKC.cjs.map → chunk-AODSKL5I.cjs.map} +1 -1
- package/dist/{chunk-M6NPDNVB.cjs → chunk-CCV6Z3CL.cjs} +26 -26
- package/dist/{chunk-M6NPDNVB.cjs.map → chunk-CCV6Z3CL.cjs.map} +1 -1
- package/dist/{chunk-JNU3YPLP.js → chunk-EKYE2VJU.js} +4 -4
- package/dist/{chunk-J6TAK6HE.js → chunk-FAUC7EA3.js} +4 -4
- package/dist/{chunk-2OOZASQE.js → chunk-GA3R77TB.js} +2 -2
- package/dist/{chunk-VTR4QDL2.js → chunk-H4WIS6RJ.js} +2 -2
- package/dist/{chunk-HPQKX5IW.cjs → chunk-HEH47UTC.cjs} +15 -15
- package/dist/{chunk-HPQKX5IW.cjs.map → chunk-HEH47UTC.cjs.map} +1 -1
- package/dist/{chunk-XNAYZHPK.cjs → chunk-HNXZFKDQ.cjs} +62 -62
- package/dist/{chunk-XNAYZHPK.cjs.map → chunk-HNXZFKDQ.cjs.map} +1 -1
- package/dist/{chunk-T5F54MN4.cjs → chunk-HWJUWOXK.cjs} +12 -12
- package/dist/{chunk-T5F54MN4.cjs.map → chunk-HWJUWOXK.cjs.map} +1 -1
- package/dist/{chunk-K5FRWPWF.js → chunk-J2UHW4UM.js} +2 -2
- package/dist/{chunk-55SNCMFS.cjs → chunk-KRMGVMIO.cjs} +31 -31
- package/dist/{chunk-55SNCMFS.cjs.map → chunk-KRMGVMIO.cjs.map} +1 -1
- package/dist/{chunk-Z6PAD2UN.cjs → chunk-NAFW3LKI.cjs} +7 -7
- package/dist/{chunk-Z6PAD2UN.cjs.map → chunk-NAFW3LKI.cjs.map} +1 -1
- package/dist/{chunk-TTHTO5H5.js → chunk-NFRHBR7W.js} +4 -4
- package/dist/{chunk-3E4BDAHJ.js → chunk-OP3H7VB2.js} +2 -2
- package/dist/{chunk-ZFK4HHRX.js → chunk-PQXUY6SB.js} +2 -2
- package/dist/{chunk-43C5ECNT.cjs → chunk-RUYZ5QMK.cjs} +37 -37
- package/dist/{chunk-43C5ECNT.cjs.map → chunk-RUYZ5QMK.cjs.map} +1 -1
- package/dist/{chunk-D4F7ZLJG.js → chunk-STEM5CPV.js} +4 -4
- package/dist/{chunk-CK5UZLIN.js → chunk-SV7IJPIH.js} +5000 -5313
- package/dist/chunk-SV7IJPIH.js.map +1 -0
- package/dist/{chunk-WCDM226S.js → chunk-TIC3UE6L.js} +2 -2
- package/dist/{chunk-BB7TZ6KS.cjs → chunk-VSPNWVIE.cjs} +59 -59
- package/dist/{chunk-BB7TZ6KS.cjs.map → chunk-VSPNWVIE.cjs.map} +1 -1
- package/dist/{chunk-SBWTMJPQ.cjs → chunk-XQILJ7FF.cjs} +5 -5
- package/dist/{chunk-SBWTMJPQ.cjs.map → chunk-XQILJ7FF.cjs.map} +1 -1
- package/dist/{chunk-ZUKBC2ZX.cjs → chunk-Y36KVLBX.cjs} +798 -1111
- package/dist/chunk-Y36KVLBX.cjs.map +1 -0
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/entity-cards/blog-card.d.ts +3 -6
- package/dist/components/chat/entity-cards/blog-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/case-study-card.d.ts +3 -7
- package/dist/components/chat/entity-cards/case-study-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/customer-interview-card.d.ts +3 -6
- package/dist/components/chat/entity-cards/customer-interview-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/index.d.ts +0 -3
- package/dist/components/chat/entity-cards/index.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/investor-update-card.d.ts +3 -6
- package/dist/components/chat/entity-cards/investor-update-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/onboarding-guide-card.d.ts +2 -4
- package/dist/components/chat/entity-cards/onboarding-guide-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/program-card.d.ts +5 -17
- package/dist/components/chat/entity-cards/program-card.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +2 -10
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +1 -9
- package/dist/components/chat/types/entities/program-types.d.ts +0 -23
- package/dist/components/chat/types/entities/program-types.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/cards-strip.d.ts.map +1 -1
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/features/video-bites-strip.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +20 -20
- package/dist/components/help-center-pages/index.js +11 -11
- package/dist/components/index.cjs +120 -128
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +9 -17
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +5 -5
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/index.cjs +2 -10
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +1 -9
- package/dist/index.cjs +2 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -9
- package/package.json +1 -1
- package/src/components/chat/entity-cards/blog-card.tsx +7 -40
- package/src/components/chat/entity-cards/case-study-card.tsx +6 -32
- package/src/components/chat/entity-cards/customer-interview-card.tsx +6 -31
- package/src/components/chat/entity-cards/index.ts +0 -18
- package/src/components/chat/entity-cards/investor-update-card.tsx +2 -28
- package/src/components/chat/entity-cards/onboarding-guide-card.tsx +1 -30
- package/src/components/chat/entity-cards/program-card.tsx +17 -69
- package/src/components/chat/types/entities/program-types.ts +0 -34
- package/src/components/features/cards-strip.tsx +8 -122
- package/src/components/features/video-bites-strip.tsx +0 -4
- package/dist/chunk-CK5UZLIN.js.map +0 -1
- package/dist/chunk-ZUKBC2ZX.cjs.map +0 -1
- package/dist/components/chat/entity-cards/entity-portrait-card.d.ts +0 -59
- package/dist/components/chat/entity-cards/entity-portrait-card.d.ts.map +0 -1
- package/dist/components/chat/entity-cards/use-cover-image-fallback.d.ts +0 -26
- package/dist/components/chat/entity-cards/use-cover-image-fallback.d.ts.map +0 -1
- package/src/components/chat/entity-cards/entity-portrait-card.tsx +0 -201
- package/src/components/chat/entity-cards/use-cover-image-fallback.ts +0 -50
- /package/dist/{chunk-IVXOR2IA.js.map → chunk-2PTEK3D6.js.map} +0 -0
- /package/dist/{chunk-JNU3YPLP.js.map → chunk-EKYE2VJU.js.map} +0 -0
- /package/dist/{chunk-J6TAK6HE.js.map → chunk-FAUC7EA3.js.map} +0 -0
- /package/dist/{chunk-2OOZASQE.js.map → chunk-GA3R77TB.js.map} +0 -0
- /package/dist/{chunk-VTR4QDL2.js.map → chunk-H4WIS6RJ.js.map} +0 -0
- /package/dist/{chunk-K5FRWPWF.js.map → chunk-J2UHW4UM.js.map} +0 -0
- /package/dist/{chunk-TTHTO5H5.js.map → chunk-NFRHBR7W.js.map} +0 -0
- /package/dist/{chunk-3E4BDAHJ.js.map → chunk-OP3H7VB2.js.map} +0 -0
- /package/dist/{chunk-ZFK4HHRX.js.map → chunk-PQXUY6SB.js.map} +0 -0
- /package/dist/{chunk-D4F7ZLJG.js.map → chunk-STEM5CPV.js.map} +0 -0
- /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
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|