@flamingo-stack/openframe-frontend-core 0.0.847 → 0.0.849

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 (188) hide show
  1. package/dist/build/export-map.json +1 -1
  2. package/dist/{case-study-card-DJOHIHYU.cjs → case-study-card-X2HKPKT7.cjs} +5 -5
  3. package/dist/{chunk-CNMXP77I.cjs → chunk-36DUCHI7.cjs} +4 -4
  4. package/dist/{chunk-MPVOPSU6.cjs → chunk-36XWCVSM.cjs} +96 -96
  5. package/dist/{chunk-J2IXYCEH.cjs → chunk-3W4ASAXN.cjs} +15 -15
  6. package/dist/{chunk-6LDL3UZE.cjs → chunk-4YBS4HIO.cjs} +20 -20
  7. package/dist/{chunk-ABL5OBSF.cjs → chunk-533QK4R6.cjs} +296 -250
  8. package/dist/{chunk-QK2A334V.cjs → chunk-5CBI2ZYV.cjs} +12 -12
  9. package/dist/{chunk-D5DMWXF2.cjs → chunk-5PIR77V5.cjs} +4 -4
  10. package/dist/{chunk-HPXET3LH.cjs → chunk-6AJYGXXS.cjs} +27 -27
  11. package/dist/{chunk-4R4HAFXI.cjs → chunk-ALRYVZ3E.cjs} +6 -6
  12. package/dist/{chunk-FLW4WULE.cjs → chunk-BU2PSRAS.cjs} +17 -61
  13. package/dist/{chunk-HBDLJUI3.cjs → chunk-C3P7IUTU.cjs} +39 -14
  14. package/dist/{chunk-SSSI2RBV.cjs → chunk-FNQWL4W4.cjs} +14 -14
  15. package/dist/{chunk-TBOWFTEN.cjs → chunk-FRG724DU.cjs} +22 -22
  16. package/dist/{chunk-SIFRIUPW.cjs → chunk-FXLROQUT.cjs} +345 -356
  17. package/dist/{chunk-3V5JWG27.cjs → chunk-GG466XFQ.cjs} +2 -2
  18. package/dist/{chunk-XCVH5BCZ.cjs → chunk-GRCOEICK.cjs} +6 -6
  19. package/dist/{chunk-5GBLXY5F.cjs → chunk-HTYJUMFB.cjs} +7 -7
  20. package/dist/{chunk-URP6GYTD.cjs → chunk-IG6ROHPS.cjs} +56 -56
  21. package/dist/{chunk-DGQDGCQH.cjs → chunk-IHY7E4EX.cjs} +2 -2
  22. package/dist/{chunk-WCK5T3UD.cjs → chunk-IKOL6NJY.cjs} +101 -69
  23. package/dist/{chunk-RAJ5FYFL.cjs → chunk-IN3LA2FU.cjs} +14 -14
  24. package/dist/{chunk-ZTO7KNYV.cjs → chunk-KXBZCUDW.cjs} +35 -35
  25. package/dist/{chunk-57JYB3E2.cjs → chunk-MC3L525K.cjs} +3 -3
  26. package/dist/{chunk-BIOSHDXQ.cjs → chunk-MDLT6D46.cjs} +61 -61
  27. package/dist/{chunk-3TOWCFIK.cjs → chunk-PMUCEUFQ.cjs} +13 -1
  28. package/dist/{chunk-PV3NKXWS.cjs → chunk-PV2XQJ2R.cjs} +99 -178
  29. package/dist/{chunk-J2ZSGOUC.cjs → chunk-R5JPZ6TY.cjs} +27 -27
  30. package/dist/{chunk-744JXTDP.cjs → chunk-SA2NNWIJ.cjs} +23 -23
  31. package/dist/{chunk-DEEQSVLX.cjs → chunk-SHQY4GOE.cjs} +90 -90
  32. package/dist/{chunk-BHOVF4KP.cjs → chunk-SOSIXRQ3.cjs} +2 -2
  33. package/dist/{chunk-Y2OEC7UW.cjs → chunk-TEKN6TJ5.cjs} +40 -40
  34. package/dist/{chunk-PZNWIJRZ.cjs → chunk-TIVPOVIM.cjs} +2 -2
  35. package/dist/{chunk-BZZE4KIV.cjs → chunk-TX4NTH22.cjs} +155 -136
  36. package/dist/{chunk-NFNSPLGC.cjs → chunk-W7OFAROY.cjs} +14 -9
  37. package/dist/{chunk-N2A24FDF.cjs → chunk-YLETC7LP.cjs} +994 -953
  38. package/dist/{chunk-YC52LSPP.cjs → chunk-YYHQY3K6.cjs} +40 -40
  39. package/dist/{chunk-2FUTZKLB.cjs → chunk-Z5UCDEKZ.cjs} +6 -6
  40. package/dist/{chunk-SWF6TNZ3.cjs → chunk-ZGEWT3SF.cjs} +2 -2
  41. package/dist/{chunk-PQDCSZ3W.cjs → chunk-ZY34R37A.cjs} +85 -85
  42. package/dist/{chunk-5SZ4ILU3.cjs → chunk-ZZDFMUEU.cjs} +3 -3
  43. package/dist/{cloud-tenants-XEUUMABT.cjs → cloud-tenants-HKUUEUAY.cjs} +21 -21
  44. package/dist/components/case-studies/index.cjs +36 -36
  45. package/dist/components/chat/chat-composer.d.ts.map +1 -1
  46. package/dist/components/chat/chat-composer.js +4 -3
  47. package/dist/components/chat/chat-container.d.ts.map +1 -1
  48. package/dist/components/chat/chat-container.js +2 -2
  49. package/dist/components/chat/entity-cards/design-doc-card.d.ts.map +1 -1
  50. package/dist/components/chat/entity-cards/design-doc-card.js +5 -17
  51. package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
  52. package/dist/components/chat/entity-cards/dispatch.js +5 -7
  53. package/dist/components/chat/index.cjs +15 -15
  54. package/dist/components/contact/index.cjs +22 -22
  55. package/dist/components/docs/doc-viewer.d.ts +7 -7
  56. package/dist/components/docs/doc-viewer.d.ts.map +1 -1
  57. package/dist/components/docs/doc-viewer.js +29 -16
  58. package/dist/components/docs/docs-hub-page.d.ts +1 -1
  59. package/dist/components/docs/docs-hub-page.d.ts.map +1 -1
  60. package/dist/components/docs/docs-hub-page.js +4 -11
  61. package/dist/components/docs/index.cjs +25 -25
  62. package/dist/components/docs/use-document-tree.d.ts.map +1 -1
  63. package/dist/components/docs/use-document-tree.js +24 -24
  64. package/dist/components/embeds/claude-embed.d.ts +2 -2
  65. package/dist/components/embeds/claude-embed.d.ts.map +1 -1
  66. package/dist/components/embeds/claude-embed.js +3 -3
  67. package/dist/components/embeds/embed-iframe.d.ts +5 -3
  68. package/dist/components/embeds/embed-iframe.d.ts.map +1 -1
  69. package/dist/components/embeds/embed-iframe.js +4 -3
  70. package/dist/components/embeds/embed-viewer-frame.d.ts +12 -9
  71. package/dist/components/embeds/embed-viewer-frame.d.ts.map +1 -1
  72. package/dist/components/embeds/embed-viewer-frame.js +21 -7
  73. package/dist/components/embeds/figma-embed.d.ts +4 -2
  74. package/dist/components/embeds/figma-embed.d.ts.map +1 -1
  75. package/dist/components/embeds/figma-embed.js +4 -3
  76. package/dist/components/embeds/file-download-card.d.ts +3 -1
  77. package/dist/components/embeds/file-download-card.d.ts.map +1 -1
  78. package/dist/components/embeds/file-download-card.js +18 -3
  79. package/dist/components/embeds/google-sheets-viewer.d.ts +3 -1
  80. package/dist/components/embeds/google-sheets-viewer.d.ts.map +1 -1
  81. package/dist/components/embeds/google-sheets-viewer.js +7 -4
  82. package/dist/components/embeds/index.cjs +22 -22
  83. package/dist/components/embeds/pdf-viewer.d.ts +3 -1
  84. package/dist/components/embeds/pdf-viewer.d.ts.map +1 -1
  85. package/dist/components/embeds/pdf-viewer.js +6 -2
  86. package/dist/components/faq/index.cjs +23 -23
  87. package/dist/components/features/index.cjs +21 -21
  88. package/dist/components/features/status-filter-component.d.ts +7 -1
  89. package/dist/components/features/status-filter-component.d.ts.map +1 -1
  90. package/dist/components/features/status-filter-component.js +9 -3
  91. package/dist/components/help-center-pages/index.cjs +140 -140
  92. package/dist/components/index.cjs +137 -135
  93. package/dist/components/layout/back-button.d.ts +7 -1
  94. package/dist/components/layout/back-button.d.ts.map +1 -1
  95. package/dist/components/layout/back-button.js +34 -2
  96. package/dist/components/loading/unified-skeleton.d.ts +6 -1
  97. package/dist/components/loading/unified-skeleton.d.ts.map +1 -1
  98. package/dist/components/loading/unified-skeleton.js +13 -1
  99. package/dist/components/meeting-scheduler/index.cjs +119 -119
  100. package/dist/components/navigation/index.cjs +24 -24
  101. package/dist/components/onboarding-guides/index.cjs +24 -24
  102. package/dist/components/product-demo/index.cjs +33 -33
  103. package/dist/components/related-content/index.cjs +11 -11
  104. package/dist/components/tickets/index.cjs +28 -28
  105. package/dist/components/ui/file-manager/file-manager-table.d.ts.map +1 -1
  106. package/dist/components/ui/file-manager/file-manager-table.js +59 -50
  107. package/dist/components/ui/file-manager/index.cjs +71 -62
  108. package/dist/components/ui/index.cjs +23 -21
  109. package/dist/components/ui/markdown/engine.d.ts +5 -4
  110. package/dist/components/ui/markdown/engine.d.ts.map +1 -1
  111. package/dist/components/ui/markdown/engine.js +15 -15
  112. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +1 -1
  113. package/dist/components/ui/markdown/rich/rich-markdown-renderer.js +7 -3
  114. package/dist/components/ui/people-cell.d.ts +9 -2
  115. package/dist/components/ui/people-cell.d.ts.map +1 -1
  116. package/dist/components/ui/people-cell.js +39 -23
  117. package/dist/components/ui/progress-bar.d.ts +6 -0
  118. package/dist/components/ui/progress-bar.d.ts.map +1 -1
  119. package/dist/components/ui/progress-bar.js +2 -0
  120. package/dist/components/ui/tag.d.ts +8 -0
  121. package/dist/components/ui/tag.d.ts.map +1 -1
  122. package/dist/components/ui/tag.js +5 -0
  123. package/dist/{devices-7YDIHVVP.cjs → devices-P7M4Z7VP.cjs} +22 -22
  124. package/dist/embed-shims/index.cjs +3 -3
  125. package/dist/embed-shims/next-image.cjs +3 -3
  126. package/dist/{embeddable-chat-Z3S37SSM.cjs → embeddable-chat-322NDFP5.cjs} +94 -94
  127. package/dist/hooks/index.cjs +6 -6
  128. package/dist/index.cjs +32 -30
  129. package/dist/{knowledge-SQVPQSKM.cjs → knowledge-TC7JLOWI.cjs} +22 -22
  130. package/dist/{logs-M5DFGUBC.cjs → logs-GPOYCP4B.cjs} +28 -28
  131. package/dist/{policies-T3VQEJUD.cjs → policies-ZUHOUMDD.cjs} +25 -25
  132. package/dist/{remote-session-EE6477O3.cjs → remote-session-NT4NG7EF.cjs} +10 -10
  133. package/dist/{scripts-JFRFJKPH.cjs → scripts-FOEVDGTH.cjs} +28 -28
  134. package/dist/{software-update-E7W3AAIY.cjs → software-update-XGTMNFST.cjs} +25 -25
  135. package/dist/{tickets-board-44HO3LR4.cjs → tickets-board-7FLGQEHF.cjs} +10 -10
  136. package/dist/types/design-doc.d.ts +45 -8
  137. package/dist/types/design-doc.d.ts.map +1 -1
  138. package/dist/types/doc-source.d.ts +2 -2
  139. package/dist/types/doc-source.d.ts.map +1 -1
  140. package/dist/utils/design-doc-readiness.d.ts +17 -15
  141. package/dist/utils/design-doc-readiness.d.ts.map +1 -1
  142. package/dist/utils/design-doc-readiness.js +5 -11
  143. package/dist/utils/doc-tree-nav.d.ts +7 -0
  144. package/dist/utils/doc-tree-nav.d.ts.map +1 -1
  145. package/dist/utils/doc-tree-nav.js +19 -0
  146. package/dist/utils/index.cjs +30 -13
  147. package/dist/utils/list-url.d.ts.map +1 -1
  148. package/dist/utils/list-url.js +2 -1
  149. package/dist/utils/source-grouping.d.ts +2 -2
  150. package/dist/utils/source-grouping.d.ts.map +1 -1
  151. package/dist/utils/source-grouping.js +1 -1
  152. package/dist/utils/source-icons.d.ts.map +1 -1
  153. package/dist/utils/source-icons.js +3 -0
  154. package/package.json +1 -1
  155. package/src/components/chat/chat-composer.tsx +10 -3
  156. package/src/components/chat/chat-container.tsx +6 -4
  157. package/src/components/chat/entity-cards/design-doc-card.tsx +12 -17
  158. package/src/components/chat/entity-cards/dispatch.tsx +10 -10
  159. package/src/components/docs/doc-viewer.tsx +56 -35
  160. package/src/components/docs/docs-hub-page.tsx +13 -37
  161. package/src/components/docs/use-document-tree.ts +33 -23
  162. package/src/components/embeds/claude-embed.tsx +4 -4
  163. package/src/components/embeds/embed-iframe.tsx +8 -5
  164. package/src/components/embeds/embed-viewer-frame.tsx +35 -15
  165. package/src/components/embeds/figma-embed.tsx +9 -4
  166. package/src/components/embeds/file-download-card.tsx +28 -4
  167. package/src/components/embeds/google-sheets-viewer.tsx +9 -3
  168. package/src/components/embeds/pdf-viewer.tsx +9 -2
  169. package/src/components/features/status-filter-component.tsx +15 -0
  170. package/src/components/layout/back-button.tsx +54 -2
  171. package/src/components/loading/unified-skeleton.tsx +18 -2
  172. package/src/components/ui/file-manager/file-manager-table.tsx +8 -3
  173. package/src/components/ui/markdown/engine.tsx +21 -30
  174. package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +4 -0
  175. package/src/components/ui/people-cell.tsx +53 -17
  176. package/src/components/ui/progress-bar.tsx +8 -0
  177. package/src/components/ui/tag.tsx +14 -0
  178. package/src/types/design-doc.ts +59 -10
  179. package/src/types/doc-source.ts +2 -2
  180. package/src/utils/design-doc-readiness.ts +22 -23
  181. package/src/utils/doc-tree-nav.ts +27 -0
  182. package/src/utils/list-url.ts +2 -1
  183. package/src/utils/source-grouping.ts +2 -2
  184. package/src/utils/source-icons.ts +3 -0
  185. package/dist/components/docs/skeletons.d.ts +0 -25
  186. package/dist/components/docs/skeletons.d.ts.map +0 -1
  187. package/dist/components/docs/skeletons.js +0 -83
  188. package/src/components/docs/skeletons.tsx +0 -131
