@flamingo-stack/openframe-frontend-core 0.0.255 → 0.0.256-snapshot.20260612014656
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-LHR7MBUY.js → chunk-2VZQJIVA.js} +48 -13
- package/dist/chunk-2VZQJIVA.js.map +1 -0
- package/dist/{chunk-GYKDSPC4.cjs → chunk-3DC6MHW6.cjs} +31 -31
- package/dist/{chunk-GYKDSPC4.cjs.map → chunk-3DC6MHW6.cjs.map} +1 -1
- package/dist/{chunk-LZL6P7SN.cjs → chunk-4MXBBBHB.cjs} +108 -11
- package/dist/chunk-4MXBBBHB.cjs.map +1 -0
- package/dist/{chunk-HMFBMVJC.js → chunk-6GBA6YJN.js} +2 -2
- package/dist/{chunk-SGJBUL2Y.cjs → chunk-7VMEOFEA.cjs} +45 -2
- package/dist/chunk-7VMEOFEA.cjs.map +1 -0
- package/dist/{chunk-UGFSMA7K.cjs → chunk-AIQ4DGO7.cjs} +77 -42
- package/dist/chunk-AIQ4DGO7.cjs.map +1 -0
- package/dist/{chunk-6TB24AUH.js → chunk-CBIQDPHK.js} +155 -236
- package/dist/chunk-CBIQDPHK.js.map +1 -0
- package/dist/{chunk-6LK4VEXE.cjs → chunk-EB76VWM4.cjs} +118 -63
- package/dist/chunk-EB76VWM4.cjs.map +1 -0
- package/dist/{chunk-RCW2IL4K.cjs → chunk-FBH73RRM.cjs} +1254 -1335
- package/dist/chunk-FBH73RRM.cjs.map +1 -0
- package/dist/{chunk-4HHYUEMD.js → chunk-IJHORDN5.js} +100 -3
- package/dist/chunk-IJHORDN5.js.map +1 -0
- package/dist/{chunk-MKVKVJQQ.js → chunk-J3QMPOS6.js} +5 -4
- package/dist/chunk-J3QMPOS6.js.map +1 -0
- package/dist/{chunk-CVGZJBHT.cjs → chunk-LCYDTEEK.cjs} +19 -19
- package/dist/{chunk-CVGZJBHT.cjs.map → chunk-LCYDTEEK.cjs.map} +1 -1
- package/dist/{chunk-2FCXJTOV.js → chunk-LDEXTWMK.js} +10 -2
- package/dist/chunk-LDEXTWMK.js.map +1 -0
- package/dist/{chunk-Z4IBEIWD.cjs → chunk-LGLPNWS6.cjs} +3 -3
- package/dist/{chunk-Z4IBEIWD.cjs.map → chunk-LGLPNWS6.cjs.map} +1 -1
- package/dist/{chunk-TU26IZPO.cjs → chunk-N5UOJC3V.cjs} +10 -2
- package/dist/chunk-N5UOJC3V.cjs.map +1 -0
- package/dist/{chunk-LKI3YCAX.js → chunk-NAOTZAKM.js} +62 -7
- package/dist/chunk-NAOTZAKM.js.map +1 -0
- package/dist/{chunk-2FAX6LZB.cjs → chunk-NNDLCSGR.cjs} +30 -30
- package/dist/{chunk-2FAX6LZB.cjs.map → chunk-NNDLCSGR.cjs.map} +1 -1
- package/dist/{chunk-VAUI3CND.js → chunk-NR535XIY.js} +44 -1
- package/dist/chunk-NR535XIY.js.map +1 -0
- package/dist/{chunk-D524LJYO.js → chunk-RUMSS73U.js} +4 -4
- package/dist/{chunk-265KKBV2.js → chunk-V4IIBNTA.js} +3 -3
- package/dist/chunk-V4IIBNTA.js.map +1 -0
- package/dist/{chunk-EFIEQ4XD.js → chunk-YMZZ5THD.js} +4 -4
- package/dist/{chunk-3TDLFCCS.cjs → chunk-ZJP2BB3E.cjs} +6 -5
- package/dist/chunk-ZJP2BB3E.cjs.map +1 -0
- package/dist/components/chat/entity-cards/entity-author-card.d.ts +13 -9
- package/dist/components/chat/entity-cards/entity-author-card.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +9 -7
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +8 -6
- package/dist/components/chat/types/entities/onboarding-guide.d.ts +2 -7
- package/dist/components/chat/types/entities/onboarding-guide.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +8 -8
- package/dist/components/contact/index.js +7 -7
- package/dist/components/faq/faq-section.d.ts.map +1 -1
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +7 -7
- package/dist/components/features/index.js +6 -6
- package/dist/components/icons/index.cjs +2 -2
- package/dist/components/icons/index.js +1 -1
- package/dist/components/index.cjs +168 -155
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +58 -45
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +7 -7
- package/dist/components/navigation/index.js +6 -6
- package/dist/components/onboarding-guides/index.cjs +43 -27
- package/dist/components/onboarding-guides/index.cjs.map +1 -1
- package/dist/components/onboarding-guides/index.js +20 -4
- package/dist/components/onboarding-guides/index.js.map +1 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts +11 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +10 -8
- package/dist/components/related-content/index.cjs.map +1 -1
- package/dist/components/related-content/index.d.ts +1 -1
- package/dist/components/related-content/index.d.ts.map +1 -1
- package/dist/components/related-content/index.js +9 -7
- package/dist/components/related-content/related-content-section.d.ts +16 -1
- package/dist/components/related-content/related-content-section.d.ts.map +1 -1
- package/dist/components/shared/article-author-byline.d.ts +58 -0
- package/dist/components/shared/article-author-byline.d.ts.map +1 -0
- package/dist/components/shared/product-release/release-detail-page.d.ts +4 -1
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/social-icon-row.d.ts +19 -4
- package/dist/components/social-icon-row.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +80 -80
- package/dist/components/tickets/index.js +8 -8
- package/dist/components/ui/button/button.d.ts +1 -1
- package/dist/components/ui/button/button.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +20 -20
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/index.cjs +9 -7
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +8 -6
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/index.cjs +14 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +20 -16
- package/dist/types/blog.d.ts +7 -0
- package/dist/types/blog.d.ts.map +1 -1
- package/dist/types/entity-author.d.ts +21 -0
- package/dist/types/entity-author.d.ts.map +1 -0
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/product-release.d.ts +2 -7
- package/dist/types/product-release.d.ts.map +1 -1
- package/dist/utils/common.d.ts +9 -0
- package/dist/utils/common.d.ts.map +1 -1
- package/dist/utils/content-href.d.ts.map +1 -1
- package/dist/utils/index.cjs +12 -3
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +12 -4
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/entity-cards/entity-author-card.tsx +30 -11
- package/src/components/chat/index.ts +1 -0
- package/src/components/chat/types/entities/onboarding-guide.ts +3 -7
- package/src/components/faq/faq-section.tsx +4 -1
- package/src/components/icons/x-logo.tsx +2 -2
- package/src/components/index.ts +5 -0
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +27 -0
- package/src/components/related-content/index.ts +1 -0
- package/src/components/related-content/related-content-section.tsx +84 -8
- package/src/components/shared/article-author-byline.tsx +187 -0
- package/src/components/shared/product-release/release-detail-page.tsx +18 -21
- package/src/components/social-icon-row.tsx +68 -22
- package/src/components/ui/button/button.tsx +13 -0
- package/src/types/blog.ts +9 -1
- package/src/types/entity-author.ts +20 -0
- package/src/types/index.ts +1 -0
- package/src/types/product-release.ts +2 -7
- package/src/utils/__tests__/content-href.test.ts +3 -0
- package/src/utils/common.ts +12 -0
- package/src/utils/content-href.ts +5 -0
- package/src/utils/index.ts +1 -1
- package/dist/chunk-265KKBV2.js.map +0 -1
- package/dist/chunk-2FCXJTOV.js.map +0 -1
- package/dist/chunk-3TDLFCCS.cjs.map +0 -1
- package/dist/chunk-4HHYUEMD.js.map +0 -1
- package/dist/chunk-6LK4VEXE.cjs.map +0 -1
- package/dist/chunk-6TB24AUH.js.map +0 -1
- package/dist/chunk-LHR7MBUY.js.map +0 -1
- package/dist/chunk-LKI3YCAX.js.map +0 -1
- package/dist/chunk-LZL6P7SN.cjs.map +0 -1
- package/dist/chunk-MKVKVJQQ.js.map +0 -1
- package/dist/chunk-RCW2IL4K.cjs.map +0 -1
- package/dist/chunk-SGJBUL2Y.cjs.map +0 -1
- package/dist/chunk-TU26IZPO.cjs.map +0 -1
- package/dist/chunk-UGFSMA7K.cjs.map +0 -1
- package/dist/chunk-VAUI3CND.js.map +0 -1
- /package/dist/{chunk-HMFBMVJC.js.map → chunk-6GBA6YJN.js.map} +0 -0
- /package/dist/{chunk-D524LJYO.js.map → chunk-RUMSS73U.js.map} +0 -0
- /package/dist/{chunk-EFIEQ4XD.js.map → chunk-YMZZ5THD.js.map} +0 -0
|
@@ -11,12 +11,12 @@ import { PageShell } from '../../layout/article-detail-layout';
|
|
|
11
11
|
import { BackButton } from '../../layout/back-button';
|
|
12
12
|
import { ReleaseChangelogSection } from '../../ui/release-changelog-section';
|
|
13
13
|
import { EntityTagBadges } from '../../features/entity-tag-badges';
|
|
14
|
-
import {
|
|
14
|
+
import { EntityMetadataAuthorCell } from '../../chat/entity-cards/entity-author-card';
|
|
15
|
+
import type { EntityAuthor } from '../../../types/entity-author';
|
|
15
16
|
import { ImageGalleryModal } from '../../ui/image-gallery-modal';
|
|
16
17
|
import { GitHubIcon } from '../../icons/github-icon';
|
|
17
18
|
import { AlertTriangle, ExternalLink, BookMarked, Sparkles, TrendingUp, Wrench } from 'lucide-react';
|
|
18
19
|
import { formatReleaseDate } from '../../../utils/date-formatters';
|
|
19
|
-
import { nameInitials } from '../../../utils/format';
|
|
20
20
|
import { Video } from '../../features/video';
|
|
21
21
|
import { DetailPageSkeleton } from '../detail-page-skeleton';
|
|
22
22
|
import type { ChangelogEntry } from '../../../types/product-release';
|
|
@@ -103,6 +103,9 @@ export interface ReleaseDetailPageProps {
|
|
|
103
103
|
* `LegalDocumentPage`. Pass `false` to hide. Default
|
|
104
104
|
* `{ label: 'Back to home', href: '/' }`. */
|
|
105
105
|
backButton?: { label?: string; href?: string } | false;
|
|
106
|
+
/** Link target for the author name in the metadata grid — the host
|
|
107
|
+
* computes it (public author page; absent ⇒ plain text). */
|
|
108
|
+
authorHref?: string;
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
// Default simple markdown renderer (just renders as text)
|
|
@@ -111,6 +114,7 @@ function DefaultMarkdownRenderer({ content }: MarkdownRendererProps) {
|
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
export function ReleaseDetailPage({
|
|
117
|
+
authorHref,
|
|
114
118
|
slug,
|
|
115
119
|
initialData,
|
|
116
120
|
useRelease,
|
|
@@ -207,7 +211,9 @@ export function ReleaseDetailPage({
|
|
|
207
211
|
const releaseType = release.release_type as string;
|
|
208
212
|
const releaseStatus = release.release_status as string;
|
|
209
213
|
const releaseMedia = release.release_media as Array<{ id?: string; media_type: string; media_url: string; title?: string }> | undefined;
|
|
210
|
-
|
|
214
|
+
// Field-cast per this file's loose-release idiom (release_type etc. above)
|
|
215
|
+
// — but to the SHARED EntityAuthor, never an inline shadow author shape.
|
|
216
|
+
const author = release.author as EntityAuthor | undefined;
|
|
211
217
|
const githubReleases = release.github_releases as Array<{ id: string; github_release_url: string }> | undefined;
|
|
212
218
|
const knowledgeBaseLinks = release.knowledge_base_links as Array<{ id?: string; kb_article_path: string }> | string[] | undefined;
|
|
213
219
|
const migrationGuideUrl = release.migration_guide_url as string | undefined;
|
|
@@ -290,24 +296,15 @@ export function ReleaseDetailPage({
|
|
|
290
296
|
</div>
|
|
291
297
|
</div>
|
|
292
298
|
|
|
293
|
-
{/* Author
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
<div className="flex flex-col gap-0 flex-1 min-w-0">
|
|
303
|
-
<p className="text-h3 tracking-[-0.36px] text-ods-text-primary truncate" title={author?.full_name || 'Unknown Author'}>
|
|
304
|
-
{author?.full_name || 'Unknown Author'}
|
|
305
|
-
</p>
|
|
306
|
-
<p className="font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary">
|
|
307
|
-
Author
|
|
308
|
-
</p>
|
|
309
|
-
</div>
|
|
310
|
-
</div>
|
|
299
|
+
{/* Author — the shared metadata-grid author cell (it was extracted
|
|
300
|
+
FROM this page; rendering the export instead of an inline copy
|
|
301
|
+
keeps the two in lockstep). The stub author preserves the legacy
|
|
302
|
+
"Unknown Author" rendering for author-less releases; the
|
|
303
|
+
job_title-over-roleLabel rule matches the guide detail page. */}
|
|
304
|
+
<EntityMetadataAuthorCell
|
|
305
|
+
author={author ?? { full_name: null, avatar_url: null }}
|
|
306
|
+
authorHref={author?.full_name ? authorHref : undefined}
|
|
307
|
+
/>
|
|
311
308
|
</div>
|
|
312
309
|
|
|
313
310
|
{/* Image Gallery - Horizontal Scrolling */}
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import { Button } from './ui/button';
|
|
4
|
-
import { GitHubIcon, RedditIcon, XLogo, LinkedInIcon, LumaIcon, WhatsAppIcon, GlobeIcon, MessageCircleIcon, TelegramIcon, YouTubeIcon, InstagramIcon, FacebookIcon, SlackIcon } from './icons';
|
|
4
|
+
import { GitHubIcon, RedditIcon, XLogo, LinkedInIcon, LumaIcon, WhatsAppIcon, GlobeIcon, MessageCircleIcon, TelegramIcon, YouTubeIcon, InstagramIcon, FacebookIcon, SlackIcon, CopyIcon } from './icons';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/** Exactly ONE of `href` (anchor, target _blank) or `onClick` (action
|
|
7
|
+
* button — share popups via window.open inside the click gesture,
|
|
8
|
+
* copy-to-clipboard) — the discriminated union makes a dead no-action
|
|
9
|
+
* entry unrepresentable. */
|
|
10
|
+
type SocialLink = {
|
|
7
11
|
platform: string;
|
|
8
|
-
href: string;
|
|
9
12
|
label?: string;
|
|
10
|
-
}
|
|
13
|
+
} & (
|
|
14
|
+
| { href: string; onClick?: never }
|
|
15
|
+
| { onClick: () => void; href?: never }
|
|
16
|
+
);
|
|
11
17
|
|
|
12
18
|
interface SocialIconRowProps {
|
|
13
19
|
className?: string;
|
|
14
20
|
links?: SocialLink[];
|
|
15
21
|
variant?: "accent" | "outline" | "transparent" | "destructive" | null | undefined;
|
|
22
|
+
/** Quiet metadata row for page-level identity/share slots: 32px ghost
|
|
23
|
+
* icon buttons (size="icon-sm", 16px glyphs), gap-2, w-fit container,
|
|
24
|
+
* variant defaulting to "transparent" (an explicit `variant` still wins).
|
|
25
|
+
* Default false: 44/48px buttons stretching across the container —
|
|
26
|
+
* the original card-width behavior (TMCG member cards, footers). */
|
|
27
|
+
compact?: boolean;
|
|
16
28
|
}
|
|
17
29
|
|
|
18
30
|
const defaultLinks: SocialLink[] = [
|
|
@@ -57,32 +69,66 @@ function renderSocialIcon(platform: string) {
|
|
|
57
69
|
case 'facebook':
|
|
58
70
|
case 'fb':
|
|
59
71
|
return <FacebookIcon className="w-5 h-5" />;
|
|
72
|
+
case 'copy':
|
|
73
|
+
// CopyIcon's default fill is grey and would mismatch its row-mates —
|
|
74
|
+
// force the themed foreground via the ODS token (white on the dark
|
|
75
|
+
// theme, tracking the theme unlike the literal the reddit/slack cases
|
|
76
|
+
// still carry).
|
|
77
|
+
return <CopyIcon className="w-5 h-5" color="var(--ods-text-primary)" />;
|
|
60
78
|
default:
|
|
61
79
|
return <GlobeIcon className="w-5 h-5" />;
|
|
62
80
|
}
|
|
63
81
|
}
|
|
64
82
|
|
|
65
|
-
export function SocialIconRow({ className = '', links = defaultLinks, variant =
|
|
83
|
+
export function SocialIconRow({ className = '', links = defaultLinks, variant, compact = false }: SocialIconRowProps) {
|
|
84
|
+
// ── Compact design rationale ──────────────────────────────────────────
|
|
85
|
+
// Page-level identity/share rows read as METADATA, not CTAs. The major
|
|
86
|
+
// design systems converge on one recipe for this slot: a ~32px ghost icon
|
|
87
|
+
// button with a ~16px glyph, tight 8px gap, transparent at rest, subtle
|
|
88
|
+
// background state-layer on hover (Carbon "ghost" sm, Primer "invisible"
|
|
89
|
+
// medium, shadcn ghost+icon-sm — all 32px; Material 3 "standard" icon
|
|
90
|
+
// button = state-layer hover). Author headers on content platforms
|
|
91
|
+
// (Medium / dev.to / Substack) use the same quiet treatment. Hence
|
|
92
|
+
// compact: size="icon-sm" + variant defaulting to "transparent" — the
|
|
93
|
+
// hover affordance comes from the bg state layer (hover:bg-ods-bg-hover
|
|
94
|
+
// inside the variant) because the brand icons carry fixed fills, not
|
|
95
|
+
// currentColor. An explicit `variant` prop still wins (e.g. outline
|
|
96
|
+
// chips). Non-compact keeps the legacy outline default + full-width
|
|
97
|
+
// stretch untouched.
|
|
98
|
+
const resolvedVariant = variant !== undefined ? variant : (compact ? 'transparent' : 'outline');
|
|
66
99
|
return (
|
|
67
|
-
<div className={`flex flex-row gap-3 w-full ${className}`}>
|
|
68
|
-
{links.map((link, index) =>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
100
|
+
<div className={`flex flex-row ${compact ? 'gap-2 w-fit' : 'gap-3 w-full'} ${className}`}>
|
|
101
|
+
{links.map((link, index) => {
|
|
102
|
+
const ariaLabel = link.label || link.platform;
|
|
103
|
+
return link.onClick ? (
|
|
104
|
+
<Button
|
|
105
|
+
key={index}
|
|
106
|
+
type="button"
|
|
107
|
+
variant={resolvedVariant}
|
|
108
|
+
size={compact ? 'icon-sm' : 'icon'}
|
|
109
|
+
className={compact ? undefined : 'flex-1'}
|
|
110
|
+
aria-label={ariaLabel}
|
|
111
|
+
onClick={link.onClick}
|
|
112
|
+
>
|
|
113
|
+
{renderSocialIcon(link.platform)}
|
|
114
|
+
</Button>
|
|
115
|
+
) : (
|
|
116
|
+
// Props-based linking — Button renders the anchor itself
|
|
117
|
+
// (openInNewTab carries target="_blank" + rel="noopener noreferrer");
|
|
118
|
+
// no asChild/<a> nesting.
|
|
119
|
+
<Button
|
|
120
|
+
key={index}
|
|
121
|
+
variant={resolvedVariant}
|
|
122
|
+
size={compact ? 'icon-sm' : 'icon'}
|
|
123
|
+
className={compact ? undefined : 'flex-1'}
|
|
77
124
|
href={link.href}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
aria-label={link.label || link.platform}
|
|
125
|
+
openInNewTab
|
|
126
|
+
aria-label={ariaLabel}
|
|
81
127
|
>
|
|
82
128
|
{renderSocialIcon(link.platform)}
|
|
83
|
-
</
|
|
84
|
-
|
|
85
|
-
)
|
|
129
|
+
</Button>
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
86
132
|
</div>
|
|
87
133
|
);
|
|
88
|
-
}
|
|
134
|
+
}
|
|
@@ -31,6 +31,12 @@ const buttonVariants = cva(
|
|
|
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",
|
|
34
|
+
// Quiet 32px icon target with a 16px glyph, fixed across breakpoints
|
|
35
|
+
// (Carbon ghost sm / Primer medium / shadcn icon-sm all pin 32px;
|
|
36
|
+
// ≥ the 24px WCAG 2.5.8 target floor). For icon actions that read as
|
|
37
|
+
// metadata rather than CTAs — author-page social rows, share rows.
|
|
38
|
+
// Pair with variant="transparent" for the ghost treatment.
|
|
39
|
+
"icon-sm": "p-[var(--spacing-system-xxs)] h-8 w-8 [&_svg]:h-4 [&_svg]:w-4",
|
|
34
40
|
},
|
|
35
41
|
fullWidth: {
|
|
36
42
|
true: "w-full",
|
|
@@ -235,6 +241,10 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
|
|
235
241
|
prefetch={prefetch}
|
|
236
242
|
target={splitAnchor.target}
|
|
237
243
|
rel={splitAnchor.rel}
|
|
244
|
+
// The Link branches don't spread {...props} (button attrs don't
|
|
245
|
+
// belong on an anchor), but the accessible name MUST survive —
|
|
246
|
+
// icon-only link buttons have no text content.
|
|
247
|
+
aria-label={props['aria-label']}
|
|
238
248
|
aria-disabled={isDisabled || undefined}
|
|
239
249
|
tabIndex={isDisabled ? -1 : undefined}
|
|
240
250
|
className={cn(shellClasses, isDisabled && "pointer-events-none")}
|
|
@@ -300,6 +310,9 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
|
|
300
310
|
prefetch={prefetch}
|
|
301
311
|
target={anchor.target}
|
|
302
312
|
rel={anchor.rel}
|
|
313
|
+
// See the splitAnchor branch — keep the accessible name on the
|
|
314
|
+
// anchor render (icon-only link buttons have no text content).
|
|
315
|
+
aria-label={props['aria-label']}
|
|
303
316
|
aria-disabled={isDisabled || undefined}
|
|
304
317
|
tabIndex={isDisabled ? -1 : undefined}
|
|
305
318
|
className={cn(classes, isDisabled && "pointer-events-none")}
|
package/src/types/blog.ts
CHANGED
|
@@ -4,12 +4,20 @@ import type { UserProfile } from "./user"
|
|
|
4
4
|
export type Platform = string;
|
|
5
5
|
export type BlogStatus = 'draft' | 'ai_drafted' | 'published' | 'ai_published' | 'scheduled' | 'archived';
|
|
6
6
|
|
|
7
|
-
// Author interface for blog posts
|
|
7
|
+
// Author interface for blog posts (camelCase — the blog contract's own
|
|
8
|
+
// shape; detail-row authors elsewhere use the shared EntityAuthor).
|
|
8
9
|
export interface BlogAuthor {
|
|
9
10
|
id: number;
|
|
10
11
|
name?: string;
|
|
11
12
|
avatar?: string;
|
|
12
13
|
bio?: string;
|
|
14
|
+
jobTitle?: string;
|
|
15
|
+
/** Public author-page slug — present only when the hub's public-author
|
|
16
|
+
* gate passes (is_real profiles only). */
|
|
17
|
+
slug?: string;
|
|
18
|
+
/** Author's LinkedIn from profile_social_links — Article schema sameAs
|
|
19
|
+
* + the author.url fallback for slug-less authors. */
|
|
20
|
+
linkedinUrl?: string;
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
// Database Models
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hydrated content author — THE shared author shape attached to detail rows
|
|
3
|
+
* by the hub's `hydrateAuthor` (product releases, onboarding guides) and the
|
|
4
|
+
* per-entity author joins. One type for every author surface: metadata-grid
|
|
5
|
+
* author cells, bylines, author-page hrefs.
|
|
6
|
+
*/
|
|
7
|
+
export interface EntityAuthor {
|
|
8
|
+
id?: string
|
|
9
|
+
full_name: string | null
|
|
10
|
+
avatar_url: string | null
|
|
11
|
+
job_title?: string | null
|
|
12
|
+
/** Author bio paragraph (`profiles.about`) — feeds the end-of-article
|
|
13
|
+
* `<ArticleAuthorByline bio>` on every detail page with an author. */
|
|
14
|
+
about?: string | null
|
|
15
|
+
/** Public author-page slug — present only when the hub's public-author
|
|
16
|
+
* gate passes (`is_real` profiles only). Feeds /authors/[slug] links. */
|
|
17
|
+
slug?: string
|
|
18
|
+
/** Present only on server/admin payloads — public DALs strip it. */
|
|
19
|
+
email?: string | null
|
|
20
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Following the blog.ts pattern for consistency
|
|
3
3
|
|
|
4
4
|
import type { PlatformRecord } from './platform'
|
|
5
|
+
import type { EntityAuthor } from './entity-author'
|
|
5
6
|
import type { BlogTag, TagAssoc } from './blog'
|
|
6
7
|
import type { VideoTeaser, TranscriptWord, SpeakerMapping } from './video-processing'
|
|
7
8
|
|
|
@@ -128,13 +129,7 @@ export interface ProductRelease {
|
|
|
128
129
|
* `hydrateAuthor` in the hub DAL. Optional so admin form + every existing
|
|
129
130
|
* consumer that doesn't read author stay backward-compatible.
|
|
130
131
|
*/
|
|
131
|
-
author?:
|
|
132
|
-
id: string
|
|
133
|
-
full_name: string
|
|
134
|
-
avatar_url: string | null
|
|
135
|
-
job_title: string | null
|
|
136
|
-
email?: string | null
|
|
137
|
-
}
|
|
132
|
+
author?: EntityAuthor
|
|
138
133
|
|
|
139
134
|
// Timestamps
|
|
140
135
|
created_at: string
|
|
@@ -132,6 +132,9 @@ describe('DEFAULT_CONTENT_SUFFIXES', () => {
|
|
|
132
132
|
webinar: 'webinars',
|
|
133
133
|
podcast: 'podcasts',
|
|
134
134
|
event: 'events',
|
|
135
|
+
// Hub-side home of the segment: AUTHORS_PATH in lib/utils/breadcrumbs.ts
|
|
136
|
+
// (PUBLIC_URL_PATHS.author derives from it).
|
|
137
|
+
author: 'authors',
|
|
135
138
|
})
|
|
136
139
|
})
|
|
137
140
|
})
|
package/src/utils/common.ts
CHANGED
|
@@ -49,6 +49,18 @@ export function truncateString(str: string, maxLength: number, suffix = "..."):
|
|
|
49
49
|
return str.substring(0, maxLength - suffix.length) + suffix
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Serialize a JSON-LD schema for a `<script type="application/ld+json">`
|
|
54
|
+
* block — THE one home of the escape rule (the hub re-exports this from
|
|
55
|
+
* lib/utils/breadcrumbs). JSON.stringify does NOT HTML-escape: a stored
|
|
56
|
+
* "</script>" inside any admin/user-entered field would terminate the tag
|
|
57
|
+
* early (stored XSS). Every "<" becomes the JSON escape sequence
|
|
58
|
+
* backslash-u003c — still valid JSON, inert in HTML.
|
|
59
|
+
*/
|
|
60
|
+
export function serializeJsonLd(schema: unknown): string {
|
|
61
|
+
return JSON.stringify(schema).replace(/</g, "\\u003c")
|
|
62
|
+
}
|
|
63
|
+
|
|
52
64
|
/**
|
|
53
65
|
* Deep clone an object
|
|
54
66
|
* @param obj - Object to clone
|
|
@@ -44,6 +44,11 @@ export const DEFAULT_CONTENT_SUFFIXES: Record<string, string> = {
|
|
|
44
44
|
webinar: 'webinars',
|
|
45
45
|
podcast: 'podcasts',
|
|
46
46
|
event: 'events',
|
|
47
|
+
// Author profile pages (`/authors/<slug>`) — identified by the profile
|
|
48
|
+
// slug. Mirrors the hub's `PUBLIC_URL_PATHS.author`, which derives from
|
|
49
|
+
// `AUTHORS_PATH` in the hub's `lib/utils/breadcrumbs.ts` (the hub-side
|
|
50
|
+
// SSOT for the segment).
|
|
51
|
+
author: 'authors',
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
/** Input to the unified content-href seam. ONE object covers both callers:
|
package/src/utils/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { formatDate, formatNumber, formatPrice, formatBytes } from './format'
|
|
|
12
12
|
// SVG path constants — re-exported here (server-safe) because icons-v2 has "use client"
|
|
13
13
|
export { PLAY_ICON_PATH } from '../components/icons-v2-generated/media-playback/play-icon'
|
|
14
14
|
export { getPlatformAccentColor, getCurrentPlatform, type ColorCategory, HEX_PATTERN } from './ods-color-utils'
|
|
15
|
-
export { delay, generateRandomString, truncateString, deepClone, getSlackCommunityJoinUrl } from './common'
|
|
15
|
+
export { delay, generateRandomString, truncateString, deepClone, getSlackCommunityJoinUrl, serializeJsonLd } from './common'
|
|
16
16
|
export { getBaseUrl } from '../utils/cn'
|
|
17
17
|
|
|
18
18
|
export * from './platform-config'
|