@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.
Files changed (211) hide show
  1. package/dist/chunk-3MCHAFHB.js +89 -0
  2. package/dist/chunk-3MCHAFHB.js.map +1 -0
  3. package/dist/{chunk-IMNGGLKL.js → chunk-3ZXUQQL4.js} +2 -2
  4. package/dist/{chunk-44ZFLMYC.cjs → chunk-4WACBTZU.cjs} +30 -30
  5. package/dist/{chunk-44ZFLMYC.cjs.map → chunk-4WACBTZU.cjs.map} +1 -1
  6. package/dist/{chunk-23UOJUDF.js → chunk-5BTZOVDQ.js} +4 -4
  7. package/dist/chunk-5KD3S25X.cjs +312 -0
  8. package/dist/chunk-5KD3S25X.cjs.map +1 -0
  9. package/dist/chunk-6N26CURS.cjs +2417 -0
  10. package/dist/chunk-6N26CURS.cjs.map +1 -0
  11. package/dist/{chunk-XX7P4IL2.js → chunk-7NM7DEUK.js} +4 -143
  12. package/dist/chunk-7NM7DEUK.js.map +1 -0
  13. package/dist/chunk-BBZ7AX5H.cjs +290 -0
  14. package/dist/chunk-BBZ7AX5H.cjs.map +1 -0
  15. package/dist/chunk-C6SCWXDP.js +290 -0
  16. package/dist/chunk-C6SCWXDP.js.map +1 -0
  17. package/dist/{chunk-GVSTRN36.cjs → chunk-D3LEFMOA.cjs} +3 -3
  18. package/dist/{chunk-GVSTRN36.cjs.map → chunk-D3LEFMOA.cjs.map} +1 -1
  19. package/dist/{chunk-P45Q4QCU.cjs → chunk-D5YY5U6J.cjs} +19 -15
  20. package/dist/chunk-D5YY5U6J.cjs.map +1 -0
  21. package/dist/chunk-DDAT4RKX.js +2417 -0
  22. package/dist/chunk-DDAT4RKX.js.map +1 -0
  23. package/dist/{chunk-PVXCVPJD.cjs → chunk-EI4WALN2.cjs} +67 -1626
  24. package/dist/chunk-EI4WALN2.cjs.map +1 -0
  25. package/dist/{chunk-ZIODAAFY.cjs → chunk-EL5GCMPU.cjs} +20 -159
  26. package/dist/chunk-EL5GCMPU.cjs.map +1 -0
  27. package/dist/{chunk-VQW7CEGS.js → chunk-FSBDVT6R.js} +1831 -3672
  28. package/dist/chunk-FSBDVT6R.js.map +1 -0
  29. package/dist/{chunk-DPKUSBQH.cjs → chunk-GDF2R2ER.cjs} +1073 -2914
  30. package/dist/chunk-GDF2R2ER.cjs.map +1 -0
  31. package/dist/chunk-HLQW7MWJ.cjs +1594 -0
  32. package/dist/chunk-HLQW7MWJ.cjs.map +1 -0
  33. package/dist/chunk-I6ZPGKZ2.cjs +151 -0
  34. package/dist/chunk-I6ZPGKZ2.cjs.map +1 -0
  35. package/dist/{chunk-J66U3LPZ.cjs → chunk-KBKZYJRI.cjs} +33 -332
  36. package/dist/chunk-KBKZYJRI.cjs.map +1 -0
  37. package/dist/{chunk-USPATRSC.js → chunk-N6ZM5PYZ.js} +45 -344
  38. package/dist/chunk-N6ZM5PYZ.js.map +1 -0
  39. package/dist/{chunk-RGALV4UX.js → chunk-POKKCWKF.js} +3 -82
  40. package/dist/chunk-POKKCWKF.js.map +1 -0
  41. package/dist/{chunk-XDI5JSWB.cjs → chunk-QCKN37OP.cjs} +45 -41
  42. package/dist/chunk-QCKN37OP.cjs.map +1 -0
  43. package/dist/{chunk-KNCDUWY2.js → chunk-QF2X6PTD.js} +10 -6
  44. package/dist/{chunk-KNCDUWY2.js.map → chunk-QF2X6PTD.js.map} +1 -1
  45. package/dist/{chunk-Z3V2SGQZ.js → chunk-R2KT5GDD.js} +324 -1883
  46. package/dist/chunk-R2KT5GDD.js.map +1 -0
  47. package/dist/{chunk-RJUGNL2A.cjs → chunk-RQ6RTBKF.cjs} +16 -12
  48. package/dist/chunk-RQ6RTBKF.cjs.map +1 -0
  49. package/dist/chunk-TFSYSWPS.cjs +89 -0
  50. package/dist/chunk-TFSYSWPS.cjs.map +1 -0
  51. package/dist/chunk-TYZEMPPH.js +312 -0
  52. package/dist/chunk-TYZEMPPH.js.map +1 -0
  53. package/dist/{chunk-O5N57CRV.js → chunk-VCQ3CTYK.js} +12 -8
  54. package/dist/{chunk-O5N57CRV.js.map → chunk-VCQ3CTYK.js.map} +1 -1
  55. package/dist/chunk-VRSXJ5QJ.js +1599 -0
  56. package/dist/chunk-VRSXJ5QJ.js.map +1 -0
  57. package/dist/{chunk-53CFL7PH.js → chunk-XQL4WDML.js} +11 -7
  58. package/dist/{chunk-53CFL7PH.js.map → chunk-XQL4WDML.js.map} +1 -1
  59. package/dist/chunk-Y4JNA4W6.cjs +1599 -0
  60. package/dist/chunk-Y4JNA4W6.cjs.map +1 -0
  61. package/dist/{chunk-ZSUNQJRO.cjs → chunk-YETA25JW.cjs} +2 -81
  62. package/dist/chunk-YETA25JW.cjs.map +1 -0
  63. package/dist/chunk-YVB3VDIQ.js +1594 -0
  64. package/dist/chunk-YVB3VDIQ.js.map +1 -0
  65. package/dist/chunk-ZLN6SM2U.js +151 -0
  66. package/dist/chunk-ZLN6SM2U.js.map +1 -0
  67. package/dist/components/chat/chat-attachment-bar.d.ts.map +1 -1
  68. package/dist/components/chat/index.cjs +12 -6
  69. package/dist/components/chat/index.cjs.map +1 -1
  70. package/dist/components/chat/index.js +45 -39
  71. package/dist/components/contact/index.cjs +11 -7
  72. package/dist/components/contact/index.cjs.map +1 -1
  73. package/dist/components/contact/index.js +10 -6
  74. package/dist/components/docs/doc-navigation-context.d.ts +39 -0
  75. package/dist/components/docs/doc-navigation-context.d.ts.map +1 -0
  76. package/dist/components/docs/doc-viewer.d.ts +66 -0
  77. package/dist/components/docs/doc-viewer.d.ts.map +1 -0
  78. package/dist/components/docs/index.cjs +34 -0
  79. package/dist/components/docs/index.cjs.map +1 -0
  80. package/dist/components/docs/index.d.ts +8 -0
  81. package/dist/components/docs/index.d.ts.map +1 -0
  82. package/dist/components/docs/index.js +34 -0
  83. package/dist/components/docs/index.js.map +1 -0
  84. package/dist/components/docs/use-document-tree.d.ts +41 -0
  85. package/dist/components/docs/use-document-tree.d.ts.map +1 -0
  86. package/dist/components/docs/use-scroll-spy.d.ts +16 -0
  87. package/dist/components/docs/use-scroll-spy.d.ts.map +1 -0
  88. package/dist/components/embeds/embed-iframe.d.ts +29 -0
  89. package/dist/components/embeds/embed-iframe.d.ts.map +1 -0
  90. package/dist/components/embeds/figma-embed.d.ts +27 -0
  91. package/dist/components/embeds/figma-embed.d.ts.map +1 -0
  92. package/dist/components/embeds/google-sheets-viewer.d.ts +8 -0
  93. package/dist/components/embeds/google-sheets-viewer.d.ts.map +1 -0
  94. package/dist/components/embeds/index.cjs +37 -0
  95. package/dist/components/embeds/index.cjs.map +1 -0
  96. package/dist/components/embeds/index.d.ts +9 -0
  97. package/dist/components/embeds/index.d.ts.map +1 -0
  98. package/dist/components/embeds/index.js +37 -0
  99. package/dist/components/embeds/index.js.map +1 -0
  100. package/dist/components/embeds/pdf-viewer.d.ts +10 -0
  101. package/dist/components/embeds/pdf-viewer.d.ts.map +1 -0
  102. package/dist/components/faq/index.cjs +12 -8
  103. package/dist/components/faq/index.cjs.map +1 -1
  104. package/dist/components/faq/index.js +11 -7
  105. package/dist/components/features/index.cjs +10 -6
  106. package/dist/components/features/index.cjs.map +1 -1
  107. package/dist/components/features/index.js +9 -5
  108. package/dist/components/index.cjs +247 -1120
  109. package/dist/components/index.cjs.map +1 -1
  110. package/dist/components/index.d.ts +2 -0
  111. package/dist/components/index.d.ts.map +1 -1
  112. package/dist/components/index.js +645 -1518
  113. package/dist/components/index.js.map +1 -1
  114. package/dist/components/navigation/index.cjs +15 -6
  115. package/dist/components/navigation/index.cjs.map +1 -1
  116. package/dist/components/navigation/index.d.ts +2 -0
  117. package/dist/components/navigation/index.d.ts.map +1 -1
  118. package/dist/components/navigation/index.js +15 -6
  119. package/dist/components/navigation/multi-level-navigation.d.ts +28 -0
  120. package/dist/components/navigation/multi-level-navigation.d.ts.map +1 -0
  121. package/dist/components/onboarding-guides/index.cjs +34 -29
  122. package/dist/components/onboarding-guides/index.cjs.map +1 -1
  123. package/dist/components/onboarding-guides/index.js +13 -8
  124. package/dist/components/onboarding-guides/index.js.map +1 -1
  125. package/dist/components/related-content/index.cjs +12 -8
  126. package/dist/components/related-content/index.cjs.map +1 -1
  127. package/dist/components/related-content/index.js +11 -7
  128. package/dist/components/tickets/index.cjs +75 -70
  129. package/dist/components/tickets/index.cjs.map +1 -1
  130. package/dist/components/tickets/index.js +16 -11
  131. package/dist/components/tickets/index.js.map +1 -1
  132. package/dist/components/ui/index.cjs +12 -6
  133. package/dist/components/ui/index.cjs.map +1 -1
  134. package/dist/components/ui/index.js +65 -59
  135. package/dist/embed-shims/index.cjs +3 -3
  136. package/dist/embed-shims/index.cjs.map +1 -1
  137. package/dist/embed-shims/index.js +8 -8
  138. package/dist/hooks/index.cjs +6 -3
  139. package/dist/hooks/index.cjs.map +1 -1
  140. package/dist/hooks/index.js +7 -4
  141. package/dist/index.cjs +62 -6
  142. package/dist/index.cjs.map +1 -1
  143. package/dist/index.js +257 -201
  144. package/dist/types/doc-source.cjs +4 -0
  145. package/dist/types/doc-source.cjs.map +1 -0
  146. package/dist/types/doc-source.d.ts +86 -0
  147. package/dist/types/doc-source.d.ts.map +1 -0
  148. package/dist/types/doc-source.js +3 -0
  149. package/dist/types/doc-source.js.map +1 -0
  150. package/dist/types/index.d.ts +1 -0
  151. package/dist/types/index.d.ts.map +1 -1
  152. package/dist/utils/doc-path-utils.d.ts +24 -0
  153. package/dist/utils/doc-path-utils.d.ts.map +1 -0
  154. package/dist/utils/doc-tree-nav.d.ts +35 -0
  155. package/dist/utils/doc-tree-nav.d.ts.map +1 -0
  156. package/dist/utils/embed-url-converters.d.ts +16 -0
  157. package/dist/utils/embed-url-converters.d.ts.map +1 -0
  158. package/dist/utils/format.d.ts +9 -0
  159. package/dist/utils/format.d.ts.map +1 -1
  160. package/dist/utils/index.cjs +297 -0
  161. package/dist/utils/index.cjs.map +1 -1
  162. package/dist/utils/index.d.ts +7 -1
  163. package/dist/utils/index.d.ts.map +1 -1
  164. package/dist/utils/index.js +276 -1
  165. package/dist/utils/index.js.map +1 -1
  166. package/dist/utils/markdown-section-extractor.d.ts +23 -0
  167. package/dist/utils/markdown-section-extractor.d.ts.map +1 -0
  168. package/dist/utils/markdown-to-plain.d.ts +32 -0
  169. package/dist/utils/markdown-to-plain.d.ts.map +1 -0
  170. package/dist/utils/tree-builder.d.ts +30 -0
  171. package/dist/utils/tree-builder.d.ts.map +1 -0
  172. package/package.json +19 -1
  173. package/src/components/chat/chat-attachment-bar.tsx +6 -11
  174. package/src/components/docs/doc-navigation-context.tsx +123 -0
  175. package/src/components/docs/doc-viewer.tsx +345 -0
  176. package/src/components/docs/index.ts +12 -0
  177. package/src/components/docs/use-document-tree.ts +427 -0
  178. package/src/components/docs/use-scroll-spy.ts +93 -0
  179. package/src/components/embeds/embed-iframe.tsx +103 -0
  180. package/src/components/embeds/figma-embed.tsx +147 -0
  181. package/src/components/embeds/google-sheets-viewer.tsx +54 -0
  182. package/src/components/embeds/index.ts +11 -0
  183. package/src/components/embeds/pdf-viewer.tsx +64 -0
  184. package/src/components/index.ts +2 -0
  185. package/src/components/navigation/index.ts +5 -0
  186. package/src/components/navigation/multi-level-navigation.tsx +358 -0
  187. package/src/types/doc-source.ts +88 -0
  188. package/src/types/index.ts +4 -0
  189. package/src/utils/doc-path-utils.ts +47 -0
  190. package/src/utils/doc-tree-nav.ts +111 -0
  191. package/src/utils/embed-url-converters.ts +77 -0
  192. package/src/utils/format.ts +16 -0
  193. package/src/utils/index.ts +11 -0
  194. package/src/utils/markdown-section-extractor.ts +106 -0
  195. package/src/utils/markdown-to-plain.ts +69 -0
  196. package/src/utils/tree-builder.ts +114 -0
  197. package/dist/chunk-DPKUSBQH.cjs.map +0 -1
  198. package/dist/chunk-J66U3LPZ.cjs.map +0 -1
  199. package/dist/chunk-P45Q4QCU.cjs.map +0 -1
  200. package/dist/chunk-PVXCVPJD.cjs.map +0 -1
  201. package/dist/chunk-RGALV4UX.js.map +0 -1
  202. package/dist/chunk-RJUGNL2A.cjs.map +0 -1
  203. package/dist/chunk-USPATRSC.js.map +0 -1
  204. package/dist/chunk-VQW7CEGS.js.map +0 -1
  205. package/dist/chunk-XDI5JSWB.cjs.map +0 -1
  206. package/dist/chunk-XX7P4IL2.js.map +0 -1
  207. package/dist/chunk-Z3V2SGQZ.js.map +0 -1
  208. package/dist/chunk-ZIODAAFY.cjs.map +0 -1
  209. package/dist/chunk-ZSUNQJRO.cjs.map +0 -1
  210. /package/dist/{chunk-IMNGGLKL.js.map → chunk-3ZXUQQL4.js.map} +0 -0
  211. /package/dist/{chunk-23UOJUDF.js.map → chunk-5BTZOVDQ.js.map} +0 -0
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared markdown section extractor
3
+ * Extracts heading sections from markdown content for navigation.
4
+ */
5
+ export interface MarkdownSection {
6
+ id: string;
7
+ title: string;
8
+ level: number;
9
+ }
10
+ export interface ExtractSectionsOptions {
11
+ /** Maximum heading level to include (default: 2 = H1-H2) */
12
+ maxLevel?: number;
13
+ /** Remove emoji characters from IDs (default: true) */
14
+ removeEmojis?: boolean;
15
+ /** Handle duplicate IDs by appending a counter suffix (default: true) */
16
+ handleDuplicateIds?: boolean;
17
+ /** Strip bold, italic, and inline code markers from titles (default: true) */
18
+ stripFormattingMarkers?: boolean;
19
+ /** Skip code blocks and YAML frontmatter (default: true) */
20
+ skipCodeAndYamlBlocks?: boolean;
21
+ }
22
+ export declare function extractSections(markdown: string, options?: ExtractSectionsOptions): MarkdownSection[];
23
+ //# sourceMappingURL=markdown-section-extractor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown-section-extractor.d.ts","sourceRoot":"","sources":["../../src/utils/markdown-section-extractor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,sBAAsB;IACrC,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uDAAuD;IACvD,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,8EAA8E;IAC9E,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,4DAA4D;IAC5D,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC;AAUD,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,sBAA2B,GACnC,eAAe,EAAE,CAoEnB"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Convert markdown source to plain text. Used by both the SEO server component
3
+ * (article-body echo in JSON-LD) and chat entity-card preview (clamped previews
4
+ * inside chat cards).
5
+ *
6
+ * The two consumers differ only in pre/post processing — `mode: 'article'`
7
+ * strips code blocks + images + headings + list bullets and preserves paragraph
8
+ * breaks; `mode: 'preview'` collapses everything to a single line. Both share
9
+ * the same inline-marker strip (link / bold / italic / inline-code).
10
+ *
11
+ * All regex patterns are linear (negated character classes, non-greedy with
12
+ * bounded targets) — no nested quantifiers, no catastrophic backtracking on
13
+ * pathological input.
14
+ */
15
+ export interface MarkdownToPlainOptions {
16
+ /**
17
+ * - `'article'` (default): preserve paragraph structure (`\n\n` as separator),
18
+ * strip code blocks + images + headings + list bullets. Used for the
19
+ * article-body echo on SEO surfaces.
20
+ * - `'preview'`: collapse all whitespace to single spaces; result is a single
21
+ * line. Used for chat-card previews / OG-card snippets where line breaks
22
+ * would wrap awkwardly.
23
+ */
24
+ mode?: 'article' | 'preview';
25
+ /** Optional max-character cap. Truncates with no ellipsis (caller-decided). */
26
+ maxChars?: number;
27
+ }
28
+ /** Strip inline markdown markers (bold, italic, inline-code, links). Shared
29
+ * by `extractSections`'s `stripFormattingMarkers` and `markdownToPlainText`. */
30
+ export declare function stripInlineMarkdown(s: string): string;
31
+ export declare function markdownToPlainText(markdown: string, options?: MarkdownToPlainOptions): string;
32
+ //# sourceMappingURL=markdown-to-plain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown-to-plain.d.ts","sourceRoot":"","sources":["../../src/utils/markdown-to-plain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,sBAAsB;IACrC;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;IAC5B,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;iFACiF;AACjF,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAMrD;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,sBAA2B,GACnC,MAAM,CA0BR"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Base interface for tree nodes. Consumers extend this with additional fields.
3
+ */
4
+ export interface TreeNodeBase {
5
+ id: string;
6
+ name: string;
7
+ path: string;
8
+ type: 'file' | 'folder';
9
+ sortOrder?: number;
10
+ children?: TreeNodeBase[];
11
+ }
12
+ /**
13
+ * Format a document name for display: remove .md extension and capitalize.
14
+ */
15
+ export declare function formatDocName(name: string): string;
16
+ /**
17
+ * Sort children: README first, then folders, then files — each group by sort_order then alphabetical.
18
+ * Recurses into folder children to ensure consistent ordering at all levels.
19
+ */
20
+ export declare function sortTreeChildren<TNode extends TreeNodeBase>(nodes: TNode[]): TNode[];
21
+ /**
22
+ * Build a hierarchical document tree from a flat list of records.
23
+ *
24
+ * 3-pass algorithm:
25
+ * 1. Create node map from flat records using the provided `mapFn`
26
+ * 2. Build parent-child hierarchy using `getParentPath`
27
+ * 3. Sort children (folders first, README, then others)
28
+ */
29
+ export declare function buildDocumentTree<TNode extends TreeNodeBase, TDoc>(docs: TDoc[], mapFn: (doc: TDoc) => TNode, getParentPath: (doc: TDoc) => string | null, getPath: (doc: TDoc) => string): TNode[];
30
+ //# sourceMappingURL=tree-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree-builder.d.ts","sourceRoot":"","sources":["../../src/utils/tree-builder.ts"],"names":[],"mappings":"AAMA;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAA;CAC1B;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CASlD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,SAAS,YAAY,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE,CAwBpF;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,SAAS,YAAY,EAAE,IAAI,EAChE,IAAI,EAAE,IAAI,EAAE,EACZ,KAAK,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,KAAK,EAC3B,aAAa,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,MAAM,GAAG,IAAI,EAC3C,OAAO,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,MAAM,GAC7B,KAAK,EAAE,CAsCT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.285",
3
+ "version": "0.0.286",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -126,6 +126,24 @@
126
126
  "require": "./dist/components/navigation/index.cjs",
