@flamingo-stack/openframe-frontend-core 0.0.285 → 0.0.286-snapshot.20260618154650
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-3MCHAFHB.js +89 -0
- package/dist/chunk-3MCHAFHB.js.map +1 -0
- package/dist/chunk-3SDBXXDP.cjs +1594 -0
- package/dist/chunk-3SDBXXDP.cjs.map +1 -0
- package/dist/{chunk-IMNGGLKL.js → chunk-3ZXUQQL4.js} +2 -2
- package/dist/chunk-5KD3S25X.cjs +312 -0
- package/dist/chunk-5KD3S25X.cjs.map +1 -0
- package/dist/{chunk-44ZFLMYC.cjs → chunk-6AW25OS6.cjs} +30 -30
- package/dist/{chunk-44ZFLMYC.cjs.map → chunk-6AW25OS6.cjs.map} +1 -1
- package/dist/{chunk-XDI5JSWB.cjs → chunk-6CSW5TMS.cjs} +45 -41
- package/dist/chunk-6CSW5TMS.cjs.map +1 -0
- package/dist/chunk-7EYWERFT.js +1594 -0
- package/dist/chunk-7EYWERFT.js.map +1 -0
- package/dist/{chunk-XX7P4IL2.js → chunk-7NM7DEUK.js} +4 -143
- package/dist/chunk-7NM7DEUK.js.map +1 -0
- package/dist/{chunk-GVSTRN36.cjs → chunk-D3LEFMOA.cjs} +3 -3
- package/dist/{chunk-GVSTRN36.cjs.map → chunk-D3LEFMOA.cjs.map} +1 -1
- package/dist/chunk-D6RK5YXX.cjs +2439 -0
- package/dist/chunk-D6RK5YXX.cjs.map +1 -0
- package/dist/{chunk-VQW7CEGS.js → chunk-EFYXPR43.js} +1831 -3672
- package/dist/chunk-EFYXPR43.js.map +1 -0
- package/dist/{chunk-PVXCVPJD.cjs → chunk-EI4WALN2.cjs} +67 -1626
- package/dist/chunk-EI4WALN2.cjs.map +1 -0
- package/dist/{chunk-ZIODAAFY.cjs → chunk-EL5GCMPU.cjs} +20 -159
- package/dist/chunk-EL5GCMPU.cjs.map +1 -0
- package/dist/{chunk-53CFL7PH.js → chunk-GJDXIVEQ.js} +11 -7
- package/dist/{chunk-53CFL7PH.js.map → chunk-GJDXIVEQ.js.map} +1 -1
- package/dist/chunk-I6ZPGKZ2.cjs +151 -0
- package/dist/chunk-I6ZPGKZ2.cjs.map +1 -0
- package/dist/{chunk-P45Q4QCU.cjs → chunk-JQ4I743L.cjs} +19 -15
- package/dist/chunk-JQ4I743L.cjs.map +1 -0
- package/dist/{chunk-J66U3LPZ.cjs → chunk-KBKZYJRI.cjs} +33 -332
- package/dist/chunk-KBKZYJRI.cjs.map +1 -0
- package/dist/chunk-MV67MBV3.js +290 -0
- package/dist/chunk-MV67MBV3.js.map +1 -0
- package/dist/chunk-MWS25U4U.cjs +290 -0
- package/dist/chunk-MWS25U4U.cjs.map +1 -0
- package/dist/{chunk-USPATRSC.js → chunk-N6ZM5PYZ.js} +45 -344
- package/dist/chunk-N6ZM5PYZ.js.map +1 -0
- package/dist/chunk-ODR6A6FC.js +2439 -0
- package/dist/chunk-ODR6A6FC.js.map +1 -0
- package/dist/{chunk-DPKUSBQH.cjs → chunk-OXC72UIP.cjs} +1073 -2914
- package/dist/chunk-OXC72UIP.cjs.map +1 -0
- package/dist/{chunk-RGALV4UX.js → chunk-POKKCWKF.js} +3 -82
- package/dist/chunk-POKKCWKF.js.map +1 -0
- package/dist/{chunk-Z3V2SGQZ.js → chunk-R2KT5GDD.js} +324 -1883
- package/dist/chunk-R2KT5GDD.js.map +1 -0
- package/dist/{chunk-RJUGNL2A.cjs → chunk-RG6FNZUA.cjs} +73 -31
- package/dist/chunk-RG6FNZUA.cjs.map +1 -0
- package/dist/{chunk-23UOJUDF.js → chunk-RWCA2ZQK.js} +4 -4
- package/dist/chunk-TFSYSWPS.cjs +89 -0
- package/dist/chunk-TFSYSWPS.cjs.map +1 -0
- package/dist/{chunk-KNCDUWY2.js → chunk-TY2EB7VK.js} +65 -23
- package/dist/chunk-TY2EB7VK.js.map +1 -0
- package/dist/chunk-TYZEMPPH.js +312 -0
- package/dist/chunk-TYZEMPPH.js.map +1 -0
- package/dist/chunk-VRSXJ5QJ.js +1599 -0
- package/dist/chunk-VRSXJ5QJ.js.map +1 -0
- package/dist/chunk-Y4JNA4W6.cjs +1599 -0
- package/dist/chunk-Y4JNA4W6.cjs.map +1 -0
- package/dist/{chunk-ZSUNQJRO.cjs → chunk-YETA25JW.cjs} +2 -81
- package/dist/chunk-YETA25JW.cjs.map +1 -0
- package/dist/chunk-ZLN6SM2U.js +151 -0
- package/dist/chunk-ZLN6SM2U.js.map +1 -0
- package/dist/{chunk-O5N57CRV.js → chunk-ZYLQMCHW.js} +12 -8
- package/dist/{chunk-O5N57CRV.js.map → chunk-ZYLQMCHW.js.map} +1 -1
- package/dist/components/chat/chat-attachment-bar.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +12 -6
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +45 -39
- package/dist/components/contact/index.cjs +11 -7
- package/dist/components/contact/index.cjs.map +1 -1
- package/dist/components/contact/index.js +10 -6
- package/dist/components/docs/doc-navigation-context.d.ts +39 -0
- package/dist/components/docs/doc-navigation-context.d.ts.map +1 -0
- package/dist/components/docs/doc-viewer.d.ts +66 -0
- package/dist/components/docs/doc-viewer.d.ts.map +1 -0
- package/dist/components/docs/index.cjs +34 -0
- package/dist/components/docs/index.cjs.map +1 -0
- package/dist/components/docs/index.d.ts +8 -0
- package/dist/components/docs/index.d.ts.map +1 -0
- package/dist/components/docs/index.js +34 -0
- package/dist/components/docs/index.js.map +1 -0
- package/dist/components/docs/use-document-tree.d.ts +41 -0
- package/dist/components/docs/use-document-tree.d.ts.map +1 -0
- package/dist/components/docs/use-scroll-spy.d.ts +16 -0
- package/dist/components/docs/use-scroll-spy.d.ts.map +1 -0
- package/dist/components/embeds/embed-iframe.d.ts +29 -0
- package/dist/components/embeds/embed-iframe.d.ts.map +1 -0
- package/dist/components/embeds/figma-embed.d.ts +27 -0
- package/dist/components/embeds/figma-embed.d.ts.map +1 -0
- package/dist/components/embeds/google-sheets-viewer.d.ts +8 -0
- package/dist/components/embeds/google-sheets-viewer.d.ts.map +1 -0
- package/dist/components/embeds/index.cjs +37 -0
- package/dist/components/embeds/index.cjs.map +1 -0
- package/dist/components/embeds/index.d.ts +9 -0
- package/dist/components/embeds/index.d.ts.map +1 -0
- package/dist/components/embeds/index.js +37 -0
- package/dist/components/embeds/index.js.map +1 -0
- package/dist/components/embeds/pdf-viewer.d.ts +10 -0
- package/dist/components/embeds/pdf-viewer.d.ts.map +1 -0
- package/dist/components/faq/faq-section.d.ts.map +1 -1
- package/dist/components/faq/index.cjs +12 -8
- package/dist/components/faq/index.cjs.map +1 -1
- package/dist/components/faq/index.js +11 -7
- package/dist/components/faq-accordion.d.ts.map +1 -1
- package/dist/components/features/index.cjs +10 -6
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +9 -5
- package/dist/components/index.cjs +247 -1120
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +645 -1518
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +15 -6
- package/dist/components/navigation/index.cjs.map +1 -1
- package/dist/components/navigation/index.d.ts +2 -0
- package/dist/components/navigation/index.d.ts.map +1 -1
- package/dist/components/navigation/index.js +15 -6
- package/dist/components/navigation/multi-level-navigation.d.ts +28 -0
- package/dist/components/navigation/multi-level-navigation.d.ts.map +1 -0
- package/dist/components/onboarding-guides/index.cjs +34 -29
- package/dist/components/onboarding-guides/index.cjs.map +1 -1
- package/dist/components/onboarding-guides/index.js +13 -8
- package/dist/components/onboarding-guides/index.js.map +1 -1
- package/dist/components/related-content/index.cjs +12 -8
- package/dist/components/related-content/index.cjs.map +1 -1
- package/dist/components/related-content/index.js +11 -7
- package/dist/components/tickets/index.cjs +75 -70
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.js +16 -11
- package/dist/components/tickets/index.js.map +1 -1
- package/dist/components/ui/index.cjs +12 -6
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +65 -59
- package/dist/embed-shims/index.cjs +3 -3
- package/dist/embed-shims/index.cjs.map +1 -1
- package/dist/embed-shims/index.js +8 -8
- package/dist/hooks/index.cjs +6 -3
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +7 -4
- package/dist/index.cjs +68 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +263 -201
- package/dist/types/doc-source.cjs +4 -0
- package/dist/types/doc-source.cjs.map +1 -0
- package/dist/types/doc-source.d.ts +86 -0
- package/dist/types/doc-source.d.ts.map +1 -0
- package/dist/types/doc-source.js +3 -0
- package/dist/types/doc-source.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/marketing.d.ts +1 -1
- package/dist/types/marketing.d.ts.map +1 -1
- package/dist/utils/doc-path-utils.d.ts +24 -0
- package/dist/utils/doc-path-utils.d.ts.map +1 -0
- package/dist/utils/doc-tree-nav.d.ts +35 -0
- package/dist/utils/doc-tree-nav.d.ts.map +1 -0
- package/dist/utils/embed-url-converters.d.ts +16 -0
- package/dist/utils/embed-url-converters.d.ts.map +1 -0
- package/dist/utils/faq-anchor.d.ts +51 -0
- package/dist/utils/faq-anchor.d.ts.map +1 -0
- package/dist/utils/format.d.ts +9 -0
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/index.cjs +320 -1
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +9 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +296 -2
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/list-url.d.ts.map +1 -1
- package/dist/utils/markdown-section-extractor.d.ts +23 -0
- package/dist/utils/markdown-section-extractor.d.ts.map +1 -0
- package/dist/utils/markdown-to-plain.d.ts +32 -0
- package/dist/utils/markdown-to-plain.d.ts.map +1 -0
- package/dist/utils/tree-builder.d.ts +30 -0
- package/dist/utils/tree-builder.d.ts.map +1 -0
- package/package.json +19 -1
- package/src/components/chat/chat-attachment-bar.tsx +6 -11
- package/src/components/docs/doc-navigation-context.tsx +123 -0
- package/src/components/docs/doc-viewer.tsx +345 -0
- package/src/components/docs/index.ts +12 -0
- package/src/components/docs/use-document-tree.ts +427 -0
- package/src/components/docs/use-scroll-spy.ts +93 -0
- package/src/components/embeds/embed-iframe.tsx +103 -0
- package/src/components/embeds/figma-embed.tsx +147 -0
- package/src/components/embeds/google-sheets-viewer.tsx +54 -0
- package/src/components/embeds/index.ts +11 -0
- package/src/components/embeds/pdf-viewer.tsx +64 -0
- package/src/components/faq/faq-section.tsx +78 -26
- package/src/components/faq-accordion.tsx +8 -1
- package/src/components/index.ts +2 -0
- package/src/components/navigation/index.ts +5 -0
- package/src/components/navigation/multi-level-navigation.tsx +358 -0
- package/src/types/doc-source.ts +88 -0
- package/src/types/index.ts +4 -0
- package/src/types/marketing.ts +1 -0
- package/src/utils/doc-path-utils.ts +47 -0
- package/src/utils/doc-tree-nav.ts +111 -0
- package/src/utils/embed-url-converters.ts +77 -0
- package/src/utils/faq-anchor.ts +70 -0
- package/src/utils/format.ts +16 -0
- package/src/utils/index.ts +18 -0
- package/src/utils/list-url.ts +1 -0
- package/src/utils/markdown-section-extractor.ts +106 -0
- package/src/utils/markdown-to-plain.ts +69 -0
- package/src/utils/tree-builder.ts +114 -0
- package/dist/chunk-DPKUSBQH.cjs.map +0 -1
- package/dist/chunk-J66U3LPZ.cjs.map +0 -1
- package/dist/chunk-KNCDUWY2.js.map +0 -1
- package/dist/chunk-P45Q4QCU.cjs.map +0 -1
- package/dist/chunk-PVXCVPJD.cjs.map +0 -1
- package/dist/chunk-RGALV4UX.js.map +0 -1
- package/dist/chunk-RJUGNL2A.cjs.map +0 -1
- package/dist/chunk-USPATRSC.js.map +0 -1
- package/dist/chunk-VQW7CEGS.js.map +0 -1
- package/dist/chunk-XDI5JSWB.cjs.map +0 -1
- package/dist/chunk-XX7P4IL2.js.map +0 -1
- package/dist/chunk-Z3V2SGQZ.js.map +0 -1
- package/dist/chunk-ZIODAAFY.cjs.map +0 -1
- package/dist/chunk-ZSUNQJRO.cjs.map +0 -1
- /package/dist/{chunk-IMNGGLKL.js.map → chunk-3ZXUQQL4.js.map} +0 -0
- /package/dist/{chunk-23UOJUDF.js.map → chunk-RWCA2ZQK.js.map} +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { Button, ToggleGroup, ToggleGroupItem } from '../ui'
|
|
5
|
+
import { FigmaIcon } from '../icons-v2-generated'
|
|
6
|
+
import { ExternalLink, Play, LayoutGrid } from 'lucide-react'
|
|
7
|
+
import { toFigmaEmbedUrl, toFigmaOriginalUrl, isFigmaSlidesUrl } from '../../utils/embed-url-converters'
|
|
8
|
+
import { EmbedIframe } from './embed-iframe'
|
|
9
|
+
|
|
10
|
+
export interface FigmaEmbedProps {
|
|
11
|
+
/** Any Figma URL (design/file/proto/board/slides/deck) or an already-resolved embed URL. */
|
|
12
|
+
url: string
|
|
13
|
+
/** Heading shown above the embed. Defaults to "Figma Design". */
|
|
14
|
+
title?: string
|
|
15
|
+
/**
|
|
16
|
+
* iframe height (CSS value). The data-room document viewer omits it (full
|
|
17
|
+
* height, `calc(100vh - 250px)`); inline markdown passes e.g. `"70vh"` so the
|
|
18
|
+
* embed sits naturally inside article content.
|
|
19
|
+
*/
|
|
20
|
+
height?: string
|
|
21
|
+
/** iframe loading strategy. Defaults to `"lazy"`; the data-room viewer passes `"eager"`. */
|
|
22
|
+
loading?: 'eager' | 'lazy'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type SlidesView = 'present' | 'browse'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Two-state present/browse toggle for Figma Slides. `present` (default) uses
|
|
29
|
+
* Figma's deck viewer (full-bleed slide + `‹ n/N ›` nav bar + keyboard nav);
|
|
30
|
+
* `browse` uses the thumbnail-rail + zoom viewer.
|
|
31
|
+
*/
|
|
32
|
+
function SlidesViewToggle({
|
|
33
|
+
view,
|
|
34
|
+
onChange,
|
|
35
|
+
}: {
|
|
36
|
+
view: SlidesView
|
|
37
|
+
onChange: (v: SlidesView) => void
|
|
38
|
+
}) {
|
|
39
|
+
const options: { key: SlidesView; label: string; Icon: typeof Play }[] = [
|
|
40
|
+
{ key: 'present', label: 'Present', Icon: Play },
|
|
41
|
+
{ key: 'browse', label: 'Browse', Icon: LayoutGrid },
|
|
42
|
+
]
|
|
43
|
+
return (
|
|
44
|
+
<ToggleGroup
|
|
45
|
+
type="single"
|
|
46
|
+
value={view}
|
|
47
|
+
onValueChange={(v: string) => {
|
|
48
|
+
if (v && v !== view) onChange(v as SlidesView)
|
|
49
|
+
}}
|
|
50
|
+
aria-label="Figma slides view mode"
|
|
51
|
+
className="flex shrink-0 items-center gap-0.5 rounded-lg border border-ods-border bg-ods-card p-0.5"
|
|
52
|
+
>
|
|
53
|
+
{options.map(({ key, label, Icon }) => {
|
|
54
|
+
const active = view === key
|
|
55
|
+
return (
|
|
56
|
+
<ToggleGroupItem
|
|
57
|
+
key={key}
|
|
58
|
+
value={key}
|
|
59
|
+
aria-label={label}
|
|
60
|
+
className={`flex items-center gap-1.5 rounded-md px-2.5 py-1 text-sm font-medium transition-colors ${
|
|
61
|
+
active
|
|
62
|
+
? 'bg-ods-accent text-ods-text-on-accent'
|
|
63
|
+
: 'text-ods-text-secondary hover:text-ods-text-primary hover:bg-ods-bg-hover'
|
|
64
|
+
}`}
|
|
65
|
+
>
|
|
66
|
+
<Icon className="h-4 w-4 shrink-0" />
|
|
67
|
+
{label}
|
|
68
|
+
</ToggleGroupItem>
|
|
69
|
+
)
|
|
70
|
+
})}
|
|
71
|
+
</ToggleGroup>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Single source of truth for every Figma surface — the data-room document viewer
|
|
77
|
+
* and in-article markdown both render this. A header (icon + title + "Open in
|
|
78
|
+
* Figma") over an interactive Figma iframe, built from the canonical
|
|
79
|
+
* `toFigmaEmbedUrl` / `toFigmaOriginalUrl` converters + the shared `<EmbedIframe>`.
|
|
80
|
+
* Only height/loading differ per surface.
|
|
81
|
+
*
|
|
82
|
+
* For Slides decks, a present/browse toggle (default = present) lets viewers flip
|
|
83
|
+
* slides with Figma's native nav bar + keyboard, or switch to the thumbnail-rail
|
|
84
|
+
* browse view.
|
|
85
|
+
*/
|
|
86
|
+
export function FigmaEmbed({ url, title, height, loading = 'lazy' }: FigmaEmbedProps) {
|
|
87
|
+
const [view, setView] = useState<SlidesView>('present')
|
|
88
|
+
const isSlides = url ? isFigmaSlidesUrl(url) : false
|
|
89
|
+
const embedSrc = url ? toFigmaEmbedUrl(url, { slidesView: view }) : null
|
|
90
|
+
const originalUrl = (() => {
|
|
91
|
+
if (!url) return null
|
|
92
|
+
try {
|
|
93
|
+
const parsed = new URL(toFigmaOriginalUrl(url))
|
|
94
|
+
const host = parsed.hostname.toLowerCase()
|
|
95
|
+
const okHost = host === 'figma.com' || host.endsWith('.figma.com')
|
|
96
|
+
const okProtocol = parsed.protocol === 'https:' || parsed.protocol === 'http:'
|
|
97
|
+
return okHost && okProtocol ? parsed.toString() : null
|
|
98
|
+
} catch {
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
})()
|
|
102
|
+
const heading = title || 'Figma Design'
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="my-6 space-y-3">
|
|
106
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
107
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
108
|
+
<FigmaIcon className="w-5 h-5 shrink-0" />
|
|
109
|
+
<span className="font-sans text-base font-semibold text-ods-text-primary truncate">
|
|
110
|
+
{heading}
|
|
111
|
+
</span>
|
|
112
|
+
</div>
|
|
113
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
|
114
|
+
{isSlides && embedSrc && <SlidesViewToggle view={view} onChange={setView} />}
|
|
115
|
+
{originalUrl && (
|
|
116
|
+
<Button
|
|
117
|
+
variant="outline"
|
|
118
|
+
size="small-legacy"
|
|
119
|
+
href={originalUrl}
|
|
120
|
+
openInNewTab
|
|
121
|
+
leftIcon={<FigmaIcon className="w-4 h-4" />}
|
|
122
|
+
rightIcon={<ExternalLink className="w-4 h-4" />}
|
|
123
|
+
className="w-full sm:w-auto"
|
|
124
|
+
>
|
|
125
|
+
Open in Figma
|
|
126
|
+
</Button>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
{embedSrc ? (
|
|
131
|
+
<EmbedIframe
|
|
132
|
+
src={embedSrc}
|
|
133
|
+
title={heading}
|
|
134
|
+
allow="clipboard-write; clipboard-read; fullscreen"
|
|
135
|
+
loading={loading}
|
|
136
|
+
height={height}
|
|
137
|
+
allowFullScreen
|
|
138
|
+
/>
|
|
139
|
+
) : (
|
|
140
|
+
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
141
|
+
<FigmaIcon className="w-16 h-16 text-ods-text-secondary mb-4" />
|
|
142
|
+
<p className="text-ods-text-secondary">Figma URL not configured</p>
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { Button } from '../ui'
|
|
5
|
+
import { ExternalLink } from 'lucide-react'
|
|
6
|
+
import { GoogleSheetsIcon } from '../icons-v2-generated'
|
|
7
|
+
import { EmbedIframe } from './embed-iframe'
|
|
8
|
+
import { toGoogleSheetsEmbedUrl, toGoogleSheetsOriginalUrl } from '../../utils/embed-url-converters'
|
|
9
|
+
|
|
10
|
+
export interface GoogleSheetsViewerProps {
|
|
11
|
+
externalUrl: string
|
|
12
|
+
fileName?: string
|
|
13
|
+
height?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function GoogleSheetsViewer({ externalUrl, fileName, height }: GoogleSheetsViewerProps) {
|
|
17
|
+
const displayName = fileName || 'Google Sheet'
|
|
18
|
+
|
|
19
|
+
if (!externalUrl) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
22
|
+
<GoogleSheetsIcon className="w-16 h-16 text-ods-text-secondary mb-4" />
|
|
23
|
+
<p className="text-ods-text-secondary">Google Sheet URL not configured</p>
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="space-y-4">
|
|
30
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
31
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
32
|
+
<GoogleSheetsIcon className="w-5 h-5 shrink-0" />
|
|
33
|
+
<h2 className="text-xl font-semibold text-ods-text-primary truncate">{displayName}</h2>
|
|
34
|
+
</div>
|
|
35
|
+
<Button
|
|
36
|
+
variant="outline"
|
|
37
|
+
size="small-legacy"
|
|
38
|
+
href={toGoogleSheetsOriginalUrl(externalUrl)}
|
|
39
|
+
openInNewTab
|
|
40
|
+
leftIcon={<GoogleSheetsIcon className="w-4 h-4" />}
|
|
41
|
+
rightIcon={<ExternalLink className="w-4 h-4" />}
|
|
42
|
+
className="w-full sm:w-auto"
|
|
43
|
+
>
|
|
44
|
+
Open in Google Sheets
|
|
45
|
+
</Button>
|
|
46
|
+
</div>
|
|
47
|
+
<EmbedIframe
|
|
48
|
+
src={toGoogleSheetsEmbedUrl(externalUrl)}
|
|
49
|
+
title={displayName}
|
|
50
|
+
height={height}
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { EmbedIframe } from './embed-iframe'
|
|
2
|
+
export type { EmbedIframeProps } from './embed-iframe'
|
|
3
|
+
|
|
4
|
+
export { PdfViewer } from './pdf-viewer'
|
|
5
|
+
export type { PdfViewerProps } from './pdf-viewer'
|
|
6
|
+
|
|
7
|
+
export { GoogleSheetsViewer } from './google-sheets-viewer'
|
|
8
|
+
export type { GoogleSheetsViewerProps } from './google-sheets-viewer'
|
|
9
|
+
|
|
10
|
+
export { FigmaEmbed } from './figma-embed'
|
|
11
|
+
export type { FigmaEmbedProps } from './figma-embed'
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { Button } from '../ui'
|
|
5
|
+
import { Download, Eye } from 'lucide-react'
|
|
6
|
+
import { AdobePdfIcon } from '../icons-v2-generated'
|
|
7
|
+
import { EmbedIframe } from './embed-iframe'
|
|
8
|
+
|
|
9
|
+
export interface PdfViewerProps {
|
|
10
|
+
src: string
|
|
11
|
+
fileName?: string
|
|
12
|
+
onPreview?: () => void
|
|
13
|
+
onDownload?: () => void
|
|
14
|
+
height?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function PdfViewer({ src, fileName, onPreview, onDownload, height }: PdfViewerProps) {
|
|
18
|
+
const displayName = fileName || 'PDF Document'
|
|
19
|
+
|
|
20
|
+
if (!src) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
23
|
+
<AdobePdfIcon className="w-16 h-16 text-ods-text-secondary mb-4" />
|
|
24
|
+
<p className="text-ods-text-secondary">PDF file not available</p>
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="space-y-4">
|
|
31
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
32
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
33
|
+
<AdobePdfIcon className="w-5 h-5 shrink-0" />
|
|
34
|
+
<h2 className="text-xl font-semibold text-ods-text-primary truncate">{displayName}</h2>
|
|
35
|
+
</div>
|
|
36
|
+
<div className="flex items-center gap-2 w-full sm:w-auto">
|
|
37
|
+
<Button
|
|
38
|
+
variant="outline"
|
|
39
|
+
size="small-legacy"
|
|
40
|
+
href={onPreview ? undefined : src}
|
|
41
|
+
openInNewTab={!onPreview}
|
|
42
|
+
onClick={onPreview}
|
|
43
|
+
leftIcon={<Eye className="w-4 h-4" />}
|
|
44
|
+
className="flex-1 sm:flex-initial"
|
|
45
|
+
>
|
|
46
|
+
Preview
|
|
47
|
+
</Button>
|
|
48
|
+
<Button
|
|
49
|
+
variant="outline"
|
|
50
|
+
size="small-legacy"
|
|
51
|
+
href={onDownload ? undefined : src}
|
|
52
|
+
openInNewTab={!onDownload}
|
|
53
|
+
onClick={onDownload}
|
|
54
|
+
leftIcon={<Download className="w-4 h-4" />}
|
|
55
|
+
className="flex-1 sm:flex-initial"
|
|
56
|
+
>
|
|
57
|
+
Download
|
|
58
|
+
</Button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<EmbedIframe src={src} title={displayName} height={height} />
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -7,6 +7,7 @@ import { useSelfFetch } from '../../hooks/use-self-fetch'
|
|
|
7
7
|
import { buildSuggestionUrl } from '../../utils/suggestion-url'
|
|
8
8
|
import { serializeJsonLd } from '../../utils/common'
|
|
9
9
|
import { scrollElementIntoView } from '../../utils/scroll-into-view'
|
|
10
|
+
import { faqSectionSlug, faqItemAnchor, parseFaqHash, type FaqHashTarget } from '../../utils/faq-anchor'
|
|
10
11
|
import { cn } from '../../utils/cn'
|
|
11
12
|
import { buildFaqJsonLdFromFaqs, type FaqSchemaOptions } from './json-ld'
|
|
12
13
|
import { SECTION_HEADING_CLASS } from '../layout/page-heading'
|
|
@@ -59,19 +60,6 @@ function buildFaqsUrl(
|
|
|
59
60
|
return buildSuggestionUrl('/api/faqs', { apiBaseUrl, entityType, entityId, count: minResults })
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
/** Stable, URL-safe anchor id for a category. Prefixed so it can't collide
|
|
63
|
-
* with other in-page ids, and so a bare numeric/blank section still yields a
|
|
64
|
-
* valid id. */
|
|
65
|
-
function sectionSlug(section: string): string {
|
|
66
|
-
return (
|
|
67
|
-
'faq-' +
|
|
68
|
-
section
|
|
69
|
-
.toLowerCase()
|
|
70
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
71
|
-
.replace(/^-+|-+$/g, '')
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
63
|
interface FaqGroup {
|
|
76
64
|
/** null → the uncategorized bucket: no heading, no jump pill, rendered last. */
|
|
77
65
|
section: string | null
|
|
@@ -98,7 +86,7 @@ function groupFaqsBySection(faqs: Faq[]): FaqGroup[] {
|
|
|
98
86
|
}
|
|
99
87
|
let group = byName.get(name)
|
|
100
88
|
if (!group) {
|
|
101
|
-
group = { section: name, slug:
|
|
89
|
+
group = { section: name, slug: faqSectionSlug(name), items: [] }
|
|
102
90
|
byName.set(name, group)
|
|
103
91
|
order.push(name)
|
|
104
92
|
}
|
|
@@ -113,6 +101,12 @@ function groupFaqsBySection(faqs: Faq[]): FaqGroup[] {
|
|
|
113
101
|
* scroll uses, so a category jump lands below the header, not under it. */
|
|
114
102
|
const FAQ_NAV_HEADER_OFFSET = 96
|
|
115
103
|
|
|
104
|
+
/** Map key for the uncategorized bucket — `group.slug` is null for it, so
|
|
105
|
+
* every per-group map (default-open ids, accordion keys) uses this sentinel
|
|
106
|
+
* to keep the lookup typed. */
|
|
107
|
+
const UNCATEGORIZED_KEY = '__uncategorized__'
|
|
108
|
+
const groupKey = (g: FaqGroup): string => g.slug ?? UNCATEGORIZED_KEY
|
|
109
|
+
|
|
116
110
|
/**
|
|
117
111
|
* Grouped FAQ layout: a category jump-nav above stacked `<h2>` category
|
|
118
112
|
* sections (each its own accordion). Isolated into its own component so the
|
|
@@ -177,6 +171,54 @@ function GroupedFaqList({
|
|
|
177
171
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
178
172
|
}, [slugKey])
|
|
179
173
|
|
|
174
|
+
// ─── Hash dispatch — `/faqs#faq-item-<id>` or `/faqs#faq-<section-slug>` ──
|
|
175
|
+
// Tracks the current hash so:
|
|
176
|
+
// 1. an item-kind hash seeds `defaultOpenIds` on the matching accordion
|
|
177
|
+
// (auto-expands the cited question);
|
|
178
|
+
// 2. either kind triggers the cancellation-proof tween scroll with the
|
|
179
|
+
// sticky-header offset (native browser hash scroll runs once, ignores
|
|
180
|
+
// our offset — re-running the tween puts the target in the right spot).
|
|
181
|
+
// Listens to `hashchange` so back/forward replays the same behavior. SSR-
|
|
182
|
+
// safe: initial null state matches the server render; the first effect
|
|
183
|
+
// tick on the client updates it.
|
|
184
|
+
const [hashTarget, setHashTarget] = useState<FaqHashTarget | null>(null)
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
const refresh = () => setHashTarget(parseFaqHash(window.location.hash))
|
|
187
|
+
refresh()
|
|
188
|
+
window.addEventListener('hashchange', refresh)
|
|
189
|
+
return () => window.removeEventListener('hashchange', refresh)
|
|
190
|
+
}, [])
|
|
191
|
+
|
|
192
|
+
// Per-group default-open set when the hash points at an item. The map key
|
|
193
|
+
// matches `groupKey(group)` so the render-time lookup is O(1) per group.
|
|
194
|
+
const defaultOpenByGroupKey = useMemo(() => {
|
|
195
|
+
if (hashTarget?.kind !== 'item') return null
|
|
196
|
+
const targetId = hashTarget.rawId
|
|
197
|
+
const result = new Map<string, (string | number)[]>()
|
|
198
|
+
for (const group of groups) {
|
|
199
|
+
const hit = group.items.find((i) => String(i.id) === targetId)
|
|
200
|
+
if (hit) result.set(groupKey(group), [hit.id])
|
|
201
|
+
}
|
|
202
|
+
return result.size > 0 ? result : null
|
|
203
|
+
}, [groups, hashTarget])
|
|
204
|
+
|
|
205
|
+
// Accordion is uncontrolled — `defaultOpenIds` is only consumed at mount,
|
|
206
|
+
// so a new item hash needs a remount to honor it. Keying off the item-id
|
|
207
|
+
// suffix triggers exactly the remount we need (and stays stable when the
|
|
208
|
+
// hash points at a section, so category navigation never disturbs the
|
|
209
|
+
// accordion's open state).
|
|
210
|
+
const accordionKeySuffix =
|
|
211
|
+
hashTarget?.kind === 'item' ? `item:${hashTarget.rawId}` : 'default'
|
|
212
|
+
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
if (!hashTarget) return
|
|
215
|
+
const elId =
|
|
216
|
+
hashTarget.kind === 'item' ? faqItemAnchor(hashTarget.rawId) : hashTarget.slug
|
|
217
|
+
const el = document.getElementById(elId)
|
|
218
|
+
if (el) scrollElementIntoView(el, { headerOffset: FAQ_NAV_HEADER_OFFSET })
|
|
219
|
+
if (hashTarget.kind === 'section') setActiveSlug(hashTarget.slug)
|
|
220
|
+
}, [hashTarget])
|
|
221
|
+
|
|
180
222
|
const handleJump = useCallback(
|
|
181
223
|
(e: React.MouseEvent<HTMLAnchorElement>, slug: string) => {
|
|
182
224
|
e.preventDefault()
|
|
@@ -215,18 +257,28 @@ function GroupedFaqList({
|
|
|
215
257
|
</nav>
|
|
216
258
|
)}
|
|
217
259
|
<div className="space-y-10">
|
|
218
|
-
{groups.map((group) =>
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
260
|
+
{groups.map((group) => {
|
|
261
|
+
const key = groupKey(group)
|
|
262
|
+
return (
|
|
263
|
+
<section
|
|
264
|
+
key={key}
|
|
265
|
+
id={group.slug ?? undefined}
|
|
266
|
+
className="scroll-mt-24 space-y-4"
|
|
267
|
+
>
|
|
268
|
+
{group.section && (
|
|
269
|
+
<CategoryHeading className={SECTION_HEADING_CLASS}>{group.section}</CategoryHeading>
|
|
270
|
+
)}
|
|
271
|
+
<FaqAccordion
|
|
272
|
+
// Re-key on item-hash changes so the remount picks up the new
|
|
273
|
+
// `defaultOpenIds` (the accordion is uncontrolled). Stable for
|
|
274
|
+
// section hashes — category navigation doesn't disturb state.
|
|
275
|
+
key={`${key}:${accordionKeySuffix}`}
|
|
276
|
+
items={group.items}
|
|
277
|
+
defaultOpenIds={defaultOpenByGroupKey?.get(key)}
|
|
278
|
+
/>
|
|
279
|
+
</section>
|
|
280
|
+
)
|
|
281
|
+
})}
|
|
230
282
|
</div>
|
|
231
283
|
</div>
|
|
232
284
|
)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import React, { useRef, useState, useEffect, useCallback } from 'react'
|
|
4
4
|
import { ChevronButton } from './ui/chevron-button'
|
|
5
5
|
import { cn } from "../utils/cn"
|
|
6
|
+
import { faqItemAnchor } from "../utils/faq-anchor"
|
|
6
7
|
|
|
7
8
|
export interface FaqItem {
|
|
8
9
|
id: number | string
|
|
@@ -60,7 +61,13 @@ export function FaqAccordion({ items, defaultOpenIds = [] }: FaqAccordionProps)
|
|
|
60
61
|
return (
|
|
61
62
|
<div
|
|
62
63
|
key={item.id}
|
|
63
|
-
|
|
64
|
+
// Per-row anchor — chat citation chips (`/faqs#faq-item-<id>`) land
|
|
65
|
+
// here via native browser hash scroll AND via `FaqSection`'s tween
|
|
66
|
+
// dispatch. `scroll-mt-24` keeps the row header below the 96px
|
|
67
|
+
// sticky nav offset (matches `<section>`'s scroll-margin for
|
|
68
|
+
// category anchors).
|
|
69
|
+
id={faqItemAnchor(item.id)}
|
|
70
|
+
className={cn('group scroll-mt-24 transition-colors hover:bg-[#1E1E1E]', isOpen ? 'bg-ods-bg' : 'bg-transparent')}
|
|
64
71
|
>
|
|
65
72
|
{/* Header */}
|
|
66
73
|
<div
|
package/src/components/index.ts
CHANGED
|
@@ -56,6 +56,11 @@ export type { HeaderGlobalSearchProps } from './header-global-search'
|
|
|
56
56
|
export { HeaderOrganizationFilter } from './header-organization-filter'
|
|
57
57
|
export type { HeaderOrganizationFilterOrganization, HeaderOrganizationFilterProps } from './header-organization-filter'
|
|
58
58
|
|
|
59
|
+
// Multi-level navigation — sidebar + mobile dropdown for the doc-viewer
|
|
60
|
+
// (DocViewer / DocSourceViewer) and any other tree-shaped navigation surface.
|
|
61
|
+
export { MultiLevelNavigation, MobileNavigationDropdown } from './multi-level-navigation'
|
|
62
|
+
export type { NavigationNode } from './multi-level-navigation'
|
|
63
|
+
|
|
59
64
|
// Re-export types from navigation types
|
|
60
65
|
export type {
|
|
61
66
|
MobileNavConfig, NavigationItem, NavigationSidebarConfig,
|