@@ -24,13 +24,13 @@
24
24
  import type { ChatSource } from '../components/chat/types/message.types';
25
25
  /**
26
26
  * Doc-table documentTypes: whole documents with their own viewer (markdown =
27
- * product docs, data_room_doc = data room). They carry an in-app `path` (handed
27
+ * product docs, data_room_doc = data room, policy_doc = company policies). They carry an in-app `path` (handed
28
28
  * to the host's composer as the slug), and a server never folds them
29
29
  * into one citation (`groupsByTable`); the strip still draws them as one chip
30
30
  * per table. The hub's per-source contract test pins this list to its
31
31
  * doc-table configs.
32
32
  */
33
- export declare const DOC_TABLE_TYPES: readonly ["markdown", "data_room_doc"];
33
+ export declare const DOC_TABLE_TYPES: readonly ["markdown", "data_room_doc", "policy_doc"];
34
34
  /**
35
35
  * Which tables a SERVER folds into one citation: rows of a knowledge table
36
36
  * share one number (and reach the model as one table) unless they are whole
@@ -1 +1 @@
1
- {"version":3,"file":"source-grouping.d.ts","sourceRoot":"","sources":["../../src/utils/source-grouping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wCAAwC,CAAC;AAGzE;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,wCAAyC,CAAC;AAEtE;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE9E;AAED,0EAA0E;AAC1E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,KAAK,WAAW,GAAG,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5D;2EAC2E;AAC3E,MAAM,MAAM,gBAAgB,GAAG,WAAW,CAAC;AAE3C;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,gBAAgB,EAAE,CAAC;IACzB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,GAAG,UAAU,CAcb;AA0DD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAqCvE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAU/D"}
1
+ {"version":3,"file":"source-grouping.d.ts","sourceRoot":"","sources":["../../src/utils/source-grouping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wCAAwC,CAAC;AAGzE;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,sDAAuD,CAAC;AAEpF;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE9E;AAED,0EAA0E;AAC1E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,KAAK,WAAW,GAAG,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5D;2EAC2E;AAC3E,MAAM,MAAM,gBAAgB,GAAG,WAAW,CAAC;AAE3C;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,gBAAgB,EAAE,CAAC;IACzB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,GAAG,UAAU,CAcb;AA0DD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAqCvE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAU/D"}
@@ -1,5 +1,5 @@
1
1
  import { defaultDocumentTypeForTableId, getSourceLabel } from "./source-icons.js";
2
- const DOC_TABLE_TYPES = ["markdown", "data_room_doc"];
2
+ const DOC_TABLE_TYPES = ["markdown", "data_room_doc", "policy_doc"];
3
3
  function groupsByTable(documentType) {
4
4
  return !DOC_TABLE_TYPES.includes(documentType ?? "");
5
5
  }
@@ -1 +1 @@
1
- {"version":3,"file":"source-icons.d.ts","sourceRoot":"","sources":["../../src/utils/source-icons.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAqFpD,CAAC;AAEF;;6DAE6D;AAC7D,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAGxF;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA4EzD,CAAC;AAEF;;kEAEkE;AAClE,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,iCAAiC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAsEpE,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEjF;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK5E"}
1
+ {"version":3,"file":"source-icons.d.ts","sourceRoot":"","sources":["../../src/utils/source-icons.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAsFpD,CAAC;AAEF;;6DAE6D;AAC7D,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAGxF;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA6EzD,CAAC;AAEF;;kEAEkE;AAClE,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,iCAAiC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAuEpE,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEjF;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK5E"}
@@ -2,6 +2,7 @@ const SOURCE_ICON_NAMES = {
2
2
  // Doc tables
3
3
  "openframe-docs": "openframe",
4
4
  "data-room-docs": "shield",
5
+ "company-policies": "book-open",
5
6
  // CMS / programs
6
7
  "blog-posts": "newspaper",
7
8
  "product-releases": "rocket",
@@ -81,6 +82,7 @@ const SOURCE_LABELS_BY_TABLE = {
81
82
  // Doc tables
82
83
  "openframe-docs": "OpenFrame Docs",
83
84
  "data-room-docs": "Data Room",
85
+ "company-policies": "Company Policies",
84
86
  // CMS / programs
85
87
  "blog-posts": "Blog Posts",
86
88
  "product-releases": "Product Releases",
@@ -151,6 +153,7 @@ const DEFAULT_DOCUMENT_TYPE_TO_TABLE_ID = {
151
153
  // Doc tables
152
154
  markdown: "openframe-docs",
153
155
  data_room_doc: "data-room-docs",
156
+ policy_doc: "company-policies",
154
157
  // CMS / programs
155
158
  blog_post: "blog-posts",
156
159
  product_release: "product-releases",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.847",
3
+ "version": "0.0.849",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -14,6 +14,13 @@ import { ModelDisplay } from './model-display';
14
14
  import type { ChatInputRef, ModelDisplayProps } from './types/component.types';
15
15
  import type { ChatContextItem, ChatContextPickerConfig } from './types/context-item.types';
16
16
 
17
+ /**
18
+ * The top strip's corners: the card's radius (`rounded-md`, `--radius` - 2px) less the card's 1px border, so the strip
19
+ * meets the border's inner curve. At the card's own radius it sits 1px inside that curve and leaves a sliver of the
20
+ * card's fill at each corner. The card cannot clip it instead: the picker and the memory popover float out of it.
21
+ */
22
+ const TOP_STRIP_ROUNDING_CLASS = 'rounded-t-[calc(var(--radius)_-_3px)]';
23
+
17
24
  /** Why the composer takes no input, in the user's words — shown where the prompt would be. */
