@celestia-island/hikari 0.4.4 → 0.30.0

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 (385) hide show
  1. package/README.md +1 -1
  2. package/package.json +22 -7
  3. package/src/DemoApp.tsx +491 -0
  4. package/src/animation/registerAnimations.test.ts +94 -0
  5. package/src/animation/registerAnimations.ts +103 -0
  6. package/src/components/HkAboutModal.scss +95 -0
  7. package/src/components/HkAboutModal.tsx +114 -0
  8. package/src/components/HkAdaptiveDialog.scss +22 -0
  9. package/src/components/HkAdaptiveDialog.test.tsx +271 -0
  10. package/src/components/HkAdaptiveDialog.tsx +167 -0
  11. package/src/components/HkAdminHeader.test.tsx +185 -0
  12. package/src/components/HkAdminHeader.tsx +308 -0
  13. package/src/components/HkAdminShell.test.tsx +237 -0
  14. package/src/components/HkAdminShell.tsx +129 -0
  15. package/src/components/HkAdminTablePage.tsx +70 -0
  16. package/src/components/HkAdminWidgets.test.ts +89 -0
  17. package/src/components/HkAlert.tsx +1 -1
  18. package/src/components/HkAltSignIn.scss +55 -0
  19. package/src/components/HkAltSignIn.test.tsx +88 -0
  20. package/src/components/HkAltSignIn.tsx +92 -0
  21. package/src/components/HkAttachmentModal.scss +49 -0
  22. package/src/components/HkAttachmentModal.tsx +318 -0
  23. package/src/components/HkAuthCard.tsx +50 -0
  24. package/src/components/HkAuthSubmitButton.scss +12 -0
  25. package/src/components/HkAuthSubmitButton.tsx +94 -0
  26. package/src/components/HkAvatar.scss +93 -0
  27. package/src/components/HkAvatar.tsx +2 -2
  28. package/src/components/HkBlockingToast.scss +123 -0
  29. package/src/components/HkBlockingToast.test.tsx +221 -0
  30. package/src/components/HkBlockingToast.tsx +182 -0
  31. package/src/components/HkBreadcrumb.scss +624 -0
  32. package/src/components/HkBreadcrumb.tsx +2 -2
  33. package/src/components/HkButton.scss +16 -2
  34. package/src/components/HkCaptchaModal.scss +14 -0
  35. package/src/components/HkCaptchaModal.tsx +62 -0
  36. package/src/components/HkCaptchaWidget.scss +30 -0
  37. package/src/components/HkCaptchaWidget.tsx +181 -0
  38. package/src/components/HkCard.scss +1 -1
  39. package/src/components/HkChatTypes.ts +65 -0
  40. package/src/components/HkCheckbox.scss +4 -9
  41. package/src/components/HkCheckbox.tsx +18 -6
  42. package/src/components/HkColorPicker.scss +120 -3
  43. package/src/components/HkColorPicker.tsx +130 -19
  44. package/src/components/HkColorPickerTouch.test.tsx +145 -0
  45. package/src/components/HkColorSchemeDialog.scss +52 -0
  46. package/src/components/HkColorSchemeDialog.test.ts +201 -0
  47. package/src/components/HkColorSchemeDialog.tsx +96 -0
  48. package/src/components/HkColorSchemeEditor.test.ts +270 -0
  49. package/src/components/HkColorSchemeEditor.tsx +410 -0
  50. package/src/components/HkConfirmDialog.scss +43 -3
  51. package/src/components/HkConfirmDialog.tsx +7 -4
  52. package/src/components/HkConnectionInfo.ts +80 -0
  53. package/src/components/HkConnectionStatus.tsx +158 -0
  54. package/src/components/HkCookieConsent.scss +22 -0
  55. package/src/components/HkCookieConsent.test.tsx +70 -0
  56. package/src/components/HkCookieConsent.tsx +56 -0
  57. package/src/components/HkCountdownDigit.tsx +105 -0
  58. package/src/components/HkDatePicker.scss +361 -0
  59. package/src/components/HkDatePicker.test.ts +459 -0
  60. package/src/components/HkDatePicker.tsx +568 -0
  61. package/src/components/HkDateTimePicker.scss +65 -37
  62. package/src/components/HkDateTimePicker.test.ts +344 -0
  63. package/src/components/HkDateTimePicker.tsx +249 -82
  64. package/src/components/HkDraggableGrid.scss +4 -1
  65. package/src/components/HkDraggableList.scss +4 -1
  66. package/src/components/HkDrawer.scss +27 -13
  67. package/src/components/HkDrawer.test.tsx +73 -0
  68. package/src/components/HkDrawer.tsx +157 -10
  69. package/src/components/HkErrorBoundary.scss +5 -2
  70. package/src/components/HkErrorBoundary.tsx +7 -4
  71. package/src/components/HkExpansionPanel.scss +143 -0
  72. package/src/components/HkExpansionPanel.test.tsx +109 -0
  73. package/src/components/HkExpansionPanel.tsx +101 -0
  74. package/src/components/HkFab.scss +162 -0
  75. package/src/components/HkFab.test.tsx +217 -0
  76. package/src/components/HkFab.tsx +214 -0
  77. package/src/components/HkHoverRevealAction.scss +6 -0
  78. package/src/components/HkHoverRevealAction.test.ts +68 -0
  79. package/src/components/HkHoverRevealAction.tsx +84 -6
  80. package/src/components/HkIcon.scss +123 -0
  81. package/src/components/HkIcon.tsx +7 -23
  82. package/src/components/HkIconButton.scss +170 -0
  83. package/src/components/HkIconButton.test.tsx +75 -0
  84. package/src/components/HkIconButton.tsx +4 -3
  85. package/src/components/HkIconButtonVars.scss +180 -0
  86. package/src/components/HkInput.affix.test.ts +253 -0
  87. package/src/components/HkInput.enter.test.ts +88 -0
  88. package/src/components/HkInput.scss +127 -8
  89. package/src/components/HkInput.tsx +245 -13
  90. package/src/components/HkKeywordSearchModal.scss +13 -0
  91. package/src/components/HkKeywordSearchModal.tsx +46 -9
  92. package/src/components/HkLabel.scss +34 -0
  93. package/src/components/HkLabel.test.tsx +61 -0
  94. package/src/components/HkLabel.tsx +45 -0
  95. package/src/components/HkListTransition.scss +26 -0
  96. package/src/components/HkListTransition.tsx +57 -0
  97. package/src/components/HkLocalePicker.scss +37 -0
  98. package/src/components/HkLocalePicker.tsx +133 -0
  99. package/src/components/HkLocalePickerPopup.tsx +35 -19
  100. package/src/components/HkLocalizedInput.scss +277 -0
  101. package/src/components/HkLocalizedInput.test.tsx +693 -0
  102. package/src/components/HkLocalizedInput.tsx +495 -0
  103. package/src/components/HkLogWindow.scss +121 -0
  104. package/src/components/HkLogWindow.tsx +195 -0
  105. package/src/components/HkLogo.scss +1 -1
  106. package/src/components/HkLogo.tsx +1 -1
  107. package/src/components/HkMarkdownRenderer.scss +22 -2
  108. package/src/components/HkMarkdownRenderer.tsx +33 -3
  109. package/src/components/HkMediaControlBar.tsx +20 -13
  110. package/src/components/HkMenu.scss +200 -0
  111. package/src/components/HkMenu.test.ts +779 -0
  112. package/src/components/HkMenu.tsx +770 -0
  113. package/src/components/HkMenuActionItem.scss +58 -0
  114. package/src/components/HkMenuActionItem.tsx +41 -0
  115. package/src/components/HkMenuIdentityItem.scss +70 -0
  116. package/src/components/HkMenuIdentityItem.tsx +42 -0
  117. package/src/components/HkMenuPanel.scss +20 -0
  118. package/src/components/HkMenuPanel.test.tsx +118 -0
  119. package/src/components/HkMenuPanel.tsx +32 -0
  120. package/src/components/HkMinimap.tsx +16 -8
  121. package/src/components/HkModal.autofollow.test.ts +44 -0
  122. package/src/components/HkModal.scss +170 -53
  123. package/src/components/HkModal.sheetgap.test.ts +82 -0
  124. package/src/components/HkModal.test.tsx +334 -0
  125. package/src/components/HkModal.tsx +314 -68
  126. package/src/components/HkModalBreadcrumb.scss +4 -1
  127. package/src/components/HkModalBreadcrumb.test.tsx +111 -0
  128. package/src/components/HkModalBreadcrumb.tsx +34 -11
  129. package/src/components/HkModelCatalog.ts +98 -0
  130. package/src/components/HkModelTag.scss +130 -0
  131. package/src/components/HkModelTag.test.tsx +111 -0
  132. package/src/components/HkModelTag.tsx +190 -0
  133. package/src/components/HkNavItem.scss +13 -2
  134. package/src/components/HkNavItem.tsx +2 -1
  135. package/src/components/HkNavSidebar.tsx +99 -0
  136. package/src/components/HkNumberInput.scss +12 -0
  137. package/src/components/HkNumberInput.tsx +15 -8
  138. package/src/components/HkPageHeader.scss +79 -0
  139. package/src/components/HkPageHeader.test.ts +67 -0
  140. package/src/components/HkPageHeader.tsx +51 -0
  141. package/src/components/HkPasswordInput.scss +29 -17
  142. package/src/components/HkPasswordInput.test.ts +281 -0
  143. package/src/components/HkPasswordInput.tsx +337 -59
  144. package/src/components/HkPickerPane.scss +158 -0
  145. package/src/components/HkPlaceholderMarquee.scss +109 -0
  146. package/src/components/HkPlaceholderMarquee.test.ts +305 -0
  147. package/src/components/HkPlaceholderMarquee.tsx +170 -0
  148. package/src/components/HkPopover.scss +134 -10
  149. package/src/components/HkPopover.sheetdock.test.ts +34 -0
  150. package/src/components/HkPopover.surfacevars.test.ts +20 -0
  151. package/src/components/HkPopover.titleSuppress.test.tsx +265 -0
  152. package/src/components/HkPopover.tsx +225 -50
  153. package/src/components/HkPopoverSheet.test.tsx +130 -0
  154. package/src/components/HkPopupSelect.scss +30 -10
  155. package/src/components/HkPopupSelect.tsx +21 -1
  156. package/src/components/HkProgressDialog.scss +31 -0
  157. package/src/components/HkProgressDialog.tsx +37 -19
  158. package/src/components/HkProtocolModal.scss +26 -0
  159. package/src/components/HkProtocolModal.tsx +112 -0
  160. package/src/components/HkRadio.scss +1 -16
  161. package/src/components/HkRadio.tsx +5 -2
  162. package/src/components/HkRichInput.scss +162 -0
  163. package/src/components/HkRichInput.tsx +335 -0
  164. package/src/components/HkRollingNumber.test.ts +90 -0
  165. package/src/components/HkRollingNumber.tsx +39 -1
  166. package/src/components/HkScrollContainer.scss +71 -54
  167. package/src/components/HkScrollContainer.test.ts +160 -0
  168. package/src/components/HkScrollContainer.tsx +187 -160
  169. package/src/components/HkSearchInput.tsx +3 -3
  170. package/src/components/HkSecretRevealModal.scss +61 -0
  171. package/src/components/HkSecretRevealModal.tsx +91 -0
  172. package/src/components/HkSelect.scss +263 -12
  173. package/src/components/HkSelect.sheetdock.test.ts +30 -0
  174. package/src/components/HkSelect.test.tsx +88 -0
  175. package/src/components/HkSelect.tsx +59 -74
  176. package/src/components/HkSelectPanel.test.tsx +666 -0
  177. package/src/components/HkSelectPanel.tsx +595 -0
  178. package/src/components/HkSelectSheet.test.tsx +132 -0
  179. package/src/components/HkSelectionGrid.scss +1 -1
  180. package/src/components/HkSelectionGrid.tsx +2 -1
  181. package/src/components/HkSelectionWaterfall.scss +1 -1
  182. package/src/components/HkShareBar.scss +40 -0
  183. package/src/components/HkShareBar.tsx +50 -0
  184. package/src/components/HkSidebar.scss +15 -10
  185. package/src/components/HkSidebar.tsx +26 -2
  186. package/src/components/HkSignInCard.test.ts +154 -0
  187. package/src/components/HkSignInCard.tsx +155 -0
  188. package/src/components/HkSlider.scss +99 -0
  189. package/src/components/HkSlider.test.tsx +155 -0
  190. package/src/components/HkSlider.tsx +173 -0
  191. package/src/components/HkSpinner.scss +36 -1
  192. package/src/components/HkSpinner.test.tsx +63 -0
  193. package/src/components/HkSpinner.tsx +16 -1
  194. package/src/components/HkSplash.scss +1 -1
  195. package/src/components/HkSplash.tsx +3 -1
  196. package/src/components/HkStatCard.scss +73 -0
  197. package/src/components/HkStatCard.tsx +46 -0
  198. package/src/components/HkStatusBar.test.tsx +178 -0
  199. package/src/components/HkStatusBar.tsx +379 -0
  200. package/src/components/HkStatusPill.scss +73 -0
  201. package/src/components/HkStatusPill.tsx +41 -0
  202. package/src/components/HkStepFlow.scss +75 -0
  203. package/src/components/HkStepFlow.test.tsx +246 -0
  204. package/src/components/HkStepFlow.tsx +115 -0
  205. package/src/components/HkSwitch.scss +5 -17
  206. package/src/components/HkSwitch.tsx +6 -3
  207. package/src/components/HkTable.scss +20 -1
  208. package/src/components/HkTable.tsx +28 -3
  209. package/src/components/HkTabs.scss +222 -48
  210. package/src/components/HkTabs.test.ts +517 -0
  211. package/src/components/HkTabs.tsx +567 -56
  212. package/src/components/HkTag.tsx +2 -1
  213. package/src/components/HkTextarea.tsx +5 -0
  214. package/src/components/HkThemeToggle.scss +232 -0
  215. package/src/components/HkThemeToggle.test.ts +231 -0
  216. package/src/components/HkThemeToggle.tsx +289 -0
  217. package/src/components/HkTimeline.scss +134 -1
  218. package/src/components/HkTimeline.test.tsx +319 -0
  219. package/src/components/HkTimeline.tsx +303 -54
  220. package/src/components/HkToast.scss +34 -5
  221. package/src/components/HkToast.test.tsx +109 -0
  222. package/src/components/HkToast.tsx +57 -8
  223. package/src/components/HkTokenUsageBadge.scss +53 -0
  224. package/src/components/HkTokenUsageBadge.test.tsx +59 -0
  225. package/src/components/HkTokenUsageBadge.tsx +54 -0
  226. package/src/components/HkTokenUsagePanel.scss +95 -0
  227. package/src/components/HkTokenUsagePanel.tsx +140 -0
  228. package/src/components/HkToolBlock.scss +428 -0
  229. package/src/components/HkToolBlock.tsx +639 -0
  230. package/src/components/HkTooltip.scss +14 -2
  231. package/src/components/HkTooltip.tsx +25 -2
  232. package/src/components/HkTrendChart.tsx +27 -16
  233. package/src/components/HkVoiceInputPopup.scss +77 -0
  234. package/src/components/HkVoiceInputPopup.tsx +97 -0
  235. package/src/components/HkZoomToolbar.scss +4 -1
  236. package/src/components/HkZoomToolbar.tsx +7 -4
  237. package/src/components/_hikari-vars.scss +6 -0
  238. package/src/composables/highlightLanguages.ts +109 -0
  239. package/src/composables/iconRegistry.ts +127 -0
  240. package/src/composables/messaging/browserTransport.ts +96 -0
  241. package/src/composables/messaging/index.ts +204 -0
  242. package/src/composables/messaging/messaging.test.ts +256 -0
  243. package/src/composables/messaging/toastTransport.ts +26 -0
  244. package/src/composables/messaging/types.ts +115 -0
  245. package/src/composables/useActionBar.ts +66 -0
  246. package/src/composables/useAttachments.ts +154 -0
  247. package/src/composables/useConnectionProbe.ts +142 -0
  248. package/src/composables/useDeferredTransition.ts +16 -0
  249. package/src/composables/useEngineHealth.ts +69 -0
  250. package/src/composables/useHighlight.ts +124 -0
  251. package/src/composables/useMeasuredHighlight.ts +150 -0
  252. package/src/composables/useOverlayScrollbar.test.ts +221 -0
  253. package/src/composables/useOverlayScrollbar.ts +296 -0
  254. package/src/composables/usePageTitle.ts +37 -0
  255. package/src/composables/useReducedMotion.ts +34 -0
  256. package/src/composables/useResourceListModal.tsx +147 -0
  257. package/src/composables/useSafeArea.ts +137 -0
  258. package/src/composables/useSendShortcut.ts +35 -0
  259. package/src/composables/useSizeMorph.test.ts +156 -0
  260. package/src/composables/useSizeMorph.ts +149 -0
  261. package/src/composables/useSurfaceTransition.test.ts +96 -0
  262. package/src/composables/useSurfaceTransition.ts +71 -0
  263. package/src/composables/useZoomPan.ts +192 -0
  264. package/src/demo.scss +9 -1
  265. package/src/demo.ts +5 -1
  266. package/src/i18n/locales/ar/chat.json +38 -0
  267. package/src/i18n/locales/ar/components.json +141 -40
  268. package/src/i18n/locales/ar/components.toml +59 -0
  269. package/src/i18n/locales/ar/platform.json +65 -0
  270. package/src/i18n/locales/de/chat.json +38 -0
  271. package/src/i18n/locales/de/components.json +139 -38
  272. package/src/i18n/locales/de/components.toml +59 -0
  273. package/src/i18n/locales/de/platform.json +65 -0
  274. package/src/i18n/locales/en/chat.json +38 -0
  275. package/src/i18n/locales/en/components.json +104 -3
  276. package/src/i18n/locales/en/components.toml +59 -0
  277. package/src/i18n/locales/en/platform.json +65 -0
  278. package/src/i18n/locales/es/chat.json +38 -0
  279. package/src/i18n/locales/es/components.json +141 -40
  280. package/src/i18n/locales/es/components.toml +59 -0
  281. package/src/i18n/locales/es/platform.json +65 -0
  282. package/src/i18n/locales/fr/chat.json +38 -0
  283. package/src/i18n/locales/fr/components.json +137 -36
  284. package/src/i18n/locales/fr/components.toml +59 -0
  285. package/src/i18n/locales/fr/platform.json +65 -0
  286. package/src/i18n/locales/ja/chat.json +38 -0
  287. package/src/i18n/locales/ja/components.json +104 -3
  288. package/src/i18n/locales/ja/components.toml +59 -0
  289. package/src/i18n/locales/ja/platform.json +65 -0
  290. package/src/i18n/locales/ko/chat.json +38 -0
  291. package/src/i18n/locales/ko/components.json +104 -3
  292. package/src/i18n/locales/ko/components.toml +59 -0
  293. package/src/i18n/locales/ko/platform.json +65 -0
  294. package/src/i18n/locales/pt/chat.json +38 -0
  295. package/src/i18n/locales/pt/components.json +141 -40
  296. package/src/i18n/locales/pt/components.toml +59 -0
  297. package/src/i18n/locales/pt/platform.json +65 -0
  298. package/src/i18n/locales/ru/chat.json +38 -0
  299. package/src/i18n/locales/ru/components.json +141 -40
  300. package/src/i18n/locales/ru/components.toml +59 -0
  301. package/src/i18n/locales/ru/platform.json +65 -0
  302. package/src/i18n/locales/zh-Hans/chat.json +38 -0
  303. package/src/i18n/locales/zh-Hans/components.json +109 -4
  304. package/src/i18n/locales/zh-Hans/components.toml +59 -0
  305. package/src/i18n/locales/zh-Hans/platform.json +65 -0
  306. package/src/i18n/locales/zh-Hant/chat.json +38 -0
  307. package/src/i18n/locales/zh-Hant/components.json +109 -4
  308. package/src/i18n/locales/zh-Hant/components.toml +59 -0
  309. package/src/i18n/locales/zh-Hant/platform.json +65 -0
  310. package/src/index.ts +225 -6
  311. package/src/plugins/bootScriptsPlugin.ts +47 -0
  312. package/src/plugins/buildHashPlugin.ts +67 -0
  313. package/src/plugins/index.ts +2 -0
  314. package/src/render.ts +1 -1
  315. package/src/runtime/animationBus.test.ts +112 -0
  316. package/src/runtime/animationBus.ts +33 -2
  317. package/src/runtime/backStack.test.ts +334 -0
  318. package/src/runtime/backStack.ts +377 -0
  319. package/src/runtime/env.ts +4 -0
  320. package/src/runtime/index.ts +15 -3
  321. package/src/runtime/intervalBus.test.ts +118 -0
  322. package/src/runtime/intervalBus.ts +189 -0
  323. package/src/runtime/mobileViewport.test.ts +132 -0
  324. package/src/runtime/mobileViewport.ts +157 -0
  325. package/src/runtime/pageLifecycle.ts +91 -0
  326. package/src/runtime/useBlockingToast.ts +112 -0
  327. package/src/runtime/useClipboard.ts +28 -0
  328. package/src/runtime/useConfirm.ts +15 -7
  329. package/src/runtime/useMediaQuery.test.ts +67 -0
  330. package/src/runtime/useMediaQuery.ts +89 -0
  331. package/src/runtime/useOverlay.test.ts +160 -0
  332. package/src/runtime/useOverlay.ts +59 -12
  333. package/src/runtime/usePopupManager.test.ts +200 -0
  334. package/src/runtime/usePopupManager.ts +106 -10
  335. package/src/runtime/useToast.test.ts +90 -0
  336. package/src/runtime/useToast.ts +20 -2
  337. package/src/styles/admin-tokens.scss +744 -0
  338. package/src/styles/index.scss +5 -3
  339. package/src/styles/theme/_glass.scss +33 -0
  340. package/src/styles/theme/_layout.scss +123 -0
  341. package/src/styles/theme/_scrollbar.scss +73 -0
  342. package/src/styles/theme/_tokens.scss +69 -0
  343. package/src/styles/theme/base.scss +617 -0
  344. package/src/styles/theme/foundation.scss +219 -0
  345. package/src/styles/theme/mixins.scss +793 -0
  346. package/src/styles/theme/themes.scss +25 -0
  347. package/src/styles/theme/variables.scss +125 -0
  348. package/src/theme/_field.scss +116 -0
  349. package/src/theme/fontContext.test.ts +179 -0
  350. package/src/theme/fontContext.ts +169 -0
  351. package/src/theme/index.ts +19 -2
  352. package/src/theme/presets.ts +86 -18
  353. package/src/theme/theme.scss +61 -1
  354. package/src/theme/tokenGroups.test.ts +534 -0
  355. package/src/theme/tokenGroups.ts +552 -0
  356. package/src/theme/useSolarTime.test.ts +113 -0
  357. package/src/theme/useSolarTime.ts +95 -20
  358. package/src/theme/useTheme.test.ts +72 -0
  359. package/src/theme/useTheme.ts +144 -9
  360. package/src/tokens.scss +110 -0
  361. package/src/utils/base64.ts +31 -0
  362. package/src/utils/connectivity.ts +51 -0
  363. package/src/utils/dom.test.ts +131 -0
  364. package/src/utils/dom.ts +133 -0
  365. package/src/utils/download.ts +22 -0
  366. package/src/utils/fileType.ts +127 -0
  367. package/src/utils/format.ts +98 -0
  368. package/src/utils/fuzzy.ts +77 -0
  369. package/src/utils/fuzzySearch.test.ts +87 -0
  370. package/src/utils/healthProbe.ts +64 -0
  371. package/src/utils/objectPath.ts +59 -0
  372. package/src/utils/password.ts +53 -0
  373. package/src/utils/pow.test.ts +45 -0
  374. package/src/utils/pow.ts +226 -0
  375. package/src/utils/powNonce.ts +95 -0
  376. package/src/utils/validation.test.ts +78 -0
  377. package/src/DemoApp.vue +0 -400
  378. package/src/components/HkCollapse.tsx +0 -48
  379. package/src/components/HkMorphingTabs.scss +0 -141
  380. package/src/components/HkMorphingTabs.tsx +0 -113
  381. package/src/composables/useBreakpoint.ts +0 -41
  382. package/src/composables/useClipboard.ts +0 -37
  383. package/src/composables/useConfirm.ts +0 -35
  384. package/src/composables/useToast.ts +0 -2
  385. package/src/runtime/useAsyncData.ts +0 -30