127
127
  "default": "./dist/components/navigation/index.js"
128
128
  },
129
+ "./components/docs": {
130
+ "types": "./dist/components/docs/index.d.ts",
131
+ "import": "./dist/components/docs/index.js",
132
+ "require": "./dist/components/docs/index.cjs",
133
+ "default": "./dist/components/docs/index.js"
134
+ },
135
+ "./components/embeds": {
136
+ "types": "./dist/components/embeds/index.d.ts",
137
+ "import": "./dist/components/embeds/index.js",
138
+ "require": "./dist/components/embeds/index.cjs",
139
+ "default": "./dist/components/embeds/index.js"
140
+ },
141
+ "./types/doc-source": {
142
+ "types": "./dist/types/doc-source.d.ts",
143
+ "import": "./dist/types/doc-source.js",
144
+ "require": "./dist/types/doc-source.cjs",
145
+ "default": "./dist/types/doc-source.js"
146
+ },
129
147
  "./hooks": {
130
148
  "types": "./dist/hooks/index.d.ts",
131
149
  "import": "./dist/hooks/index.js",
@@ -1,6 +1,8 @@
1
1
  'use client'
2
2
  // compact-size support added for the ticket-drawer reply composer.
3
3
 
4
+ import { formatFileSize } from '../../utils/format'
5
+
4
6
  /**
5
7
  * Chat-attachment UI primitives.
6
8
  *
@@ -321,19 +323,12 @@ function AttachmentChip({ attachment, onRemove, disabled, size = 'default' }: At
321
323
  }
322
324
 
323
325
  // ---------------------------------------------------------------------------
324
- // Helpers — inlined from hub upload-utils.ts to keep this file standalone
326
+ // Helpers
325
327
  // ---------------------------------------------------------------------------
326
328
 
327
- /** Same byte-rounding shape as the hub `formatFileSize` in
328
- * `lib/utils/upload-utils.ts`. Inlined so the lib doesn't pull in the
329
- * hub upload service. */
330
- function formatFileSize(bytes: number): string {
331
- if (bytes === 0) return '0 B'
332
- const k = 1024
333
- const sizes = ['B', 'KB', 'MB', 'GB']
334
- const i = Math.floor(Math.log(bytes) / Math.log(k))
335
- return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
336
- }
329
+ // formatFileSize was previously inlined here as a copy of the hub's helper. It
330
+ // now lives in `../../utils/format.ts` (lifted during the doc-viewer unification).
331
+ // Imported at the top of this file.
337
332
 
338
333
  function extLabel(fileName: string): string {
339
334
  const dot = fileName.lastIndexOf('.')
@@ -0,0 +1,123 @@
1
+ "use client"
2
+
3
+ import React, { createContext, useCallback, useContext, useRef, type ReactNode } from 'react'
4
+ import type { DocNode } from '../../types/doc-source'
5
+
6
+ // =============================================================================
7
+ // Types
8
+ // =============================================================================
9
+
10
+ export interface DocNavigator {
11
+ /** Base route this navigator owns (e.g., '/knowledge-base', '/data-room'). */
12
+ baseRoute: string
13
+ /** Look up a node in the tree by its storage path (e.g., 'openframe-cli/README.md'). */
14
+ findNodeByPath: (path: string) => DocNode | null
15
+ /** Navigate to a node using the same flow as a sidebar menu click. */
16
+ selectNode: (node: DocNode) => void
17
+ }
18
+
19
+ interface DocNavigationContextValue {
20
+ /**
21
+ * Register a navigator for a `baseRoute`. Returns a cleanup function that
22
+ * removes the navigator from the map IF the slot still owns it (StrictMode-safe).
23
+ * Duplicate registration for the same baseRoute logs a console.warn and replaces.
24
+ */
25
+ register: (nav: DocNavigator) => () => void
26
+ /**
27
+ * Navigate to a path inside whichever registered navigator owns its baseRoute prefix.
28
+ * Returns true if handled, false otherwise (caller falls back to opening in a new tab).
29
+ */
30
+ navigate: (path: string) => boolean
31
+ /** Whether ANY navigator is currently mounted. */
32
+ isAvailable: () => boolean
33
+ }
34
+
35
+ // =============================================================================
36
+ // Context
37
+ // =============================================================================
38
+
39
+ const DocNavigationContext = createContext<DocNavigationContextValue | null>(null)
40
+
41
+ /**
42
+ * Bridges `useDocumentTree` instances (deep in the page tree) to `GlobalAskAI`
43
+ * (high up near the root) without global events or URL parsing.
44
+ *
45
+ * Multi-navigator design: a `Map<baseRoute, DocNavigator>` lets multiple
46
+ * viewers coexist (nested viewers, dual-pane scenarios). Today only one
47
+ * viewer mounts at a time, so behavior is unchanged.
48
+ */
49
+ export function DocNavigationProvider({ children }: { children: ReactNode }) {
50
+ const navMap = useRef<Map<string, DocNavigator>>(new Map())
51
+
52
+ const register = useCallback((nav: DocNavigator) => {
53
+ const existing = navMap.current.get(nav.baseRoute)
54
+ if (existing && existing !== nav) {
55
+ // eslint-disable-next-line no-console
56
+ console.warn(
57
+ `[DocNavigationContext] duplicate registration for ${nav.baseRoute}; replacing`,
58
+ )
59
+ }
60
+ navMap.current.set(nav.baseRoute, nav)
61
+ return () => {
62
+ // StrictMode-safe identity check: only delete if THIS nav still owns the slot.
63
+ if (navMap.current.get(nav.baseRoute) === nav) {
64
+ navMap.current.delete(nav.baseRoute)
65
+ }
66
+ }
67
+ }, [])
68
+
69
+ const findOwningNavigator = useCallback((path: string): DocNavigator | null => {
70
+ // First try: a navigator whose baseRoute is a prefix of `path`. Walks the
71
+ // longest match first so nested routes win over a parent.
72
+ const baseRoutes = Array.from(navMap.current.keys()).sort((a, b) => b.length - a.length)
73
+ for (const baseRoute of baseRoutes) {
74
+ if (path === baseRoute || path.startsWith(`${baseRoute}/`)) {
75
+ return navMap.current.get(baseRoute) ?? null
76
+ }
77
+ }
78
+ // Fallback: a single navigator handles any path (existing behavior — chat
79
+ // chips emit storage paths, not absolute URLs, so prefix matching misses).
80
+ if (navMap.current.size === 1) {
81
+ return navMap.current.values().next().value ?? null
82
+ }
83
+ return null
84
+ }, [])
85
+
86
+ const navigate = useCallback(
87
+ (path: string): boolean => {
88
+ const nav = findOwningNavigator(path)
89
+ if (!nav) return false
90
+ const node = nav.findNodeByPath(path)
91
+ if (!node) return false
92
+ nav.selectNode(node)
93
+ return true
94
+ },
95
+ [findOwningNavigator],
96
+ )
97
+
98
+ const isAvailable = useCallback(() => navMap.current.size > 0, [])
99
+
100
+ const value = React.useMemo(
101
+ () => ({ register, navigate, isAvailable }),
102
+ [register, navigate, isAvailable],
103
+ )
104
+
105
+ return <DocNavigationContext.Provider value={value}>{children}</DocNavigationContext.Provider>
106
+ }
107
+
108
+ // =============================================================================
109
+ // Hooks
110
+ // =============================================================================
111
+
112
+ export function useDocNavigation(): DocNavigationContextValue {
113
+ const ctx = useContext(DocNavigationContext)
114
+ if (!ctx) {
115
+ // Safe no-op fallback when used outside the provider.
116
+ return {
117
+ register: () => () => {},
118
+ navigate: () => false,
119
+ isAvailable: () => false,
120
+ }
121
+ }
122
+ return ctx
123
+ }
@@ -0,0 +1,345 @@
1
+ "use client"
2
+
3
+ import React, { useMemo } from "react"
4
+ import { MultiLevelNavigation, MobileNavigationDropdown } from "../navigation/multi-level-navigation"
5
+ import { PageHeading } from "../layout/page-heading"
6
+ import { PersistentSidebar, PersistentMobileDropdown } from "../persistent-filter-controls"
7
+ import { CategorySidebarSkeleton } from "../loading/page-layout-skeleton"
8
+ import { DocSearchBar, useDocSearch } from "../shared/doc-search"
9
+ import { StickySectionNav } from "../navigation/sticky-section-nav"
10
+ import { useDocumentTree } from "./use-document-tree"
11
+ import { useScrollSpy } from "./use-scroll-spy"
12
+ import { useDocNavigation } from "./doc-navigation-context"
13
+ import { findDocNodeByPath } from "../../utils/doc-tree-nav"
14
+ import type { DocContent, DocNode, DocRenderHandlers, DocSourceId } from "../../types/doc-source"
15
+
16
+ /** Color tokens for the doc-viewer chrome. Hub-side `DocViewer` callers share
17
+ * this constant; no need to override per source — the palette is intentionally
18
+ * uniform across knowledge-base + data-room (both use ODS dark tokens). */
19
+ export const DEFAULT_DOC_VIEWER_PALETTE = {
20
+ background: "bg-ods-bg",
21
+ containerBackground: "transparent",
22
+ headerText: "text-ods-text-primary",
23
+ primaryText: "text-ods-text-primary",
24
+ secondaryText: "text-ods-text-secondary",
25
+ accent: "var(--ods-accent)",
26
+ border: "border-ods-border",
27
+ cardBackground: "bg-ods-card",
28
+ } as const
29
+
30
+ export interface DocViewerProps {
31
+ /**
32
+ * Registry source id (`'openframe-docs'`, `'data-room-docs'`, …). Flowed through
33
+ * `renderContent`'s handlers for `/api/resolve-link` POSTs.
34
+ */
35
+ sourceId: DocSourceId
36
+
37
+ /**
38
+ * Render the content body. The page shell owns this — it picks the markdown
39
+ * renderer, dispatches PDF/Figma/Sheets/file branches, etc. No renderer
40
+ * interface in between.
41
+ */
42
+ renderContent: (content: DocContent, handlers: DocRenderHandlers) => React.ReactNode
43
+
44
+ /**
45
+ * Render the loading skeleton. Receives the selected node's `documentType`
46
+ * (undefined while structure is still loading) so the caller can return a
47
+ * markdown-shaped skeleton vs an embed-shaped skeleton.
48
+ */
49
+ renderSkeleton: (documentType: DocNode['documentType']) => React.ReactNode
50
+
51
+ /**
52
+ * Chat-source identifier — passed in by the page shell from server-side
53
+ * `currentPlatform()`. Lib has no platform context; the page shell is the
54
+ * trusted boundary that wires this. NEVER pass user input here.
55
+ */
56
+ chatSource: string
57
+
58
+ title: string | React.ReactNode
59
+ /** Override the default ODS palette. Optional — most callers should omit. */
60
+ colorPalette?: typeof DEFAULT_DOC_VIEWER_PALETTE
61
+ className?: string
62
+
63
+ /** Initial doc path (URL `[...path]`). */
64
+ docPath?: string
65
+
66
+ /** Sidebar header copy (`'DOCUMENTATION'`, `'DATA ROOM'`). */
67
+ sidebarLabel?: string
68
+
69
+ /**
70
+ * API endpoint for fetching the document tree structure. Defaults to the
71
+ * dispatcher path `/api/docs/sources/${sourceId}/structure`. Override only
72
+ * if hosting the viewer behind a different route.
73
+ */
74
+ structureEndpoint?: string
75
+ /** Same shape as `structureEndpoint`. Defaults to `/api/docs/sources/${sourceId}/content`. */
76
+ contentEndpoint?: string
77
+ /** Base route path for URL navigation. */
78
+ baseRoute: string
79
+
80
+ /** Empty state copy when no doc is selected. */
81
+ emptyStateText?: string
82
+
83
+ /** Whether to render the doc-search bar (bound to chat). */
84
+ showAIChat?: boolean
85
+
86
+ /** Folder-index filename (default `'README.md'`). */
87
+ folderIndexFile?: string
88
+ }
89
+
90
+ export function DocViewer(props: DocViewerProps) {
91
+ return <DocViewerContent {...props} />
92
+ }
93
+
94
+ function DocViewerContent({
95
+ sourceId,
96
+ renderContent,
97
+ renderSkeleton,
98
+ chatSource,
99
+ title,
100
+ colorPalette = DEFAULT_DOC_VIEWER_PALETTE,
101
+ className = "",
102
+ docPath,
103
+ sidebarLabel = "DOCUMENTATION",
104
+ structureEndpoint,
105
+ contentEndpoint,
106
+ baseRoute,
107
+ emptyStateText,
108
+ showAIChat = false,
109
+ folderIndexFile,
110
+ }: DocViewerProps) {
111
+ // Default endpoints derived from sourceId. Hub callers omit the props in 99%
112
+ // of cases; the override is for embed contexts where the doc-viewer sits
113
+ // behind a non-standard route.
114
+ const resolvedStructureEndpoint =
115
+ structureEndpoint ?? `/api/docs/sources/${sourceId}/structure`
116
+ const resolvedContentEndpoint =
117
+ contentEndpoint ?? `/api/docs/sources/${sourceId}/content`
118
+ const {
119
+ structure,
120
+ selectedPath,
121
+ content,
122
+ isLoadingStructure,
123
+ isLoadingContent,
124
+ error,
125
+ expandedNodes,
126
+ selectNode,
127
+ toggleNode,
128
+ navigateToDoc,
129
+ } = useDocumentTree(
130
+ {
131
+ structureEndpoint: resolvedStructureEndpoint,
132
+ contentEndpoint: resolvedContentEndpoint,
133
+ baseRoute,
134
+ folderIndexFile,
135
+ },
136
+ docPath,
137
+ )
138
+
139
+ const { activeSection, handleSectionClick } = useScrollSpy(content?.sections)
140
+
141
+ const docNav = useDocNavigation()
142
+ const docSearch = useDocSearch({
143
+ source: chatSource,
144
+ baseRoute,
145
+ onNavigate: (path) => navigateToDoc(path, { fromInternalLink: true }),
146
+ onInPageSwap: (path) => docNav.navigate(path),
147
+ })
148
+
149
+ const renderedContent = useMemo(() => {
150
+ if (!content) return null
151
+ return renderContent(content, {
152
+ onInternalLinkClick: navigateToDoc,
153
+ currentPath: selectedPath,
154
+ sourceId,
155
+ })
156
+ }, [content, selectedPath, renderContent, navigateToDoc, sourceId])
157
+
158
+ // Selected node's documentType drives:
159
+ // - which skeleton the caller renders during fetch (markdown vs embed)
160
+ // - the article max-width + sticky-nav visibility (markdown only)
161
+ // `undefined` documentType is treated as `'markdown'` (per the DocNode
162
+ // discriminator's documented default).
163
+ const selectedNodeDocType =
164
+ selectedPath && structure.length > 0
165
+ ? findDocNodeByPath(selectedPath, structure)?.documentType
166
+ : undefined
167
+ // During loading, the in-flight content's type isn't known yet — fall back
168
+ // to the selected node's type (or markdown if neither is set).
169
+ const activeDocType = content?.documentType ?? selectedNodeDocType
170
+ const isMarkdownContent = !activeDocType || activeDocType === 'markdown'
171
+ const showStickyNav = isMarkdownContent
172
+
173
+ const stickyNavSections =
174
+ content?.sections?.map((s) => ({ id: s.id, label: s.title })) ?? []
175
+
176
+ const isColorValue =
177
+ colorPalette.background.startsWith('#') ||
178
+ colorPalette.background.startsWith('rgb') ||
179
+ colorPalette.background.startsWith('var(')
180
+
181
+ const bgStyle = isColorValue ? { backgroundColor: colorPalette.background } : {}
182
+ const bgClass = !isColorValue ? colorPalette.background : ''
183
+ const containerBgStyle =
184
+ colorPalette.containerBackground !== 'transparent'
185
+ ? { backgroundColor: colorPalette.containerBackground }
186
+ : {}
187
+
188
+ const defaultEmptyText =
189
+ structure.length > 0
190
+ ? 'Select a document from the sidebar to view'
191
+ : 'No documents yet. Add content from the admin panel.'
192
+ const resolvedEmptyText = emptyStateText || defaultEmptyText
193
+
194
+ return (
195
+ <section className={`${bgClass} ${className}`} style={bgStyle}>
196
+ <div
197
+ className="max-w-[1920px] px-6 md:px-20 py-6 md:py-10 mx-auto"
198
+ style={containerBgStyle}
199
+ >
200
+ <div className="flex flex-col gap-6">
201
+ <div className="flex flex-col gap-4">
202
+ {typeof title === 'string' ? <PageHeading>{title}</PageHeading> : title}
203
+ </div>
204
+
205
+ {showAIChat && (
206
+ <DocSearchBar
207
+ placeholder={`Search ${sidebarLabel?.toLowerCase() || 'documents'}...`}
208
+ query={docSearch.query}
209
+ onQueryChange={docSearch.setQuery}
210
+ results={docSearch.results}
211
+ isLoading={docSearch.isLoading}
212
+ onResultSelect={docSearch.handleResultSelect}
213
+ showDropdown={docSearch.keepDropdownOpen}
214
+ />
215
+ )}
216
+
217
+ {error && (
218
+ <div className="flex justify-center">
219
+ <div className="rounded-lg border bg-ods-card p-8 text-center max-w-md border-ods-border">
220
+ <h2 className="text-xl font-semibold text-ods-text-primary">
221
+ Error Loading Documents
222
+ </h2>
223
+ <p className="mt-2 text-ods-text-secondary">{error}. Please try again later.</p>
224
+ </div>
225
+ </div>
226
+ )}
227
+
228
+ {!error && (
229
+ <div className="flex flex-col lg:flex-row gap-6 lg:gap-10 items-start flex-1">
230
+ <div className="w-full lg:w-[320px] lg:shrink-0">
231
+ <div className="lg:sticky lg:top-20">
232
+ {isLoadingStructure ? (
233
+ <CategorySidebarSkeleton />
234
+ ) : (
235
+ <>
236
+ <PersistentMobileDropdown isLoading={false}>
237
+ <MobileNavigationDropdown
238
+ nodes={structure}
239
+ selectedPath={selectedPath}
240
+ expandedNodes={expandedNodes}
241
+ onNodeClick={selectNode}
242
+ onToggleExpand={toggleNode}
243
+ isLoading={false}
244
+ folderIndexFile={folderIndexFile}
245
+ />
246
+ </PersistentMobileDropdown>
247
+
248
+ <PersistentSidebar isLoading={false}>
249
+ <div className="hidden lg:block">
250
+ <div className="space-y-4">
251
+ <h3 className="text-[14px] font-['Azeret_Mono'] font-semibold uppercase text-ods-text-secondary tracking-[-0.02em] leading-[1.43em]">
252
+ {sidebarLabel}
253
+ </h3>
254
+ <MultiLevelNavigation
255
+ nodes={structure}
256
+ selectedPath={selectedPath}
257
+ expandedNodes={expandedNodes}
258
+ onNodeClick={selectNode}
259
+ onToggleExpand={toggleNode}
260
+ isLoading={false}
261
+ folderIndexFile={folderIndexFile}
262
+ />
263
+ </div>
264
+ </div>
265
+ </PersistentSidebar>
266
+ </>
267
+ )}
268
+ </div>
269
+ </div>
270
+
271
+ <div className="flex-1 min-w-0 w-full">
272
+ <div
273
+ className={`grid grid-cols-1 ${
274
+ (showStickyNav && stickyNavSections.length > 0) ||
275
+ isLoadingContent ||
276
+ isLoadingStructure
277
+ ? 'lg:grid-cols-[1fr_280px]'
278
+ : ''
279
+ } gap-8`}
280
+ >
281
+ <div className={`w-full ${isMarkdownContent ? 'max-w-4xl mx-auto' : ''}`}>
282
+ <article className="space-y-2">
283
+ {(isLoadingContent || isLoadingStructure) ? (
284
+ renderSkeleton(selectedNodeDocType)
285
+ ) : !content ? (
286
+ <div className="text-center py-16">
287
+ <p className="text-xl text-ods-text-secondary">{resolvedEmptyText}</p>
288
+ </div>
289
+ ) : (
290
+ renderedContent
291
+ )}
292
+ </article>
293
+ </div>
294
+
295
+ {(isLoadingContent || isLoadingStructure) && (
296
+ <div className="hidden lg:block">
297
+ <div className="sticky top-24">
298
+ <div className="h-[14px] w-28 bg-ods-border rounded animate-pulse mb-5" />
299
+ <div className="space-y-0">
300
+ {[130, 170, 190, 220, 110, 200, 80, 100, 120, 140, 90].map((w, i) => (
301
+ <div
302
+ key={i}
303
+ className={`py-[13px] pl-3 border-l-2 ${
304
+ i === 0 ? 'border-ods-accent' : 'border-transparent'
305
+ }`}
306
+ >
307
+ <div
308
+ className="h-[13px] bg-ods-border rounded animate-pulse"
309
+ style={{ width: w }}
310
+ />
311
+ </div>
312
+ ))}
313
+ </div>
314
+ </div>
315
+ </div>
316
+ )}
317
+
318
+ {showStickyNav &&
319
+ content &&
320
+ stickyNavSections.length > 0 &&
321
+ !isLoadingContent && (
322
+ <div className="hidden lg:block">
323
+ <div className="sticky top-24">
324
+ <h3 className="text-[14px] font-['Azeret_Mono'] font-semibold uppercase text-ods-text-secondary tracking-[-0.02em] leading-[1.43em] mb-4">
325
+ ON THIS PAGE
326
+ </h3>
327
+ <StickySectionNav
328
+ sections={stickyNavSections}
329
+ activeSection={activeSection}
330
+ onSectionClick={handleSectionClick}
331
+ ribbonPosition="left"
332
+ ribbonColor="var(--ods-accent)"
333
+ />
334
+ </div>
335
+ </div>
336
+ )}
337
+ </div>
338
+ </div>
339
+ </div>
340
+ )}
341
+ </div>
342
+ </div>
343
+ </section>
344
+ )
345
+ }
@@ -0,0 +1,12 @@
1
+ "use client"
2
+
3
+ export { DocViewer } from './doc-viewer'
4
+ export type { DocViewerProps } from './doc-viewer'
5
+
6
+ export { useDocumentTree } from './use-document-tree'
7
+ export type { UseDocumentTreeConfig } from './use-document-tree'
8
+
9
+ export { useScrollSpy } from './use-scroll-spy'
10
+
11
+ export { DocNavigationProvider, useDocNavigation } from './doc-navigation-context'
12
+ export type { DocNavigator } from './doc-navigation-context'