18
25
  export interface ChatComposerLock {
19
26
  placeholder: string;
@@ -248,7 +255,7 @@ export function ChatComposer({
248
255
  // `border-b`) stack flush above the input; the card owns the border
249
256
  // / radius / focus ring. No `overflow-hidden` so the picker and the
250
257
  // memory popover can float above the input (whichever strip comes
251
- // first gets `rounded-t`).
258
+ // first gets `TOP_STRIP_ROUNDING_CLASS`).
252
259
  // `has-[[data-editor]:focus]` (not `focus-within`) so the accent
253
260
  // border reacts ONLY to the composer editor — not the picker's search
254
261
  // input or item buttons, which also live inside this card. The editor
@@ -264,7 +271,7 @@ export function ChatComposer({
264
271
  resolveIcon={resolveContextIcon}
265
272
  disabled={sending || locked}
266
273
  leading={contextLeading}
267
- className="rounded-t-md"
274
+ className={TOP_STRIP_ROUNDING_CLASS}
268
275
  />
269
276
  {selected.length > 0 && (
270
277
  <ChatContextChipStrip
@@ -275,7 +282,7 @@ export function ChatComposer({
275
282
  className={cn(
276
283
  'border-b border-ods-border bg-ods-bg p-2',
277
284
  // Only the TOP strip rounds into the card corner.
278
- memory.length === 0 && !contextLeading && 'rounded-t-md',
285
+ memory.length === 0 && !contextLeading && TOP_STRIP_ROUNDING_CLASS,
279
286
  )}
280
287
  />
281
288
  )}
@@ -111,7 +111,9 @@ const ChatHeader = forwardRef<HTMLDivElement, ChatHeaderProps>(
111
111
  className={cn(cardClasses, 'absolute right-full top-0 mr-[var(--spacing-system-s)] hover:bg-ods-bg-hover')}
112
112
  />
113
113
  )}
114
- <div className={cardClasses}>
114
+ {/* The card clips its content at the border's inner curve, so a filled section at its edge (the MSP card)
115
+ meets the border; nothing in it floats out. */}
116
+ <div className={cn(cardClasses, !bare && 'overflow-hidden')}>
115
117
  <div className="flex items-center justify-between gap-4 px-4 py-3">
116
118
  <div className="flex items-center gap-3">
117
119
  {userIcon && !isLoading ? (
@@ -176,8 +178,8 @@ const ChatHeader = forwardRef<HTMLDivElement, ChatHeaderProps>(
176
178
  </div>
177
179
  {/* MSP branding slot — home-screen only; `ticketInfo` (open chat)
178
180
  claims this space, so it wins when both are provided. The card
179
- already draws the chrome: strip the section's own ring and keep
180
- only the bottom rounding so its bg fits the card corners. */}
181
+ already draws the chrome and clips its corners: strip the
182
+ section's own ring and rounding. */}
181
183
  {!ticketInfo && (mspLoading || mspOrganization) && (
182
184
  <>
183
185
  <div className="h-px bg-ods-border" />
@@ -185,7 +187,7 @@ const ChatHeader = forwardRef<HTMLDivElement, ChatHeaderProps>(
185
187
  name=""
186
188
  {...mspOrganization}
187
189
  loading={mspLoading || !mspOrganization}
188
- className={cn('rounded-none ring-0', !bare && 'rounded-b-md', mspOrganization?.className)}
190
+ className={cn('rounded-none ring-0', mspOrganization?.className)}
189
191
  />
190
192
  </>
191
193
  )}
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * DesignDocCard (pure presentation) — a design doc as the entity card every surface shows it with: the full card
5
5
  * (`default`, the `RoadmapCard` layout without its image slot, since a doc has no image of its own and a constant glyph
6
- * says nothing: title, DRI and date, readiness (the hub's own wording, `design-doc-readiness`), summary, sign-off caption and the people) for admin surfaces, and the chat's compact card (`DesignDocChatCard` in `dispatch.tsx`, which reads
6
+ * says nothing: title, DRI and date, its status (the status a person set, else its readiness: the hub's own wording, `design-doc-readiness`), summary, sign-off caption and the people) for admin surfaces, and the chat's compact card (`DesignDocChatCard` in `dispatch.tsx`, which reads
7
7
  * the same readiness and meta). The card writes no click logic beyond its `href` anchor.
8
8
  */
9
9
 
@@ -12,14 +12,11 @@ import { TEXT_LINES_MIN } from '../../../styles/component-sizes';
12
12
  import type { DesignDoc } from '../../../types/design-doc';
13
13
  import { cn } from '../../../utils/cn';
14
14
  import { formatDateShort } from '../../../utils/date-formatters';
15
- import {
16
- DESIGN_DOC_READINESS_DISPLAY,
17
- designDocReadiness,
18
- formatCompletionLabelWithBlockers,
19
- } from '../../../utils/design-doc-readiness';
15
+ import { designDocStatusDisplay, formatCompletionLabelWithBlockers } from '../../../utils/design-doc-readiness';
20
16
  import { TextLineSkeleton } from '../../loading/unified-skeleton';
21
17
  import { AvatarStack, type AvatarStackPerson } from '../../ui/avatar-stack';
22
18
  import { useLoadingScope } from '../../ui/loading-scope';
19
+ import { PaletteBadge } from '../../ui/palette-badge';
23
20
  import { StatusBadge } from '../../ui/status-badge';
24
21
  import { safeHref } from '../utils/compact-card-classes';
25
22
  import { CONTENT_CARD_FRAME_CLASS, CONTENT_CARD_SKELETON_FRAME_CLASS } from './content-card-frame';
@@ -102,7 +99,7 @@ function DesignDocCardSkeleton({ className }: { className?: string } = {}) {
102
99
  }
103
100
 
104
101
  export function DesignDocCard({ doc, href, target, rel, className }: DesignDocCardProps) {
105
- const readiness = DESIGN_DOC_READINESS_DISPLAY[designDocReadiness(doc.completion)];
102
+ const status = designDocStatusDisplay(doc);
106
103
  const people = designDocPeople(doc);
107
104
  const link = safeHref(href ?? null);
108
105
  if (useLoadingScope()) return <DesignDocCardSkeleton className={className} />;
@@ -117,17 +114,15 @@ export function DesignDocCard({ doc, href, target, rel, className }: DesignDocCa
117
114
  <p className="truncate text-ods-text-secondary text-h5">{designDocMetaLine(doc)}</p>
118
115
  </div>
119
116
  </div>
120
- <StatusBadge
121
- text={readiness.label}
122
- colorScheme={readiness.scheme}
123
- variant="button"
124
- singleLine
125
- className="hidden md:inline-flex"
126
- />
127
- </div>
128
- <div className="md:hidden">
129
- <StatusBadge text={readiness.label} colorScheme={readiness.scheme} variant="button" singleLine />
117
+ {status ? (
118
+ <PaletteBadge text={status.label} color={status.color_scheme} className="hidden md:inline-flex" />
119
+ ) : null}
130
120
  </div>
121
+ {status ? (
122
+ <div className="md:hidden">
123
+ <PaletteBadge text={status.label} color={status.color_scheme} />
124
+ </div>
125
+ ) : null}
131
126
  <div className={DESIGN_DOC_CARD_SUMMARY_ROW}>
132
127
  <p className="line-clamp-3 text-ods-text-secondary text-h4">{doc.summary || ''}</p>
133
128
  </div>
@@ -48,11 +48,7 @@ import {
48
48
  } from '../../../types/trust-center';
49
49
  import { isAppOriginUrl } from '../../../utils/app-origins';
50
50
  import { formatDateShort } from '../../../utils/date-formatters';
51
- import {
52
- DESIGN_DOC_READINESS_DISPLAY,
53
- designDocReadiness,
54
- formatCompletionLabel,
55
- } from '../../../utils/design-doc-readiness';
51
+ import { designDocStatusDisplay, formatCompletionLabel } from '../../../utils/design-doc-readiness';
56
52
  import { faqItemAnchor } from '../../../utils/faq-anchor';
57
53
  import { formatDateUTC as formatDate } from '../../../utils/format';
58
54
  import { programMetaFormatters, programMetaLine } from '../../../utils/program-instant';
@@ -88,6 +84,7 @@ import type { LoadedProductReleaseCardProps } from '../../shared/product-release
88
84
  import { ProductReleaseCard } from '../../shared/product-release/product-release-card';
89
85
  import type { ActionsMenuGroup } from '../../ui/actions-menu';
90
86
  import { ExternalMediaNotice } from '../../ui/markdown/external-media-notice';
87
+ import { tagVariantForBadgeColor } from '../../ui/tag';
91
88
  import { VendorIcon } from '../../vendor-icon';
92
89
  import { useChatPanel } from '../chat-panel-context';
93
90
  import type { ChatRef } from '../chat-ref.types';
@@ -1046,8 +1043,8 @@ function CustomerInterviewChatCard({
1046
1043
  }
1047
1044
 
1048
1045
  /**
1049
- * Design doc → the doc glyph, its readiness as the pill (the hub's own wording: "Ready to build" / "Not ready",
1050
- * `design-doc-readiness`) and "DRI · updated · n/m reviews signed off" under the title. The hub's card route attaches the doc's list row as
1046
+ * Design doc → the doc glyph, its status as the pill (the status a person set, else its readiness, both vocabulary rows the hub resolves onto the
1047
+ * doc: `designDocStatusDisplay`) and "DRI · updated · n/m signed off" under the title. The hub's card route attaches the doc's list row as
1051
1048
  * `item.doc`; a row without it (an older hub) renders the ref's own title and preview.
1052
1049
  */
1053
1050
  function DesignDocChatCard({
@@ -1063,7 +1060,7 @@ function DesignDocChatCard({
1063
1060
  }) {
1064
1061
  const displayRef = fetchedItemDisplayRef(item, chatRef);
1065
1062
  const doc = item?.doc;
1066
- const readiness = doc ? DESIGN_DOC_READINESS_DISPLAY[designDocReadiness(doc.completion)] : null;
1063
+ const status = doc ? designDocStatusDisplay(doc) : null;
1067
1064
  return (
1068
1065
  <MingoInfoCard
1069
1066
  title={doc?.title ?? displayRef.title}
@@ -1074,8 +1071,8 @@ function DesignDocChatCard({
1074
1071
  }
1075
1072
  icon={<FileContentIcon size={24} />}
1076
1073
  status={
1077
- readiness
1078
- ? { label: readiness.label, variant: readiness.scheme === 'success' ? 'success' : 'grey' }
1074
+ status
1075
+ ? { label: status.label, variant: tagVariantForBadgeColor(status.color_scheme) }
1079
1076
  : { label: 'Design doc', variant: 'grey' }
1080
1077
  }
1081
1078
  anchorProps={buildAnchorProps(displayRef.url, isNewTab, displayRef.targetPlatform)}
@@ -1738,6 +1735,9 @@ export const CHAT_CARD_REGISTRY: Record<string, ChatCardRegistryEntry> = {
1738
1735
  data_room_doc: refHydratedEntry('data_room_doc', 'Data-room doc', (displayRef, opts) => (
1739
1736
  <DataRoomDocChatCard chatRef={displayRef} isNewTab={opts.isNewTab} discuss={opts.discuss} />
1740
1737
  )),
1738
+ policy_doc: refHydratedEntry('policy_doc', 'Company policy', (displayRef, opts) => (
1739
+ <DataRoomDocChatCard chatRef={displayRef} isNewTab={opts.isNewTab} discuss={opts.discuss} />
1740
+ )),
1741
1741
  markdown: refHydratedEntry('markdown', 'Doc page (markdown)', (displayRef, opts) => (
1742
1742
  <DataRoomDocChatCard chatRef={displayRef} isNewTab={opts.isNewTab} discuss={opts.discuss} />
1743
1743
  )),
@@ -4,15 +4,15 @@ import type React from 'react';
4
4
  import { useMemo } from 'react';
5
5
  import { useRouter } from '../../embed-shims/next-navigation';
6
6
  import type { DocContent, DocNode, DocRenderHandlers, DocSourceId } from '../../types/doc-source';
7
- import { findDocNodeByPath } from '../../utils/doc-tree-nav';
7
+ import { findDocNodeByPath, rootDocumentPath } from '../../utils/doc-tree-nav';
8
8
  import { PageShell } from '../layout/article-detail-layout';
9
9
  import { PageLayout } from '../layout/page-layout';
10
- import { UnifiedSkeleton } from '../loading/unified-skeleton';
11
10
  import { MultiLevelNavigation, MobileNavigationDropdown } from '../navigation/multi-level-navigation';
12
11
  import { StickySectionNav } from '../navigation/sticky-section-nav';
13
12
  import { PersistentSidebar, PersistentMobileDropdown } from '../persistent-filter-controls';
14
13
  import { DocSearchBar, useDocSearch } from '../shared/doc-search';
15
14
  import { DataSyncLine } from '../ui/data-attribution';
15
+ import { LoadingScope } from '../ui/loading-scope';
16
16
  import { useDocNavigation } from './doc-navigation-context';
17
17
  import { useDocsResolveLink } from './use-docs-resolve-link';
18
18
  import { useDocumentTree } from './use-document-tree';
@@ -46,13 +46,6 @@ export interface DocViewerProps {
46
46
  */
47
47
  renderContent: (content: DocContent, handlers: DocRenderHandlers) => React.ReactNode;
48
48
 
49
- /**
50
- * Render the loading skeleton. Receives the selected node's `documentType`
51
- * (undefined while structure is still loading) so the caller can return a
52
- * markdown-shaped skeleton vs an embed-shaped skeleton.
53
- */
54
- renderSkeleton: (documentType: DocNode['documentType']) => React.ReactNode;
55
-
56
49
  /**
57
50
  * Chat-source identifier — passed in by the page shell from server-side
58
51
  * `currentPlatform()`. Lib has no platform context; the page shell is the
@@ -88,6 +81,13 @@ export interface DocViewerProps {
88
81
  /** Initial doc path (URL `[...path]`). */
89
82
  docPath?: string;
90
83
 
84
+ /**
85
+ * The type of the document at `docPath`, when the page knows it before the tree is read (the host read it on the
86
+ * server). The content's loading state takes its shape from it on a page's first load (a Figma deck draws the embed
87
+ * skeleton, not a text page); once the tree is read, the selected node's own type decides.
88
+ */
89
+ initialDocumentType?: DocNode['documentType'];
90
+
91
91
  /** Sidebar header copy (`'DOCUMENTATION'`, `'DATA ROOM'`). */
92
92
  sidebarLabel?: string;
93
93
 
@@ -113,7 +113,7 @@ export interface DocViewerProps {
113
113
  /** Base route path for URL navigation. */
114
114
  baseRoute: string;
115
115
 
116
- /** Empty state copy when no doc is selected. */
116
+ /** What the viewer says when the source holds no documents yet (default "No documents yet."). */
117
117
  emptyStateText?: string;
118
118
 
119
119
  /** Whether to render the doc-search bar (bound to chat). */
@@ -141,12 +141,13 @@ export function DocViewer(props: DocViewerProps) {
141
141
 
142
142
  /** The "On this page" column's heading: static copy, drawn for real loaded and loading. */
143
143
  const ON_THIS_PAGE_LABEL = 'ON THIS PAGE';
144
+ /** The loading section list's click handler: it has no sections to click. */
145
+ const NO_SECTION_CLICK = () => {};
144
146
  const ON_THIS_PAGE_HEADING_CLASS = 'mb-4 font-semibold text-ods-text-secondary text-h5';
145
147
 
146
148
  function DocViewerContent({
147
149
  sourceId,
148
150
  renderContent,
149
- renderSkeleton,
150
151
  chatSource,
151
152
  title,
152
153
  subtitle,
@@ -154,6 +155,7 @@ function DocViewerContent({
154
155
  className = '',
155
156
  shell = true,
156
157
  docPath,
158
+ initialDocumentType,
157
159
  sidebarLabel = 'DOCUMENTATION',
158
160
  structureEndpoint,
159
161
  contentEndpoint,
@@ -246,11 +248,31 @@ function DocViewerContent({
246
248
  // - the article max-width + sticky-nav visibility (markdown only)
247
249
  // `undefined` documentType is treated as `'markdown'` (per the DocNode
248
250
  // discriminator's documented default).
251
+ // Before the tree is read, the host's `initialDocumentType` (the page's own document) stands in. Once it is read, the
252
+ // root takes the type of the document it shows (`rootDocumentPath`), so a root Figma or PDF never flips to the text
253
+ // layout between the tree arriving and the document arriving.
254
+ const selectedNodePath =
255
+ structure.length > 0 && !selectedPath ? rootDocumentPath(structure, folderIndexFile) : selectedPath;
249
256
  const selectedNodeDocType =
250
- selectedPath && structure.length > 0 ? findDocNodeByPath(selectedPath, structure)?.documentType : undefined;
251
- // During loading, the in-flight content's type isn't known yet — fall back
252
- // to the selected node's type (or markdown if neither is set).
253
- const activeDocType = content?.documentType ?? selectedNodeDocType;
257
+ structure.length > 0
258
+ ? selectedNodePath
259
+ ? findDocNodeByPath(selectedNodePath, structure)?.documentType
260
+ : undefined
261
+ : initialDocumentType;
262
+ // While a document loads, `content` is still the PREVIOUS document's: the layout (the "On this page" column, the
263
+ // article width) follows the document being opened, never the one being left, or opening a Figma deck from a text
264
+ // page draws the text page's column beside the deck's skeleton. Loaded, the content's own type decides.
265
+ const isLoadingDocument = isLoadingContent || isLoadingStructure;
266
+ // While a document loads: its renderer with an EMPTY document of its type (the selected node's, or the host's
267
+ // `initialDocumentType` before the tree is read), drawn in a loading scope, so every renderer shows its own loading
268
+ // form.
269
+ const loadingContent = isLoadingDocument
270
+ ? renderContent(
271
+ { content: '', sections: [], path: selectedPath ?? '', documentType: selectedNodeDocType },
272
+ { onInternalLinkClick: navigateToDoc, currentPath: selectedPath ?? '', sourceId },
273
+ )
274
+ : null;
275
+ const activeDocType = isLoadingDocument ? selectedNodeDocType : (content?.documentType ?? selectedNodeDocType);
254
276
  const isMarkdownContent = !activeDocType || activeDocType === 'markdown';
255
277
  const showStickyNav = isMarkdownContent;
256
278
 
@@ -266,11 +288,10 @@ function DocViewerContent({
266
288
  const containerBgStyle =
267
289
  colorPalette.containerBackground !== 'transparent' ? { backgroundColor: colorPalette.containerBackground } : {};
268
290
 
269
- const defaultEmptyText =
270
- structure.length > 0
271
- ? 'Select a document from the sidebar to view'
272
- : 'No documents yet. Add content from the admin panel.';
273
- const resolvedEmptyText = emptyStateText || defaultEmptyText;
291
+ // A source with documents and none open asks for one; an empty source says so in the host's words (a reader may
292
+ // not be the one who adds documents, so the default names no admin screen).
293
+ const resolvedEmptyText =
294
+ structure.length > 0 ? 'Select a document from the sidebar to view' : emptyStateText || 'No documents yet.';
274
295
 
275
296
  // Unified header: title/subtitle route through the canonical (frozen)
276
297
  // `PageLayout` `TitleBlock` (text-h2) — same as every other help-center page —
@@ -365,8 +386,10 @@ function DocViewerContent({
365
386
  (app-globals.css) so content wraps instead of overflowing. */}
366
387
  <div className={`w-full min-w-0 ${isMarkdownContent ? 'mx-auto max-w-4xl' : ''}`}>
367
388
  <article className="space-y-2">
368
- {isLoadingContent || isLoadingStructure ? (
369
- renderSkeleton(selectedNodeDocType)
389
+ {isLoadingDocument ? (
390
+ // The document being opened draws its own loading form: its renderer, in a loading scope,
391
+ // with an empty document of its type (no words, no address), never a skeleton picked here.
392
+ <LoadingScope>{loadingContent}</LoadingScope>
370
393
  ) : !content ? (
371
394
  <div className="py-16 text-center">
372
395
  <p className="text-ods-text-secondary text-h4">{resolvedEmptyText}</p>
@@ -377,22 +400,20 @@ function DocViewerContent({
377
400
  </article>
378
401
  </div>
379
402
 
380
- {isMarkdownContent && (isLoadingContent || isLoadingStructure) && (
403
+ {isMarkdownContent && isLoadingDocument && (
381
404
  <div className="hidden content-lg:block">
382
405
  <div className="sticky top-24">
383
406
  <h3 className={ON_THIS_PAGE_HEADING_CLASS}>{ON_THIS_PAGE_LABEL}</h3>
384
- <div className="space-y-0">
385
- {[130, 170, 190, 220, 110, 200, 80, 100, 120, 140, 90].map((w, i) => (
386
- <div
387
- key={i}
388
- className={`border-l-2 py-[13px] pl-3 ${
389
- i === 0 ? 'border-ods-accent' : 'border-transparent'
390
- }`}
391
- >
392
- <UnifiedSkeleton className="h-[13px] rounded" width={w} />
393
- </div>
394
- ))}
395
- </div>
407
+ {/* The section list's own loading form (`StickySectionNav` with no sections, in a scope). */}
408
+ <LoadingScope>
409
+ <StickySectionNav
410
+ sections={[]}
411
+ activeSection=""
412
+ onSectionClick={NO_SECTION_CLICK}
413
+ ribbonPosition="left"
414
+ ribbonColor="var(--ods-accent)"
415
+ />
416
+ </LoadingScope>
396
417
  </div>
397
418
  </div>
398
419
  )}
@@ -5,7 +5,6 @@ import { FileDownloadCard } from '../embeds/file-download-card';
5
5
  import { GoogleSheetsViewer } from '../embeds/google-sheets-viewer';
6
6
  import { PdfViewer } from '../embeds/pdf-viewer';
7
7
  import { DocViewer, type DocViewerProps } from './doc-viewer';
8
- import { MarkdownSkeleton, EmbedSkeleton } from './skeletons';
9
8
 
10
9
  type DocRenderer = (content: DocContent, handlers: DocRenderHandlers) => React.ReactNode;
11
10
 
@@ -20,7 +19,7 @@ type DocRenderer = (content: DocContent, handlers: DocRenderHandlers) => React.R
20
19
  */
21
20
  export type DocumentTypeRenderers = { markdown: DocRenderer } & Partial<Record<DocumentType, DocRenderer>>;
22
21
 
23
- export interface DocsHubPageProps extends Omit<DocViewerProps, 'renderContent' | 'renderSkeleton' | 'showAIChat'> {
22
+ export interface DocsHubPageProps extends Omit<DocViewerProps, 'renderContent' | 'showAIChat'> {
24
23
  /** Per-document-type renderer map. `markdown` is REQUIRED. */
25
24
  documentTypeRenderers: DocumentTypeRenderers;
26
25
 
@@ -42,32 +41,20 @@ const defaultFallbackRenderer: DocRenderer = () => (
42
41
  </div>
43
42
  );
44
43
 
45
- // When the DAL hasn't populated the URL field for a rich-content type, the
46
- // embed-viewer components render a broken iframe (empty src). Fall back to
47
- // the same lib-styled "Unsupported document type" panel the explicit
48
- // fallback uses — the surface is honest about the missing data instead of
49
- // pretending to load. `FileDownloadCard` handles its own missing-URL state
50
- // (hides the Download button), so it doesn't need this guard.
51
- const defaultPdfRenderer: DocRenderer = (content, handlers) =>
52
- content.fileUrl ? (
53
- <PdfViewer src={content.fileUrl} fileName={content.fileName} />
54
- ) : (
55
- defaultFallbackRenderer(content, handlers)
56
- );
44
+ // Each viewer owns both its states without a document: in a loading scope (the document is being read) it draws its
45
+ // own loading form, otherwise its "not available" state when the DAL left the URL empty. So the renderers always
46
+ // render the viewer, and the viewer never mounts a frame without a source.
47
+ const defaultPdfRenderer: DocRenderer = content => (
48
+ <PdfViewer src={content.fileUrl ?? ''} fileName={content.fileName} />
49
+ );
57
50
 
58
- const defaultGoogleSheetRenderer: DocRenderer = (content, handlers) =>
59
- content.externalUrl ? (
60
- <GoogleSheetsViewer externalUrl={content.externalUrl} fileName={content.fileName} />
61
- ) : (
62
- defaultFallbackRenderer(content, handlers)
63
- );
51
+ const defaultGoogleSheetRenderer: DocRenderer = content => (
52
+ <GoogleSheetsViewer externalUrl={content.externalUrl ?? ''} fileName={content.fileName} />
53
+ );
64
54
 
65
- const defaultFigmaRenderer: DocRenderer = (content, handlers) =>
66
- content.externalUrl ? (
67
- <FigmaEmbed url={content.externalUrl} title={content.fileName} loading="eager" />
68
- ) : (
69
- defaultFallbackRenderer(content, handlers)
70
- );
55
+ const defaultFigmaRenderer: DocRenderer = content => (
56
+ <FigmaEmbed url={content.externalUrl ?? ''} title={content.fileName} iframeLoading="eager" />
57
+ );
71
58
 
72
59
  const defaultFileRenderer: DocRenderer = content => (
73
60
  <FileDownloadCard
@@ -78,16 +65,6 @@ const defaultFileRenderer: DocRenderer = content => (
78
65
  />
79
66
  );
80
67
 
81
- /** The page's own loading form per document type: the markdown article's, else the embed viewer's. */
82
- const documentLoadingForm = (documentType: DocumentType | undefined) =>
83
- !documentType || documentType === 'markdown' ? (
84
- <MarkdownSkeleton />
85
- ) : (
86
- // Forward the documentType so the embed skeleton renders the right
87
- // shape (PDF=2 buttons, sheets/figma=1 button, file=centered card).
88
- <EmbedSkeleton documentType={documentType} />
89
- );
90
-
91
68
  /**
92
69
  * Embeddable docs-hub page. Bundles `<DocViewer>` with safe defaults so the
93
70
  * minimum embed is a one-line mount (consumer only has to supply
@@ -132,7 +109,6 @@ export function DocsHubPage({
132
109
  className={className}
133
110
  sidebarLabel={sidebarLabel}
134
111
  renderContent={renderContent}
135
- renderSkeleton={documentLoadingForm}
136
112
  />
137
113
  );
138
114
  }
@@ -9,6 +9,8 @@ import {
9
9
  findDocNodeByPath,
10
10
  getDocAncestorNodeIds,
11
11
  DEFAULT_FOLDER_INDEX_FILE,
12
+ findFirstDocPath,
13
+ rootDocumentPath,
12
14
  } from '../../utils/doc-tree-nav';
13
15
  import { contentFetch } from '../../utils/embed-content-fetch';
14
16
  import { navigateSamePageHash, HUB_HEADER_OFFSET_PX } from '../../utils/same-page-hash-nav';
@@ -34,21 +36,6 @@ function scrollToContent() {
34
36
  * its README) instead of a blank panel or a redundant in-page listing — the
35
37
  * sidebar tree is the directory browser. Returns null for a folder with no docs.
36
38
  */
37
- function findFirstDocPath(folder: DocNode): string | null {
38
- const children = folder.children ?? [];
39
- for (const child of children) {
40
- if (child.type === 'file' && !child.path.toLowerCase().endsWith('.mmd')) {
41
- return child.path;
42
- }
43
- }
44
- for (const child of children) {
45
- if (child.type === 'folder') {
46
- const nested = findFirstDocPath(child);
47
- if (nested) return nested;
48
- }
49
- }
50
- return null;
51
- }
52
39
 
53
40
  // ---------------------------------------------------------------------------
54
41
  // Wire decoding for the two doc-source endpoints
@@ -220,6 +207,11 @@ export function useDocumentTree(config: UseDocumentTreeConfig, initialPath?: str
220
207
  const cleanInitialPath = stripFolderIndexFromPath(initialPath?.replace(/\/$/, '') || '', folderIndexFile);
221
208
 
222
209
  const [structure, setStructure] = useState<DocNode[]>([]);
210
+ // The latest committed tree, for a request that settles after the tree arrived (its closure holds the old one).
211
+ const structureRef = useRef(structure);
212
+ useEffect(() => {
213
+ structureRef.current = structure;
214
+ }, [structure]);
223
215
  const [selectedPath, setSelectedPath] = useState<string>(cleanInitialPath);
224
216
  const [content, setContent] = useState<DocContent | null>(null);
225
217
  const [isLoadingStructure, setIsLoadingStructure] = useState(true);
@@ -409,12 +401,16 @@ export function useDocumentTree(config: UseDocumentTreeConfig, initialPath?: str
409
401
  // No-README FOLDER → stay silent: the content effect resolves it to
410
402
  // the folder's first child, so the folder-path 404 from the direct-
411
403
  // load speculative fetch is expected, not an error to flash.
412
- const probe = findDocNodeByPath(stripFolderIndexFromPath(path, folderIndexFile), structure);
404
+ // Classified against the LATEST tree (`structureRef`), never the one captured when this request went out: the tree
405
+ // often arrives while the request is in flight, and judged against the empty tree of the call a missing
406
+ // document read as "not classified yet, wait", so nothing settled it and the page stayed a skeleton.
407
+ const latestStructure = structureRef.current;
408
+ const probe = findDocNodeByPath(stripFolderIndexFromPath(path, folderIndexFile), latestStructure);
413
409
  const probeIsNoReadmeFolder = !!probe && probe.type === 'folder' && !probe.hasReadme;
414
410
  // Before the structure has loaded to classify the path (`structure`
415
411
  // is [] in the speculative call's closure), only silence FOLDER-LIKE
416
412
  // paths (no `.md`) — a genuinely missing `*.md` leaf must still error.
417
- const preStructureFolderLike = structure.length === 0 && !path.endsWith('.md');
413
+ const preStructureFolderLike = latestStructure.length === 0 && !path.endsWith('.md');
418
414
  if (probeIsNoReadmeFolder || preStructureFolderLike) {
419
415
  // Superseded by the targeted fetch (first-child / reclassified path)
420
416
  // the structure effect fires. Null the request id so the `finally`
@@ -507,7 +503,15 @@ export function useDocumentTree(config: UseDocumentTreeConfig, initialPath?: str
507
503
  let pathToFetch: string | null = null;
508
504
 
509
505
  if (selectedPath === '') {
510
- pathToFetch = folderIndexFile;
506
+ // The root shows its landing document (the folder index); a source with none shows its FIRST document, the
507
+ // rule a no-README folder follows. Asking for the missing index again would 404 for ever.
508
+ const rootPath = rootDocumentPath(structure, folderIndexFile);
509
+ if (!rootPath) {
510
+ // A tree with folders and no document at all: nothing to fetch; the empty state is derived below.
511
+ lastFetchedPath.current = null;
512
+ return;
513
+ }
514
+ pathToFetch = rootPath;
511
515
  } else {
512
516
  const node = findDocNodeByPath(selectedPath, structure);
513
517
 
@@ -731,19 +735,25 @@ export function useDocumentTree(config: UseDocumentTreeConfig, initialPath?: str
731
735
  // the content effect, which reached the same conclusion one commit later and
732
736
  // meanwhile left the PREVIOUS document's body on screen.
733
737
  const isEmptyFolder = useMemo(() => {
734
- if (!selectedPath || structure.length === 0) return false;
738
+ if (structure.length === 0) return false;
739
+ // The root of a source whose tree holds no document at all is an empty folder too.
740
+ if (!selectedPath) return !rootDocumentPath(structure, folderIndexFile);
735
741
  const node = findDocNodeByPath(selectedPath, structure);
736
742
  return !!node && node.type === 'folder' && !node.hasReadme && !findFirstDocPath(node);
737
- }, [selectedPath, structure]);
743
+ }, [selectedPath, structure, folderIndexFile]);
744
+ // A source with no documents at all (its tree is read and empty): there is nothing to load, so the viewer shows the
745
+ // source's empty state instead of waiting for a landing document that does not exist.
746
+ const isEmptySource = !isLoadingStructure && structure.length === 0;
747
+ const settled = isEmptyFolder || isEmptySource;
738
748
 
739
749
  return {
740
750
  structure,
741
751
  selectedPath,
742
752
  setSelectedPath,
743
- content: isEmptyFolder ? null : content,
753
+ content: settled ? null : content,
744
754
  isLoadingStructure,
745
- isLoadingContent: isEmptyFolder ? false : isLoadingContent,
746
- error: isEmptyFolder ? null : error,
755
+ isLoadingContent: settled ? false : isLoadingContent,
756
+ error: settled ? null : error,
747
757
  expandedNodes,
748
758
  toggleNode,
749
759
  selectNode,