@@ -0,0 +1,666 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { createApp, defineComponent, h, nextTick, ref } from "vue";
3
+ import type { ComponentPublicInstance } from "vue";
4
+
5
+ import HkSelectPanel from "./HkSelectPanel";
6
+
7
+ const mounts: ReturnType<typeof createApp>[] = [];
8
+ const containers: HTMLElement[] = [];
9
+
10
+ afterEach(() => {
11
+ for (const app of mounts.splice(0)) app.unmount();
12
+ for (const el of containers.splice(0)) el.remove();
13
+ // happy-dom never fires transitionend, so leave transitions never
14
+ // finish and teleported panels linger on <body> — strip them or the
15
+ // next test's querySelector sees this test's panel.
16
+ document.body
17
+ .querySelectorAll(".hk-popover-panel, .hk-popover-scrim, .hk-select-popout, .hk-select-popout-host, .hk-select-sheet-panel, .hk-select-sheet-scrim")
18
+ .forEach((el) => el.remove());
19
+ setViewport(1200);
20
+ });
21
+
22
+ function setViewport(width: number) {
23
+ window.innerWidth = width;
24
+ window.dispatchEvent(new Event("resize"));
25
+ }
26
+
27
+ /**
28
+ * The custom-invocation rig: a plain button (NOT a dropdown trigger) that
29
+ * anchors a panel filled with checkbox rows — the exact "not a dropdown,
30
+ * but opens the dropdown's panel" shape downstream apps need.
31
+ */
32
+ function mountPanel(props: Record<string, unknown> = {}) {
33
+ const container = document.createElement("div");
34
+ document.body.appendChild(container);
35
+ containers.push(container);
36
+
37
+ const open = ref(false);
38
+ const checked = ref(["a"]);
39
+ const anchorEl = ref<HTMLElement | null>(null);
40
+ const keydownSpy = vi.fn();
41
+ const Wrapper = defineComponent({
42
+ setup() {
43
+ return () =>
44
+ h("div", [
45
+ h(
46
+ "button",
47
+ {
48
+ ref: (el: Element | ComponentPublicInstance | null) => {
49
+ anchorEl.value = el as HTMLElement | null;
50
+ },
51
+ type: "button",
52
+ onClick: () => { open.value = !open.value; },
53
+ },
54
+ "filter",
55
+ ),
56
+ h(
57
+ HkSelectPanel,
58
+ {
59
+ open: open.value,
60
+ "onUpdate:open": (v: boolean) => { open.value = v; },
61
+ anchorRef: anchorEl.value,
62
+ title: "Filters",
63
+ placement: "top-start",
64
+ onKeydown: keydownSpy,
65
+ ...props,
66
+ },
67
+ {
68
+ default: () =>
69
+ ["a", "b"].map((k) =>
70
+ h("label", { class: "row", key: k }, [
71
+ h("input", {
72
+ type: "checkbox",
73
+ checked: checked.value.includes(k),
74
+ onChange: () => {
75
+ checked.value = checked.value.includes(k)
76
+ ? checked.value.filter((x) => x !== k)
77
+ : [...checked.value, k];
78
+ },
79
+ }),
80
+ k,
81
+ ]),
82
+ ),
83
+ },
84
+ ),
85
+ ]);
86
+ },
87
+ });
88
+ const app = createApp(Wrapper);
89
+ mounts.push(app);
90
+ app.mount(container);
91
+ return { open, checked, container, keydownSpy };
92
+ }
93
+
94
+ describe("HkSelectPanel custom invocation", () => {
95
+ it("renders custom slot rows in an anchored popout on desktop", async () => {
96
+ const { container } = mountPanel();
97
+ await nextTick();
98
+
99
+ container.querySelector<HTMLButtonElement>("button")!.click();
100
+ await nextTick();
101
+
102
+ const popout = document.body.querySelector<HTMLElement>(".hk-select-popout")!;
103
+ expect(popout).toBeTruthy();
104
+ expect(popout.querySelectorAll("label.row")).toHaveLength(2);
105
+ // No sheet form on desktop widths.
106
+ expect(document.body.querySelector(".hk-select-sheet-panel")).toBeNull();
107
+ });
108
+
109
+ it("keeps the panel open while rows toggle (no auto-close)", async () => {
110
+ const { container, open } = mountPanel();
111
+ await nextTick();
112
+
113
+ container.querySelector<HTMLButtonElement>("button")!.click();
114
+ await nextTick();
115
+
116
+ const box = document.body.querySelector<HTMLInputElement>(".hk-select-popout label.row input")!;
117
+ box.click();
118
+ await nextTick();
119
+ expect(open.value).toBe(true);
120
+ });
121
+
122
+ it("scrolling through the overlay track does not dismiss the popout", async () => {
123
+ const { container, open } = mountPanel();
124
+ await nextTick();
125
+
126
+ container.querySelector<HTMLButtonElement>("button")!.click();
127
+ await nextTick();
128
+ await nextTick(); // the overlay attaches on a post-open nextTick
129
+
130
+ // The track is appended to the fixed HOST wrapper — a sibling of the
131
+ // popout panel, NOT contained by it. A click there (which reaches the
132
+ // capture-phase document dismissal listener before any track handler)
133
+ // must read as inside the panel, exactly like the native scrollbar it
134
+ // replaced — native bars never emit page-visible clicks.
135
+ const track = document.body.querySelector<HTMLElement>(
136
+ ".hk-select-popout-host > .hk-scrollbar-track",
137
+ );
138
+ expect(track).toBeTruthy();
139
+ track!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
140
+ await nextTick();
141
+ expect(open.value).toBe(true);
142
+ });
143
+
144
+ it("closes on outside click but not on row clicks", async () => {
145
+ const { open } = mountPanel();
146
+ await nextTick();
147
+ open.value = true;
148
+ await nextTick();
149
+
150
+ document.body.querySelector<HTMLElement>(".hk-select-popout label.row")!.click();
151
+ await nextTick();
152
+ expect(open.value).toBe(true);
153
+
154
+ const outside = document.createElement("div");
155
+ document.body.appendChild(outside);
156
+ outside.click();
157
+ await nextTick();
158
+ expect(open.value).toBe(false);
159
+ outside.remove();
160
+ });
161
+
162
+ it("forwards surface keydown to the owner and closes on surface Escape only", async () => {
163
+ const { open, keydownSpy } = mountPanel();
164
+ await nextTick();
165
+ open.value = true;
166
+ await nextTick();
167
+
168
+ document.dispatchEvent(
169
+ new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
170
+ );
171
+ expect(keydownSpy).not.toHaveBeenCalled(); // document-level ≠ panel surface
172
+
173
+ document.body
174
+ .querySelector<HTMLElement>(".hk-select-popout")!
175
+ .dispatchEvent(
176
+ new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
177
+ );
178
+ expect(keydownSpy).toHaveBeenCalled();
179
+
180
+ // Escape on an unrelated input must NOT close the panel (no
181
+ // document-capture listener — Escape is surface-attached).
182
+ document.dispatchEvent(
183
+ new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
184
+ );
185
+ await nextTick();
186
+ expect(open.value).toBe(true);
187
+
188
+ document.body
189
+ .querySelector<HTMLElement>(".hk-select-popout")!
190
+ .dispatchEvent(
191
+ new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
192
+ );
193
+ await nextTick();
194
+ expect(open.value).toBe(false);
195
+ });
196
+
197
+ it("docks as a titled bottom sheet on mobile widths", async () => {
198
+ setViewport(375);
199
+ const { open } = mountPanel();
200
+ await nextTick();
201
+ open.value = true;
202
+ await nextTick();
203
+
204
+ const sheet = document.body.querySelector<HTMLElement>(".hk-select-sheet-panel")!;
205
+ expect(sheet).toBeTruthy();
206
+ expect(sheet.getAttribute("aria-modal")).toBe("true");
207
+ expect(sheet.querySelector(".hk-select-sheet-title")!.textContent).toBe("Filters");
208
+ expect(sheet.querySelectorAll("label.row")).toHaveLength(2);
209
+ expect(document.body.querySelector(".hk-select-popout")).toBeNull();
210
+
211
+ (document.body.querySelector<HTMLElement>(".hk-select-sheet-scrim"))!.click();
212
+ await nextTick();
213
+ expect(open.value).toBe(false);
214
+ });
215
+
216
+ it("keeps the sheet mounted through its close so the leave transition runs", async () => {
217
+ setViewport(375);
218
+ const { open } = mountPanel();
219
+ await nextTick();
220
+ open.value = true;
221
+ await nextTick();
222
+
223
+ const panel = document.body.querySelector<HTMLElement>(".hk-select-sheet-panel")!;
224
+ expect(panel).toBeTruthy();
225
+
226
+ open.value = false;
227
+ await nextTick();
228
+ // happy-dom never finishes leave transitions, so the panel lingers —
229
+ // exactly what a running slide-down leave looks like. A synchronous
230
+ // unmount (the regression this guards against) removes it instantly.
231
+ expect(document.body.querySelector(".hk-select-sheet-panel")).toBeTruthy();
232
+ });
233
+
234
+ it("registers with the popup manager when mounted already open", async () => {
235
+ // immediate:true — a panel mounted with open=true must land in the
236
+ // z band (handle z ≥ 1000), not the unregistered fallback.
237
+ const container = document.createElement("div");
238
+ document.body.appendChild(container);
239
+ containers.push(container);
240
+
241
+ const open = ref(true);
242
+ const anchorEl = ref<HTMLElement | null>(null);
243
+ const Wrapper = defineComponent({
244
+ setup() {
245
+ return () =>
246
+ h("div", [
247
+ h("button", {
248
+ ref: (el: Element | ComponentPublicInstance | null) => {
249
+ anchorEl.value = el as HTMLElement | null;
250
+ },
251
+ type: "button",
252
+ }, "anchor"),
253
+ h(
254
+ HkSelectPanel,
255
+ {
256
+ open: open.value,
257
+ "onUpdate:open": (v: boolean) => { open.value = v; },
258
+ anchorRef: anchorEl.value,
259
+ title: "Filters",
260
+ },
261
+ { default: () => h("label", { class: "row" }, "row") },
262
+ ),
263
+ ]);
264
+ },
265
+ });
266
+ const app = createApp(Wrapper);
267
+ mounts.push(app);
268
+ app.mount(container);
269
+ await nextTick();
270
+
271
+ // Inline geometry (coords + popup-manager z) lives on the fixed
272
+ // positioning wrapper that hosts the overlay scrollbar tracks.
273
+ const host = document.body.querySelector<HTMLElement>(".hk-select-popout-host")!;
274
+ expect(host).toBeTruthy();
275
+ const z = Number.parseInt(host.style.zIndex, 10);
276
+ expect(z).toBeGreaterThanOrEqual(1000);
277
+ });
278
+
279
+ it("places a top-start popout above the anchor and flips when cramped", async () => {
280
+ const { container, open } = mountPanel();
281
+ await nextTick();
282
+
283
+ const anchor = container.querySelector<HTMLButtonElement>("button")!;
284
+ anchor.getBoundingClientRect = () =>
285
+ ({ top: 500, bottom: 520, left: 40, right: 140, width: 100, height: 20 }) as DOMRect;
286
+ open.value = true;
287
+ await nextTick();
288
+
289
+ const host = document.body.querySelector<HTMLElement>(".hk-select-popout-host")!;
290
+ const top = Number.parseInt(host.style.top, 10);
291
+ // Anchor bottom at 520 → a top-start panel sits above 520 (minus offset).
292
+ expect(top).toBeLessThan(520);
293
+ expect(host.style.minWidth).toBe("100px");
294
+
295
+ // Starved top side flips the panel below the anchor instead.
296
+ anchor.getBoundingClientRect = () =>
297
+ ({ top: 0, bottom: 8, left: 40, right: 140, width: 100, height: 8 }) as DOMRect;
298
+ window.dispatchEvent(new Event("resize"));
299
+ await nextTick();
300
+ const flipped = Number.parseInt(host.style.top, 10);
301
+ expect(flipped).toBeGreaterThan(8);
302
+ });
303
+
304
+ it("clamps a tall flipped popout into the viewport instead of going negative", async () => {
305
+ // The taller viewport-relative CSS cap admits ~576px panels: anchored
306
+ // mid-viewport (top 380 / bottom 424 on an 800px-tall viewport), a
307
+ // bottom-start panel cannot fit below (428 + 576 > 792) and flips
308
+ // top-side into 380 - 4 - 576 = -200 — the single flip never
309
+ // re-checks, and the raw negative top used to be applied verbatim.
310
+ const prevHeight = window.innerHeight;
311
+ window.innerHeight = 800;
312
+ try {
313
+ const { container, open } = mountPanel({ placement: "bottom-start" });
314
+ await nextTick();
315
+
316
+ const anchor = container.querySelector<HTMLButtonElement>("button")!;
317
+ anchor.getBoundingClientRect = () =>
318
+ ({ top: 380, bottom: 424, left: 40, right: 140, width: 100, height: 44 }) as DOMRect;
319
+ open.value = true;
320
+ await nextTick();
321
+
322
+ const popout = document.body.querySelector<HTMLElement>(".hk-select-popout")!;
323
+ const host = document.body.querySelector<HTMLElement>(".hk-select-popout-host")!;
324
+ // happy-dom lays nothing out (offsetHeight 0 → the 200px fallback in
325
+ // positionPanel), so fake the near-max-height box the new cap allows
326
+ // and re-run geometry via the same resize path the cramped test uses.
327
+ // (positionPanel measures the popout; the applied coords land on the
328
+ // host wrapper.)
329
+ Object.defineProperty(popout, "offsetHeight", { value: 576, configurable: true });
330
+ window.dispatchEvent(new Event("resize"));
331
+ await nextTick();
332
+
333
+ const top = Number.parseInt(host.style.top, 10);
334
+ // Whole panel on-screen: top edge at/inside the viewport pad and the
335
+ // bottom edge inside it too — overflow beyond that is the panel's
336
+ // own internal scroll, never off-screen geometry.
337
+ expect(top).toBeGreaterThanOrEqual(8);
338
+ expect(top + 576).toBeLessThanOrEqual(800 - 8);
339
+ } finally {
340
+ window.innerHeight = prevHeight;
341
+ }
342
+ });
343
+ });
344
+
345
+ describe("HkSelectPanel back-guard (window-first back priority)", () => {
346
+ /** nextTick + one macrotask: lets deferred rewinds (setTimeout 0) flush. */
347
+ async function settle(): Promise<void> {
348
+ await nextTick();
349
+ await new Promise((resolve) => setTimeout(resolve, 0));
350
+ await nextTick();
351
+ }
352
+
353
+ /** Wait until pred() holds (history navigation settles asynchronously). */
354
+ async function until(pred: () => boolean, ms = 500): Promise<void> {
355
+ const deadline = Date.now() + ms;
356
+ while (!pred() && Date.now() < deadline) {
357
+ await new Promise((resolve) => setTimeout(resolve, 10));
358
+ await nextTick();
359
+ }
360
+ }
361
+
362
+ it("back closes the open panel instead of navigating the page", async () => {
363
+ const { open, container } = mountPanel();
364
+ await nextTick();
365
+ container.querySelector<HTMLButtonElement>("button")!.click();
366
+ await settle();
367
+
368
+ // The open panel owns one marked history entry above the page base.
369
+ expect(
370
+ (window.history.state as Record<string, unknown>)?.__hkBack,
371
+ ).toEqual(expect.any(String));
372
+
373
+ // Browser/system back: closes the panel, stays on the page.
374
+ window.history.back();
375
+ await until(() => open.value === false);
376
+ await until(() => window.history.state === null);
377
+ expect(open.value).toBe(false);
378
+ });
379
+
380
+ it("closing via outside click rewinds the pushed entry (no dead back)", async () => {
381
+ const { open, container } = mountPanel();
382
+ await nextTick();
383
+ container.querySelector<HTMLButtonElement>("button")!.click();
384
+ await settle();
385
+ expect(
386
+ (window.history.state as Record<string, unknown>)?.__hkBack,
387
+ ).toEqual(expect.any(String));
388
+
389
+ // An ordinary close (outside click) rewinds the entry so no dead
390
+ // back remains — assert BOTH the logical close and the history base,
391
+ // so a broken rewind fails loudly instead of timing out silently.
392
+ const outside = document.createElement("div");
393
+ document.body.appendChild(outside);
394
+ outside.click();
395
+ outside.remove();
396
+ await until(() => open.value === false);
397
+ expect(open.value).toBe(false);
398
+ await until(() => window.history.state === null);
399
+ expect(window.history.state).toBeNull();
400
+ });
401
+
402
+ it("unmounting an open panel rewinds its entry (no stranded marker)", async () => {
403
+ const { container } = mountPanel();
404
+ await nextTick();
405
+ container.querySelector<HTMLButtonElement>("button")!.click();
406
+ await settle();
407
+ expect(
408
+ (window.history.state as Record<string, unknown>)?.__hkBack,
409
+ ).toEqual(expect.any(String));
410
+
411
+ // Unmount (e.g. route change) must not strand a dead marker entry.
412
+ const app = mounts.pop()!;
413
+ app.unmount();
414
+ await settle();
415
+ await until(() => window.history.state === null);
416
+ expect(window.history.state).toBeNull();
417
+ });
418
+
419
+ it("a same-tick close→reopen keeps the reopened panel guarded", async () => {
420
+ const { open, container } = mountPanel();
421
+ await nextTick();
422
+ container.querySelector<HTMLButtonElement>("button")!.click();
423
+ await settle();
424
+
425
+ // Close, let the watcher's close branch run (release claims the
426
+ // rewind), then reopen before the deferred flush fires — the flush
427
+ // must not consume the freshly pushed entry: after it settles the
428
+ // reopened panel still owns exactly one marked entry.
429
+ open.value = false;
430
+ await nextTick();
431
+ open.value = true;
432
+ await settle();
433
+ const st = window.history.state as Record<string, unknown> | null;
434
+ expect(st?.__hkBack).toEqual(expect.any(String));
435
+
436
+ // And the back gesture still closes it (not the page).
437
+ window.history.back();
438
+ await until(() => open.value === false);
439
+ expect(open.value).toBe(false);
440
+ await until(() => window.history.state === null);
441
+ expect(window.history.state).toBeNull();
442
+ });
443
+ });
444
+
445
+ describe("HkSelectPanel mobile-sheet duplicate-title filter", () => {
446
+ /** Composition-mode rig with a reactive title and arbitrary slot
447
+ * content — the shape HkMenu pickers render on phones. */
448
+ function mountSheet(opts: {
449
+ title?: ReturnType<typeof ref<string>>;
450
+ slot?: () => unknown;
451
+ } = {}) {
452
+ const container = document.createElement("div");
453
+ document.body.appendChild(container);
454
+ containers.push(container);
455
+
456
+ const open = ref(false);
457
+ const anchorEl = ref<HTMLElement | null>(null);
458
+ const Wrapper = defineComponent({
459
+ setup() {
460
+ return () =>
461
+ h("div", [
462
+ h("button", {
463
+ ref: (el: Element | ComponentPublicInstance | null) => {
464
+ anchorEl.value = el as HTMLElement | null;
465
+ },
466
+ type: "button",
467
+ }, "anchor"),
468
+ h(
469
+ HkSelectPanel,
470
+ {
471
+ open: open.value,
472
+ "onUpdate:open": (v: boolean) => { open.value = v; },
473
+ anchorRef: anchorEl.value,
474
+ title: opts.title?.value ?? "Workspaces",
475
+ },
476
+ { default: opts.slot ?? (() => h("div", { class: "rows" }, "rows")) },
477
+ ),
478
+ ]);
479
+ },
480
+ });
481
+ const app = createApp(Wrapper);
482
+ mounts.push(app);
483
+ app.mount(container);
484
+ return { open, container };
485
+ }
486
+
487
+ /** Mobile viewport + open, settled past the post-open nextTick sync. */
488
+ async function openSheet(open: ReturnType<typeof ref<boolean>>): Promise<void> {
489
+ setViewport(375);
490
+ await nextTick();
491
+ open.value = true;
492
+ await nextTick();
493
+ await new Promise((resolve) => setTimeout(resolve, 0));
494
+ await nextTick();
495
+ }
496
+
497
+ it("hides a leading in-content heading that repeats the sheet title", async () => {
498
+ const { open } = mountSheet({
499
+ slot: () =>
500
+ h("div", { class: "menu" }, [
501
+ h("div", { class: "menu-label" }, "Workspaces"),
502
+ h("div", { class: "hint" }, "No workspaces found."),
503
+ ]),
504
+ });
505
+ await openSheet(open);
506
+
507
+ const list = document.body.querySelector<HTMLElement>(".hk-select-sheet-list")!;
508
+ // The repeated heading is filtered; the rest of the content stays.
509
+ expect(list.querySelector<HTMLElement>(".menu-label")!.classList.contains("hk-sheet-dup-title")).toBe(true);
510
+ expect(list.querySelector<HTMLElement>(".hint")!.classList.contains("hk-sheet-dup-title")).toBe(false);
511
+ // The sheet header still names the sheet.
512
+ expect(document.body.querySelector<HTMLElement>(".hk-select-sheet-title")!.textContent).toBe("Workspaces");
513
+ });
514
+
515
+ it("keeps headings that merely contain the title and non-matching text", async () => {
516
+ const { open } = mountSheet({
517
+ slot: () =>
518
+ h("div", { class: "menu" }, [
519
+ h("div", { class: "menu-label" }, "Workspaces and devices"),
520
+ h("div", { class: "hint" }, "pick one"),
521
+ ]),
522
+ });
523
+ await openSheet(open);
524
+
525
+ const list = document.body.querySelector<HTMLElement>(".hk-select-sheet-list")!;
526
+ // Partial ("contains") matches are real content — only an exact,
527
+ // non-interactive match hides.
528
+ expect(list.querySelector<HTMLElement>(".menu-label")!.classList.contains("hk-sheet-dup-title")).toBe(false);
529
+ expect(list.querySelector<HTMLElement>(".hint")!.classList.contains("hk-sheet-dup-title")).toBe(false);
530
+ });
531
+
532
+ it("never hides interactive rows or option blocks that say the title", async () => {
533
+ const { open } = mountSheet({
534
+ slot: () =>
535
+ h("div", { class: "menu" }, [
536
+ h("button", { class: "row", type: "button" }, "Workspaces"),
537
+ h("div", { class: "hk-select-option" }, "Workspaces"),
538
+ h("div", { class: "hk-menu-row" }, "Workspaces"),
539
+ ]),
540
+ });
541
+ await openSheet(open);
542
+
543
+ for (const sel of ["button.row", ".hk-select-option", ".hk-menu-row"]) {
544
+ const el = document.body.querySelector<HTMLElement>(`.hk-select-sheet-list ${sel}`)!;
545
+ expect(el.classList.contains("hk-sheet-dup-title")).toBe(false);
546
+ }
547
+ });
548
+
549
+ it("does not hide a wrapper whose text matches only via a contained row", async () => {
550
+ // Data-driven HkMenu shape: a menu level wrapping a single row whose
551
+ // label equals the title — the wrapper must survive with its row.
552
+ const { open } = mountSheet({
553
+ slot: () =>
554
+ h("div", { class: "hk-menu-level", role: "menu" }, [
555
+ h("button", { class: "hk-menu-row", type: "button" }, "Workspaces"),
556
+ ]),
557
+ });
558
+ await openSheet(open);
559
+
560
+ const list = document.body.querySelector<HTMLElement>(".hk-select-sheet-list")!;
561
+ expect(list.querySelector<HTMLElement>(".hk-menu-level")!.classList.contains("hk-sheet-dup-title")).toBe(false);
562
+ expect(list.querySelector<HTMLElement>("button.hk-menu-row")!.classList.contains("hk-sheet-dup-title")).toBe(false);
563
+ });
564
+
565
+ it("keeps the in-content heading on the desktop popout (no dedupe there)", async () => {
566
+ setViewport(1200);
567
+ const { open } = mountSheet({
568
+ slot: () => h("div", { class: "menu-label" }, "Workspaces"),
569
+ });
570
+ await nextTick();
571
+ open.value = true;
572
+ await nextTick();
573
+
574
+ const popout = document.body.querySelector<HTMLElement>(".hk-select-popout")!;
575
+ expect(popout.querySelector<HTMLElement>(".menu-label")!.classList.contains("hk-sheet-dup-title")).toBe(false);
576
+ });
577
+
578
+ it("re-syncs when late content adds a duplicate heading while open", async () => {
579
+ const { open } = mountSheet({
580
+ slot: () => h("div", { class: "hint" }, "loading…"),
581
+ });
582
+ await openSheet(open);
583
+
584
+ const list = document.body.querySelector<HTMLElement>(".hk-select-sheet-list")!;
585
+ const late = document.createElement("div");
586
+ late.className = "menu-label";
587
+ late.textContent = "Workspaces";
588
+ list.appendChild(late);
589
+ await new Promise((resolve) => setTimeout(resolve, 0));
590
+ await nextTick();
591
+ expect(late.classList.contains("hk-sheet-dup-title")).toBe(true);
592
+ });
593
+
594
+ it("restores the hidden heading when the open sheet is retitled", async () => {
595
+ const title = ref("Workspaces");
596
+ const { open } = mountSheet({
597
+ title,
598
+ slot: () => h("div", { class: "menu-label" }, "Workspaces"),
599
+ });
600
+ await openSheet(open);
601
+ const label = document.body.querySelector<HTMLElement>(".hk-select-sheet-list .menu-label")!;
602
+ expect(label.classList.contains("hk-sheet-dup-title")).toBe(true);
603
+
604
+ title.value = "Devices";
605
+ await nextTick();
606
+ await new Promise((resolve) => setTimeout(resolve, 0));
607
+ await nextTick();
608
+ expect(label.classList.contains("hk-sheet-dup-title")).toBe(false);
609
+ });
610
+ });
611
+
612
+ describe("HkSelectPanel desktop popout motion", () => {
613
+ it("grows the popout out of the resolved anchor side (auto-flip reflected in data-side)", async () => {
614
+ // mountPanel defaults to placement="top-start"; in the test layout
615
+ // the anchor sits at the very top, so the panel flips to the bottom
616
+ // side — the flip must be visible in data-side (it drives the pop
617
+ // transition's transform-origin).
618
+ const { open } = mountPanel();
619
+ await nextTick();
620
+ open.value = true;
621
+ await nextTick();
622
+ await new Promise((resolve) => setTimeout(resolve, 0));
623
+ await nextTick();
624
+
625
+ const host = document.body.querySelector<HTMLElement>(".hk-select-popout-host")!;
626
+ expect(host).toBeTruthy();
627
+ expect(host.dataset.side).toBe("bottom"); // flipped from top-start
628
+ expect(host.dataset.align).toBe("start");
629
+ });
630
+
631
+ it("keeps the desktop popout mounted through its close so the pop leave runs", async () => {
632
+ const { open } = mountPanel();
633
+ await nextTick();
634
+ open.value = true;
635
+ await nextTick();
636
+
637
+ expect(document.body.querySelector(".hk-select-popout-host")).toBeTruthy();
638
+
639
+ open.value = false;
640
+ await nextTick();
641
+ // happy-dom never finishes leave transitions, so the host lingers —
642
+ // exactly what a running pop-out leave looks like. A synchronous
643
+ // unmount (the pre-transition regression) removes it instantly.
644
+ expect(document.body.querySelector(".hk-select-popout-host")).toBeTruthy();
645
+ });
646
+
647
+ it("reopens with a fresh popout after a close", async () => {
648
+ const { open } = mountPanel();
649
+ await nextTick();
650
+ open.value = true;
651
+ await nextTick();
652
+ open.value = false;
653
+ await nextTick();
654
+ open.value = true;
655
+ await nextTick();
656
+ await new Promise((resolve) => setTimeout(resolve, 0));
657
+ await nextTick();
658
+
659
+ const hosts = Array.from(
660
+ document.body.querySelectorAll<HTMLElement>(".hk-select-popout-host"),
661
+ );
662
+ expect(hosts.length).toBeGreaterThanOrEqual(1);
663
+ // The newest host (last in DOM order) carries live content.
664
+ expect(hosts.at(-1)!.textContent).toContain("a");
665
+ });
666
+ });