@flamingo-stack/openframe-frontend-core 0.0.285 → 0.0.286
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-IMNGGLKL.js → chunk-3ZXUQQL4.js} +2 -2
- package/dist/{chunk-44ZFLMYC.cjs → chunk-4WACBTZU.cjs} +30 -30
- package/dist/{chunk-44ZFLMYC.cjs.map → chunk-4WACBTZU.cjs.map} +1 -1
- package/dist/{chunk-23UOJUDF.js → chunk-5BTZOVDQ.js} +4 -4
- package/dist/chunk-5KD3S25X.cjs +312 -0
- package/dist/chunk-5KD3S25X.cjs.map +1 -0
- package/dist/chunk-6N26CURS.cjs +2417 -0
- package/dist/chunk-6N26CURS.cjs.map +1 -0
- package/dist/{chunk-XX7P4IL2.js → chunk-7NM7DEUK.js} +4 -143
- package/dist/chunk-7NM7DEUK.js.map +1 -0
- package/dist/chunk-BBZ7AX5H.cjs +290 -0
- package/dist/chunk-BBZ7AX5H.cjs.map +1 -0
- package/dist/chunk-C6SCWXDP.js +290 -0
- package/dist/chunk-C6SCWXDP.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-P45Q4QCU.cjs → chunk-D5YY5U6J.cjs} +19 -15
- package/dist/chunk-D5YY5U6J.cjs.map +1 -0
- package/dist/chunk-DDAT4RKX.js +2417 -0
- package/dist/chunk-DDAT4RKX.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-VQW7CEGS.js → chunk-FSBDVT6R.js} +1831 -3672
- package/dist/chunk-FSBDVT6R.js.map +1 -0
- package/dist/{chunk-DPKUSBQH.cjs → chunk-GDF2R2ER.cjs} +1073 -2914
- package/dist/chunk-GDF2R2ER.cjs.map +1 -0
- package/dist/chunk-HLQW7MWJ.cjs +1594 -0
- package/dist/chunk-HLQW7MWJ.cjs.map +1 -0
- package/dist/chunk-I6ZPGKZ2.cjs +151 -0
- package/dist/chunk-I6ZPGKZ2.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-USPATRSC.js → chunk-N6ZM5PYZ.js} +45 -344
- package/dist/chunk-N6ZM5PYZ.js.map +1 -0
- package/dist/{chunk-RGALV4UX.js → chunk-POKKCWKF.js} +3 -82
- package/dist/chunk-POKKCWKF.js.map +1 -0
- package/dist/{chunk-XDI5JSWB.cjs → chunk-QCKN37OP.cjs} +45 -41
- package/dist/chunk-QCKN37OP.cjs.map +1 -0
- package/dist/{chunk-KNCDUWY2.js → chunk-QF2X6PTD.js} +10 -6
- package/dist/{chunk-KNCDUWY2.js.map → chunk-QF2X6PTD.js.map} +1 -1
- package/dist/{chunk-Z3V2SGQZ.js → chunk-R2KT5GDD.js} +324 -1883
- package/dist/chunk-R2KT5GDD.js.map +1 -0
- package/dist/{chunk-RJUGNL2A.cjs → chunk-RQ6RTBKF.cjs} +16 -12
- package/dist/chunk-RQ6RTBKF.cjs.map +1 -0
- package/dist/chunk-TFSYSWPS.cjs +89 -0
- package/dist/chunk-TFSYSWPS.cjs.map +1 -0
- package/dist/chunk-TYZEMPPH.js +312 -0
- package/dist/chunk-TYZEMPPH.js.map +1 -0
- package/dist/{chunk-O5N57CRV.js → chunk-VCQ3CTYK.js} +12 -8
- package/dist/{chunk-O5N57CRV.js.map → chunk-VCQ3CTYK.js.map} +1 -1
- package/dist/chunk-VRSXJ5QJ.js +1599 -0
- package/dist/chunk-VRSXJ5QJ.js.map +1 -0
- package/dist/{chunk-53CFL7PH.js → chunk-XQL4WDML.js} +11 -7
- package/dist/{chunk-53CFL7PH.js.map → chunk-XQL4WDML.js.map} +1 -1
- 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-YVB3VDIQ.js +1594 -0
- package/dist/chunk-YVB3VDIQ.js.map +1 -0
- package/dist/chunk-ZLN6SM2U.js +151 -0
- package/dist/chunk-ZLN6SM2U.js.map +1 -0
- 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/index.cjs +12 -8
- package/dist/components/faq/index.cjs.map +1 -1
- package/dist/components/faq/index.js +11 -7
- 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 +62 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +257 -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/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/format.d.ts +9 -0
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/index.cjs +297 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +7 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +276 -1
- package/dist/utils/index.js.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/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/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/format.ts +16 -0
- package/src/utils/index.ts +11 -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-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-5BTZOVDQ.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
|
+
}
|
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,
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { ChevronRight, ChevronDown, ChevronUp, FileText, Folder, FolderOpen } from 'lucide-react'
|
|
4
|
+
import { cn } from '../../utils/cn'
|
|
5
|
+
import { DEFAULT_FOLDER_INDEX_FILE as CANONICAL_FOLDER_INDEX_FILE } from '../../utils/doc-tree-nav'
|
|
6
|
+
|
|
7
|
+
export interface NavigationNode {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
path: string
|
|
11
|
+
type: 'file' | 'folder'
|
|
12
|
+
hasReadme?: boolean
|
|
13
|
+
children?: NavigationNode[]
|
|
14
|
+
slug?: string
|
|
15
|
+
// Mirror DocNode's extra optional fields so a DocNode tree is structurally
|
|
16
|
+
// assignable to NavigationNode[] without an `as` cast. NavigationNode
|
|
17
|
+
// intentionally never reads these — they're carried for type-compat only.
|
|
18
|
+
documentType?: 'markdown' | 'pdf' | 'google_sheet' | 'figma' | 'file'
|
|
19
|
+
sortOrder?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface MultiLevelNavigationProps {
|
|
23
|
+
nodes: NavigationNode[]
|
|
24
|
+
selectedPath: string
|
|
25
|
+
expandedNodes: Set<string>
|
|
26
|
+
onNodeClick: (node: NavigationNode) => void
|
|
27
|
+
onToggleExpand?: (nodeId: string) => void
|
|
28
|
+
isLoading?: boolean
|
|
29
|
+
className?: string
|
|
30
|
+
/** Folder-index filename (default `'README.md'`, case-insensitive). When the
|
|
31
|
+
* selectedPath is a folder with this file, the visual ribbon moves to the
|
|
32
|
+
* child file. */
|
|
33
|
+
folderIndexFile?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// SSOT lives in `doc-tree-nav` (canonical case `'README.md'`). The visual
|
|
37
|
+
// comparator below lowercases both sides, so re-exporting the canonical
|
|
38
|
+
// constant under the same local name preserves call-site syntax without
|
|
39
|
+
// drifting from the SSOT.
|
|
40
|
+
const DEFAULT_FOLDER_INDEX_FILE = CANONICAL_FOLDER_INDEX_FILE
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Compute the visual "selected" state for a navigation node.
|
|
44
|
+
*
|
|
45
|
+
* Folder-with-README convention: when `selectedPath` is a folder that has a
|
|
46
|
+
* README, the FOLDER doesn't get the ribbon — its README child does. The folder
|
|
47
|
+
* is just a container; the README is the actual document being viewed.
|
|
48
|
+
*
|
|
49
|
+
* Mirror case: a README file whose parent folder is the `selectedPath` gets the
|
|
50
|
+
* visual selection ribbon (implicit). This handles both the auto-select-first-
|
|
51
|
+
* folder landing path AND the user-clicks-folder case.
|
|
52
|
+
*/
|
|
53
|
+
function isNodeVisuallySelected(
|
|
54
|
+
node: NavigationNode,
|
|
55
|
+
selectedPath: string,
|
|
56
|
+
folderIndexFile: string = DEFAULT_FOLDER_INDEX_FILE,
|
|
57
|
+
): boolean {
|
|
58
|
+
const FOLDER_INDEX_FILE = folderIndexFile.toLowerCase()
|
|
59
|
+
// Path comparisons keyed on `node.path` (raw, includes `.md`). Do NOT use
|
|
60
|
+
// `node.name` — the tree-builder runs it through `formatDocName` which strips
|
|
61
|
+
// the `.md` extension, so `node.name.toLowerCase() === 'readme.md'` is always
|
|
62
|
+
// false.
|
|
63
|
+
if (selectedPath === node.path) {
|
|
64
|
+
// Explicit match — but folder-with-README defers to its README child.
|
|
65
|
+
return !(node.type === 'folder' && node.hasReadme)
|
|
66
|
+
}
|
|
67
|
+
if (node.type !== 'file') return false
|
|
68
|
+
const pathLower = node.path.toLowerCase()
|
|
69
|
+
// Implicit: README child whose parent folder is the selectedPath.
|
|
70
|
+
if (
|
|
71
|
+
selectedPath !== '' &&
|
|
72
|
+
pathLower === `${selectedPath.toLowerCase()}/${FOLDER_INDEX_FILE}`
|
|
73
|
+
) {
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
// Implicit: ROOT README on the landing page (selectedPath === '' means
|
|
77
|
+
// "no explicit doc selected" — the viewer falls back to the root folder-
|
|
78
|
+
// index file, so that file IS the active document).
|
|
79
|
+
if (selectedPath === '' && pathLower === FOLDER_INDEX_FILE) {
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function MultiLevelNavigation({
|
|
86
|
+
nodes,
|
|
87
|
+
selectedPath,
|
|
88
|
+
expandedNodes,
|
|
89
|
+
onNodeClick,
|
|
90
|
+
onToggleExpand,
|
|
91
|
+
isLoading,
|
|
92
|
+
className,
|
|
93
|
+
folderIndexFile = DEFAULT_FOLDER_INDEX_FILE,
|
|
94
|
+
}: MultiLevelNavigationProps) {
|
|
95
|
+
if (isLoading) {
|
|
96
|
+
return (
|
|
97
|
+
<div className={cn("space-y-2", className)}>
|
|
98
|
+
{[1, 2, 3, 4].map((i) => (
|
|
99
|
+
<div key={i} className="p-3 bg-ods-skeleton rounded-lg animate-pulse h-12" />
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div className={cn("space-y-2", className)} role="list" aria-label="Navigation list">
|
|
107
|
+
{nodes.map(node => (
|
|
108
|
+
<NavigationItem
|
|
109
|
+
key={node.id}
|
|
110
|
+
node={node}
|
|
111
|
+
selectedPath={selectedPath}
|
|
112
|
+
expandedNodes={expandedNodes}
|
|
113
|
+
onNodeClick={onNodeClick}
|
|
114
|
+
onToggleExpand={onToggleExpand}
|
|
115
|
+
level={0}
|
|
116
|
+
folderIndexFile={folderIndexFile}
|
|
117
|
+
/>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface NavigationItemProps {
|
|
124
|
+
node: NavigationNode
|
|
125
|
+
selectedPath: string
|
|
126
|
+
expandedNodes: Set<string>
|
|
127
|
+
onNodeClick: (node: NavigationNode) => void
|
|
128
|
+
onToggleExpand?: (nodeId: string) => void
|
|
129
|
+
level: number
|
|
130
|
+
folderIndexFile: string
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function NavigationItem({
|
|
134
|
+
node,
|
|
135
|
+
selectedPath,
|
|
136
|
+
expandedNodes,
|
|
137
|
+
onNodeClick,
|
|
138
|
+
onToggleExpand,
|
|
139
|
+
level,
|
|
140
|
+
folderIndexFile,
|
|
141
|
+
}: NavigationItemProps) {
|
|
142
|
+
const isExpanded = expandedNodes.has(node.id)
|
|
143
|
+
const isSelected = isNodeVisuallySelected(node, selectedPath, folderIndexFile)
|
|
144
|
+
const hasChildren = node.children && node.children.length > 0
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div className="space-y-1" role="listitem">
|
|
148
|
+
<div className={cn("px-2", level > 0 && "ml-4")}>
|
|
149
|
+
<div
|
|
150
|
+
className={cn(
|
|
151
|
+
"w-full rounded-lg transition-all duration-150 border relative",
|
|
152
|
+
isSelected
|
|
153
|
+
? "bg-ods-border border-ods-border"
|
|
154
|
+
: "bg-ods-card border-ods-border"
|
|
155
|
+
)}
|
|
156
|
+
>
|
|
157
|
+
<div className="flex items-center relative">
|
|
158
|
+
<button
|
|
159
|
+
className={cn(
|
|
160
|
+
"w-full flex items-center h-12 px-2 rounded-lg text-[16px] font-medium font-['DM_Sans'] transition-all duration-150 leading-[1.33em] text-ods-text-primary",
|
|
161
|
+
!isSelected && "hover:bg-ods-bg-secondary",
|
|
162
|
+
hasChildren && "pr-12"
|
|
163
|
+
)}
|
|
164
|
+
onClick={() => onNodeClick(node)}
|
|
165
|
+
aria-label={`${isSelected ? 'Selected' : 'Select'} ${node.name}`}
|
|
166
|
+
>
|
|
167
|
+
<span className="flex-shrink-0 mr-2">
|
|
168
|
+
{node.type === 'folder' ? (
|
|
169
|
+
isExpanded ? <FolderOpen className="h-4 w-4" /> : <Folder className="h-4 w-4" />
|
|
170
|
+
) : (
|
|
171
|
+
<FileText className="h-4 w-4" />
|
|
172
|
+
)}
|
|
173
|
+
</span>
|
|
174
|
+
<span className="text-left truncate flex-1 min-w-0">
|
|
175
|
+
{node.name.endsWith('.md') ? node.name.replace('.md', '') : node.name}
|
|
176
|
+
</span>
|
|
177
|
+
{node.type === 'folder' && node.hasReadme && (
|
|
178
|
+
<span className="text-[10px] bg-ods-bg-secondary text-ods-text-tertiary px-1.5 py-0.5 rounded mr-2">
|
|
179
|
+
README
|
|
180
|
+
</span>
|
|
181
|
+
)}
|
|
182
|
+
</button>
|
|
183
|
+
|
|
184
|
+
{hasChildren && (
|
|
185
|
+
<button
|
|
186
|
+
className="absolute right-0 top-0 flex items-center justify-center w-12 h-12 text-ods-text-secondary hover:text-ods-text-primary transition-colors duration-150"
|
|
187
|
+
onClick={(e) => {
|
|
188
|
+
e.stopPropagation()
|
|
189
|
+
if (onToggleExpand) {
|
|
190
|
+
onToggleExpand(node.id)
|
|
191
|
+
} else {
|
|
192
|
+
onNodeClick(node)
|
|
193
|
+
}
|
|
194
|
+
}}
|
|
195
|
+
aria-label={`${isExpanded ? 'Collapse' : 'Expand'} ${node.name}`}
|
|
196
|
+
>
|
|
197
|
+
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
|
198
|
+
</button>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{isSelected && (
|
|
203
|
+
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-1 h-[calc(100%-8px)] bg-ods-accent rounded-l" />
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{hasChildren && isExpanded && (
|
|
209
|
+
<div className="space-y-1">
|
|
210
|
+
{node.children!.map(child => (
|
|
211
|
+
<NavigationItem
|
|
212
|
+
key={child.id}
|
|
213
|
+
node={child}
|
|
214
|
+
selectedPath={selectedPath}
|
|
215
|
+
expandedNodes={expandedNodes}
|
|
216
|
+
onNodeClick={onNodeClick}
|
|
217
|
+
onToggleExpand={onToggleExpand}
|
|
218
|
+
level={level + 1}
|
|
219
|
+
folderIndexFile={folderIndexFile}
|
|
220
|
+
/>
|
|
221
|
+
))}
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function MobileNavigationDropdown({
|
|
229
|
+
nodes,
|
|
230
|
+
selectedPath,
|
|
231
|
+
expandedNodes,
|
|
232
|
+
onNodeClick,
|
|
233
|
+
onToggleExpand,
|
|
234
|
+
isLoading,
|
|
235
|
+
className,
|
|
236
|
+
folderIndexFile = DEFAULT_FOLDER_INDEX_FILE,
|
|
237
|
+
}: MultiLevelNavigationProps) {
|
|
238
|
+
if (isLoading) {
|
|
239
|
+
return (
|
|
240
|
+
<div className={cn("space-y-2", className)}>
|
|
241
|
+
{[1, 2, 3].map((i) => (
|
|
242
|
+
<div key={i} className="p-3 bg-ods-skeleton rounded-lg animate-pulse h-10" />
|
|
243
|
+
))}
|
|
244
|
+
</div>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<div className={cn("space-y-2", className)}>
|
|
250
|
+
{nodes.map(node => (
|
|
251
|
+
<MobileNavigationItem
|
|
252
|
+
key={node.id}
|
|
253
|
+
node={node}
|
|
254
|
+
selectedPath={selectedPath}
|
|
255
|
+
expandedNodes={expandedNodes}
|
|
256
|
+
onNodeClick={onNodeClick}
|
|
257
|
+
onToggleExpand={onToggleExpand}
|
|
258
|
+
level={0}
|
|
259
|
+
folderIndexFile={folderIndexFile}
|
|
260
|
+
/>
|
|
261
|
+
))}
|
|
262
|
+
</div>
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function MobileNavigationItem({
|
|
267
|
+
node,
|
|
268
|
+
selectedPath,
|
|
269
|
+
expandedNodes,
|
|
270
|
+
onNodeClick,
|
|
271
|
+
onToggleExpand,
|
|
272
|
+
level,
|
|
273
|
+
folderIndexFile,
|
|
274
|
+
}: NavigationItemProps) {
|
|
275
|
+
const isExpanded = expandedNodes.has(node.id)
|
|
276
|
+
const isSelected = isNodeVisuallySelected(node, selectedPath, folderIndexFile)
|
|
277
|
+
const hasChildren = node.children && node.children.length > 0
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<div className="space-y-1">
|
|
281
|
+
<div className={cn("px-2", level > 0 && "ml-3")}>
|
|
282
|
+
<div
|
|
283
|
+
className={cn(
|
|
284
|
+
"w-full rounded-lg transition-all duration-150 border relative",
|
|
285
|
+
isSelected
|
|
286
|
+
? "bg-ods-border border-ods-border"
|
|
287
|
+
: "bg-ods-card border-ods-border"
|
|
288
|
+
)}
|
|
289
|
+
>
|
|
290
|
+
<div className="flex items-center relative">
|
|
291
|
+
<button
|
|
292
|
+
className={cn(
|
|
293
|
+
"w-full flex items-center h-11 px-2 rounded-lg text-[15px] font-medium font-['DM_Sans'] transition-all duration-150 leading-[1.33em] text-ods-text-primary",
|
|
294
|
+
!isSelected && "hover:bg-ods-bg-secondary",
|
|
295
|
+
hasChildren && "pr-11"
|
|
296
|
+
)}
|
|
297
|
+
onClick={() => onNodeClick(node)}
|
|
298
|
+
>
|
|
299
|
+
<span className="flex-shrink-0 mr-2">
|
|
300
|
+
{node.type === 'folder' ? (
|
|
301
|
+
isExpanded ? <FolderOpen className="h-4 w-4" /> : <Folder className="h-4 w-4" />
|
|
302
|
+
) : (
|
|
303
|
+
<FileText className="h-4 w-4" />
|
|
304
|
+
)}
|
|
305
|
+
</span>
|
|
306
|
+
<span className="text-left truncate flex-1 min-w-0 text-sm">
|
|
307
|
+
{node.name.endsWith('.md') ? node.name.replace('.md', '') : node.name}
|
|
308
|
+
</span>
|
|
309
|
+
{node.type === 'folder' && node.hasReadme && (
|
|
310
|
+
<span className="text-[10px] bg-ods-bg-secondary text-ods-text-tertiary px-1.5 py-0.5 rounded mr-2">
|
|
311
|
+
README
|
|
312
|
+
</span>
|
|
313
|
+
)}
|
|
314
|
+
</button>
|
|
315
|
+
|
|
316
|
+
{hasChildren && (
|
|
317
|
+
<button
|
|
318
|
+
className="absolute right-0 top-0 flex items-center justify-center w-11 h-11 text-ods-text-secondary hover:text-ods-text-primary transition-colors duration-150"
|
|
319
|
+
onClick={(e) => {
|
|
320
|
+
e.stopPropagation()
|
|
321
|
+
if (onToggleExpand) {
|
|
322
|
+
onToggleExpand(node.id)
|
|
323
|
+
} else {
|
|
324
|
+
onNodeClick(node)
|
|
325
|
+
}
|
|
326
|
+
}}
|
|
327
|
+
aria-label={`${isExpanded ? 'Collapse' : 'Expand'} ${node.name}`}
|
|
328
|
+
>
|
|
329
|
+
{isExpanded ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />}
|
|
330
|
+
</button>
|
|
331
|
+
)}
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
{isSelected && (
|
|
335
|
+
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-1 h-[calc(100%-8px)] bg-ods-accent rounded-l" />
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
{hasChildren && isExpanded && (
|
|
341
|
+
<div className="space-y-1">
|
|
342
|
+
{node.children!.map(child => (
|
|
343
|
+
<MobileNavigationItem
|
|
344
|
+
key={child.id}
|
|
345
|
+
node={child}
|
|
346
|
+
selectedPath={selectedPath}
|
|
347
|
+
expandedNodes={expandedNodes}
|
|
348
|
+
onNodeClick={onNodeClick}
|
|
349
|
+
onToggleExpand={onToggleExpand}
|
|
350
|
+
level={level + 1}
|
|
351
|
+
folderIndexFile={folderIndexFile}
|
|
352
|
+
/>
|
|
353
|
+
))}
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
357
|
+
)
|
|
358
|
+
}
|