@flamingo-stack/openframe-frontend-core 0.0.254 → 0.0.255-snapshot.20260611213621
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-ELF4OOHC.cjs → chunk-3DC6MHW6.cjs} +33 -33
- package/dist/{chunk-ELF4OOHC.cjs.map → chunk-3DC6MHW6.cjs.map} +1 -1
- package/dist/{chunk-QCRQNKRO.js → chunk-46DHPQY6.js} +4 -4
- package/dist/{chunk-ZITMX2IQ.cjs → chunk-6F5CGBVT.cjs} +745 -767
- package/dist/chunk-6F5CGBVT.cjs.map +1 -0
- package/dist/{chunk-4RDJZIHW.js → chunk-6GBA6YJN.js} +3 -3
- package/dist/{chunk-4HHYUEMD.js → chunk-7RLV3U5C.js} +2 -2
- package/dist/{chunk-SGJBUL2Y.cjs → chunk-7VMEOFEA.cjs} +45 -2
- package/dist/chunk-7VMEOFEA.cjs.map +1 -0
- package/dist/{chunk-TSY72CQ6.cjs → chunk-CMWRZECW.cjs} +77 -42
- package/dist/chunk-CMWRZECW.cjs.map +1 -0
- package/dist/{chunk-CAPVFI5A.js → chunk-HCMVLEC3.js} +4 -4
- package/dist/{chunk-LZL6P7SN.cjs → chunk-IG4QRNYZ.cjs} +10 -10
- package/dist/{chunk-LZL6P7SN.cjs.map → chunk-IG4QRNYZ.cjs.map} +1 -1
- package/dist/{chunk-HYJ5IV4Z.cjs → chunk-ISALYOFX.cjs} +19 -19
- package/dist/{chunk-HYJ5IV4Z.cjs.map → chunk-ISALYOFX.cjs.map} +1 -1
- package/dist/{chunk-MKVKVJQQ.js → chunk-J3QMPOS6.js} +5 -4
- package/dist/chunk-J3QMPOS6.js.map +1 -0
- package/dist/{chunk-BE472J6X.js → chunk-JK25REDL.js} +48 -13
- package/dist/chunk-JK25REDL.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-VAUI3CND.js → chunk-NR535XIY.js} +44 -1
- package/dist/chunk-NR535XIY.js.map +1 -0
- package/dist/{chunk-CZR7ARBA.js → chunk-PHWQLKVE.js} +6 -2
- package/dist/chunk-PHWQLKVE.js.map +1 -0
- package/dist/{chunk-OZ3GH6OQ.cjs → chunk-PXXS27EE.cjs} +6 -2
- package/dist/chunk-PXXS27EE.cjs.map +1 -0
- package/dist/{chunk-2FCXJTOV.js → chunk-QXSRQVRO.js} +3 -1
- package/dist/chunk-QXSRQVRO.js.map +1 -0
- package/dist/{chunk-NP5U35FA.js → chunk-S7IX7YU4.js} +198 -220
- package/dist/chunk-S7IX7YU4.js.map +1 -0
- package/dist/{chunk-6LK4VEXE.cjs → chunk-TSZBJVYK.cjs} +35 -31
- package/dist/chunk-TSZBJVYK.cjs.map +1 -0
- package/dist/{chunk-TU26IZPO.cjs → chunk-U6UNAC32.cjs} +3 -1
- package/dist/chunk-U6UNAC32.cjs.map +1 -0
- package/dist/{chunk-265KKBV2.js → chunk-V4IIBNTA.js} +3 -3
- package/dist/chunk-V4IIBNTA.js.map +1 -0
- package/dist/{chunk-65JN25VM.cjs → chunk-V4Q5PIBV.cjs} +30 -30
- package/dist/{chunk-65JN25VM.cjs.map → chunk-V4Q5PIBV.cjs.map} +1 -1
- package/dist/{chunk-LKI3YCAX.js → chunk-V5L4D454.js} +10 -6
- package/dist/chunk-V5L4D454.js.map +1 -0
- 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/hooks/use-nats-dialog-subscription.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +10 -8
- 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 +9 -7
- 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 +9 -9
- package/dist/components/contact/index.js +8 -8
- 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 +8 -8
- package/dist/components/features/index.js +7 -7
- package/dist/components/features/notifications/notification-drawer.d.ts.map +1 -1
- package/dist/components/icons/index.cjs +2 -2
- package/dist/components/icons/index.js +1 -1
- package/dist/components/index.cjs +167 -157
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +55 -45
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/app-layout-drawer.d.ts +1 -1
- package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +8 -8
- package/dist/components/navigation/index.js +7 -7
- package/dist/components/navigation/navigation-sidebar-item.d.ts.map +1 -1
- package/dist/components/onboarding-guides/index.cjs +25 -23
- package/dist/components/onboarding-guides/index.cjs.map +1 -1
- package/dist/components/onboarding-guides/index.js +6 -4
- package/dist/components/onboarding-guides/index.js.map +1 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts +4 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +11 -9
- 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 +10 -8
- 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/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 +17 -4
- package/dist/components/social-icon-row.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +81 -81
- package/dist/components/tickets/index.js +9 -9
- package/dist/components/ui/button/button.d.ts.map +1 -1
- package/dist/components/ui/drawer.d.ts +6 -2
- package/dist/components/ui/drawer.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 +10 -8
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +9 -7
- package/dist/hooks/index.cjs +4 -4
- package/dist/hooks/index.js +3 -3
- package/dist/index.cjs +13 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +17 -13
- package/dist/nats/index.cjs +2 -2
- package/dist/nats/index.js +1 -1
- package/dist/nats/nats-provider.d.ts.map +1 -1
- package/dist/nats/nats.d.ts.map +1 -1
- package/dist/types/blog.d.ts +7 -0
- package/dist/types/blog.d.ts.map +1 -1
- package/dist/types/entity-author.d.ts +18 -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/navigation.d.ts +1 -0
- package/dist/types/navigation.d.ts.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/index.cjs +6 -2
- 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 +6 -3
- 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/hooks/use-nats-dialog-subscription.ts +6 -4
- 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/features/notifications/notification-drawer.tsx +16 -13
- package/src/components/icons/x-logo.tsx +2 -2
- package/src/components/navigation/navigation-sidebar-item.tsx +14 -1
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +5 -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/product-release/release-detail-page.tsx +18 -21
- package/src/components/social-icon-row.tsx +51 -22
- package/src/components/ui/button/button.tsx +7 -0
- package/src/components/ui/drawer.tsx +10 -3
- package/src/nats/nats-provider.tsx +1 -0
- package/src/nats/nats.ts +5 -1
- package/src/types/blog.ts +9 -1
- package/src/types/entity-author.ts +17 -0
- package/src/types/index.ts +1 -0
- package/src/types/navigation.ts +1 -0
- package/src/types/product-release.ts +2 -7
- package/src/utils/common.ts +12 -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-6LK4VEXE.cjs.map +0 -1
- package/dist/chunk-BE472J6X.js.map +0 -1
- package/dist/chunk-CZR7ARBA.js.map +0 -1
- package/dist/chunk-LKI3YCAX.js.map +0 -1
- package/dist/chunk-MKVKVJQQ.js.map +0 -1
- package/dist/chunk-NP5U35FA.js.map +0 -1
- package/dist/chunk-OZ3GH6OQ.cjs.map +0 -1
- package/dist/chunk-SGJBUL2Y.cjs.map +0 -1
- package/dist/chunk-TSY72CQ6.cjs.map +0 -1
- package/dist/chunk-TU26IZPO.cjs.map +0 -1
- package/dist/chunk-VAUI3CND.js.map +0 -1
- package/dist/chunk-ZITMX2IQ.cjs.map +0 -1
- /package/dist/{chunk-QCRQNKRO.js.map → chunk-46DHPQY6.js.map} +0 -0
- /package/dist/{chunk-4RDJZIHW.js.map → chunk-6GBA6YJN.js.map} +0 -0
- /package/dist/{chunk-4HHYUEMD.js.map → chunk-7RLV3U5C.js.map} +0 -0
- /package/dist/{chunk-CAPVFI5A.js.map → chunk-HCMVLEC3.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import Link from '../../../embed-shims/next-link'
|
|
2
|
+
import type { EntityAuthor } from '../../../types/entity-author'
|
|
1
3
|
import { SquareAvatar } from '../../ui/square-avatar'
|
|
2
4
|
import { formatDate, nameInitials } from '../../../utils/format'
|
|
3
5
|
|
|
@@ -23,8 +25,10 @@ import { formatDate, nameInitials } from '../../../utils/format'
|
|
|
23
25
|
* Renders null when `author` is null/empty unless `renderEmptyAuthor` is set.
|
|
24
26
|
*/
|
|
25
27
|
/**
|
|
26
|
-
* Documented empty-author placeholder
|
|
27
|
-
*
|
|
28
|
+
* Documented empty-author placeholder for CHAT CARD surfaces ("—" name,
|
|
29
|
+
* "Unknown" role). The release detail page deliberately keeps its legacy
|
|
30
|
+
* empty rendering instead ("Unknown Author"/"Author" via a null-field stub
|
|
31
|
+
* — see release-detail-page.tsx) — two documented empty styles, one cell.
|
|
28
32
|
*/
|
|
29
33
|
export const EMPTY_AUTHOR_PLACEHOLDER = {
|
|
30
34
|
full_name: '—',
|
|
@@ -33,14 +37,14 @@ export const EMPTY_AUTHOR_PLACEHOLDER = {
|
|
|
33
37
|
} as const
|
|
34
38
|
|
|
35
39
|
export interface EntityAuthorCardProps {
|
|
36
|
-
author:
|
|
37
|
-
full_name: string | null
|
|
38
|
-
avatar_url: string | null
|
|
39
|
-
job_title: string | null
|
|
40
|
-
} | null | undefined
|
|
40
|
+
author: EntityAuthor | null | undefined
|
|
41
41
|
/** Role label rendered under the name. Defaults to "Author". Override
|
|
42
42
|
* to e.g. "Presenter" / "Contributor" if semantics differ. */
|
|
43
43
|
roleLabel?: string
|
|
44
|
+
/** Link target for the author name (e.g. the public author page). The
|
|
45
|
+
* HOST computes it (route availability is host knowledge) — absent ⇒
|
|
46
|
+
* plain text, exactly the prior render. */
|
|
47
|
+
authorHref?: string
|
|
44
48
|
/** Optional publication date. When provided, the component renders as a
|
|
45
49
|
* 2-cell grid (Published | Author). When omitted, only the Author cell
|
|
46
50
|
* renders inside the same bordered container. */
|
|
@@ -102,13 +106,19 @@ export function EntityMetadataValueCell({
|
|
|
102
106
|
export function EntityMetadataAuthorCell({
|
|
103
107
|
author,
|
|
104
108
|
roleLabel = 'Author',
|
|
109
|
+
authorHref,
|
|
105
110
|
className,
|
|
106
111
|
}: {
|
|
107
112
|
author: NonNullable<EntityAuthorCardProps['author']>
|
|
108
113
|
roleLabel?: string
|
|
114
|
+
authorHref?: string
|
|
109
115
|
className?: string
|
|
110
116
|
}) {
|
|
111
|
-
|
|
117
|
+
// Trimmed real name gates the LINK — a placeholder/unknown label must
|
|
118
|
+
// never render as a clickable author (an authorHref passed alongside an
|
|
119
|
+
// empty name would link "Unknown Author").
|
|
120
|
+
const trimmedName = typeof author.full_name === 'string' ? author.full_name.trim() : ''
|
|
121
|
+
const fullName = trimmedName || 'Unknown Author'
|
|
112
122
|
return (
|
|
113
123
|
<div className={`bg-ods-card p-4 flex items-center gap-3 ${className ?? ''}`}>
|
|
114
124
|
<SquareAvatar
|
|
@@ -119,8 +129,16 @@ export function EntityMetadataAuthorCell({
|
|
|
119
129
|
variant="round"
|
|
120
130
|
/>
|
|
121
131
|
<div className="flex flex-col gap-0 flex-1 min-w-0">
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
{/* title = full-name tooltip on truncation (carried over from the
|
|
133
|
+
release page's cell when it adopted this shared one). */}
|
|
134
|
+
<p className="text-h3 tracking-[-0.36px] text-ods-text-primary truncate" title={fullName}>
|
|
135
|
+
{authorHref && trimmedName ? (
|
|
136
|
+
<Link href={authorHref} className="hover:text-ods-accent transition-colors">
|
|
137
|
+
{fullName}
|
|
138
|
+
</Link>
|
|
139
|
+
) : (
|
|
140
|
+
fullName
|
|
141
|
+
)}
|
|
124
142
|
</p>
|
|
125
143
|
<p className="font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary">
|
|
126
144
|
{author.job_title || roleLabel}
|
|
@@ -133,6 +151,7 @@ export function EntityMetadataAuthorCell({
|
|
|
133
151
|
export function EntityAuthorCard({
|
|
134
152
|
author,
|
|
135
153
|
roleLabel = 'Author',
|
|
154
|
+
authorHref,
|
|
136
155
|
publishedAt,
|
|
137
156
|
publishedLabel = 'Published',
|
|
138
157
|
extraCells,
|
|
@@ -187,7 +206,7 @@ export function EntityAuthorCard({
|
|
|
187
206
|
className={dividerClass}
|
|
188
207
|
/>
|
|
189
208
|
)}
|
|
190
|
-
<EntityMetadataAuthorCell author={effectiveAuthor} roleLabel={roleLabel} />
|
|
209
|
+
<EntityMetadataAuthorCell author={effectiveAuthor} roleLabel={roleLabel} authorHref={authorHref} />
|
|
191
210
|
</div>
|
|
192
211
|
)
|
|
193
212
|
}
|
|
@@ -136,10 +136,8 @@ export function useNatsDialogSubscription({
|
|
|
136
136
|
abortControllerRef.current = null
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
// This hook treats 'error' as a disconnect (pre-existing behaviour); JetStream's
|
|
140
|
-
// hook does not — it skips retry on protocol errors.
|
|
141
139
|
const isDisconnectStatus = (status: NatsStatus) =>
|
|
142
|
-
status === 'closed' || status === 'disconnected'
|
|
140
|
+
status === 'closed' || status === 'disconnected'
|
|
143
141
|
|
|
144
142
|
const lifecycle = startConnectionLifecycle({
|
|
145
143
|
conn: sharedConn,
|
|
@@ -148,7 +146,7 @@ export function useNatsDialogSubscription({
|
|
|
148
146
|
backoff: reconnectionBackoffRef.current,
|
|
149
147
|
getFreshUrl: () => getNatsWsUrlRef.current(),
|
|
150
148
|
shouldRetryOn: isDisconnectStatus,
|
|
151
|
-
onStatusChange: (status) => {
|
|
149
|
+
onStatusChange: (status, evt) => {
|
|
152
150
|
if (status === 'connected') {
|
|
153
151
|
setIsConnected(true)
|
|
154
152
|
if (hadConnectionBeforeRef.current) {
|
|
@@ -157,6 +155,10 @@ export function useNatsDialogSubscription({
|
|
|
157
155
|
hadConnectionBeforeRef.current = true
|
|
158
156
|
onConnectRef.current?.()
|
|
159
157
|
}
|
|
158
|
+
if (status === 'error') {
|
|
159
|
+
console.warn('[NATS] protocol error:', evt.data)
|
|
160
|
+
return
|
|
161
|
+
}
|
|
160
162
|
if (isDisconnectStatus(status)) {
|
|
161
163
|
setIsConnected(false)
|
|
162
164
|
setIsSubscribed(false)
|
|
@@ -16,6 +16,7 @@ export * from './chat-message-row'
|
|
|
16
16
|
export * from './chat-message-skeleton'
|
|
17
17
|
|
|
18
18
|
export * from './chat-quick-action'
|
|
19
|
+
export * from './chat-quick-action-row'
|
|
19
20
|
export * from './chat-ticket-list'
|
|
20
21
|
export * from './chat-typing-indicator'
|
|
21
22
|
export * from './tool-execution-display'
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { EntityAuthor } from '../../../../types/entity-author';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Onboarding Guide entity — wire shape for the chat onboarding-guide
|
|
3
5
|
* card + the `/api/onboarding-guides` reads.
|
|
@@ -52,13 +54,7 @@ export interface OnboardingGuide {
|
|
|
52
54
|
status: string;
|
|
53
55
|
published_at: string | null;
|
|
54
56
|
author_id: string | null;
|
|
55
|
-
author?:
|
|
56
|
-
id: string;
|
|
57
|
-
full_name: string;
|
|
58
|
-
avatar_url: string | null;
|
|
59
|
-
job_title: string | null;
|
|
60
|
-
email?: string | null;
|
|
61
|
-
};
|
|
57
|
+
author?: EntityAuthor;
|
|
62
58
|
custom_instructions: string | null;
|
|
63
59
|
config: Record<string, unknown> | null;
|
|
64
60
|
ai_effort_score: number | null;
|
|
@@ -5,6 +5,7 @@ import type { Faq } from '../../types/faq'
|
|
|
5
5
|
import { FaqAccordion, type FaqItem } from '../faq-accordion'
|
|
6
6
|
import { useSelfFetch } from '../../hooks/use-self-fetch'
|
|
7
7
|
import { buildSuggestionUrl } from '../../utils/suggestion-url'
|
|
8
|
+
import { serializeJsonLd } from '../../utils/common'
|
|
8
9
|
import { buildFaqJsonLdFromFaqs, type FaqSchemaOptions } from './json-ld'
|
|
9
10
|
|
|
10
11
|
export interface FaqSectionProps {
|
|
@@ -202,7 +203,9 @@ export function FaqSection({
|
|
|
202
203
|
<script
|
|
203
204
|
type="application/ld+json"
|
|
204
205
|
// eslint-disable-next-line react/no-danger
|
|
205
|
-
|
|
206
|
+
// serializeJsonLd, NOT raw JSON.stringify — FAQ answers are
|
|
207
|
+
// admin-entered; an embedded "</script>" must not break the tag.
|
|
208
|
+
dangerouslySetInnerHTML={{ __html: serializeJsonLd(schema) }}
|
|
206
209
|
/>
|
|
207
210
|
)}
|
|
208
211
|
</>
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
|
4
3
|
import { useEffect, useRef } from 'react'
|
|
5
4
|
import { BellOffIcon } from '../../icons-v2-generated/interface/bell-off-icon'
|
|
6
5
|
import { ClockHistoryIcon } from '../../icons-v2-generated/date-and-time/clock-history-icon'
|
|
7
6
|
import { Button } from '../../ui/button/button'
|
|
8
|
-
import { Drawer, DrawerContent } from '../../ui/drawer'
|
|
7
|
+
import { Drawer, DrawerContent, DrawerTitle } from '../../ui/drawer'
|
|
9
8
|
import { Switch } from '../../ui/switch'
|
|
10
9
|
import { cn } from '../../../utils/cn'
|
|
11
10
|
import { useOptionalNotifications } from './notifications-context'
|
|
@@ -61,18 +60,22 @@ export function NotificationDrawer({
|
|
|
61
60
|
className,
|
|
62
61
|
)}
|
|
63
62
|
>
|
|
64
|
-
<div className="
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
<div className="px-[var(--spacing-system-m)] pt-[var(--spacing-system-m)]">
|
|
64
|
+
<DrawerTitle
|
|
65
|
+
className="truncate"
|
|
66
|
+
actions={
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
disabled={unreadCount === 0}
|
|
70
|
+
onClick={markAllRead}
|
|
71
|
+
className="self-center text-h6 text-ods-text-secondary underline transition-colors hover:text-ods-text-primary disabled:opacity-40"
|
|
72
|
+
>
|
|
73
|
+
Complete All
|
|
74
|
+
</button>
|
|
75
|
+
}
|
|
73
76
|
>
|
|
74
|
-
|
|
75
|
-
</
|
|
77
|
+
New Notifications
|
|
78
|
+
</DrawerTitle>
|
|
76
79
|
</div>
|
|
77
80
|
|
|
78
81
|
<DrawerScrollList
|
|
@@ -16,8 +16,8 @@ export function XLogo({ className = '', color = 'white', ...props }: XLogoProps)
|
|
|
16
16
|
className={className}
|
|
17
17
|
{...props}
|
|
18
18
|
>
|
|
19
|
-
<path d="M9.75292 12.6562C10.1923 12.2169 10.9044 12.2169 11.3437 12.6562C11.7831 13.0956 11.7831 13.8077 11.3437 14.2471L3.7954 21.7954C3.35606 22.2348 2.64392 22.2348 2.20458 21.7954C1.76524 21.3561 1.76524 20.6439 2.20458 20.2046L9.75292 12.6562ZM20.2046 2.20459C20.6439 1.76525 21.3561 1.76525 21.7954 2.20459C22.2347 2.64393 22.2347 3.35607 21.7954 3.79541L14.2471 11.3438C13.8077 11.7831 13.0956 11.7831 12.6562 11.3438C12.2169 10.9044 12.2169 10.1923 12.6562 9.75293L20.2046 2.20459Z" fill=
|
|
20
|
-
<path d="M7.9995 1.875L8.13426 1.88232C8.444 1.91951 8.72702 2.08473 8.9121 2.34082L21.9126 20.3408C22.1599 20.6833 22.1943 21.1365 22.0019 21.5127C21.8096 21.8887 21.4223 22.125 21 22.125H16.0005C15.6393 22.125 15.2994 21.9519 15.0879 21.6592L2.08739 3.65918C1.84004 3.31669 1.8057 2.86346 1.99803 2.4873C2.19042 2.11133 2.57763 1.875 2.99999 1.875H7.9995ZM16.5747 19.875H18.7983L7.42528 4.125H5.20165L16.5747 19.875Z" fill=
|
|
19
|
+
<path d="M9.75292 12.6562C10.1923 12.2169 10.9044 12.2169 11.3437 12.6562C11.7831 13.0956 11.7831 13.8077 11.3437 14.2471L3.7954 21.7954C3.35606 22.2348 2.64392 22.2348 2.20458 21.7954C1.76524 21.3561 1.76524 20.6439 2.20458 20.2046L9.75292 12.6562ZM20.2046 2.20459C20.6439 1.76525 21.3561 1.76525 21.7954 2.20459C22.2347 2.64393 22.2347 3.35607 21.7954 3.79541L14.2471 11.3438C13.8077 11.7831 13.0956 11.7831 12.6562 11.3438C12.2169 10.9044 12.2169 10.1923 12.6562 9.75293L20.2046 2.20459Z" fill={color} />
|
|
20
|
+
<path d="M7.9995 1.875L8.13426 1.88232C8.444 1.91951 8.72702 2.08473 8.9121 2.34082L21.9126 20.3408C22.1599 20.6833 22.1943 21.1365 22.0019 21.5127C21.8096 21.8887 21.4223 22.125 21 22.125H16.0005C15.6393 22.125 15.2994 21.9519 15.0879 21.6592L2.08739 3.65918C1.84004 3.31669 1.8057 2.86346 1.99803 2.4873C2.19042 2.11133 2.57763 1.875 2.99999 1.875H7.9995ZM16.5747 19.875H18.7983L7.42528 4.125H5.20165L16.5747 19.875Z" fill={color} />
|
|
21
21
|
</svg>
|
|
22
22
|
);
|
|
23
23
|
}
|
|
@@ -23,6 +23,8 @@ export function NavigationSidebarItemButton({
|
|
|
23
23
|
onClick,
|
|
24
24
|
}: NavigationSidebarItemButtonProps) {
|
|
25
25
|
const isActive = item.isActive ?? false
|
|
26
|
+
const unreadCount = item.unreadCount ?? 0
|
|
27
|
+
const hasUnread = unreadCount > 0
|
|
26
28
|
|
|
27
29
|
return (
|
|
28
30
|
<button
|
|
@@ -49,10 +51,13 @@ export function NavigationSidebarItemButton({
|
|
|
49
51
|
aria-label={item.label}
|
|
50
52
|
aria-current={isActive ? 'page' : undefined}
|
|
51
53
|
>
|
|
52
|
-
<div className="flex items-center justify-center flex-shrink-0">
|
|
54
|
+
<div className="relative flex items-center justify-center flex-shrink-0">
|
|
53
55
|
{cloneElement(item.icon as ReactElement<IconProps>, {
|
|
54
56
|
color: isActive && !disabled ? "text-ods-accent" : "text-ods-text-secondary",
|
|
55
57
|
})}
|
|
58
|
+
{hasUnread && !showLabel && (
|
|
59
|
+
<span className="absolute top-0 right-0 bg-ods-warning rounded-full w-2 h-2" />
|
|
60
|
+
)}
|
|
56
61
|
</div>
|
|
57
62
|
|
|
58
63
|
<span
|
|
@@ -65,6 +70,14 @@ export function NavigationSidebarItemButton({
|
|
|
65
70
|
{item.label}
|
|
66
71
|
</span>
|
|
67
72
|
|
|
73
|
+
{hasUnread && showLabel && (
|
|
74
|
+
<span className="bg-ods-accent flex items-center justify-center flex-shrink-0 p-2 rounded-md size-6">
|
|
75
|
+
<span className="text-xs font-medium text-ods-text-on-accent">
|
|
76
|
+
{unreadCount > 99 ? '99+' : unreadCount}
|
|
77
|
+
</span>
|
|
78
|
+
</span>
|
|
79
|
+
)}
|
|
80
|
+
|
|
68
81
|
{item.badge && showLabel && (
|
|
69
82
|
<span
|
|
70
83
|
className={cn(
|
|
@@ -50,6 +50,9 @@ export interface OnboardingGuideDetailViewProps {
|
|
|
50
50
|
* e.g. `(s) => \`/content/api/onboarding-guides/${s}\``. */
|
|
51
51
|
guideEndpoint?: (slug: string) => string
|
|
52
52
|
related?: OnboardingGuide[]
|
|
53
|
+
/** Link target for the author name in the metadata grid — the host
|
|
54
|
+
* computes it (public author page; absent ⇒ plain text). */
|
|
55
|
+
authorHref?: string
|
|
53
56
|
/** Optional markdown renderer override. Defaults to lib
|
|
54
57
|
* `<SimpleMarkdownRenderer>`. */
|
|
55
58
|
MarkdownRenderer?: ComponentType<{ content: string }>
|
|
@@ -65,6 +68,7 @@ export interface OnboardingGuideDetailViewProps {
|
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
export function OnboardingGuideDetailView({
|
|
71
|
+
authorHref,
|
|
68
72
|
initialData,
|
|
69
73
|
slug,
|
|
70
74
|
guideEndpoint,
|
|
@@ -139,6 +143,7 @@ export function OnboardingGuideDetailView({
|
|
|
139
143
|
{/* Metadata grid — Section · Step | Published | Author. */}
|
|
140
144
|
<EntityAuthorCard
|
|
141
145
|
author={guide.author}
|
|
146
|
+
authorHref={authorHref}
|
|
142
147
|
publishedAt={guide.published_at}
|
|
143
148
|
extraCells={[
|
|
144
149
|
{
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
* content type, add it BOTH there and here (cards + skeleton + list URL).
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
|
-
import React, { useMemo } from 'react';
|
|
45
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
46
46
|
import {
|
|
47
47
|
CONTENT_REF_GROUPS,
|
|
48
48
|
getContentRefLabelOrTitleCase,
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
} from '../../utils/content-ref-groups';
|
|
52
52
|
import type { ContentRef, ContentRefWithReason } from '../../types/content-ref';
|
|
53
53
|
import { useSelfFetch } from '../../hooks/use-self-fetch';
|
|
54
|
+
import { Pagination } from '../pagination';
|
|
54
55
|
import { extractItems, extractItemId } from '../../utils/extract-items';
|
|
55
56
|
import { buildListUrl as libBuildListUrl, canonicalContentRefType } from '../../utils/list-url';
|
|
56
57
|
import { buildSuggestionUrl } from '../../utils/suggestion-url';
|
|
@@ -342,6 +343,16 @@ function resolveGroupConfig(type: string): ContentRefGroupConfig {
|
|
|
342
343
|
};
|
|
343
344
|
}
|
|
344
345
|
|
|
346
|
+
/** Items per page within one type group. Groups larger than this paginate
|
|
347
|
+
* with the standard Pagination control (NO nested scrolling — a bounded
|
|
348
|
+
* scrollbox inside the page traps wheel events and hides the sections
|
|
349
|
+
* below it). MUST stay at or above the largest suggestion fill
|
|
350
|
+
* (RELATED_SAME_TYPE_COUNT in the hub's lib/constants/suggestions.ts) so
|
|
351
|
+
* current rails never paginate — only genuinely big groups (author pages)
|
|
352
|
+
* do. Exported through the subpath barrel for the hub's module-load
|
|
353
|
+
* assertion of that relation (entity-suggestion-sections.tsx). */
|
|
354
|
+
export const GROUP_PAGE_SIZE = 12;
|
|
355
|
+
|
|
345
356
|
function ContentGroup({
|
|
346
357
|
type,
|
|
347
358
|
refs,
|
|
@@ -371,11 +382,38 @@ function ContentGroup({
|
|
|
371
382
|
const isListLayout = config.layout === 'list';
|
|
372
383
|
const cardSize = config.gridSize;
|
|
373
384
|
|
|
385
|
+
// Per-group pagination for big groups (author pages): GROUP_PAGE_SIZE items
|
|
386
|
+
// per page with the standard Pagination control below the group. Client-side
|
|
387
|
+
// slicing — useGroupItems already fetched every row in one batched call, so
|
|
388
|
+
// page flips are instant. Hooks live above every early return (file
|
|
389
|
+
// convention). Page is clamped so a shrinking refs array (suggestion
|
|
390
|
+
// refetch) can never strand the view past the last page, and RESET when the
|
|
391
|
+
// ref set actually changes (shrink→grow must not return to a stale page).
|
|
392
|
+
const [page, setPage] = useState(1);
|
|
393
|
+
const refsKey = refs.map((r) => r.id).join('|');
|
|
394
|
+
const prevRefsKeyRef = useRef(refsKey);
|
|
395
|
+
useEffect(() => {
|
|
396
|
+
if (prevRefsKeyRef.current !== refsKey) {
|
|
397
|
+
prevRefsKeyRef.current = refsKey;
|
|
398
|
+
setPage(1);
|
|
399
|
+
}
|
|
400
|
+
}, [refsKey]);
|
|
401
|
+
const totalGroupPages = Math.max(1, Math.ceil(refs.length / GROUP_PAGE_SIZE));
|
|
402
|
+
const safePage = Math.min(page, totalGroupPages);
|
|
403
|
+
const visibleGroupRefs =
|
|
404
|
+
refs.length > GROUP_PAGE_SIZE
|
|
405
|
+
? refs.slice((safePage - 1) * GROUP_PAGE_SIZE, safePage * GROUP_PAGE_SIZE)
|
|
406
|
+
: refs;
|
|
407
|
+
const groupPagination =
|
|
408
|
+
totalGroupPages > 1 ? (
|
|
409
|
+
<Pagination currentPage={safePage} totalPages={totalGroupPages} onPageChange={setPage} />
|
|
410
|
+
) : null;
|
|
411
|
+
|
|
374
412
|
// Skeleton gate: `isLoading && !items` — SSR HTML and the client's first
|
|
375
413
|
// paint render identical skeletons (useSelfFetch starts isLoading=true on
|
|
376
414
|
// both sides), and once items exist they are never replaced by skeletons.
|
|
377
415
|
if (isLoading && !items) {
|
|
378
|
-
const skeletons =
|
|
416
|
+
const skeletons = visibleGroupRefs.map((r) => (
|
|
379
417
|
<div key={r.id}>{renderSkeletonForType(type, cardSize, adminCampaignCard)}</div>
|
|
380
418
|
));
|
|
381
419
|
return (
|
|
@@ -404,7 +442,7 @@ function ContentGroup({
|
|
|
404
442
|
(items as any[]).map((it) => [extractItemId(type, it) ?? String((it as any)?.id), it]),
|
|
405
443
|
);
|
|
406
444
|
|
|
407
|
-
const cards =
|
|
445
|
+
const cards = visibleGroupRefs
|
|
408
446
|
.map((contentRef) => {
|
|
409
447
|
const itemId = String(contentRef.id);
|
|
410
448
|
const item = itemById.get(itemId);
|
|
@@ -436,7 +474,22 @@ function ContentGroup({
|
|
|
436
474
|
})
|
|
437
475
|
.filter(Boolean);
|
|
438
476
|
|
|
439
|
-
if (cards.length === 0)
|
|
477
|
+
if (cards.length === 0) {
|
|
478
|
+
// Current PAGE resolved zero cards (rows deleted between the ref fetch
|
|
479
|
+
// and the group fetch, or a stricter list-API gate dropped them). When a
|
|
480
|
+
// pager exists the user must keep the controls to navigate back —
|
|
481
|
+
// dropping the whole group would strand them. A genuinely empty group
|
|
482
|
+
// (no pager) still vanishes with its heading.
|
|
483
|
+
if (groupPagination) {
|
|
484
|
+
return (
|
|
485
|
+
<div className="space-y-4">
|
|
486
|
+
{heading}
|
|
487
|
+
{groupPagination}
|
|
488
|
+
</div>
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
440
493
|
|
|
441
494
|
return (
|
|
442
495
|
<div className="space-y-4">
|
|
@@ -446,6 +499,7 @@ function ContentGroup({
|
|
|
446
499
|
) : (
|
|
447
500
|
<div className={gridClassFor(columns)}>{cards}</div>
|
|
448
501
|
)}
|
|
502
|
+
{groupPagination}
|
|
449
503
|
</div>
|
|
450
504
|
);
|
|
451
505
|
}
|
|
@@ -467,6 +521,12 @@ export interface RelatedContentSectionProps {
|
|
|
467
521
|
* `contentRefs` is provided. */
|
|
468
522
|
entityType?: string;
|
|
469
523
|
entityId?: number | string;
|
|
524
|
+
/** AUTHOR mode: self-fetch ALL published content authored by this profile
|
|
525
|
+
* from `{apiBaseUrl}/api/related-content?authorId=…` (grouped per type,
|
|
526
|
+
* endless within each group). Ignored when `contentRefs` is provided;
|
|
527
|
+
* takes precedence over the entityType/entityId suggestion scope.
|
|
528
|
+
* SSR-hydrate via `initialItems`, same as suggestion mode. */
|
|
529
|
+
authorId?: string;
|
|
470
530
|
/** Maps to the suggestion API's `count` param — the PER-TYPE fill target
|
|
471
531
|
* for every candidate type EXCEPT the host's own. Absent → param not sent
|
|
472
532
|
* (server default applies). */
|
|
@@ -532,6 +592,7 @@ export function RelatedContentSection({
|
|
|
532
592
|
contentRefs,
|
|
533
593
|
entityType,
|
|
534
594
|
entityId,
|
|
595
|
+
authorId,
|
|
535
596
|
minResults,
|
|
536
597
|
sameTypeMinResults,
|
|
537
598
|
includeTypes,
|
|
@@ -555,8 +616,24 @@ export function RelatedContentSection({
|
|
|
555
616
|
// entirely (an empty-string `types=` param would be dropped by the URL
|
|
556
617
|
// builder and read server-side as "all candidates") AND ignore SSR refs.
|
|
557
618
|
const suggestionsDisabled = includeTypes?.length === 0;
|
|
619
|
+
// Shared type-filter params — one spelling for both fetch modes so a future
|
|
620
|
+
// normalization (trim/dedupe) can't diverge between them.
|
|
621
|
+
const typeFilterParams = {
|
|
622
|
+
types: includeTypes !== undefined ? includeTypes.join(',') : undefined,
|
|
623
|
+
excludeTypes: excludeTypes && excludeTypes.length > 0 ? excludeTypes.join(',') : undefined,
|
|
624
|
+
};
|
|
625
|
+
// AUTHOR mode beats suggestion mode: when `authorId` is set the rail lists
|
|
626
|
+
// everything that profile authored (the server returns ALL, no count).
|
|
627
|
+
const authorUrl =
|
|
628
|
+
contentRefs === undefined && authorId && !suggestionsDisabled
|
|
629
|
+
? buildSuggestionUrl('/api/related-content', {
|
|
630
|
+
apiBaseUrl,
|
|
631
|
+
extraParams: { authorId, ...typeFilterParams },
|
|
632
|
+
})
|
|
633
|
+
: null;
|
|
558
634
|
const suggestUrl =
|
|
559
|
-
|
|
635
|
+
authorUrl ??
|
|
636
|
+
(contentRefs === undefined &&
|
|
560
637
|
entityType &&
|
|
561
638
|
entityId !== undefined &&
|
|
562
639
|
entityId !== null &&
|
|
@@ -569,11 +646,10 @@ export function RelatedContentSection({
|
|
|
569
646
|
count: minResults,
|
|
570
647
|
extraParams: {
|
|
571
648
|
sameTypeCount: sameTypeMinResults !== undefined ? String(sameTypeMinResults) : undefined,
|
|
572
|
-
|
|
573
|
-
excludeTypes: excludeTypes && excludeTypes.length > 0 ? excludeTypes.join(',') : undefined,
|
|
649
|
+
...typeFilterParams,
|
|
574
650
|
},
|
|
575
651
|
})
|
|
576
|
-
: null;
|
|
652
|
+
: null);
|
|
577
653
|
// Memoize the initialData wrapper — useSelfFetch re-syncs on [initialData],
|
|
578
654
|
// and a fresh per-render object would loop setState under re-rendering
|
|
579
655
|
// parents (the latent FaqSection bug, fixed there in the same change).
|
|
@@ -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 */}
|