@flamingo-stack/openframe-frontend-core 0.0.296-snapshot.20260621021605 → 0.0.296
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/README.md +0 -9
- package/dist/chunk-26PKDALD.js +2379 -0
- package/dist/chunk-26PKDALD.js.map +1 -0
- package/dist/chunk-3MCHAFHB.js +89 -0
- package/dist/chunk-3MCHAFHB.js.map +1 -0
- package/dist/{chunk-PI4WSYQV.js → chunk-3ZXUQQL4.js} +2 -2
- package/dist/{chunk-WMSTJAZT.cjs → chunk-5E2HOSSH.cjs} +51 -913
- package/dist/chunk-5E2HOSSH.cjs.map +1 -0
- package/dist/{chunk-IL47XWV5.js → chunk-5P3B2LZW.js} +14 -8
- package/dist/{chunk-IL47XWV5.js.map → chunk-5P3B2LZW.js.map} +1 -1
- package/dist/chunk-66AANIOC.cjs +619 -0
- package/dist/chunk-66AANIOC.cjs.map +1 -0
- package/dist/{chunk-AD6C23QY.js → chunk-6GCI7JOE.js} +7 -8
- package/dist/{chunk-AD6C23QY.js.map → chunk-6GCI7JOE.js.map} +1 -1
- package/dist/chunk-6JINAOI7.cjs +311 -0
- package/dist/chunk-6JINAOI7.cjs.map +1 -0
- package/dist/{chunk-2QG57XOJ.js → chunk-7RIYT7ZH.js} +205 -1067
- package/dist/chunk-7RIYT7ZH.js.map +1 -0
- package/dist/{chunk-L6PSSIUQ.cjs → chunk-AQOWFSMB.cjs} +1 -1
- package/dist/chunk-AQOWFSMB.cjs.map +1 -0
- package/dist/chunk-BOCFIKYS.cjs +3009 -0
- package/dist/chunk-BOCFIKYS.cjs.map +1 -0
- package/dist/{chunk-54KNMC2R.cjs → chunk-D3LEFMOA.cjs} +3 -3
- package/dist/{chunk-54KNMC2R.cjs.map → chunk-D3LEFMOA.cjs.map} +1 -1
- package/dist/chunk-D652TJBQ.js +3009 -0
- package/dist/chunk-D652TJBQ.js.map +1 -0
- package/dist/{chunk-PWQUAVA3.js → chunk-E4XABBSU.js} +98 -338
- package/dist/chunk-E4XABBSU.js.map +1 -0
- package/dist/{chunk-JALO4TAZ.js → chunk-EL6QLAWX.js} +55 -357
- package/dist/chunk-EL6QLAWX.js.map +1 -0
- package/dist/{chunk-6C526VNN.cjs → chunk-EYEW6PTA.cjs} +118 -358
- package/dist/chunk-EYEW6PTA.cjs.map +1 -0
- package/dist/chunk-FQJK446R.js +1606 -0
- package/dist/chunk-FQJK446R.js.map +1 -0
- package/dist/{chunk-4PSQS3SW.cjs → chunk-GLLDTKZK.cjs} +9 -7
- package/dist/chunk-GLLDTKZK.cjs.map +1 -0
- package/dist/{chunk-FQOTC3UU.cjs → chunk-IE6OU3WQ.cjs} +16 -318
- package/dist/chunk-IE6OU3WQ.cjs.map +1 -0
- package/dist/chunk-J54Z3OCR.cjs +1606 -0
- package/dist/chunk-J54Z3OCR.cjs.map +1 -0
- package/dist/{chunk-PC746XCO.js → chunk-K2PFPBMF.js} +5563 -15048
- package/dist/chunk-K2PFPBMF.js.map +1 -0
- package/dist/chunk-KXCRGTRN.cjs +2379 -0
- package/dist/chunk-KXCRGTRN.cjs.map +1 -0
- package/dist/{chunk-IZ7JSBFP.js → chunk-LCNMR277.js} +1 -1
- package/dist/chunk-LCNMR277.js.map +1 -0
- package/dist/chunk-LFGGF7OT.cjs +449 -0
- package/dist/chunk-LFGGF7OT.cjs.map +1 -0
- package/dist/chunk-M2OCXTNT.js +311 -0
- package/dist/chunk-M2OCXTNT.js.map +1 -0
- package/dist/{chunk-L7ULJKG7.js → chunk-MBFWU2EM.js} +10 -6
- package/dist/{chunk-L7ULJKG7.js.map → chunk-MBFWU2EM.js.map} +1 -1
- package/dist/chunk-ME4EVDFP.js +619 -0
- package/dist/chunk-ME4EVDFP.js.map +1 -0
- package/dist/chunk-OQ6X7ZOC.js +449 -0
- package/dist/chunk-OQ6X7ZOC.js.map +1 -0
- package/dist/{chunk-4TLE6VLU.js → chunk-OY7OF7E7.js} +24 -30
- package/dist/chunk-OY7OF7E7.js.map +1 -0
- package/dist/chunk-POKKCWKF.js +354 -0
- package/dist/chunk-POKKCWKF.js.map +1 -0
- package/dist/{chunk-GUTS7HGA.cjs → chunk-QHIXS3W2.cjs} +2514 -11999
- package/dist/chunk-QHIXS3W2.cjs.map +1 -0
- package/dist/chunk-TFSYSWPS.cjs +89 -0
- package/dist/chunk-TFSYSWPS.cjs.map +1 -0
- package/dist/{chunk-53FUMSZ5.cjs → chunk-W6M2FLLT.cjs} +46 -40
- package/dist/chunk-W6M2FLLT.cjs.map +1 -0
- package/dist/{chunk-3JIQVE7T.js → chunk-WHMATDVP.js} +15 -9
- package/dist/{chunk-3JIQVE7T.js.map → chunk-WHMATDVP.js.map} +1 -1
- package/dist/{chunk-YBYI62OE.cjs → chunk-X647HY3F.cjs} +37 -33
- package/dist/chunk-X647HY3F.cjs.map +1 -0
- package/dist/{chunk-UNVE2SDJ.cjs → chunk-X6BV7MB7.cjs} +31 -37
- package/dist/chunk-X6BV7MB7.cjs.map +1 -0
- package/dist/{chunk-7OVGB2DQ.cjs → chunk-XREEV72C.cjs} +25 -19
- package/dist/chunk-XREEV72C.cjs.map +1 -0
- package/dist/chunk-YETA25JW.cjs +354 -0
- package/dist/chunk-YETA25JW.cjs.map +1 -0
- package/dist/{chunk-FCDQNTDG.cjs → chunk-YIGPRLQY.cjs} +20 -21
- package/dist/chunk-YIGPRLQY.cjs.map +1 -0
- package/dist/{chunk-X4DOXQRT.js → chunk-ZP4AVIZP.js} +6 -4
- package/dist/{chunk-X4DOXQRT.js.map → chunk-ZP4AVIZP.js.map} +1 -1
- package/dist/components/chat/index.cjs +18 -8
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +85 -75
- package/dist/components/contact/index.cjs +15 -8
- package/dist/components/contact/index.cjs.map +1 -1
- package/dist/components/contact/index.js +14 -7
- package/dist/components/docs/doc-viewer.d.ts +2 -39
- package/dist/components/docs/doc-viewer.d.ts.map +1 -1
- package/dist/components/docs/index.cjs +9 -17
- package/dist/components/docs/index.cjs.map +1 -1
- package/dist/components/docs/index.d.ts +0 -4
- package/dist/components/docs/index.d.ts.map +1 -1
- package/dist/components/docs/index.js +8 -16
- package/dist/components/docs/use-document-tree.d.ts.map +1 -1
- package/dist/components/embeds/embed-iframe.d.ts.map +1 -1
- package/dist/components/embeds/index.cjs +15 -38
- package/dist/components/embeds/index.cjs.map +1 -1
- package/dist/components/embeds/index.d.ts +0 -8
- package/dist/components/embeds/index.d.ts.map +1 -1
- package/dist/components/embeds/index.js +17 -40
- package/dist/components/faq/index.cjs +16 -9
- package/dist/components/faq/index.cjs.map +1 -1
- package/dist/components/faq/index.js +15 -8
- package/dist/components/features/index.cjs +16 -8
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +32 -24
- package/dist/components/index.cjs +452 -257
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +976 -781
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/page-layout.d.ts +1 -10
- package/dist/components/layout/page-layout.d.ts.map +1 -1
- package/dist/components/layout/title-block.d.ts +1 -17
- package/dist/components/layout/title-block.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +15 -7
- package/dist/components/navigation/index.cjs.map +1 -1
- package/dist/components/navigation/index.js +17 -9
- package/dist/components/onboarding-guides/index.cjs +36 -35
- package/dist/components/onboarding-guides/index.cjs.map +1 -1
- package/dist/components/onboarding-guides/index.js +14 -13
- package/dist/components/onboarding-guides/index.js.map +1 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts +1 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +16 -9
- package/dist/components/related-content/index.cjs.map +1 -1
- package/dist/components/related-content/index.js +15 -8
- package/dist/components/shared/dev-section/dev-section-page.d.ts +0 -9
- package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -1
- package/dist/components/shared/dev-section/dev-section-view.d.ts.map +1 -1
- package/dist/components/shared/dev-section/index.d.ts +1 -1
- package/dist/components/shared/dev-section/index.d.ts.map +1 -1
- package/dist/components/shared/doc-search/use-doc-search.d.ts.map +1 -1
- package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +112 -100
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.js +32 -20
- package/dist/components/tickets/index.js.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +52 -50
- package/dist/components/ui/file-manager/index.cjs.map +1 -1
- package/dist/components/ui/file-manager/index.js +6 -4
- package/dist/components/ui/file-manager/index.js.map +1 -1
- package/dist/components/ui/index.cjs +19 -13
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +0 -2
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +139 -133
- package/dist/components/ui/release-changelog-section.d.ts +2 -6
- package/dist/components/ui/release-changelog-section.d.ts.map +1 -1
- package/dist/components/ui/simple-markdown-renderer.d.ts +8 -2
- package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
- package/dist/contexts/chat-runtime-context.d.ts +0 -14
- package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
- package/dist/contexts/index.cjs +3 -3
- package/dist/contexts/index.js +5 -5
- package/dist/embed-shims/index.cjs +3 -3
- package/dist/embed-shims/index.cjs.map +1 -1
- package/dist/embed-shims/index.js +4 -4
- package/dist/hooks/index.cjs +9 -4
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +11 -6
- package/dist/index.cjs +20 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +364 -358
- package/dist/types/doc-source.d.ts +1 -31
- package/dist/types/doc-source.d.ts.map +1 -1
- package/dist/utils/index.cjs +0 -4
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -4
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -7
- package/src/components/chat/embeddable-chat.tsx +1 -1
- package/src/components/docs/doc-viewer.tsx +19 -111
- package/src/components/docs/index.ts +0 -17
- package/src/components/docs/use-document-tree.ts +0 -21
- package/src/components/embeds/embed-iframe.tsx +9 -7
- package/src/components/embeds/index.ts +0 -30
- package/src/components/embeds/og-link-preview.tsx +13 -13
- package/src/components/layout/page-layout.tsx +1 -14
- package/src/components/layout/title-block.tsx +62 -40
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +3 -3
- package/src/components/shared/dev-section/dev-section-page.tsx +1 -9
- package/src/components/shared/dev-section/dev-section-view.tsx +9 -14
- package/src/components/shared/dev-section/index.ts +1 -1
- package/src/components/shared/doc-search/use-doc-search.ts +3 -7
- package/src/components/shared/legal-document/legal-document-page.tsx +2 -2
- package/src/components/shared/product-release/release-detail-page.tsx +4 -6
- package/src/components/ui/index.ts +0 -2
- package/src/components/ui/release-changelog-section.tsx +2 -7
- package/src/components/ui/simple-markdown-renderer.tsx +11 -7
- package/src/contexts/chat-runtime-context.tsx +0 -14
- package/src/types/doc-source.ts +1 -33
- package/src/utils/index.ts +0 -1
- package/dist/chunk-2QG57XOJ.js.map +0 -1
- package/dist/chunk-4PSQS3SW.cjs.map +0 -1
- package/dist/chunk-4TLE6VLU.js.map +0 -1
- package/dist/chunk-53FUMSZ5.cjs.map +0 -1
- package/dist/chunk-6C526VNN.cjs.map +0 -1
- package/dist/chunk-7OVGB2DQ.cjs.map +0 -1
- package/dist/chunk-F5OB2YAL.cjs +0 -144
- package/dist/chunk-F5OB2YAL.cjs.map +0 -1
- package/dist/chunk-FBWXMMRB.cjs +0 -2
- package/dist/chunk-FBWXMMRB.cjs.map +0 -1
- package/dist/chunk-FCDQNTDG.cjs.map +0 -1
- package/dist/chunk-FQOTC3UU.cjs.map +0 -1
- package/dist/chunk-GUTS7HGA.cjs.map +0 -1
- package/dist/chunk-GZ4C3XW6.js +0 -2
- package/dist/chunk-GZ4C3XW6.js.map +0 -1
- package/dist/chunk-IZ7JSBFP.js.map +0 -1
- package/dist/chunk-JALO4TAZ.js.map +0 -1
- package/dist/chunk-L6PSSIUQ.cjs.map +0 -1
- package/dist/chunk-PC746XCO.js.map +0 -1
- package/dist/chunk-PWQUAVA3.js.map +0 -1
- package/dist/chunk-SA2WPJVO.js +0 -144
- package/dist/chunk-SA2WPJVO.js.map +0 -1
- package/dist/chunk-UNVE2SDJ.cjs.map +0 -1
- package/dist/chunk-WMSTJAZT.cjs.map +0 -1
- package/dist/chunk-YBYI62OE.cjs.map +0 -1
- package/dist/components/case-studies/index.cjs +0 -126
- package/dist/components/case-studies/index.cjs.map +0 -1
- package/dist/components/case-studies/index.d.ts +0 -2
- package/dist/components/case-studies/index.d.ts.map +0 -1
- package/dist/components/case-studies/index.js +0 -126
- package/dist/components/case-studies/index.js.map +0 -1
- package/dist/components/case-studies/share-experience-section.d.ts +0 -48
- package/dist/components/case-studies/share-experience-section.d.ts.map +0 -1
- package/dist/components/docs/docs-hub-page.d.ts +0 -46
- package/dist/components/docs/docs-hub-page.d.ts.map +0 -1
- package/dist/components/docs/skeletons.d.ts +0 -32
- package/dist/components/docs/skeletons.d.ts.map +0 -1
- package/dist/components/docs/use-docs-resolve-link.d.ts +0 -20
- package/dist/components/docs/use-docs-resolve-link.d.ts.map +0 -1
- package/dist/components/embeds/embed-container.d.ts +0 -37
- package/dist/components/embeds/embed-container.d.ts.map +0 -1
- package/dist/components/embeds/file-download-card.d.ts +0 -18
- package/dist/components/embeds/file-download-card.d.ts.map +0 -1
- package/dist/components/embeds/linkedin-embed-client.d.ts +0 -8
- package/dist/components/embeds/linkedin-embed-client.d.ts.map +0 -1
- package/dist/components/embeds/markdown-image.d.ts +0 -5
- package/dist/components/embeds/markdown-image.d.ts.map +0 -1
- package/dist/components/embeds/reddit-embed-client.d.ts +0 -7
- package/dist/components/embeds/reddit-embed-client.d.ts.map +0 -1
- package/dist/components/embeds/rich-markdown-runtime.d.ts +0 -46
- package/dist/components/embeds/rich-markdown-runtime.d.ts.map +0 -1
- package/dist/components/embeds/twitter-embed-client.d.ts +0 -8
- package/dist/components/embeds/twitter-embed-client.d.ts.map +0 -1
- package/dist/components/layout/page-header.d.ts +0 -78
- package/dist/components/layout/page-header.d.ts.map +0 -1
- package/dist/components/layout/page-with-header.d.ts +0 -67
- package/dist/components/layout/page-with-header.d.ts.map +0 -1
- package/dist/components/ui/rich-markdown-renderer.d.ts +0 -34
- package/dist/components/ui/rich-markdown-renderer.d.ts.map +0 -1
- package/dist/utils/page-header-constants.d.ts +0 -15
- package/dist/utils/page-header-constants.d.ts.map +0 -1
- package/dist/utils/social-embed-cache.d.ts +0 -29
- package/dist/utils/social-embed-cache.d.ts.map +0 -1
- package/src/components/case-studies/index.ts +0 -4
- package/src/components/case-studies/share-experience-section.tsx +0 -185
- package/src/components/docs/docs-hub-page.tsx +0 -149
- package/src/components/docs/skeletons.tsx +0 -138
- package/src/components/docs/use-docs-resolve-link.ts +0 -52
- package/src/components/embeds/embed-container.tsx +0 -80
- package/src/components/embeds/file-download-card.tsx +0 -54
- package/src/components/embeds/linkedin-embed-client.tsx +0 -100
- package/src/components/embeds/markdown-image.tsx +0 -88
- package/src/components/embeds/reddit-embed-client.tsx +0 -550
- package/src/components/embeds/rich-markdown-runtime.tsx +0 -79
- package/src/components/embeds/twitter-embed-client.tsx +0 -308
- package/src/components/layout/page-header.tsx +0 -182
- package/src/components/layout/page-with-header.tsx +0 -110
- package/src/components/ui/rich-markdown-renderer.tsx +0 -1203
- package/src/utils/page-header-constants.ts +0 -15
- package/src/utils/social-embed-cache.ts +0 -391
- /package/dist/{chunk-PI4WSYQV.js.map → chunk-3ZXUQQL4.js.map} +0 -0
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { DocViewer, type DocViewerProps } from './doc-viewer'
|
|
3
|
-
import { MarkdownSkeleton, EmbedSkeleton } from './skeletons'
|
|
4
|
-
import { PdfViewer } from '../embeds/pdf-viewer'
|
|
5
|
-
import { GoogleSheetsViewer } from '../embeds/google-sheets-viewer'
|
|
6
|
-
import { FigmaEmbed } from '../embeds/figma-embed'
|
|
7
|
-
import { FileDownloadCard } from '../embeds/file-download-card'
|
|
8
|
-
import type {
|
|
9
|
-
DocContent,
|
|
10
|
-
DocRenderHandlers,
|
|
11
|
-
DocumentType,
|
|
12
|
-
} from '../../types/doc-source'
|
|
13
|
-
|
|
14
|
-
type DocRenderer = (content: DocContent, handlers: DocRenderHandlers) => React.ReactNode
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Per-document-type renderer map. `markdown` is required (the lib does NOT
|
|
18
|
-
* ship a default markdown renderer — embedders pick their own library +
|
|
19
|
-
* sanitization to avoid an XSS surface in the lib).
|
|
20
|
-
*
|
|
21
|
-
* `pdf` / `google_sheet` / `figma` / `file` are optional — the lib provides
|
|
22
|
-
* defaults from `components/embeds`. Override only when you want different
|
|
23
|
-
* props than the default (e.g. a custom PDF toolbar, embedded credentials).
|
|
24
|
-
*/
|
|
25
|
-
export type DocumentTypeRenderers = { markdown: DocRenderer } & Partial<
|
|
26
|
-
Record<DocumentType, DocRenderer>
|
|
27
|
-
>
|
|
28
|
-
|
|
29
|
-
export interface DocsHubPageProps
|
|
30
|
-
extends Omit<DocViewerProps, 'renderContent' | 'renderSkeleton' | 'showAIChat'> {
|
|
31
|
-
/** Per-document-type renderer map. `markdown` is REQUIRED. */
|
|
32
|
-
documentTypeRenderers: DocumentTypeRenderers
|
|
33
|
-
|
|
34
|
-
/** Renderer for unknown / future document types. Defaults to a lib-styled
|
|
35
|
-
* "Unsupported document type" message. */
|
|
36
|
-
fallbackRenderer?: DocRenderer
|
|
37
|
-
|
|
38
|
-
/** Loading skeleton picker. Defaults: `markdown` / `undefined` →
|
|
39
|
-
* `<MarkdownSkeleton>`, everything else → `<EmbedSkeleton>`. */
|
|
40
|
-
renderSkeleton?: (documentType: DocumentType | undefined) => React.ReactNode
|
|
41
|
-
|
|
42
|
-
/** Defaults to `true` (the embeddable wrapper favors the chat-enabled
|
|
43
|
-
* experience). Only mounts the in-source RAG search bar
|
|
44
|
-
* (`<DocSearchBar>`) — does NOT require `ChatRuntimeContext`. */
|
|
45
|
-
showAIChat?: boolean
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const DEFAULT_TITLE = 'Documents'
|
|
49
|
-
|
|
50
|
-
const defaultFallbackRenderer: DocRenderer = () => (
|
|
51
|
-
<div className="text-center py-16">
|
|
52
|
-
<p className="text-ods-text-secondary">Unsupported document type</p>
|
|
53
|
-
</div>
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
// When the DAL hasn't populated the URL field for a rich-content type, the
|
|
57
|
-
// embed-viewer components render a broken iframe (empty src). Fall back to
|
|
58
|
-
// the same lib-styled "Unsupported document type" panel the explicit
|
|
59
|
-
// fallback uses — the surface is honest about the missing data instead of
|
|
60
|
-
// pretending to load. `FileDownloadCard` handles its own missing-URL state
|
|
61
|
-
// (hides the Download button), so it doesn't need this guard.
|
|
62
|
-
const defaultPdfRenderer: DocRenderer = (content, handlers) =>
|
|
63
|
-
content.fileUrl ? (
|
|
64
|
-
<PdfViewer src={content.fileUrl} fileName={content.fileName} />
|
|
65
|
-
) : (
|
|
66
|
-
defaultFallbackRenderer(content, handlers)
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
const defaultGoogleSheetRenderer: DocRenderer = (content, handlers) =>
|
|
70
|
-
content.externalUrl ? (
|
|
71
|
-
<GoogleSheetsViewer externalUrl={content.externalUrl} fileName={content.fileName} />
|
|
72
|
-
) : (
|
|
73
|
-
defaultFallbackRenderer(content, handlers)
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
const defaultFigmaRenderer: DocRenderer = (content, handlers) =>
|
|
77
|
-
content.externalUrl ? (
|
|
78
|
-
<FigmaEmbed url={content.externalUrl} title={content.fileName} loading="eager" />
|
|
79
|
-
) : (
|
|
80
|
-
defaultFallbackRenderer(content, handlers)
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
const defaultFileRenderer: DocRenderer = (content) => (
|
|
84
|
-
<FileDownloadCard
|
|
85
|
-
fileName={content.fileName}
|
|
86
|
-
mimeType={content.mimeType}
|
|
87
|
-
fileSize={content.fileSize}
|
|
88
|
-
fileUrl={content.fileUrl}
|
|
89
|
-
/>
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
const defaultRenderSkeleton = (documentType: DocumentType | undefined) =>
|
|
93
|
-
!documentType || documentType === 'markdown' ? (
|
|
94
|
-
<MarkdownSkeleton />
|
|
95
|
-
) : (
|
|
96
|
-
// Forward the documentType so the embed skeleton renders the right
|
|
97
|
-
// shape (PDF=2 buttons, sheets/figma=1 button, file=centered card).
|
|
98
|
-
<EmbedSkeleton documentType={documentType} />
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Embeddable docs-hub page. Bundles `<DocViewer>` with safe defaults so the
|
|
103
|
-
* minimum embed is a one-line mount (consumer only has to supply
|
|
104
|
-
* `documentTypeRenderers.markdown`).
|
|
105
|
-
*
|
|
106
|
-
* Used by the hub at `/knowledge-base` and `/data-room`, and by third-party
|
|
107
|
-
* React apps that embed the docs experience behind their own proxy. See
|
|
108
|
-
* `docs/EMBEDDING_DOCS_HUB.md` for the embedder setup.
|
|
109
|
-
*
|
|
110
|
-
* SEO note: this component is `'use client'` (via the docs barrel) — server-
|
|
111
|
-
* side SEO is the host's responsibility. The hub's `<DocSeoContent>` is the
|
|
112
|
-
* canonical implementation embedders can reference.
|
|
113
|
-
*/
|
|
114
|
-
export function DocsHubPage({
|
|
115
|
-
title = DEFAULT_TITLE,
|
|
116
|
-
documentTypeRenderers,
|
|
117
|
-
fallbackRenderer = defaultFallbackRenderer,
|
|
118
|
-
renderSkeleton = defaultRenderSkeleton,
|
|
119
|
-
showAIChat = true,
|
|
120
|
-
className = 'min-h-screen',
|
|
121
|
-
sidebarLabel = 'DOCUMENTATION',
|
|
122
|
-
...docViewerProps
|
|
123
|
-
}: DocsHubPageProps) {
|
|
124
|
-
const resolvedRenderers: DocumentTypeRenderers = {
|
|
125
|
-
markdown: documentTypeRenderers.markdown,
|
|
126
|
-
pdf: documentTypeRenderers.pdf ?? defaultPdfRenderer,
|
|
127
|
-
google_sheet: documentTypeRenderers.google_sheet ?? defaultGoogleSheetRenderer,
|
|
128
|
-
figma: documentTypeRenderers.figma ?? defaultFigmaRenderer,
|
|
129
|
-
file: documentTypeRenderers.file ?? defaultFileRenderer,
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const renderContent: DocViewerProps['renderContent'] = (content, handlers) => {
|
|
133
|
-
const type = (content.documentType ?? 'markdown') as DocumentType
|
|
134
|
-
const renderer = resolvedRenderers[type] ?? fallbackRenderer
|
|
135
|
-
return renderer(content, handlers)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return (
|
|
139
|
-
<DocViewer
|
|
140
|
-
{...docViewerProps}
|
|
141
|
-
title={title}
|
|
142
|
-
showAIChat={showAIChat}
|
|
143
|
-
className={className}
|
|
144
|
-
sidebarLabel={sidebarLabel}
|
|
145
|
-
renderContent={renderContent}
|
|
146
|
-
renderSkeleton={renderSkeleton}
|
|
147
|
-
/>
|
|
148
|
-
)
|
|
149
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Text-style skeleton — matches a rendered markdown article layout.
|
|
5
|
-
* Used by `<DocsHubPage>` as the default for `markdown` (and unknown
|
|
6
|
-
* document types). Embedders can override via `renderSkeleton`.
|
|
7
|
-
*/
|
|
8
|
-
export function MarkdownSkeleton() {
|
|
9
|
-
return (
|
|
10
|
-
<div className="space-y-7 mt-6">
|
|
11
|
-
<div className="space-y-[14px]">
|
|
12
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
13
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
14
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
15
|
-
<div className="h-[16px] bg-ods-border rounded w-3/4 animate-pulse" />
|
|
16
|
-
</div>
|
|
17
|
-
<div className="space-y-[14px]">
|
|
18
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
19
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
20
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
21
|
-
<div className="h-[16px] bg-ods-border rounded w-5/6 animate-pulse" />
|
|
22
|
-
</div>
|
|
23
|
-
<div className="space-y-[14px]">
|
|
24
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
25
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
26
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
27
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
28
|
-
<div className="h-[16px] bg-ods-border rounded w-2/3 animate-pulse" />
|
|
29
|
-
</div>
|
|
30
|
-
<div className="h-[88px] bg-ods-card border border-ods-border rounded-lg animate-pulse" />
|
|
31
|
-
<div className="h-7 bg-ods-border rounded w-1/3 animate-pulse" />
|
|
32
|
-
<div className="space-y-[14px]">
|
|
33
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
34
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
35
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
36
|
-
<div className="h-[16px] bg-ods-border rounded w-[72%] animate-pulse" />
|
|
37
|
-
</div>
|
|
38
|
-
<div className="space-y-[14px]">
|
|
39
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
40
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
41
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
42
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
43
|
-
<div className="h-[16px] bg-ods-border rounded w-[58%] animate-pulse" />
|
|
44
|
-
</div>
|
|
45
|
-
<div className="h-7 bg-ods-border rounded w-2/5 animate-pulse" />
|
|
46
|
-
<div className="space-y-[14px]">
|
|
47
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
48
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
49
|
-
<div className="h-[16px] bg-ods-border rounded w-[90%] animate-pulse" />
|
|
50
|
-
</div>
|
|
51
|
-
<div className="space-y-[14px]">
|
|
52
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
53
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
54
|
-
<div className="h-[16px] bg-ods-border rounded w-full animate-pulse" />
|
|
55
|
-
<div className="h-[16px] bg-ods-border rounded w-[70%] animate-pulse" />
|
|
56
|
-
</div>
|
|
57
|
-
<div className="h-[88px] bg-ods-card border border-ods-border rounded-lg animate-pulse" />
|
|
58
|
-
</div>
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Embed-style skeleton — matches the iframe loading state for `pdf`,
|
|
64
|
-
* `google_sheet`, `figma`, and `file` document types. Used by
|
|
65
|
-
* `<DocsHubPage>` as the default for non-markdown documentTypes.
|
|
66
|
-
*
|
|
67
|
-
* The skeleton is documentType-aware so its layout matches the actual
|
|
68
|
-
* viewer that will replace it:
|
|
69
|
-
* - `pdf` → header with title + TWO buttons (Preview, Download)
|
|
70
|
-
* - `google_sheet`/`figma` → header with title + ONE button/toggle
|
|
71
|
-
* - `file` → centered FileDownloadCard-style box
|
|
72
|
-
* - undefined / others → generic (1-button header)
|
|
73
|
-
*
|
|
74
|
-
* IMPORTANT: bars use `bg-ods-border` (NOT `bg-ods-skeleton`). The
|
|
75
|
-
* `--ods-skeleton` token resolves to TRANSPARENT in this build, leaving
|
|
76
|
-
* the skeleton box visually empty — the embed skeleton was the loudest
|
|
77
|
-
* surface affected (a full-height iframe area showing nothing). Same fix
|
|
78
|
-
* the chat-message-row skeleton already documents in its inline comment.
|
|
79
|
-
*/
|
|
80
|
-
export interface EmbedSkeletonProps {
|
|
81
|
-
/** When provided, the header layout matches the eventual viewer's
|
|
82
|
-
* button count + arrangement, so the layout doesn't shift on load. */
|
|
83
|
-
documentType?: 'pdf' | 'google_sheet' | 'figma' | 'file' | string
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function EmbedSkeleton({ documentType }: EmbedSkeletonProps = {}) {
|
|
87
|
-
// Centered card shape for the `file` documentType — matches
|
|
88
|
-
// `<FileDownloadCard>`'s `flex flex-col items-center justify-center py-16`
|
|
89
|
-
// + bordered card with icon, name, type/size row, Download button.
|
|
90
|
-
if (documentType === 'file') {
|
|
91
|
-
return (
|
|
92
|
-
<div className="flex flex-col items-center justify-center py-16">
|
|
93
|
-
<div className="bg-ods-card border border-ods-border rounded-xl p-8 max-w-md w-full text-center space-y-4">
|
|
94
|
-
<div className="w-16 h-16 rounded mx-auto bg-ods-border animate-pulse" />
|
|
95
|
-
<div className="space-y-2">
|
|
96
|
-
<div className="h-5 w-2/3 mx-auto rounded bg-ods-border animate-pulse" />
|
|
97
|
-
<div className="h-4 w-1/2 mx-auto rounded bg-ods-border animate-pulse" />
|
|
98
|
-
</div>
|
|
99
|
-
<div className="h-10 w-full rounded bg-ods-border animate-pulse" />
|
|
100
|
-
</div>
|
|
101
|
-
</div>
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// PDF viewer has TWO buttons (Preview + Download); Sheets / Figma
|
|
106
|
-
// render ONE (Open / view-toggle). Default to one for unknown types.
|
|
107
|
-
const buttonCount = documentType === 'pdf' ? 2 : 1
|
|
108
|
-
|
|
109
|
-
return (
|
|
110
|
-
<div className="space-y-4">
|
|
111
|
-
{/* Header — matches the actual viewer's
|
|
112
|
-
* `flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`
|
|
113
|
-
* (mobile-stacked, desktop-row). */}
|
|
114
|
-
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
115
|
-
{/* Left: icon + title */}
|
|
116
|
-
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
117
|
-
<div className="w-5 h-5 shrink-0 rounded bg-ods-border animate-pulse" />
|
|
118
|
-
<div className="h-6 w-2/3 rounded bg-ods-border animate-pulse" />
|
|
119
|
-
</div>
|
|
120
|
-
{/* Right: 1 or 2 buttons. Mobile = full-width; desktop = auto. */}
|
|
121
|
-
<div className="flex items-center gap-2 w-full sm:w-auto">
|
|
122
|
-
{Array.from({ length: buttonCount }).map((_, i) => (
|
|
123
|
-
<div
|
|
124
|
-
key={i}
|
|
125
|
-
className="h-10 w-full sm:w-32 rounded bg-ods-border animate-pulse flex-1 sm:flex-initial"
|
|
126
|
-
/>
|
|
127
|
-
))}
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
{/* Body — clean iframe-sized rectangle, no fake inner placeholder
|
|
131
|
-
* cruft. Matches the viewer's default `calc(100vh - 250px)` height. */}
|
|
132
|
-
<div
|
|
133
|
-
className="w-full rounded-lg border border-ods-border bg-ods-card animate-pulse"
|
|
134
|
-
style={{ height: 'calc(100vh - 250px)' }}
|
|
135
|
-
/>
|
|
136
|
-
</div>
|
|
137
|
-
)
|
|
138
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { useCallback } from 'react'
|
|
2
|
-
import { useChatRuntime } from '../../contexts/chat-runtime-context'
|
|
3
|
-
import type { ResolveLinkResult } from '../../types/doc-source'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* `useDocsResolveLink(sourceId, override?)` — POST `/api/docs/resolve-link`
|
|
7
|
-
* (or the override / `ChatRuntime.endpoints.docsResolveLinkUrl`) for a
|
|
8
|
-
* relative href inside a doc body, returning a `ResolveLinkResult`
|
|
9
|
-
* envelope.
|
|
10
|
-
*
|
|
11
|
-
* The endpoint chain (`override ?? runtime.endpoints.docsResolveLinkUrl
|
|
12
|
-
* ?? '/api/docs/resolve-link'`) mirrors `searchEndpoint` resolution in
|
|
13
|
-
* `<DocViewer>` so embedders configure both the same way: per-instance
|
|
14
|
-
* prop OR ambient `ChatRuntimeProvider`.
|
|
15
|
-
*
|
|
16
|
-
* The full fetch + JSON-parse pipeline is wrapped in try/catch so a
|
|
17
|
-
* network throw (DNS / CORS / offline) or a non-JSON response surfaces
|
|
18
|
-
* as `{ success: false, error }` — the markdown renderer's broken-link
|
|
19
|
-
* badge handles that branch instead of swallowing an unhandled rejection
|
|
20
|
-
* past the click handler.
|
|
21
|
-
*/
|
|
22
|
-
export function useDocsResolveLink(
|
|
23
|
-
sourceId: string,
|
|
24
|
-
resolveLinkEndpoint?: string | null,
|
|
25
|
-
) {
|
|
26
|
-
const chatRuntime = useChatRuntime()
|
|
27
|
-
const resolvedResolveLinkEndpoint =
|
|
28
|
-
resolveLinkEndpoint ?? chatRuntime?.endpoints.docsResolveLinkUrl ?? '/api/docs/resolve-link'
|
|
29
|
-
|
|
30
|
-
return useCallback(
|
|
31
|
-
async (href: string, currentPath: string): Promise<ResolveLinkResult> => {
|
|
32
|
-
try {
|
|
33
|
-
const response = await fetch(resolvedResolveLinkEndpoint, {
|
|
34
|
-
method: 'POST',
|
|
35
|
-
headers: { 'Content-Type': 'application/json' },
|
|
36
|
-
body: JSON.stringify({ link: href, currentPath, source: sourceId }),
|
|
37
|
-
})
|
|
38
|
-
if (!response.ok) {
|
|
39
|
-
return { success: false, error: `Resolve failed: ${response.status}` }
|
|
40
|
-
}
|
|
41
|
-
const json = await response.json()
|
|
42
|
-
return (json.data ?? json) as ResolveLinkResult
|
|
43
|
-
} catch (error) {
|
|
44
|
-
return {
|
|
45
|
-
success: false,
|
|
46
|
-
error: error instanceof Error ? error.message : 'Resolve failed',
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
[resolvedResolveLinkEndpoint, sourceId],
|
|
51
|
-
)
|
|
52
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { cn } from '../../utils/cn';
|
|
3
|
-
|
|
4
|
-
// Base container sizes for different embed types
|
|
5
|
-
export const EMBED_SIZES = {
|
|
6
|
-
youtube: 'max-w-3xl', // 768px - Video content needs more space
|
|
7
|
-
twitter: 'max-w-md', // 448px - Narrow tweets, mobile-first
|
|
8
|
-
reddit: 'max-w-xl', // 576px - Medium width for discussion threads
|
|
9
|
-
linkedin: 'max-w-lg', // 512px - LinkedIn post embed, mobile-first
|
|
10
|
-
linkPreview: 'max-w-lg' // 512px - Balanced width for cards
|
|
11
|
-
} as const;
|
|
12
|
-
|
|
13
|
-
export type EmbedSize = keyof typeof EMBED_SIZES;
|
|
14
|
-
|
|
15
|
-
interface EmbedContainerProps {
|
|
16
|
-
size: EmbedSize;
|
|
17
|
-
children: React.ReactNode;
|
|
18
|
-
className?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Base container for all embeds
|
|
22
|
-
export function EmbedContainer({
|
|
23
|
-
size,
|
|
24
|
-
children,
|
|
25
|
-
className = ""
|
|
26
|
-
}: EmbedContainerProps) {
|
|
27
|
-
return (
|
|
28
|
-
<div className={cn(
|
|
29
|
-
"mx-auto rounded-lg overflow-hidden",
|
|
30
|
-
"bg-ods-card border border-ods-border",
|
|
31
|
-
"transition-all duration-200 ease-in-out",
|
|
32
|
-
"hover:border-ods-accent/30 hover:shadow-lg hover:shadow-ods-accent/10",
|
|
33
|
-
EMBED_SIZES[size],
|
|
34
|
-
className
|
|
35
|
-
)}>
|
|
36
|
-
{children}
|
|
37
|
-
</div>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Specific containers for each platform
|
|
42
|
-
export function YouTubeContainer({ children, className }: { children: React.ReactNode; className?: string }) {
|
|
43
|
-
return (
|
|
44
|
-
<EmbedContainer size="youtube" className={cn("my-6", className)}>
|
|
45
|
-
{children}
|
|
46
|
-
</EmbedContainer>
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function TwitterContainer({ children, className }: { children: React.ReactNode; className?: string }) {
|
|
51
|
-
return (
|
|
52
|
-
<EmbedContainer size="twitter" className={cn("my-6", className)}>
|
|
53
|
-
{children}
|
|
54
|
-
</EmbedContainer>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function RedditContainer({ children, className }: { children: React.ReactNode; className?: string }) {
|
|
59
|
-
return (
|
|
60
|
-
<EmbedContainer size="reddit" className={cn("my-6", className)}>
|
|
61
|
-
{children}
|
|
62
|
-
</EmbedContainer>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function LinkPreviewContainer({ children, className }: { children: React.ReactNode; className?: string }) {
|
|
67
|
-
return (
|
|
68
|
-
<EmbedContainer size="linkPreview" className={cn("my-6", className)}>
|
|
69
|
-
{children}
|
|
70
|
-
</EmbedContainer>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function LinkedInContainer({ children, className }: { children: React.ReactNode; className?: string }) {
|
|
75
|
-
return (
|
|
76
|
-
<EmbedContainer size="linkedin" className={cn("my-6", className)}>
|
|
77
|
-
{children}
|
|
78
|
-
</EmbedContainer>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { Button } from '../ui'
|
|
3
|
-
import { FileText, Download } from 'lucide-react'
|
|
4
|
-
import { formatFileSize } from '../../utils'
|
|
5
|
-
|
|
6
|
-
export interface FileDownloadCardProps {
|
|
7
|
-
fileName?: string
|
|
8
|
-
mimeType?: string
|
|
9
|
-
fileSize?: number
|
|
10
|
-
fileUrl?: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Generic downloadable-file card for the `file` document type. Used by
|
|
15
|
-
* `<DocsHubPage>`'s default `documentTypeRenderers.file`. Embedders can
|
|
16
|
-
* override the default by passing their own `file` renderer.
|
|
17
|
-
*
|
|
18
|
-
* When `fileUrl` is missing, the Download button is omitted (the card still
|
|
19
|
-
* renders the filename + type + size so the user knows what they were
|
|
20
|
-
* about to download).
|
|
21
|
-
*/
|
|
22
|
-
export function FileDownloadCard({
|
|
23
|
-
fileName,
|
|
24
|
-
mimeType,
|
|
25
|
-
fileSize,
|
|
26
|
-
fileUrl,
|
|
27
|
-
}: FileDownloadCardProps) {
|
|
28
|
-
return (
|
|
29
|
-
<div className="flex flex-col items-center justify-center py-16">
|
|
30
|
-
<div className="bg-ods-card border border-ods-border rounded-xl p-8 max-w-md w-full text-center space-y-4">
|
|
31
|
-
<FileText className="w-16 h-16 text-ods-text-secondary mx-auto" />
|
|
32
|
-
<div>
|
|
33
|
-
<h3 className="text-lg font-semibold text-ods-text-primary">
|
|
34
|
-
{fileName || 'File'}
|
|
35
|
-
</h3>
|
|
36
|
-
<div className="flex items-center justify-center gap-3 mt-2 text-sm text-ods-text-secondary">
|
|
37
|
-
{mimeType && <span>{mimeType}</span>}
|
|
38
|
-
{typeof fileSize === 'number' && <span>{formatFileSize(fileSize)}</span>}
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
{fileUrl && (
|
|
42
|
-
<Button
|
|
43
|
-
variant="accent"
|
|
44
|
-
href={fileUrl}
|
|
45
|
-
openInNewTab
|
|
46
|
-
leftIcon={<Download className="w-4 h-4" />}
|
|
47
|
-
>
|
|
48
|
-
Download File
|
|
49
|
-
</Button>
|
|
50
|
-
)}
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
)
|
|
54
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useMemo, useState } from 'react';
|
|
4
|
-
import { LinkedInContainer } from './embed-container';
|
|
5
|
-
import { LinkedinIcon } from '../icons-v2-generated/brand-logos/linkedin-icon';
|
|
6
|
-
import { ExternalLink } from 'lucide-react';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Derive LinkedIn's official embed URL from any post URL or URN.
|
|
10
|
-
* LinkedIn renders public posts at /embed/feed/update/<urn>. Returns '' when no
|
|
11
|
-
* URN can be derived, so the component falls back to a link instead of a broken
|
|
12
|
-
* (X-Frame-blocked) iframe.
|
|
13
|
-
*/
|
|
14
|
-
function toLinkedInEmbedUrl(url: string): string {
|
|
15
|
-
if (!url) return '';
|
|
16
|
-
if (url.includes('linkedin.com/embed/')) return url.split('?')[0];
|
|
17
|
-
let m = url.match(/urn:li:(activity|share|ugcPost):(\d+)/i);
|
|
18
|
-
if (m) return `https://www.linkedin.com/embed/feed/update/urn:li:${m[1]}:${m[2]}`;
|
|
19
|
-
m = url.match(/activity[-:](\d{15,25})/i);
|
|
20
|
-
if (m) return `https://www.linkedin.com/embed/feed/update/urn:li:activity:${m[1]}`;
|
|
21
|
-
m = url.match(/-(\d{15,25})(?:-[A-Za-z0-9_-]+)?\/?(?:\?.*)?$/);
|
|
22
|
-
if (m) return `https://www.linkedin.com/embed/feed/update/urn:li:activity:${m[1]}`;
|
|
23
|
-
return '';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface LinkedInEmbedProps {
|
|
27
|
-
url: string;
|
|
28
|
-
/** Fixed iframe height — LinkedIn embeds don't auto-resize. */
|
|
29
|
-
height?: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function LinkedInEmbedClient({ url, height = 600 }: LinkedInEmbedProps) {
|
|
33
|
-
const embedUrl = useMemo(() => toLinkedInEmbedUrl(url), [url]);
|
|
34
|
-
const [loaded, setLoaded] = useState(false);
|
|
35
|
-
|
|
36
|
-
// No derivable URN → graceful fallback card with a link (mirrors reddit's error state)
|
|
37
|
-
if (!embedUrl) {
|
|
38
|
-
return (
|
|
39
|
-
<LinkedInContainer>
|
|
40
|
-
<div className="p-6">
|
|
41
|
-
<div className="flex items-center space-x-3 text-ods-text-secondary mb-4">
|
|
42
|
-
<LinkedinIcon className="w-5 h-5 shrink-0" />
|
|
43
|
-
<span>LinkedIn post</span>
|
|
44
|
-
</div>
|
|
45
|
-
<a
|
|
46
|
-
href={url}
|
|
47
|
-
target="_blank"
|
|
48
|
-
rel="noopener noreferrer"
|
|
49
|
-
className="inline-flex items-center space-x-2 px-4 py-2 bg-ods-card border border-ods-border text-ods-text-primary rounded-md text-sm font-medium hover:bg-ods-bg-secondary transition-colors"
|
|
50
|
-
>
|
|
51
|
-
<LinkedinIcon className="w-4 h-4" />
|
|
52
|
-
<span>View on LinkedIn</span>
|
|
53
|
-
</a>
|
|
54
|
-
</div>
|
|
55
|
-
</LinkedInContainer>
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<LinkedInContainer>
|
|
61
|
-
<div className="relative w-full" style={{ height }}>
|
|
62
|
-
{!loaded && (
|
|
63
|
-
<div className="absolute inset-0 p-6 animate-pulse">
|
|
64
|
-
<div className="flex items-center space-x-3 mb-4">
|
|
65
|
-
<div className="w-12 h-12 bg-ods-border rounded-full" />
|
|
66
|
-
<div>
|
|
67
|
-
<div className="h-4 bg-ods-border rounded w-32 mb-2" />
|
|
68
|
-
<div className="h-3 bg-ods-border rounded w-24" />
|
|
69
|
-
</div>
|
|
70
|
-
</div>
|
|
71
|
-
<div className="space-y-2">
|
|
72
|
-
<div className="h-4 bg-ods-border rounded w-full" />
|
|
73
|
-
<div className="h-4 bg-ods-border rounded w-3/4" />
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
)}
|
|
77
|
-
<iframe
|
|
78
|
-
src={embedUrl}
|
|
79
|
-
title="Embedded LinkedIn post"
|
|
80
|
-
className="w-full h-full"
|
|
81
|
-
style={{ border: 0 }}
|
|
82
|
-
loading="lazy"
|
|
83
|
-
allowFullScreen
|
|
84
|
-
onLoad={() => setLoaded(true)}
|
|
85
|
-
/>
|
|
86
|
-
</div>
|
|
87
|
-
<div className="px-4 py-3 bg-ods-bg-secondary border-t border-ods-border">
|
|
88
|
-
<a
|
|
89
|
-
href={url}
|
|
90
|
-
target="_blank"
|
|
91
|
-
rel="noopener noreferrer"
|
|
92
|
-
className="inline-flex items-center space-x-2 text-ods-accent hover:text-ods-accent/80 transition-colors text-sm font-medium"
|
|
93
|
-
>
|
|
94
|
-
<ExternalLink className="w-4 h-4" />
|
|
95
|
-
<span>View on LinkedIn</span>
|
|
96
|
-
</a>
|
|
97
|
-
</div>
|
|
98
|
-
</LinkedInContainer>
|
|
99
|
-
);
|
|
100
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
|
-
import Image from '../../embed-shims/next-image';
|
|
5
|
-
import { useRichMarkdownRuntime } from './rich-markdown-runtime';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* In-article markdown image.
|
|
9
|
-
*
|
|
10
|
-
* Markdown images have unknown intrinsic dimensions — which is what broke the old
|
|
11
|
-
* approach: a fixed width/height made the Supabase loader `resize=cover` CROP tall
|
|
12
|
-
* screenshots, and `w-full` then blew them up to the column width. This renders
|
|
13
|
-
* through the lib's `next/image` shim (full Next.js Image Optimization on the hub,
|
|
14
|
-
* plain `<img>` everywhere else) and fixes both problems at the source:
|
|
15
|
-
*
|
|
16
|
-
* - `object-contain` flips the hub's Supabase loader (injected via
|
|
17
|
-
* `transformImageSrc` from the {@link RichMarkdownRuntimeProvider}) to
|
|
18
|
-
* `resize=contain` — aspect preserved, never cropped.
|
|
19
|
-
* - We learn the real aspect ratio from a tiny probe (the same
|
|
20
|
-
* `transformImageSrc` at 48px when available) and pass matching width/height
|
|
21
|
-
* so `next/image`'s aspect-ratio box is correct.
|
|
22
|
-
* - CSS (`max-w-full` + `max-h` + auto) caps the on-page size so a tall
|
|
23
|
-
* screenshot can't dominate the article, while the browser keeps the true
|
|
24
|
-
* aspect ratio.
|
|
25
|
-
*
|
|
26
|
-
* Embedders that don't pass `transformImageSrc` get an identity fallback — the
|
|
27
|
-
* raw `src` is used both for the probe and the display copy.
|
|
28
|
-
*/
|
|
29
|
-
const MAX_H_REM = 32; // ~512px on-page cap
|
|
30
|
-
const DISPLAY_W = 768; // logical width hint; the optimizer handles srcset + retina
|
|
31
|
-
|
|
32
|
-
export function MarkdownImage({ src, alt }: { src: string; alt?: string }) {
|
|
33
|
-
const { transformImageSrc } = useRichMarkdownRuntime();
|
|
34
|
-
const [ratio, setRatio] = useState<number | null>(null); // width / height
|
|
35
|
-
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
let cancelled = false;
|
|
38
|
-
// Reset on src change so a reused instance doesn't briefly size the new image with
|
|
39
|
-
// the previous image's ratio.
|
|
40
|
-
setRatio(null);
|
|
41
|
-
// Probe a tiny aspect-preserving variant so we learn the real ratio without
|
|
42
|
-
// downloading the full image; the display copy is then fetched once, at the right size.
|
|
43
|
-
// When no transformer is wired (embedders), fall back to the raw src.
|
|
44
|
-
const probeSrc = transformImageSrc(src, { width: 48, resize: 'contain', quality: 20 }) ?? src;
|
|
45
|
-
const probe = new window.Image();
|
|
46
|
-
probe.onload = () => {
|
|
47
|
-
if (!cancelled && probe.naturalWidth && probe.naturalHeight) {
|
|
48
|
-
setRatio(probe.naturalWidth / probe.naturalHeight);
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
probe.onerror = () => {
|
|
52
|
-
if (!cancelled) setRatio(1.5); // neutral fallback so we still render something
|
|
53
|
-
};
|
|
54
|
-
probe.src = probeSrc;
|
|
55
|
-
return () => {
|
|
56
|
-
cancelled = true;
|
|
57
|
-
};
|
|
58
|
-
}, [src, transformImageSrc]);
|
|
59
|
-
|
|
60
|
-
// Reserve a neutral box while probing so the layout doesn't jump when the image appears.
|
|
61
|
-
if (!ratio) {
|
|
62
|
-
return (
|
|
63
|
-
<span
|
|
64
|
-
className="mx-auto my-2 block w-full max-w-full animate-pulse rounded-lg bg-ods-card"
|
|
65
|
-
style={{ aspectRatio: '3 / 2', maxHeight: `${MAX_H_REM}rem` }}
|
|
66
|
-
aria-hidden
|
|
67
|
-
/>
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<Image
|
|
73
|
-
src={src}
|
|
74
|
-
alt={alt ?? 'No image available'}
|
|
75
|
-
width={DISPLAY_W}
|
|
76
|
-
height={Math.round(DISPLAY_W / ratio)}
|
|
77
|
-
sizes="(max-width: 768px) 100vw, 768px"
|
|
78
|
-
loading="lazy"
|
|
79
|
-
// `object-contain` → SupabaseOptimizedImage uses `resize=contain` (no crop).
|
|
80
|
-
// `w-full` (not `w-auto`) gives the img a definite width so it lays out + lazy-loads
|
|
81
|
-
// even before decode. Height follows via the aspect-ratio box; `maxHeight` caps tall
|
|
82
|
-
// images and `maxWidth = maxHeight × ratio` shrinks the width in lockstep so the box
|
|
83
|
-
// stays snug to the image (no letterbox bars).
|
|
84
|
-
className="mx-auto my-2 block h-auto w-full rounded-lg object-contain"
|
|
85
|
-
style={{ maxWidth: `calc(${MAX_H_REM}rem * ${ratio})`, maxHeight: `${MAX_H_REM}rem` }}
|
|
86
|
-
/>
|
|
87
|
-
);
|
|
88
|
-
}
|