@flamingo-stack/openframe-frontend-core 0.0.702 → 0.0.703-snapshot.20261006080901

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 (341) hide show
  1. package/dist/{chunk-CYYTD5ZK.cjs → chunk-262YQFOS.cjs} +47 -47
  2. package/dist/{chunk-SAA7B3DA.js → chunk-2G546OND.js} +500 -67
  3. package/dist/{chunk-6JPQDYYY.js → chunk-2JDVL7UV.js} +2614 -2001
  4. package/dist/{chunk-ASN65P3F.cjs → chunk-4DQMTBMK.cjs} +15 -15
  5. package/dist/{chunk-CZ34HXDW.js → chunk-4KGXLVCF.js} +686 -725
  6. package/dist/{chunk-TCZCMD5Q.js → chunk-5HBHO5TB.js} +1 -1
  7. package/dist/{chunk-446KS5BG.js → chunk-5QJDM6UV.js} +5 -5
  8. package/dist/{chunk-UJJWTJ6S.js → chunk-6WQIUQUC.js} +30 -16
  9. package/dist/{chunk-YR6JPJF4.js → chunk-6XIF2Z7I.js} +4 -4
  10. package/dist/{chunk-YGRHITPY.cjs → chunk-7JIL3WOH.cjs} +24 -24
  11. package/dist/{chunk-RS7HWAYX.js → chunk-7LFIKJX2.js} +3 -3
  12. package/dist/{chunk-4OGAXAEG.cjs → chunk-ABU3TGXA.cjs} +3 -3
  13. package/dist/{chunk-3WSFIV5G.cjs → chunk-AMYY3PNL.cjs} +69 -55
  14. package/dist/{chunk-BRXOGCH3.cjs → chunk-BIZNSF3U.cjs} +3 -3
  15. package/dist/{chunk-2A4XES7R.cjs → chunk-BM5MVGH3.cjs} +3 -3
  16. package/dist/{chunk-IIMICN7L.cjs → chunk-C3BWIU7R.cjs} +4 -44
  17. package/dist/{chunk-LZNJH23A.cjs → chunk-C7XGRLT5.cjs} +12 -12
  18. package/dist/{chunk-6YCXA7CN.js → chunk-CH5232HV.js} +20 -19
  19. package/dist/{chunk-UIBSSVON.cjs → chunk-D7BTSVMS.cjs} +2 -2
  20. package/dist/{chunk-BO5ZHDA4.js → chunk-ERCV6HHJ.js} +7 -7
  21. package/dist/{chunk-5V6O73EH.js → chunk-FLKJYMVB.js} +3 -3
  22. package/dist/{chunk-Z5HDCZAS.js → chunk-FXENTYE5.js} +15 -15
  23. package/dist/{chunk-HIXGAY2L.cjs → chunk-HFJW24JJ.cjs} +121 -121
  24. package/dist/{chunk-HPJ4XS2L.js → chunk-HJAYVKWQ.js} +24 -24
  25. package/dist/{chunk-PLK6GHN7.js → chunk-I7DFUQMN.js} +10 -10
  26. package/dist/{chunk-KJ5LWATV.js → chunk-IHU5ANUY.js} +7 -7
  27. package/dist/{chunk-AEQANLBW.cjs → chunk-IZGDG5X6.cjs} +505 -72
  28. package/dist/{chunk-2V4Z44G6.js → chunk-JEYRXT5L.js} +17 -17
  29. package/dist/{chunk-B3LZJ6J3.cjs → chunk-L3IC42IF.cjs} +32 -32
  30. package/dist/{chunk-RTGPIHVO.cjs → chunk-LDPEQGKT.cjs} +2243 -1630
  31. package/dist/{chunk-FD4DBTFO.cjs → chunk-LTSTC3N5.cjs} +1253 -1292
  32. package/dist/{chunk-OGLCD3UL.js → chunk-MFFCUVWG.js} +6 -6
  33. package/dist/{chunk-3L7KCOGO.cjs → chunk-MGGGBHYD.cjs} +30 -30
  34. package/dist/{chunk-QFW3RUZS.cjs → chunk-MI3JUJ75.cjs} +33 -33
  35. package/dist/{chunk-EMCQGFXD.cjs → chunk-MMOODW57.cjs} +28 -27
  36. package/dist/{chunk-Q76ZTHNV.js → chunk-MP2C3LJ4.js} +1 -1
  37. package/dist/{chunk-E3DH543A.cjs → chunk-N3TO7JV3.cjs} +98 -98
  38. package/dist/{chunk-2T6QWNXU.js → chunk-NDESEPW5.js} +99 -11
  39. package/dist/{chunk-ETFFEA3J.js → chunk-NRLJUBCS.js} +7 -7
  40. package/dist/{chunk-HUOLB7ZN.cjs → chunk-OHFS7VOU.cjs} +9 -9
  41. package/dist/{chunk-MWUBBBWZ.js → chunk-QVSFBSWV.js} +10 -10
  42. package/dist/{chunk-MDBHZBW7.cjs → chunk-RKV5B7EU.cjs} +92 -4
  43. package/dist/{chunk-3G76LRGL.cjs → chunk-SYXQLETR.cjs} +29 -29
  44. package/dist/{chunk-FHM7GD5B.cjs → chunk-UK62YTH6.cjs} +28 -14
  45. package/dist/{chunk-HUMWQMCU.js → chunk-UT4YHQZW.js} +3 -3
  46. package/dist/{chunk-NGJCWEYX.cjs → chunk-VHCACDKP.cjs} +46 -46
  47. package/dist/{chunk-5FFNNJM7.js → chunk-VR2ZLFCP.js} +1 -1
  48. package/dist/{chunk-IZKOQVXO.js → chunk-VTASHWER.js} +14 -14
  49. package/dist/{chunk-64O4L7WN.js → chunk-WUJNYGO2.js} +11 -51
  50. package/dist/{chunk-JOY6HVPV.js → chunk-XATTBO5X.js} +25 -11
  51. package/dist/{chunk-L7KZA7ZD.cjs → chunk-XIFOBQRQ.cjs} +6 -6
  52. package/dist/{chunk-CXDJUKC6.cjs → chunk-XJKZ4IKT.cjs} +4 -4
  53. package/dist/{chunk-XCIFHW4E.cjs → chunk-XOYH2YQE.cjs} +2 -2
  54. package/dist/{chunk-HYFLTFFB.js → chunk-ZQFZSFQI.js} +2 -2
  55. package/dist/components/case-studies/index.cjs +24 -24
  56. package/dist/components/case-studies/index.js +10 -10
  57. package/dist/components/chat/chat-archive-page.d.ts +3 -3
  58. package/dist/components/chat/chat-archive-page.d.ts.map +1 -1
  59. package/dist/components/chat/chat-nav-sidebar.d.ts +4 -0
  60. package/dist/components/chat/chat-nav-sidebar.d.ts.map +1 -1
  61. package/dist/components/chat/chat-panel-header.d.ts +3 -2
  62. package/dist/components/chat/chat-panel-header.d.ts.map +1 -1
  63. package/dist/components/chat/embeddable-chat.d.ts +36 -2
  64. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  65. package/dist/components/chat/index.cjs +14 -6
  66. package/dist/components/chat/index.d.ts +4 -1
  67. package/dist/components/chat/index.d.ts.map +1 -1
  68. package/dist/components/chat/index.js +13 -5
  69. package/dist/components/chat/mingo-archive-list.d.ts +17 -0
  70. package/dist/components/chat/mingo-archive-list.d.ts.map +1 -0
  71. package/dist/components/chat/mingo-chat-header.d.ts +31 -0
  72. package/dist/components/chat/mingo-chat-header.d.ts.map +1 -0
  73. package/dist/components/chat/mingo-chat-rail.d.ts +54 -0
  74. package/dist/components/chat/mingo-chat-rail.d.ts.map +1 -0
  75. package/dist/components/chat/model-display.d.ts.map +1 -1
  76. package/dist/components/contact/contact-form.d.ts.map +1 -1
  77. package/dist/components/contact/index.cjs +11 -11
  78. package/dist/components/contact/index.js +10 -10
  79. package/dist/components/content-loading-container.d.ts +1 -1
  80. package/dist/components/content-loading-container.d.ts.map +1 -1
  81. package/dist/components/docs/index.cjs +14 -14
  82. package/dist/components/docs/index.js +13 -13
  83. package/dist/components/docs/skeletons.d.ts.map +1 -1
  84. package/dist/components/embeds/index.cjs +11 -11
  85. package/dist/components/embeds/index.js +10 -10
  86. package/dist/components/faq/index.cjs +12 -12
  87. package/dist/components/faq/index.js +11 -11
  88. package/dist/components/features/figma-prototype-viewer.d.ts.map +1 -1
  89. package/dist/components/features/index.cjs +10 -10
  90. package/dist/components/features/index.js +9 -9
  91. package/dist/components/features/media-gallery-manager.d.ts.map +1 -1
  92. package/dist/components/features/parallax-image-showcase.d.ts.map +1 -1
  93. package/dist/components/features/policy-configuration-panel.d.ts.map +1 -1
  94. package/dist/components/features/section-selector.d.ts.map +1 -1
  95. package/dist/components/help-center-pages/index.cjs +119 -119
  96. package/dist/components/help-center-pages/index.js +39 -39
  97. package/dist/components/help-center-pages/trust-center-sections.d.ts +2 -2
  98. package/dist/components/help-center-pages/trust-center-sections.d.ts.map +1 -1
  99. package/dist/components/index.cjs +94 -86
  100. package/dist/components/index.js +32 -24
  101. package/dist/components/layout/article-detail-layout.d.ts +1 -1
  102. package/dist/components/layout/article-detail-layout.d.ts.map +1 -1
  103. package/dist/components/layout/page-container.d.ts.map +1 -1
  104. package/dist/components/layout/title-block.d.ts +2 -2
  105. package/dist/components/layout/title-block.d.ts.map +1 -1
  106. package/dist/components/loading/page-layout-skeleton.d.ts.map +1 -1
  107. package/dist/components/loading/unified-skeleton.d.ts.map +1 -1
  108. package/dist/components/meeting-scheduler/booking-form.d.ts +4 -4
  109. package/dist/components/meeting-scheduler/booking-form.d.ts.map +1 -1
  110. package/dist/components/meeting-scheduler/context-panel.d.ts.map +1 -1
  111. package/dist/components/meeting-scheduler/index.cjs +127 -127
  112. package/dist/components/meeting-scheduler/index.d.ts +2 -2
  113. package/dist/components/meeting-scheduler/index.d.ts.map +1 -1
  114. package/dist/components/meeting-scheduler/index.js +50 -50
  115. package/dist/components/navigation/app-header.d.ts +4 -0
  116. package/dist/components/navigation/app-header.d.ts.map +1 -1
  117. package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -1
  118. package/dist/components/navigation/app-layout-side-panel.d.ts +142 -0
  119. package/dist/components/navigation/app-layout-side-panel.d.ts.map +1 -0
  120. package/dist/components/navigation/app-layout.d.ts +11 -1
  121. package/dist/components/navigation/app-layout.d.ts.map +1 -1
  122. package/dist/components/navigation/header-mingo-button.d.ts +4 -1
  123. package/dist/components/navigation/header-mingo-button.d.ts.map +1 -1
  124. package/dist/components/navigation/index.cjs +13 -13
  125. package/dist/components/navigation/index.d.ts +1 -0
  126. package/dist/components/navigation/index.d.ts.map +1 -1
  127. package/dist/components/navigation/index.js +12 -12
  128. package/dist/components/navigation/navigation-sidebar.d.ts +8 -1
  129. package/dist/components/navigation/navigation-sidebar.d.ts.map +1 -1
  130. package/dist/components/navigation/sliding-sidebar.d.ts.map +1 -1
  131. package/dist/components/onboarding-guides/index.cjs +14 -14
  132. package/dist/components/onboarding-guides/index.js +13 -13
  133. package/dist/components/persistent-filter-controls.d.ts.map +1 -1
  134. package/dist/components/related-content/index.cjs +7 -7
  135. package/dist/components/related-content/index.js +6 -6
  136. package/dist/components/shared/detail-page-skeleton.d.ts.map +1 -1
  137. package/dist/components/tickets/help-center-create-form.d.ts +4 -4
  138. package/dist/components/tickets/index.cjs +15 -15
  139. package/dist/components/tickets/index.js +14 -14
  140. package/dist/components/ui/announcement-bar-view.d.ts.map +1 -1
  141. package/dist/components/ui/breadcrumb.d.ts.map +1 -1
  142. package/dist/components/ui/checkbox-block.d.ts.map +1 -1
  143. package/dist/components/ui/dashboard-info-card.d.ts +1 -1
  144. package/dist/components/ui/dashboard-info-card.d.ts.map +1 -1
  145. package/dist/components/ui/data-table/data-table-body.d.ts +2 -2
  146. package/dist/components/ui/data-table/data-table-skeleton.d.ts +2 -2
  147. package/dist/components/ui/data-table/data-table-skeleton.d.ts.map +1 -1
  148. package/dist/components/ui/data-table/utils.d.ts +2 -2
  149. package/dist/components/ui/dialog.d.ts.map +1 -1
  150. package/dist/components/ui/drawer.d.ts +7 -3
  151. package/dist/components/ui/drawer.d.ts.map +1 -1
  152. package/dist/components/ui/entity-image.d.ts +2 -2
  153. package/dist/components/ui/file-manager/index.cjs +36 -36
  154. package/dist/components/ui/file-manager/index.js +3 -3
  155. package/dist/components/ui/filter-modal.d.ts.map +1 -1
  156. package/dist/components/ui/index.cjs +18 -10
  157. package/dist/components/ui/index.js +17 -9
  158. package/dist/components/ui/info-card-row.d.ts.map +1 -1
  159. package/dist/components/ui/menubar.d.ts +1 -1
  160. package/dist/components/ui/menubar.d.ts.map +1 -1
  161. package/dist/components/ui/modal-v2.d.ts.map +1 -1
  162. package/dist/components/ui/modal.d.ts.map +1 -1
  163. package/dist/components/ui/no-data/no-data-actions.d.ts.map +1 -1
  164. package/dist/components/ui/no-data/no-data-styles.d.ts +1 -1
  165. package/dist/components/ui/no-data/no-data-styles.d.ts.map +1 -1
  166. package/dist/components/ui/page-actions.d.ts.map +1 -1
  167. package/dist/components/ui/panel-resize-handle.d.ts +25 -0
  168. package/dist/components/ui/panel-resize-handle.d.ts.map +1 -0
  169. package/dist/components/ui/sheet.d.ts.map +1 -1
  170. package/dist/components/ui/table/table-skeleton.d.ts +1 -1
  171. package/dist/components/ui/table/table-skeleton.d.ts.map +1 -1
  172. package/dist/hooks/index.cjs +23 -9
  173. package/dist/hooks/index.js +24 -10
  174. package/dist/hooks/ui/index.d.ts +1 -0
  175. package/dist/hooks/ui/index.d.ts.map +1 -1
  176. package/dist/hooks/ui/use-content-breakpoint.d.ts +32 -0
  177. package/dist/hooks/ui/use-content-breakpoint.d.ts.map +1 -0
  178. package/dist/hooks/ui/use-isomorphic-layout-effect.d.ts +2 -2
  179. package/dist/hooks/ui/use-isomorphic-layout-effect.d.ts.map +1 -1
  180. package/dist/hooks/ui/use-media-query.d.ts.map +1 -1
  181. package/dist/hooks/ui/use-resizable-panel-size.d.ts +46 -0
  182. package/dist/hooks/ui/use-resizable-panel-size.d.ts.map +1 -0
  183. package/dist/index.cjs +39 -17
  184. package/dist/index.js +41 -19
  185. package/dist/styles/layout-steps.d.ts +30 -0
  186. package/dist/styles/layout-steps.d.ts.map +1 -0
  187. package/dist/tailwind.config.cjs +36 -11
  188. package/dist/tailwind.config.js +36 -11
  189. package/eslint-config/README.md +9 -0
  190. package/eslint-config/base.js +17 -1
  191. package/eslint-config/layout-variants.js +57 -0
  192. package/package.json +3 -1
  193. package/src/ODS_TOKEN_RULES.md +40 -0
  194. package/src/components/chat/chat-archive-page.tsx +9 -7
  195. package/src/components/chat/chat-nav-sidebar.tsx +10 -4
  196. package/src/components/chat/chat-panel-header.tsx +8 -5
  197. package/src/components/chat/embeddable-chat.tsx +686 -494
  198. package/src/components/chat/index.ts +4 -0
  199. package/src/components/chat/mingo-archive-list.tsx +108 -0
  200. package/src/components/chat/mingo-chat-header.tsx +104 -0
  201. package/src/components/chat/mingo-chat-rail.tsx +361 -0
  202. package/src/components/chat/model-display.tsx +8 -1
  203. package/src/components/contact/contact-form.tsx +11 -9
  204. package/src/components/content-loading-container.tsx +4 -2
  205. package/src/components/docs/doc-viewer.tsx +7 -7
  206. package/src/components/docs/skeletons.tsx +7 -4
  207. package/src/components/dynamic-skeleton.tsx +1 -1
  208. package/src/components/embeds/claude-embed.tsx +1 -1
  209. package/src/components/embeds/embed-viewer-frame.tsx +4 -4
  210. package/src/components/embeds/figma-embed.tsx +2 -2
  211. package/src/components/embeds/google-sheets-viewer.tsx +1 -1
  212. package/src/components/embeds/pdf-viewer.tsx +3 -3
  213. package/src/components/empty-state.tsx +6 -6
  214. package/src/components/faq/faq-section.tsx +2 -2
  215. package/src/components/faq-accordion.tsx +1 -1
  216. package/src/components/features/board/board-column-header.tsx +1 -1
  217. package/src/components/features/command-box.tsx +3 -3
  218. package/src/components/features/entity-video-section.tsx +2 -2
  219. package/src/components/features/figma-prototype-viewer.tsx +4 -1
  220. package/src/components/features/media-gallery-manager.tsx +3 -1
  221. package/src/components/features/parallax-image-showcase.tsx +9 -6
  222. package/src/components/features/policy-configuration-panel.tsx +9 -7
  223. package/src/components/features/provider-button.tsx +1 -1
  224. package/src/components/features/section-selector.tsx +11 -6
  225. package/src/components/features/select-button.tsx +4 -4
  226. package/src/components/features/seo-editor-preview.tsx +2 -2
  227. package/src/components/features/video-ratio-tabs.tsx +3 -3
  228. package/src/components/features/waitlist-form.tsx +1 -1
  229. package/src/components/help-center-pages/trust-center-page.tsx +1 -1
  230. package/src/components/help-center-pages/trust-center-sections.tsx +14 -14
  231. package/src/components/icon-display.tsx +1 -1
  232. package/src/components/layout/article-detail-layout.tsx +4 -2
  233. package/src/components/layout/list-page-layout.tsx +2 -2
  234. package/src/components/layout/page-container.tsx +26 -14
  235. package/src/components/layout/page-heading.tsx +1 -1
  236. package/src/components/layout/page-layout.tsx +1 -1
  237. package/src/components/layout/title-block.tsx +13 -13
  238. package/src/components/loading/card-skeleton.tsx +11 -11
  239. package/src/components/loading/category-vendor-selector-skeleton.tsx +1 -1
  240. package/src/components/loading/content-skeleton.tsx +6 -6
  241. package/src/components/loading/device-card-skeleton.tsx +1 -1
  242. package/src/components/loading/margin-report-skeleton.tsx +7 -7
  243. package/src/components/loading/msp-profile-form-skeleton.tsx +3 -3
  244. package/src/components/loading/organization-card-skeleton.tsx +2 -2
  245. package/src/components/loading/page-layout-skeleton.tsx +80 -68
  246. package/src/components/loading/unified-skeleton.tsx +16 -6
  247. package/src/components/media-carousel.tsx +3 -3
  248. package/src/components/meeting-scheduler/booking-form.tsx +9 -8
  249. package/src/components/meeting-scheduler/context-panel.tsx +8 -6
  250. package/src/components/meeting-scheduler/directory.tsx +4 -4
  251. package/src/components/meeting-scheduler/index.tsx +15 -15
  252. package/src/components/meeting-scheduler/slot-picker.tsx +12 -12
  253. package/src/components/navigation/app-header.tsx +6 -0
  254. package/src/components/navigation/app-layout-drawer.tsx +14 -237
  255. package/src/components/navigation/app-layout-side-panel.tsx +594 -0
  256. package/src/components/navigation/app-layout.tsx +69 -4
  257. package/src/components/navigation/header-mingo-button.tsx +13 -3
  258. package/src/components/navigation/index.ts +5 -0
  259. package/src/components/navigation/navigation-sidebar.tsx +17 -3
  260. package/src/components/navigation/sliding-sidebar.tsx +2 -1
  261. package/src/components/pagination.tsx +3 -3
  262. package/src/components/persistent-filter-controls.tsx +6 -2
  263. package/src/components/platform/ScriptArguments.tsx +1 -1
  264. package/src/components/platform/ScriptInfoSection.tsx +4 -4
  265. package/src/components/shared/article-author-byline.tsx +1 -1
  266. package/src/components/shared/delivery/delivery-lists.tsx +2 -2
  267. package/src/components/shared/delivery/delivery-row.tsx +7 -7
  268. package/src/components/shared/delivery/delivery-table.tsx +3 -3
  269. package/src/components/shared/detail-page-skeleton.tsx +11 -5
  270. package/src/components/shared/dev-section/dev-card-row.tsx +6 -6
  271. package/src/components/shared/dev-section/dev-section-view.tsx +1 -1
  272. package/src/components/shared/product-release/product-release-card-skeleton.tsx +8 -8
  273. package/src/components/shared/product-release/product-release-card.tsx +9 -9
  274. package/src/components/shared/product-release/product-releases-view.tsx +1 -1
  275. package/src/components/shared/product-release/release-detail-page.tsx +5 -5
  276. package/src/components/shared/roadmap/roadmap-grid-skeleton.tsx +1 -1
  277. package/src/components/shared/roadmap/roadmap-grid.tsx +3 -3
  278. package/src/components/tickets/help-center-card.tsx +1 -1
  279. package/src/components/tickets/help-center-create-form.tsx +12 -12
  280. package/src/components/ui/announcement-bar-view.tsx +2 -10
  281. package/src/components/ui/benefit-card.tsx +18 -18
  282. package/src/components/ui/brand-association-grid.tsx +4 -4
  283. package/src/components/ui/breadcrumb.tsx +4 -1
  284. package/src/components/ui/card.tsx +1 -1
  285. package/src/components/ui/checkbox-block.tsx +9 -7
  286. package/src/components/ui/content-loader.tsx +3 -3
  287. package/src/components/ui/cursor-pagination.tsx +3 -3
  288. package/src/components/ui/dashboard-info-card.tsx +13 -9
  289. package/src/components/ui/data-attribution.tsx +2 -2
  290. package/src/components/ui/data-table/data-table-body.tsx +2 -2
  291. package/src/components/ui/data-table/data-table-header.tsx +15 -15
  292. package/src/components/ui/data-table/data-table-skeleton.tsx +6 -6
  293. package/src/components/ui/data-table/utils.ts +5 -5
  294. package/src/components/ui/dialog.tsx +4 -1
  295. package/src/components/ui/drawer.tsx +43 -189
  296. package/src/components/ui/duration-input.tsx +1 -1
  297. package/src/components/ui/entity-image.tsx +3 -3
  298. package/src/components/ui/feature-card.tsx +10 -10
  299. package/src/components/ui/file-manager/file-manager-action-bar.tsx +1 -1
  300. package/src/components/ui/file-manager/file-manager.tsx +1 -1
  301. package/src/components/ui/filter-modal.tsx +8 -1
  302. package/src/components/ui/highlight-card.tsx +7 -7
  303. package/src/components/ui/info-card-row.tsx +10 -6
  304. package/src/components/ui/modal-v2.tsx +7 -2
  305. package/src/components/ui/modal.tsx +3 -2
  306. package/src/components/ui/no-data/no-data-actions.tsx +10 -4
  307. package/src/components/ui/no-data/no-data-styles.ts +1 -1
  308. package/src/components/ui/no-data/no-data.tsx +1 -1
  309. package/src/components/ui/organization-card.tsx +1 -1
  310. package/src/components/ui/page-actions.tsx +34 -5
  311. package/src/components/ui/panel-resize-handle.tsx +184 -0
  312. package/src/components/ui/progress-bar.tsx +2 -2
  313. package/src/components/ui/query-report-table/query-report-table.tsx +1 -1
  314. package/src/components/ui/search-input.tsx +2 -2
  315. package/src/components/ui/service-card.tsx +2 -2
  316. package/src/components/ui/sheet.tsx +2 -1
  317. package/src/components/ui/skeleton.tsx +3 -3
  318. package/src/components/ui/stacked-rows-panel.tsx +4 -4
  319. package/src/components/ui/tab-navigation.tsx +2 -2
  320. package/src/components/ui/tab-selector.tsx +2 -2
  321. package/src/components/ui/table/table-header.tsx +3 -3
  322. package/src/components/ui/table/table-skeleton.tsx +3 -3
  323. package/src/components/ui/table/utils.ts +5 -5
  324. package/src/components/ui/tag-search-input.tsx +3 -3
  325. package/src/components/ui/tags-manager.tsx +5 -5
  326. package/src/components/ui/test-run.tsx +2 -2
  327. package/src/components/ui/ticket-info-section.tsx +1 -1
  328. package/src/components/ui/ticket-notes-section.tsx +1 -1
  329. package/src/components/ui/ticket-status-config-row.tsx +15 -15
  330. package/src/hooks/ui/index.ts +1 -0
  331. package/src/hooks/ui/use-content-breakpoint.ts +58 -0
  332. package/src/hooks/ui/use-media-query.ts +3 -2
  333. package/src/hooks/ui/use-resizable-panel-size.ts +114 -0
  334. package/src/styles/index.css +2 -0
  335. package/src/styles/layout-steps.ts +14 -0
  336. package/src/styles/ods-colors.css +15 -0
  337. package/src/styles/ods-content-area-tokens.css +27 -0
  338. package/src/styles/ods-content-area.css +31 -0
  339. package/src/styles/ods-effects.css +4 -2
  340. package/src/styles/ods-responsive-tokens.css +14 -4
  341. package/tailwind.config.ts +42 -7
