@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,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
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single home for doc-source tree + content types.
|
|
3
|
+
*
|
|
4
|
+
* Consumed by every doc-viewer consumer (knowledge base, data room, future
|
|
5
|
+
* sources). Replaces the per-source `OpenFrameDocNode` / `OpenFrameDocContent`
|
|
6
|
+
* / `DataRoomDocNode` / `DocNavNode` types that previously lived in the hub.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A single node in a doc-source tree. The `documentType` discriminator
|
|
11
|
+
* defaults to `'markdown'` when undefined; rich-content viewers handle
|
|
12
|
+
* `'pdf' | 'google_sheet' | 'figma' | 'file'`.
|
|
13
|
+
*/
|
|
14
|
+
export interface DocNode {
|
|
15
|
+
id: string
|
|
16
|
+
name: string
|
|
17
|
+
slug: string
|
|
18
|
+
path: string
|
|
19
|
+
type: 'file' | 'folder'
|
|
20
|
+
hasReadme?: boolean
|
|
21
|
+
/** Per-row sort key — populated by data-room rows, undefined elsewhere. */
|
|
22
|
+
sortOrder?: number
|
|
23
|
+
/** Discriminator for the rich renderer. `undefined` is treated as `'markdown'`. */
|
|
24
|
+
documentType?: 'markdown' | 'pdf' | 'google_sheet' | 'figma' | 'file'
|
|
25
|
+
children?: DocNode[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Content payload returned by a doc-source DAL's `getContent` call. Carries
|
|
30
|
+
* everything any consumer's renderer might need; markdown-only fields and
|
|
31
|
+
* rich-only fields are both optional.
|
|
32
|
+
*/
|
|
33
|
+
export interface DocContent {
|
|
34
|
+
content: string
|
|
35
|
+
sections: Array<{ id: string; title: string; level: number }>
|
|
36
|
+
path: string
|
|
37
|
+
/** Defaults to `'markdown'` when undefined. */
|
|
38
|
+
documentType?: DocNode['documentType']
|
|
39
|
+
// Markdown-only
|
|
40
|
+
brokenLinks?: string[]
|
|
41
|
+
// Rich-content (data-room) — optional
|
|
42
|
+
fileUrl?: string
|
|
43
|
+
externalUrl?: string
|
|
44
|
+
mimeType?: string
|
|
45
|
+
fileName?: string
|
|
46
|
+
fileSize?: number
|
|
47
|
+
/** ISO timestamps for SEO `datePublished` / `dateModified` — DAL populates
|
|
48
|
+
* from the underlying row's sync/update timestamp. Undefined when unknown. */
|
|
49
|
+
publishedAt?: string
|
|
50
|
+
updatedAt?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Doc-source data-access contract. Each doc source's hub-side DAL conforms
|
|
55
|
+
* to this. Generic over the Supabase-client type so the lib doesn't pull in
|
|
56
|
+
* `@supabase/supabase-js`; hub callers tighten `Client` to `SupabaseClient`.
|
|
57
|
+
*/
|
|
58
|
+
export interface DocSourceDal<Client = unknown> {
|
|
59
|
+
getStructure(client: Client): Promise<DocNode[]>
|
|
60
|
+
getContent(client: Client, path: string): Promise<DocContent | null>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Doc-source registry id. The lib's `<DocViewer>` is generic over this union
|
|
65
|
+
* — the hub's `DOC_SOURCES` registry defines the actual ids. Listed here as a
|
|
66
|
+
* type-only union so the lib can flow the id into `DocRenderHandlers.sourceId`
|
|
67
|
+
* without callers having to cast `string` → `'openframe-docs' | 'data-room-docs'`.
|
|
68
|
+
*
|
|
69
|
+
* Adding a new source = add the id here AND register it in the hub. The lib
|
|
70
|
+
* never reaches into the registry — this is purely a type-narrowing handle.
|
|
71
|
+
*/
|
|
72
|
+
export type DocSourceId = 'openframe-docs' | 'data-room-docs'
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Handlers the viewer passes to a consumer's `renderContent` callback.
|
|
76
|
+
* The page shell wires `renderContent` directly — no `DocContentRenderer`
|
|
77
|
+
* interface layer between the viewer and the consumer's render logic.
|
|
78
|
+
*/
|
|
79
|
+
export interface DocRenderHandlers {
|
|
80
|
+
onInternalLinkClick: (
|
|
81
|
+
path: string,
|
|
82
|
+
options?: { expandFolder?: boolean; fromInternalLink?: boolean },
|
|
83
|
+
) => void
|
|
84
|
+
currentPath: string
|
|
85
|
+
/** Registry source id (e.g. `'openframe-docs'`, `'data-room-docs'`) — used by
|
|
86
|
+
* the consumer's `/api/resolve-link` POST to disambiguate the doc source. */
|
|
87
|
+
sourceId: DocSourceId
|
|
88
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
// Core platform types
|
|
5
5
|
export * from './announcement'
|
|
6
|
+
// Doc-source viewer types — DocNode / DocContent / DocSourceDal /
|
|
7
|
+
// DocRenderHandlers. Consumed by knowledge-base, data-room, and future
|
|
8
|
+
// doc-source viewers.
|
|
9
|
+
export * from './doc-source'
|
|
6
10
|
export type { Category, CategoryCardProps, RealCategoryCardProps } from './categories'
|
|
7
11
|
export * from './category'
|
|
8
12
|
export * from './media'
|
package/src/types/marketing.ts
CHANGED
|
@@ -347,6 +347,7 @@ export type ContentSourceType =
|
|
|
347
347
|
| 'investor_update' // Investor updates
|
|
348
348
|
| 'onboarding_guide' // Onboarding guides (lives on openframe platform)
|
|
349
349
|
| 'what_i_shipped' // What I Shipped employee check-ins (lives on people-hub)
|
|
350
|
+
| 'faq' // FAQ Q&A pair (single-page /faqs index; deep-link by category anchor)
|
|
350
351
|
| 'from_scratch';
|
|
351
352
|
|
|
352
353
|
export type URLInjectionPreference = 'none' | 'in_post' | 'as_comment';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared document path utilities.
|
|
3
|
+
* Single source of truth for path/slug operations used by all doc-source viewers
|
|
4
|
+
* (knowledge base, data room, future).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { DEFAULT_FOLDER_INDEX_FILE } from './doc-tree-nav'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert a document name to a URL-safe path slug.
|
|
11
|
+
* Used when creating, moving, or renaming documents.
|
|
12
|
+
* e.g., "Investor Thesis" → "investor-thesis"
|
|
13
|
+
*/
|
|
14
|
+
export function toDocSlug(name: string): string {
|
|
15
|
+
return name.trim().toLowerCase().replace(/\s+/g, '-')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Convert a full document path to a DOM-safe node ID.
|
|
20
|
+
* Used for tree node IDs and sidebar expand/collapse state.
|
|
21
|
+
* e.g., "legal/cap-table/safe.md" → "legal-cap-table-safe.md"
|
|
22
|
+
*/
|
|
23
|
+
export function pathToNodeId(path: string): string {
|
|
24
|
+
return path.replace(/[\/\s]/g, '-').toLowerCase()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Normalize a raw URL path from a route handler.
|
|
29
|
+
* Strips folder-index suffixes (default README.md) and lowercases segments.
|
|
30
|
+
* Used by both data-room and knowledge-base [...path] route handlers.
|
|
31
|
+
*/
|
|
32
|
+
export function normalizeDocPath(
|
|
33
|
+
segments: string[],
|
|
34
|
+
folderIndexFile: string = DEFAULT_FOLDER_INDEX_FILE,
|
|
35
|
+
): string {
|
|
36
|
+
// Lowercase BOTH sides BEFORE comparison so paths from URL segments
|
|
37
|
+
// (`readme.md`) and the canonical folder-index constant (`README.md`)
|
|
38
|
+
// strip uniformly. Lowercasing only at the end (the previous shape) left
|
|
39
|
+
// lowercase URL inputs unstripped → bogus content lookups.
|
|
40
|
+
const lowerPath = (segments?.join('/') || '').toLowerCase()
|
|
41
|
+
const lowerIndex = folderIndexFile.toLowerCase()
|
|
42
|
+
|
|
43
|
+
const suffix = `/${lowerIndex}`
|
|
44
|
+
if (lowerPath.endsWith(suffix)) return lowerPath.slice(0, -suffix.length)
|
|
45
|
+
if (lowerPath === lowerIndex) return ''
|
|
46
|
+
return lowerPath
|
|
47
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for doc-source tree navigation — shared by client hook and RSC SSR.
|
|
3
|
+
* README convention (folder-index file) is configurable but defaults to 'README.md'.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { DocNode } from '../types/doc-source'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Canonical folder-index filename. Single SSOT consumed by `doc-tree-nav`
|
|
10
|
+
* pure helpers, `useDocumentTree` (default config), `DocSeoContent` (fallback
|
|
11
|
+
* fetch path), and `multi-level-navigation`'s visual-selection logic. Stays
|
|
12
|
+
* in canonical case here; consumers that do case-insensitive comparison
|
|
13
|
+
* (the navigation) lowercase locally.
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_FOLDER_INDEX_FILE = 'README.md'
|
|
16
|
+
|
|
17
|
+
/** @deprecated Re-exported under the new name; remove after consumers migrate. */
|
|
18
|
+
const DEFAULT_FOLDER_INDEX = DEFAULT_FOLDER_INDEX_FILE
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Strip the folder-index filename from a path, returning the folder path.
|
|
22
|
+
* For default 'README.md': 'folder/README.md' → 'folder', 'README.md' → '', 'folder/file.md' → 'folder/file.md'
|
|
23
|
+
*/
|
|
24
|
+
export function stripFolderIndexFromPath(
|
|
25
|
+
path: string,
|
|
26
|
+
folderIndexFile: string = DEFAULT_FOLDER_INDEX,
|
|
27
|
+
): string {
|
|
28
|
+
const suffix = `/${folderIndexFile}`
|
|
29
|
+
if (path.endsWith(suffix)) return path.slice(0, -suffix.length)
|
|
30
|
+
if (path === folderIndexFile) return ''
|
|
31
|
+
return path
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function findDocNodeByPath(path: string, nodes: DocNode[]): DocNode | null {
|
|
35
|
+
for (const node of nodes) {
|
|
36
|
+
if (node.path === path) return node
|
|
37
|
+
if (node.children) {
|
|
38
|
+
const found = findDocNodeByPath(path, node.children)
|
|
39
|
+
if (found) return found
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Given a node path like "folder1/subfolder2", return DOM-style node IDs for each segment
|
|
47
|
+
* (accordion sidebar — matches use-document-tree).
|
|
48
|
+
*/
|
|
49
|
+
export function getDocAncestorNodeIds(nodePath: string): string[] {
|
|
50
|
+
const parts = nodePath.split('/')
|
|
51
|
+
const ids: string[] = []
|
|
52
|
+
let current = ''
|
|
53
|
+
for (const part of parts) {
|
|
54
|
+
current = current ? `${current}-${part}` : part
|
|
55
|
+
ids.push(current.toLowerCase())
|
|
56
|
+
}
|
|
57
|
+
return ids
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve which storage path to load (matches use-document-tree content effect).
|
|
62
|
+
*/
|
|
63
|
+
export function resolveContentFetchPath(
|
|
64
|
+
selectedPath: string,
|
|
65
|
+
structure: DocNode[],
|
|
66
|
+
folderIndexFile: string = DEFAULT_FOLDER_INDEX,
|
|
67
|
+
): string | null {
|
|
68
|
+
if (selectedPath === '') {
|
|
69
|
+
return folderIndexFile
|
|
70
|
+
}
|
|
71
|
+
const node = findDocNodeByPath(selectedPath, structure)
|
|
72
|
+
if (node && node.type === 'folder' && !node.hasReadme) {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
let pathToFetch = selectedPath
|
|
76
|
+
if (node && node.type === 'folder' && node.hasReadme) {
|
|
77
|
+
pathToFetch = `${selectedPath}/${folderIndexFile}`
|
|
78
|
+
}
|
|
79
|
+
return pathToFetch
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getInitialExpandedNodeIds(cleanInitialPath: string): string[] {
|
|
83
|
+
if (!cleanInitialPath) return []
|
|
84
|
+
const pathForExpansion = cleanInitialPath.includes('.')
|
|
85
|
+
? cleanInitialPath.substring(0, cleanInitialPath.lastIndexOf('/'))
|
|
86
|
+
: cleanInitialPath
|
|
87
|
+
if (!pathForExpansion) return []
|
|
88
|
+
return getDocAncestorNodeIds(pathForExpansion)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* When a doc source's root has no top-level folder-index file, navigate to the first
|
|
93
|
+
* folder that does. Matches the historical knowledge-base default.
|
|
94
|
+
*/
|
|
95
|
+
export function getDocSourceDefaultPath(
|
|
96
|
+
structure: DocNode[],
|
|
97
|
+
folderIndexFile: string = DEFAULT_FOLDER_INDEX,
|
|
98
|
+
): string | null {
|
|
99
|
+
if (!structure.length) return null
|
|
100
|
+
const hasRootIndex = structure.some(
|
|
101
|
+
(node) => node.type === 'file' && node.path === folderIndexFile
|
|
102
|
+
)
|
|
103
|
+
if (hasRootIndex) return null
|
|
104
|
+
// Scan ALL root nodes for the first folder with a README — not just
|
|
105
|
+
// structure[0]. Otherwise a leading non-folder root (file / folder without
|
|
106
|
+
// README) silently returns null even when a later root folder has one.
|
|
107
|
+
const firstFolderWithReadme = structure.find(
|
|
108
|
+
(node) => node.type === 'folder' && node.hasReadme && !!node.path,
|
|
109
|
+
)
|
|
110
|
+
return firstFolderWithReadme?.path ?? null
|
|
111
|
+
}
|