@@ -0,0 +1,594 @@
1
+ 'use client';
2
+
3
+ import {
4
+ type KeyboardEvent,
5
+ type PointerEvent,
6
+ type ReactNode,
7
+ useEffect,
8
+ useId,
9
+ useRef,
10
+ useState,
11
+ useSyncExternalStore,
12
+ } from 'react';
13
+ import { useIsomorphicLayoutEffect } from '../../hooks/ui/use-isomorphic-layout-effect';
14
+ import { breakpoints, useMediaQuery } from '../../hooks/ui/use-media-query';
15
+ import { cn } from '../../utils/cn';
16
+ import { Menu01Icon } from '../icons-v2-generated';
17
+
18
+ /**
19
+ * - `docked`: a column beside the content, resizable.
20
+ * - `full`: covers the content area (not the navigation). Reached by dragging
21
+ * past the content's minimum width, or from the header toggle when there is
22
+ * no room to dock.
23
+ * - `hidden`: no room to dock (or a phone) and not opened from the header.
24
+ * - `overlay`: a phone, opened from the header; covers the content area.
25
+ */
26
+ export type AppLayoutSidePanelMode = 'docked' | 'full' | 'hidden' | 'overlay';
27
+
28
+ export interface AppLayoutSidePanelRenderState {
29
+ /** Width the panel body is drawn at, in px. */
30
+ width: number;
31
+ mode: AppLayoutSidePanelMode;
32
+ /**
33
+ * The panel was opened from the header (no room to dock, or a phone) and can
34
+ * be put away again: offer a close control. A docked panel cannot close.
35
+ */
36
+ canClose: boolean;
37
+ /** Put the panel away again; a no-op while it is docked. */
38
+ close: () => void;
39
+ /**
40
+ * Where `collapse` takes the panel, one step back at a time: from the whole
41
+ * area to the column it was in before (`column`), from the column to its
42
+ * minimum (`minimum`, e.g. a list alone). Null when there is no step back.
43
+ */
44
+ collapsesTo: 'column' | 'minimum' | null;
45
+ /** Step the panel back (see `collapsesTo`), animated. */
46
+ collapse: () => void;
47
+ }
48
+
49
+ export interface AppLayoutSidePanelConfig {
50
+ /** Panel body. Told its width so it can choose what fits. */
51
+ children: (state: AppLayoutSidePanelRenderState) => ReactNode;
52
+ /** Narrowest docked width, and the width it starts at: the narrowest its content draws. */
53
+ minWidth: number;
54
+ /** Narrowest the content may get before the panel takes over. Default 400. */
55
+ minContentWidth?: number;
56
+ /** localStorage key for the chosen width. Without it the width lasts the session. */
57
+ storageKey?: string;
58
+ /**
59
+ * Pages that need the whole content width set this: the panel starts at its
60
+ * minimum there. The user can still widen it; the width they chose
61
+ * elsewhere is kept for the other pages.
62
+ */
63
+ collapsed?: boolean;
64
+ /** Accessible name of the panel region. Default "Side panel". */
65
+ label?: string;
66
+ /**
67
+ * Opened from the header while it cannot dock (or on a phone). Pass it to
68
+ * open the panel from elsewhere (a deep link, a "send to chat" action);
69
+ * omit to let the header own it. Docked, it has no effect.
70
+ */
71
+ open?: boolean;
72
+ /** Called whenever the header-open state should change. */
73
+ onOpenChange?: (open: boolean) => void;
74
+ }
75
+
76
+ /** Inset of the docked card from the window edge and the header. */
77
+ export const SIDE_PANEL_INSET = 16;
78
+ const MOBILE_QUERY = `not all and ${breakpoints.md}`;
79
+ const REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)';
80
+ /** Length of the docked <-> full morph; keep in step with `duration-300` below. */
81
+ const MORPH_MS = 300;
82
+ const STORAGE_EVENT = 'of:side-panel-size';
83
+
84
+ export interface SidePanelSize {
85
+ /** Docked width the user chose. */
86
+ width: number;
87
+ /** Dragged past the content minimum: the panel takes the whole area. */
88
+ expanded: boolean;
89
+ }
90
+
91
+ export interface SidePanelLayoutInput {
92
+ /** Width of the row the content and the panel share; 0 before it is measured. */
93
+ rowWidth: number;
94
+ isMobile: boolean;
95
+ /** Opened from the header (only meaningful while it cannot dock). */
96
+ isOpen: boolean;
97
+ size: SidePanelSize;
98
+ minWidth: number;
99
+ minContentWidth: number;
100
+ }
101
+
102
+ export interface SidePanelLayout {
103
+ mode: AppLayoutSidePanelMode;
104
+ /** Width the panel body is drawn at. */
105
+ width: number;
106
+ /** The panel can sit beside the content. */
107
+ canDock: boolean;
108
+ /** Widest docked width; one pixel more takes the whole area. */
109
+ maxDockedWidth: number;
110
+ /** Width the panel docks at, kept while it is full so it can return there. */
111
+ dockedWidth: number;
112
+ }
113
+
114
+ /**
115
+ * Where the panel goes for a given room. Pure, so the rules read in one place:
116
+ * a phone gets the overlay; a row with room for both minimums docks (or is
117
+ * full when dragged past); otherwise it hides until opened from the header.
118
+ * An unmeasured row docks, so the first paint matches the common case.
119
+ */
120
+ export function resolveSidePanelLayout({
121
+ rowWidth,
122
+ isMobile,
123
+ isOpen,
124
+ size,
125
+ minWidth,
126
+ minContentWidth,
127
+ }: SidePanelLayoutInput): SidePanelLayout {
128
+ const measured = rowWidth > 0;
129
+ const room = rowWidth - minContentWidth - SIDE_PANEL_INSET;
130
+ const canDock = !isMobile && (!measured || room >= minWidth);
131
+ const maxDockedWidth = measured ? Math.max(minWidth, room) : Number.POSITIVE_INFINITY;
132
+
133
+ let mode: AppLayoutSidePanelMode;
134
+ if (isMobile) mode = isOpen ? 'overlay' : 'hidden';
135
+ else if (!canDock) mode = isOpen ? 'full' : 'hidden';
136
+ else mode = size.expanded ? 'full' : 'docked';
137
+
138
+ const dockedWidth = Math.min(Math.max(size.width, minWidth), maxDockedWidth);
139
+ let width: number;
140
+ if (mode === 'docked') width = dockedWidth;
141
+ else if (mode === 'overlay') width = rowWidth;
142
+ else width = Math.max(0, rowWidth - 2 * SIDE_PANEL_INSET);
143
+
144
+ return { mode, width, canDock, maxDockedWidth, dockedWidth };
145
+ }
146
+
147
+ function parseSize(raw: string | null): SidePanelSize | null {
148
+ if (!raw) return null;
149
+ try {
150
+ const parsed = JSON.parse(raw) as Partial<SidePanelSize> | null;
151
+ if (!parsed || typeof parsed.width !== 'number' || !Number.isFinite(parsed.width)) return null;
152
+ return { width: parsed.width, expanded: parsed.expanded === true };
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+
158
+ function subscribeToStoredSize(onChange: () => void) {
159
+ window.addEventListener('storage', onChange);
160
+ window.addEventListener(STORAGE_EVENT, onChange);
161
+ return () => {
162
+ window.removeEventListener('storage', onChange);
163
+ window.removeEventListener(STORAGE_EVENT, onChange);
164
+ };
165
+ }
166
+
167
+ /**
168
+ * The persisted size, read through `useSyncExternalStore` so the server and
169
+ * the hydrating client both render the default and the stored width arrives
170
+ * on the render after: a size in the first render would be a hydration
171
+ * mismatch on the panel's inline width.
172
+ */
173
+ function useStoredSize(storageKey: string | undefined, fallback: SidePanelSize) {
174
+ const raw = useSyncExternalStore(
175
+ subscribeToStoredSize,
176
+ () => {
177
+ if (!storageKey) return null;
178
+ try {
179
+ return window.localStorage.getItem(storageKey);
180
+ } catch {
181
+ return null;
182
+ }
183
+ },
184
+ () => null,
185
+ );
186
+ const [sessionSize, setSessionSize] = useState<SidePanelSize | null>(null);
187
+ const size = (storageKey ? parseSize(raw) : sessionSize) ?? fallback;
188
+
189
+ const setSize = (next: SidePanelSize) => {
190
+ if (!storageKey) {
191
+ setSessionSize(next);
192
+ return;
193
+ }
194
+ try {
195
+ window.localStorage.setItem(storageKey, JSON.stringify(next));
196
+ } catch {
197
+ // Storage refused (quota, privacy mode): keep it for the session.
198
+ setSessionSize(next);
199
+ }
200
+ window.dispatchEvent(new Event(STORAGE_EVENT));
201
+ };
202
+
203
+ return [size, setSize] as const;
204
+ }
205
+
206
+ /**
207
+ * The panel is growing into the whole area (`expand`), shrinking back into the
208
+ * column (`collapse`) or narrowing within it (`narrow`). While it grows the
209
+ * content stays where it is under it, and is hidden only once the panel covers
210
+ * it; while it shrinks the content is already laid out at its new width.
211
+ */
212
+ export type SidePanelMorph = 'expand' | 'collapse' | 'narrow';
213
+
214
+ export interface AppLayoutSidePanelState extends SidePanelLayout {
215
+ minWidth: number;
216
+ /** Opened from the header while it cannot dock. */
217
+ isOpen: boolean;
218
+ morph: SidePanelMorph | null;
219
+ /**
220
+ * Dragged wider than any column can be and not yet let go: the panel follows
221
+ * the pointer over the content, laid out at its narrowest beneath it.
222
+ */
223
+ peekWidth: number | null;
224
+ /** The panel covers the content area: the content can be hidden. */
225
+ coversContent: boolean;
226
+ /** The panel is drawn over part of the content (a morph or a peek). */
227
+ overlapsContent: boolean;
228
+ endMorph: () => void;
229
+ collapsesTo: AppLayoutSidePanelRenderState['collapsesTo'];
230
+ collapse: () => void;
231
+ toggle: () => void;
232
+ close: () => void;
233
+ resize: (next: number) => void;
234
+ /**
235
+ * A pointer drag to `next`. Wider than any column can be, the panel follows
236
+ * the pointer over the content instead of jumping to the whole area.
237
+ */
238
+ drag: (next: number) => void;
239
+ /**
240
+ * The drag ended at `next`: a panel left between the widest column and the
241
+ * whole area settles on the nearer of the two, animated.
242
+ */
243
+ release: (next: number) => void;
244
+ }
245
+
246
+ /**
247
+ * Owns the side panel's geometry. Lives in `AppLayout`, not the panel, because
248
+ * the header's toggle reads it too. Returns null without a config.
249
+ */
250
+ export function useAppLayoutSidePanel(
251
+ config: AppLayoutSidePanelConfig | undefined,
252
+ row: HTMLElement | null,
253
+ ): AppLayoutSidePanelState | null {
254
+ const enabled = config !== undefined;
255
+ const minWidth = config?.minWidth ?? 0;
256
+ const minContentWidth = config?.minContentWidth ?? 400;
257
+ const collapsed = config?.collapsed ?? false;
258
+ const minimum: SidePanelSize = { width: minWidth, expanded: false };
259
+
260
+ const [rowWidth, setRowWidth] = useState(0);
261
+ useIsomorphicLayoutEffect(() => {
262
+ if (!enabled || !row) return undefined;
263
+ const update = () => setRowWidth(row.clientWidth);
264
+ update();
265
+ const observer = new ResizeObserver(update);
266
+ observer.observe(row);
267
+ return () => observer.disconnect();
268
+ }, [enabled, row]);
269
+
270
+ const isMobile = useMediaQuery(MOBILE_QUERY) === true;
271
+ const reduceMotion = useMediaQuery(REDUCED_MOTION_QUERY) === true;
272
+ const [morph, setMorph] = useState<SidePanelMorph | null>(null);
273
+ const [liveWidth, setLiveWidth] = useState<number | null>(null);
274
+ const [storedSize, setStoredSize] = useStoredSize(config?.storageKey, minimum);
275
+ const [ownIsOpen, setOwnIsOpen] = useState(false);
276
+ const isOpen = config?.open ?? ownIsOpen;
277
+ const onOpenChange = config?.onOpenChange;
278
+ const setIsOpen = (next: boolean) => {
279
+ if (config?.open === undefined) setOwnIsOpen(next);
280
+ onOpenChange?.(next);
281
+ };
282
+
283
+ // A page that needs the full width starts the panel at its minimum without
284
+ // touching the width the user chose elsewhere: a drag there lasts until the
285
+ // page lets go.
286
+ const [collapsedSize, setCollapsedSize] = useState<SidePanelSize | null>(null);
287
+ const [prevCollapsed, setPrevCollapsed] = useState(collapsed);
288
+ if (collapsed !== prevCollapsed) {
289
+ setPrevCollapsed(collapsed);
290
+ setCollapsedSize(null);
291
+ }
292
+ const size = collapsed ? (collapsedSize ?? minimum) : storedSize;
293
+ const setSize = collapsed ? setCollapsedSize : setStoredSize;
294
+
295
+ const layout = resolveSidePanelLayout({ rowWidth, isMobile, isOpen, size, minWidth, minContentWidth });
296
+
297
+ // Room came back (the navigation collapsed, the window widened): return to
298
+ // the column. Dropping the header-open flag means losing the room again
299
+ // hides the panel rather than throwing it over the content.
300
+ const [prevCanDock, setPrevCanDock] = useState(layout.canDock);
301
+ if (layout.canDock !== prevCanDock) {
302
+ setPrevCanDock(layout.canDock);
303
+ setOwnIsOpen(false);
304
+ }
305
+ // A controlled host hears about the return after the commit (a parent's
306
+ // setState cannot run in this render), and only on the transition: a host
307
+ // may open the panel while it is docked (a deep link), which is not undone.
308
+ const prevCanDockRef = useRef(layout.canDock);
309
+ useEffect(() => {
310
+ const couldDock = prevCanDockRef.current;
311
+ prevCanDockRef.current = layout.canDock;
312
+ if (!couldDock && layout.canDock && config?.open) onOpenChange?.(false);
313
+ }, [layout.canDock, config?.open, onOpenChange]);
314
+
315
+ if (!enabled) return null;
316
+
317
+ const animate = (next: SidePanelMorph) => {
318
+ if (!reduceMotion) setMorph(next);
319
+ };
320
+ const fullWidth = Math.max(0, rowWidth - 2 * SIDE_PANEL_INSET);
321
+ // Only while there is a column to peek out of: losing the room (or a phone)
322
+ // drops it.
323
+ const peekWidth = layout.canDock ? liveWidth : null;
324
+ const dock = (next: number) => setSize({ width: Math.max(minWidth, Math.round(next)), expanded: false });
325
+
326
+ const resize = (next: number) => {
327
+ // Past the content's minimum the panel takes the whole area; dragging
328
+ // back under it docks again at the dragged width.
329
+ const expand = next > layout.maxDockedWidth;
330
+ // Crossing between the column and the whole area is animated, not a
331
+ // jump. Only for a resize: a page load or room coming back just lands.
332
+ if (layout.canDock) {
333
+ if (expand && layout.mode === 'docked') animate('expand');
334
+ else if (!expand && layout.mode === 'full') animate('collapse');
335
+ }
336
+ if (expand) setSize({ width: size.width, expanded: true });
337
+ else dock(next);
338
+ };
339
+
340
+ let collapsesTo: AppLayoutSidePanelRenderState['collapsesTo'] = null;
341
+ if (layout.mode === 'full' && layout.canDock) collapsesTo = layout.dockedWidth > minWidth ? 'column' : 'minimum';
342
+ else if (layout.mode === 'docked' && layout.width > minWidth) collapsesTo = 'minimum';
343
+
344
+ return {
345
+ ...layout,
346
+ minWidth,
347
+ isOpen,
348
+ morph,
349
+ peekWidth,
350
+ coversContent: layout.mode === 'full' && morph !== 'expand' && peekWidth === null,
351
+ overlapsContent: morph !== null || peekWidth !== null,
352
+ endMorph: () => setMorph(null),
353
+ collapsesTo,
354
+ collapse: () => {
355
+ if (collapsesTo === null) return;
356
+ if (layout.mode === 'full') {
357
+ // Back to the column it left, at the width it had there.
358
+ animate('collapse');
359
+ setSize({ width: size.width, expanded: false });
360
+ } else {
361
+ animate('narrow');
362
+ setSize({ width: minWidth, expanded: false });
363
+ }
364
+ },
365
+ toggle: () => setIsOpen(!isOpen),
366
+ close: () => setIsOpen(false),
367
+ resize,
368
+ drag: next => {
369
+ if (next > layout.maxDockedWidth) {
370
+ // Wider than a column can be: follow the pointer over the content, which
371
+ // is laid out at its narrowest (the widest column) underneath.
372
+ if (size.expanded || size.width !== layout.maxDockedWidth) dock(layout.maxDockedWidth);
373
+ setLiveWidth(Math.min(Math.round(next), fullWidth));
374
+ return;
375
+ }
376
+ // Within the column: an ordinary resize, picking up where a peek was.
377
+ setLiveWidth(null);
378
+ dock(next);
379
+ },
380
+ release: next => {
381
+ setLiveWidth(null);
382
+ // Let go within the column: the drag already put it there.
383
+ if (next <= layout.maxDockedWidth) return;
384
+ // Settle on the nearer of the two, from where the pointer left it.
385
+ if (next > (layout.maxDockedWidth + fullWidth) / 2) {
386
+ // Dragged all the way: nothing left to animate.
387
+ if (next < fullWidth) animate('expand');
388
+ setSize({ width: layout.maxDockedWidth, expanded: true });
389
+ } else {
390
+ animate('narrow');
391
+ dock(layout.maxDockedWidth);
392
+ }
393
+ },
394
+ };
395
+ }
396
+
397
+ interface SidePanelResizeHandleProps {
398
+ state: AppLayoutSidePanelState;
399
+ label: string;
400
+ controls: string;
401
+ }
402
+
403
+ function SidePanelResizeHandle({ state, label, controls }: SidePanelResizeHandleProps) {
404
+ const { width, minWidth, maxDockedWidth, mode, peekWidth, resize, drag, release } = state;
405
+ const startRef = useRef<{ x: number; width: number } | null>(null);
406
+ // Pointer moves outpace frames; resize once per frame with the latest one.
407
+ const frameRef = useRef<number | null>(null);
408
+ const pendingRef = useRef<number | null>(null);
409
+ // Unmeasured row (first paint): no upper bound to announce or jump to yet.
410
+ const measured = Number.isFinite(maxDockedWidth);
411
+ // The width the keyboard steps from: in `full`, one past the docked maximum.
412
+ const current = peekWidth ?? (mode === 'full' && measured ? maxDockedWidth + 1 : width);
413
+
414
+ useEffect(
415
+ () => () => {
416
+ if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);
417
+ },
418
+ [],
419
+ );
420
+
421
+ const scheduleDrag = (next: number) => {
422
+ pendingRef.current = next;
423
+ if (frameRef.current !== null) return;
424
+ frameRef.current = requestAnimationFrame(() => {
425
+ frameRef.current = null;
426
+ if (pendingRef.current !== null) drag(pendingRef.current);
427
+ });
428
+ };
429
+
430
+ const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {
431
+ if (event.button !== 0 && event.pointerType === 'mouse') return;
432
+ event.preventDefault();
433
+ // From the whole area the drag starts at the panel's real width, so its
434
+ // edge stays under the pointer instead of jumping to the widest column.
435
+ startRef.current = { x: event.clientX, width: mode === 'full' && measured ? width : current };
436
+ pendingRef.current = null;
437
+ event.currentTarget.setPointerCapture(event.pointerId);
438
+ document.body.style.cursor = 'col-resize';
439
+ document.body.style.userSelect = 'none';
440
+ };
441
+
442
+ const handlePointerMove = (event: PointerEvent<HTMLDivElement>) => {
443
+ const start = startRef.current;
444
+ if (!start) return;
445
+ // The handle sits on the panel's left edge: dragging left widens it.
446
+ scheduleDrag(start.width - (event.clientX - start.x));
447
+ };
448
+
449
+ const endDrag = (event: PointerEvent<HTMLDivElement>) => {
450
+ const start = startRef.current;
451
+ if (!start) return;
452
+ startRef.current = null;
453
+ // Land the last move before deciding where the panel settles.
454
+ if (frameRef.current !== null) {
455
+ cancelAnimationFrame(frameRef.current);
456
+ frameRef.current = null;
457
+ if (pendingRef.current !== null) drag(pendingRef.current);
458
+ }
459
+ if (pendingRef.current !== null) release(pendingRef.current);
460
+ pendingRef.current = null;
461
+ try {
462
+ event.currentTarget.releasePointerCapture(event.pointerId);
463
+ } catch {
464
+ // already released
465
+ }
466
+ document.body.style.cursor = '';
467
+ document.body.style.userSelect = '';
468
+ };
469
+
470
+ const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
471
+ const step = event.shiftKey ? 40 : 16;
472
+ let next: number | null = null;
473
+ if (event.key === 'ArrowLeft') next = current + step;
474
+ else if (event.key === 'ArrowRight') next = Math.min(current, maxDockedWidth) - step;
475
+ else if (event.key === 'Home') next = minWidth;
476
+ else if (event.key === 'End' && measured) next = maxDockedWidth + 1;
477
+ if (next === null) return;
478
+ event.preventDefault();
479
+ resize(next);
480
+ };
481
+
482
+ return (
483
+ <div
484
+ role="separator"
485
+ tabIndex={0}
486
+ aria-controls={controls}
487
+ aria-orientation="vertical"
488
+ aria-label={label}
489
+ aria-valuenow={Math.round(current)}
490
+ aria-valuemin={minWidth}
491
+ aria-valuemax={measured ? Math.round(maxDockedWidth + 1) : undefined}
492
+ aria-valuetext={mode === 'full' ? 'Full width' : `${Math.round(width)} pixels`}
493
+ onPointerDown={handlePointerDown}
494
+ onPointerMove={handlePointerMove}
495
+ onPointerUp={endDrag}
496
+ onPointerCancel={endDrag}
497
+ onKeyDown={handleKeyDown}
498
+ className="group absolute inset-y-0 left-0 z-10 flex w-4 -translate-x-1/2 cursor-col-resize touch-none select-none items-center justify-center outline-none"
499
+ >
500
+ <span
501
+ aria-hidden
502
+ className="flex h-8 w-4 items-center justify-center rounded-sm border border-ods-border bg-ods-card text-ods-text-secondary transition-colors group-hover:border-ods-border-hover group-focus-visible:border-ods-accent group-focus-visible:text-ods-accent"
503
+ >
504
+ <Menu01Icon size={12} />
505
+ </span>
506
+ </div>
507
+ );
508
+ }
509
+
510
+ interface AppLayoutSidePanelProps {
511
+ config: AppLayoutSidePanelConfig;
512
+ state: AppLayoutSidePanelState;
513
+ }
514
+
515
+ export function AppLayoutSidePanel({ config, state }: AppLayoutSidePanelProps) {
516
+ const panelId = useId();
517
+ const { mode, width, dockedWidth, canDock, isOpen, close, morph, endMorph, peekWidth } = state;
518
+
519
+ // A safety net only: `transitionend` does not fire when the width ends up
520
+ // unchanged (a resize that lands where it started). Generous, so a busy or
521
+ // throttled page still finishes the animation before the content is hidden.
522
+ useEffect(() => {
523
+ if (!morph) return undefined;
524
+ const timer = window.setTimeout(endMorph, MORPH_MS * 3);
525
+ return () => window.clearTimeout(timer);
526
+ }, [morph, endMorph]);
527
+
528
+ if (mode === 'hidden') return null;
529
+ const label = config.label ?? 'Side panel';
530
+ // The column's footprint: docked, and while growing over the content, which
531
+ // stays laid out at its width until the panel covers it.
532
+ const inColumn = mode === 'docked' || morph === 'expand' || peekWidth !== null;
533
+
534
+ // Opened from the header: Escape puts it away again.
535
+ const handleKeyDown = (event: KeyboardEvent<HTMLElement>) => {
536
+ if (event.key !== 'Escape' || !isOpen || event.defaultPrevented) return;
537
+ event.preventDefault();
538
+ close();
539
+ };
540
+
541
+ return (
542
+ <aside
543
+ id={panelId}
544
+ aria-label={label}
545
+ onKeyDown={handleKeyDown}
546
+ className={cn(
547
+ 'relative min-w-0',
548
+ // Docked is decided before the viewport is known (server render, first
549
+ // paint): keep it off phones until JS says otherwise.
550
+ mode !== 'overlay' && inColumn && 'hidden shrink-0 md:block',
551
+ mode !== 'overlay' && !inColumn && 'flex-1',
552
+ // z-[99]: above the page's own layers (sticky headers, the z-50 bottom
553
+ // action bar), BELOW the mobile burger menu (backdrop z-[100], panel
554
+ // z-[101]), so the menu opens over a panel covering the content.
555
+ mode === 'overlay' && 'absolute inset-0 z-[99] bg-ods-bg',
556
+ )}
557
+ style={mode !== 'overlay' && inColumn ? { width: dockedWidth + SIDE_PANEL_INSET } : undefined}
558
+ >
559
+ {/* Pinned to the right edge with an explicit width, so the move between
560
+ the column and the whole area is one animated width. */}
561
+ <div
562
+ onTransitionEnd={event => {
563
+ if (event.target === event.currentTarget) endMorph();
564
+ }}
565
+ onTransitionCancel={event => {
566
+ if (event.target === event.currentTarget) endMorph();
567
+ }}
568
+ className={cn(
569
+ 'absolute flex',
570
+ mode === 'overlay' ? 'inset-0' : 'inset-y-[var(--spacing-system-mf)] right-[var(--spacing-system-mf)] z-[1]',
571
+ morph && 'transition-[width] duration-300 ease-in-out motion-reduce:transition-none',
572
+ )}
573
+ style={mode === 'overlay' ? undefined : { width: peekWidth ?? width }}
574
+ >
575
+ {canDock && <SidePanelResizeHandle state={state} label={`Resize ${label}`} controls={panelId} />}
576
+ <div
577
+ className={cn(
578
+ 'flex min-h-0 min-w-0 flex-1 overflow-hidden bg-ods-bg',
579
+ mode !== 'overlay' && 'rounded-md border border-ods-border',
580
+ )}
581
+ >
582
+ {config.children({
583
+ width: peekWidth ?? width,
584
+ mode,
585
+ canClose: mode === 'overlay' || (mode === 'full' && !canDock),
586
+ close,
587
+ collapsesTo: state.collapsesTo,
588
+ collapse: state.collapse,
589
+ })}
590
+ </div>
591
+ </div>
592
+ </aside>
593
+ );
594
+ }