@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,237 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { createApp, h, nextTick } from "vue";
3
+
4
+ // HDrawer teleports and animates; replace it with an inline passthrough
5
+ // exposing the body/footer split so the drawer contract is testable.
6
+ vi.mock("@celestia-island/hikari", async (importOriginal) => {
7
+ const actual = await importOriginal<typeof import("@celestia-island/hikari")>();
8
+ const { defineComponent, h } = await import("vue");
9
+ const HDrawerStub = defineComponent({
10
+ name: "HDrawer",
11
+ props: {
12
+ modelValue: { type: Boolean, default: false },
13
+ side: { type: String, default: "left" },
14
+ size: { type: String, default: "280px" },
15
+ title: { type: String, default: undefined },
16
+ panelClass: { type: String, default: undefined },
17
+ },
18
+ setup(props, { slots }) {
19
+ return () =>
20
+ props.modelValue
21
+ ? h("div", { class: ["drawer-stub", props.panelClass] }, [
22
+ h("div", { class: "drawer-stub-body" }, slots.default?.()),
23
+ slots.footer
24
+ ? h("div", { class: "drawer-stub-footer" }, slots.footer())
25
+ : null,
26
+ ])
27
+ : null;
28
+ },
29
+ });
30
+ return { ...actual, HDrawer: HDrawerStub };
31
+ });
32
+
33
+ import { HkAdminShell } from "./HkAdminShell";
34
+
35
+ /**
36
+ * HkAdminShell contract tests for the generalized slot surface ported
37
+ * from the chest plana-legacy fork:
38
+ * 1. Scoped slots are functions: rendering `slots.overlays` without
39
+ * calling it stringifies the compiled slot source into a text node.
40
+ * 2. The mobile drawer footer carries the `userPanel` slot so the
41
+ * account block (identity + actions) rides the drawer bottom.
42
+ * 3. The header slot receives `onOpenDrawer` so a header trigger (the
43
+ * avatar in drawer action mode) can open the nav drawer.
44
+ * 4. Content padding rides an inner wrapper inside the scroll viewport
45
+ * (card box-shadows are never clipped at the viewport edges).
46
+ *
47
+ * (Repo test convention: raw createApp + container queries, no
48
+ * @vue/test-utils dependency.)
49
+ */
50
+
51
+ const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
52
+
53
+ function mount(node: ReturnType<typeof h>) {
54
+ const container = document.createElement("div");
55
+ document.body.appendChild(container);
56
+ const app = createApp({ render: () => node });
57
+ app.mount(container);
58
+ mounts.push({ app, container });
59
+ return container;
60
+ }
61
+
62
+ afterEach(() => {
63
+ for (const { app, container } of mounts.splice(0)) {
64
+ app.unmount();
65
+ container.remove();
66
+ }
67
+ setWidth(1024);
68
+ });
69
+
70
+ const setWidth = (w: number) => {
71
+ Object.defineProperty(window, "innerWidth", {
72
+ configurable: true,
73
+ writable: true,
74
+ value: w,
75
+ });
76
+ window.dispatchEvent(new Event("resize"));
77
+ };
78
+
79
+ function shellNode(
80
+ props: Record<string, unknown>,
81
+ slots: Record<string, unknown>,
82
+ ) {
83
+ return h(HkAdminShell, props as never, slots as never);
84
+ }
85
+
86
+ const NAV = () => h("nav", { class: "nav-mock" }, "NAV");
87
+ const CONTENT = () => h("main", null, "CONTENT");
88
+
89
+ describe("HkAdminShell", () => {
90
+ it("renders the overlays slot content, never its source text", () => {
91
+ setWidth(1024);
92
+ const c = mount(shellNode(
93
+ { navTitle: "Navigation" },
94
+ {
95
+ header: () => null,
96
+ sidebar: NAV,
97
+ content: CONTENT,
98
+ overlays: () => h("div", { class: "overlay-mock" }, "OVERLAY-MARK"),
99
+ },
100
+ ));
101
+ const html = c.innerHTML;
102
+ expect(html).toContain("OVERLAY-MARK");
103
+ // The regression: a slot function rendered un-invoked stringifies to
104
+ // its minified source (arrow params, r._d, etc.).
105
+ expect(html).not.toContain("=>");
106
+ expect(c.querySelector(".overlay-mock")).toBeTruthy();
107
+ });
108
+
109
+ it("renders the desktop sidebar inline and skips the drawer", () => {
110
+ setWidth(1024);
111
+ const c = mount(shellNode(
112
+ { navTitle: "Navigation" },
113
+ { header: () => null, sidebar: NAV, content: CONTENT },
114
+ ));
115
+ expect(c.querySelector("aside .nav-mock")).toBeTruthy();
116
+ // No drawer (mocked HDrawer) in the tree at desktop width.
117
+ expect(c.querySelector(".drawer-stub")).toBeNull();
118
+ });
119
+
120
+ it("opens the mobile drawer with nav body and userPanel footer", async () => {
121
+ setWidth(500);
122
+ let openDrawer: (() => void) | null = null;
123
+ const c = mount(shellNode(
124
+ { navTitle: "Navigation", drawerPanelClass: "my-drawer" },
125
+ {
126
+ header: (ctx: { onOpenDrawer: () => void }) => {
127
+ openDrawer = ctx.onOpenDrawer;
128
+ return null;
129
+ },
130
+ sidebar: NAV,
131
+ userPanel: () => h("div", { class: "s-drawer-user-panel" }, "USER-PANEL"),
132
+ content: CONTENT,
133
+ },
134
+ ));
135
+ expect(openDrawer).toBeTruthy();
136
+ openDrawer!();
137
+ await nextTick();
138
+ await nextTick();
139
+ // Drawer carries the nav in its body…
140
+ expect(c.querySelector(".drawer-stub-body .nav-mock")).toBeTruthy();
141
+ // …and the user panel in its footer, unwrapped (the slot's own root
142
+ // carries the styling hooks).
143
+ const panel = c.querySelector(".drawer-stub-footer .s-drawer-user-panel");
144
+ expect(panel).toBeTruthy();
145
+ expect(panel?.textContent).toContain("USER-PANEL");
146
+ // The drawer panel class passes through for consumer-side nesting
147
+ // adjustments.
148
+ expect(c.querySelector(".drawer-stub.my-drawer")).toBeTruthy();
149
+ });
150
+
151
+ it("hands the userPanel slot an onNavigate that closes the drawer", async () => {
152
+ setWidth(500);
153
+ let openDrawer: (() => void) | null = null;
154
+ let userPanelNavigate: (() => void) | null = null;
155
+ const c = mount(shellNode(
156
+ { navTitle: "Navigation" },
157
+ {
158
+ header: (ctx: { onOpenDrawer: () => void }) => {
159
+ openDrawer = ctx.onOpenDrawer;
160
+ return null;
161
+ },
162
+ sidebar: NAV,
163
+ userPanel: (ctx: { onNavigate: () => void }) => {
164
+ userPanelNavigate = ctx.onNavigate;
165
+ return h("div", { class: "s-drawer-user-panel" }, "USER-PANEL");
166
+ },
167
+ content: CONTENT,
168
+ },
169
+ ));
170
+ openDrawer!();
171
+ await nextTick();
172
+ await nextTick();
173
+ expect(c.querySelector(".drawer-stub")).toBeTruthy();
174
+ // The slot scope carries the drawer-close callback (mirrors the
175
+ // sidebar slot's onNavigate contract)…
176
+ expect(userPanelNavigate).toBeTruthy();
177
+ // …and invoking it dismisses the drawer (a "go to frontend" row
178
+ // must not leave the drawer hovering over the swapped layout).
179
+ userPanelNavigate!();
180
+ await nextTick();
181
+ await nextTick();
182
+ expect(c.querySelector(".drawer-stub")).toBeNull();
183
+ });
184
+
185
+ it("omits the drawer footer wrapper when no userPanel slot is given", async () => {
186
+ setWidth(500);
187
+ let openDrawer: (() => void) | null = null;
188
+ const c = mount(shellNode(
189
+ { navTitle: "Navigation" },
190
+ {
191
+ header: (ctx: { onOpenDrawer: () => void }) => {
192
+ openDrawer = ctx.onOpenDrawer;
193
+ return null;
194
+ },
195
+ sidebar: NAV,
196
+ content: CONTENT,
197
+ },
198
+ ));
199
+ openDrawer!();
200
+ await nextTick();
201
+ await nextTick();
202
+ expect(c.querySelector(".drawer-stub-footer")).toBeNull();
203
+ expect(c.querySelector(".s-drawer-user-panel")).toBeNull();
204
+ });
205
+
206
+ it("honors a custom mobileBreakpoint for the desktop takeover", async () => {
207
+ // Default 1024: 900px reads as mobile (drawer path).
208
+ setWidth(900);
209
+ const narrow = mount(shellNode(
210
+ { navTitle: "Navigation" },
211
+ { header: () => null, sidebar: NAV, content: CONTENT },
212
+ ));
213
+ expect(narrow.querySelector("aside .nav-mock")).toBeNull();
214
+
215
+ // Custom 768: the same 900px reads as desktop (inline sidebar).
216
+ const wide = mount(shellNode(
217
+ { navTitle: "Navigation", mobileBreakpoint: 768 },
218
+ { header: () => null, sidebar: NAV, content: CONTENT },
219
+ ));
220
+ expect(wide.querySelector("aside .nav-mock")).toBeTruthy();
221
+ });
222
+
223
+ it("applies the content padding inside the scroll viewport so shadows are not clipped", () => {
224
+ setWidth(1024);
225
+ const c = mount(shellNode(
226
+ { navTitle: "Navigation", contentPadding: "2rem" },
227
+ { header: () => null, sidebar: NAV, content: CONTENT },
228
+ ));
229
+ // The padded inner wrapper exists INSIDE the scroll viewport (not as
230
+ // padding on the container itself).
231
+ const viewport = c.querySelector(".hk-scroll-container-viewport")
232
+ ?? c.querySelector(".hk-scroll-container");
233
+ const inner = viewport?.firstElementChild as HTMLElement | null | undefined;
234
+ expect(inner).toBeTruthy();
235
+ expect(inner?.style.padding).toBe("2rem");
236
+ });
237
+ });
@@ -0,0 +1,129 @@
1
+ import { computed, defineComponent, ref } from "vue";
2
+ import { HDrawer, HScrollContainer, useBreakpoint } from "@celestia-island/hikari";
3
+ import { provideActionBar } from "../composables/useActionBar";
4
+ import { useI18n } from "../i18n/context";
5
+
6
+ export const HkAdminShell = defineComponent({
7
+ name: "HkAdminShell",
8
+ props: {
9
+ sidebarCollapsed: { type: Boolean, default: false },
10
+ sidebarWidth: { type: String, default: "224px" },
11
+ /** Viewport width at which the desktop layout takes over (below it
12
+ * the nav collapses into the mobile drawer). Defaults to the shared
13
+ * 1024px "lg" breakpoint; lower it (e.g. 768) for tablet-friendly
14
+ * admins that keep the sidebar at md widths. */
15
+ mobileBreakpoint: { type: Number, default: 1024 },
16
+ footerHeight: { type: String, default: "var(--s-footer-height)" },
17
+ navTitle: { type: String, default: undefined },
18
+ /** Extra class for the mobile nav drawer's panel — lets consumers zero
19
+ * nested paddings so drawer nav rows and a `userPanel` footer share
20
+ * one left edge. */
21
+ drawerPanelClass: { type: String, default: undefined },
22
+ /** Content-area padding (inside the scroll viewport). Padding is
23
+ * applied to an inner wrapper rather than the scroll container so
24
+ * card box-shadows are never clipped at the viewport edges. */
25
+ contentPadding: { type: String, default: "1.5rem" },
26
+ },
27
+ setup(props, { slots }) {
28
+ const { t } = useI18n();
29
+ const { width: viewportWidth } = useBreakpoint();
30
+ const isDesktop = computed(() => viewportWidth.value >= props.mobileBreakpoint);
31
+ const sidebarOpen = ref(false);
32
+
33
+ const actionBar = provideActionBar();
34
+
35
+ const toggleHamburger = () => {
36
+ sidebarOpen.value = !sidebarOpen.value;
37
+ };
38
+
39
+ const openSidebar = () => {
40
+ sidebarOpen.value = true;
41
+ };
42
+
43
+ const closeSidebar = () => {
44
+ sidebarOpen.value = false;
45
+ };
46
+
47
+ return () => (
48
+ <div class="flex flex-col h-full w-full overflow-hidden">
49
+ {slots.header && (
50
+ <div style={{ flexShrink: 0 }}>
51
+ {slots.header({
52
+ isDesktop: isDesktop.value,
53
+ showHamburger: !isDesktop.value,
54
+ compact: !isDesktop.value,
55
+ actions: actionBar.actions.value ? actionBar.actions.value() : [],
56
+ onHamburger: toggleHamburger,
57
+ // Lets a header trigger (e.g. the avatar in "drawer" action
58
+ // mode) open the mobile nav drawer directly.
59
+ onOpenDrawer: openSidebar,
60
+ })}
61
+ </div>
62
+ )}
63
+
64
+ <div class="flex flex-1 min-h-0" style={{ paddingBottom: props.footerHeight }}>
65
+ {isDesktop.value && !props.sidebarCollapsed && slots.sidebar && (
66
+ <aside
67
+ style={{
68
+ width: props.sidebarWidth,
69
+ flexShrink: 0,
70
+ borderRight: "1px solid var(--border-faint, rgb(var(--color-border) / 10%))",
71
+ background: "rgb(var(--color-surface))",
72
+ overflow: "hidden",
73
+ }}
74
+ >
75
+ {slots.sidebar({ collapsed: false, onNavigate: closeSidebar })}
76
+ </aside>
77
+ )}
78
+ <main class="flex-1 flex flex-col min-w-0 min-h-0">
79
+ <HScrollContainer class="flex-1 min-h-0">
80
+ {/* Padding lives INSIDE the scroll viewport (an inner
81
+ wrapper) so card box-shadows are not clipped at the
82
+ viewport edges. */}
83
+ <div style={{ padding: props.contentPadding }}>{slots.content?.()}</div>
84
+ </HScrollContainer>
85
+ </main>
86
+
87
+ {!isDesktop.value && (
88
+ <HDrawer
89
+ modelValue={sidebarOpen.value}
90
+ onUpdate:modelValue={(v: boolean) => (sidebarOpen.value = v)}
91
+ side="left"
92
+ size="280px"
93
+ title={props.navTitle ?? t("hikari::adminShell.navTitle", "Navigation")}
94
+ panelClass={props.drawerPanelClass}
95
+ >
96
+ {/* The drawer body carries the nav; a `userPanel` slot rides
97
+ the drawer footer (identity + account actions) so mobile
98
+ gets the same content the desktop user menu exposes.
99
+ `inDrawer` lets the sidebar slot fill the drawer width.
100
+ The userPanel receives `onNavigate` (closes the drawer)
101
+ so its action rows — e.g. "go to frontend", which swaps
102
+ the whole layout underneath — can dismiss the drawer
103
+ instead of leaving it hovering over the new page. */}
104
+ {{
105
+ default: () =>
106
+ slots.sidebar?.({ collapsed: false, onNavigate: closeSidebar, inDrawer: true }),
107
+ footer: slots.userPanel
108
+ ? () => slots.userPanel!({ onNavigate: closeSidebar })
109
+ : undefined,
110
+ }}
111
+ </HDrawer>
112
+ )}
113
+ </div>
114
+
115
+ {slots.footer && (
116
+ <footer class="s-status-bar" style={{ position: "fixed", bottom: 0, left: 0, right: 0, // Footer band (was a bare 40 — the pre-band chrome value).
117
+ zIndex: "var(--z-footer, 110)" }}>
118
+ {slots.footer()}
119
+ </footer>
120
+ )}
121
+
122
+ {/* Scoped slots are functions — rendering the slot itself instead of
123
+ calling it would stringify the compiled withCtx source into a text
124
+ node (normalizeVNode String()s non-vnode children). */}
125
+ {slots.overlays?.()}
126
+ </div>
127
+ );
128
+ },
129
+ });
@@ -0,0 +1,70 @@
1
+ import { defineComponent, type PropType } from "vue";
2
+ import { HAlert, HCard, HEmptyState, HSpinner, HTable, useI18n } from "@celestia-island/hikari";
3
+ import { HkPageHeader } from "./HkPageHeader";
4
+
5
+
6
+ /** Column definition passed through to HTable. */
7
+ export interface HTableColumn {
8
+ key: string;
9
+ title: string;
10
+ width?: string;
11
+ sortable?: boolean;
12
+ align?: "left" | "center" | "right";
13
+ }
14
+
15
+ /**
16
+ * CRUD table-page scaffold: HkPageHeader + loading spinner + error alert +
17
+ * empty state + HTable. Slots:
18
+ * - `actions` — header actions (e.g. a "Create" button)
19
+ * - `cell-<key>` — per-column cell templates, forwarded to HTable
20
+ * - `create-modal` — create dialog, rendered after the table
21
+ * - `edit-modal` — edit dialog, rendered after the table
22
+ */
23
+ export const HkAdminTablePage = defineComponent({
24
+ name: "HkAdminTablePage",
25
+ props: {
26
+ title: { type: String, default: "" },
27
+ loading: { type: Boolean, default: false },
28
+ error: { type: String as PropType<string | undefined>, default: undefined },
29
+ rows: { type: Array as PropType<Record<string, unknown>[]>, required: true },
30
+ columns: { type: Array as PropType<HTableColumn[]>, required: true },
31
+ rowKey: { type: String, default: "id" },
32
+ emptyTitle: { type: String, default: "" },
33
+ emptyDescription: { type: String as PropType<string | undefined>, default: undefined },
34
+ },
35
+ setup(props, { slots }) {
36
+
37
+ return () => {
38
+ const { t } = useI18n();
39
+ const emptyTitle = props.emptyTitle || t("hikari::tablePage.emptyTitle", "No data");
40
+ return (
41
+ <div>
42
+ {props.title ? (
43
+ <HkPageHeader title={props.title}>
44
+ {{ actions: () => slots.actions?.() }}
45
+ </HkPageHeader>
46
+ ) : slots.actions ? (
47
+ <div style={{ display: "flex", justifyContent: "flex-end", marginBottom: "var(--space-16, 1rem)" }}>
48
+ {slots.actions()}
49
+ </div>
50
+ ) : null}
51
+ {props.error ? (
52
+ <HAlert message={props.error} />
53
+ ) : props.loading && !props.rows.length ? (
54
+ <HSpinner center />
55
+ ) : !props.rows.length ? (
56
+ <HEmptyState title={emptyTitle} description={props.emptyDescription} />
57
+ ) : (
58
+ <HCard padded={false}>
59
+ <HTable columns={props.columns} rows={props.rows} rowKey={props.rowKey}>
60
+ {slots}
61
+ </HTable>
62
+ </HCard>
63
+ )}
64
+ {slots["create-modal"]?.()}
65
+ {slots["edit-modal"]?.()}
66
+ </div>
67
+ );
68
+ };
69
+ },
70
+ });
@@ -0,0 +1,89 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { createApp, h } from "vue";
3
+
4
+ import { HkStatCard } from "./HkStatCard";
5
+ import { HkStatusPill } from "./HkStatusPill";
6
+ import { HkShareBar } from "./HkShareBar";
7
+
8
+ const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
9
+
10
+ function mount(node: ReturnType<typeof h>) {
11
+ const container = document.createElement("div");
12
+ document.body.appendChild(container);
13
+ const app = createApp({ render: () => node });
14
+ app.mount(container);
15
+ mounts.push({ app, container });
16
+ return container;
17
+ }
18
+
19
+ afterEach(() => {
20
+ for (const { app, container } of mounts.splice(0)) {
21
+ app.unmount();
22
+ container.remove();
23
+ }
24
+ });
25
+
26
+ describe("HkStatCard", () => {
27
+ it("renders label, value, hint and a tone dot", () => {
28
+ const c = mount(h(HkStatCard, { label: "Hub health", value: "2/2", tone: "success", hint: "hubs" }));
29
+ expect(c.querySelector(".hk-stat-card-label")?.textContent).toBe("Hub health");
30
+ expect(c.querySelector(".hk-stat-card-value")?.textContent).toBe("2/2");
31
+ expect(c.querySelector(".hk-stat-card-hint")?.textContent).toBe("hubs");
32
+ expect(c.querySelector(".hk-stat-card-dot-success")).toBeTruthy();
33
+ expect(c.querySelector(".hk-stat-card-clickable")).toBeNull();
34
+ });
35
+
36
+ it("marks clickable cards and emits role=button", () => {
37
+ const c = mount(h(HkStatCard, { label: "L", value: 1, clickable: true }));
38
+ const el = c.querySelector(".hk-stat-card") as HTMLElement;
39
+ expect(el.getAttribute("role")).toBe("button");
40
+ expect(el.className).toContain("hk-stat-card-clickable");
41
+ });
42
+
43
+ it("omits the hint node when absent", () => {
44
+ const c = mount(h(HkStatCard, { label: "L", value: 1 }));
45
+ expect(c.querySelector(".hk-stat-card-hint")).toBeNull();
46
+ });
47
+ });
48
+
49
+ describe("HkStatusPill", () => {
50
+ it("renders state dot with label, latency and version", () => {
51
+ const c = mount(h(HkStatusPill, { state: "ok", label: "online", latencyMs: 34, version: "0.2.0" }));
52
+ expect(c.querySelector(".hk-status-pill-ok")).toBeTruthy();
53
+ expect(c.querySelector(".hk-status-pill-label")?.textContent).toBe("online");
54
+ expect(c.querySelector(".hk-status-pill-latency")?.textContent).toBe("34 ms");
55
+ expect(c.querySelector(".hk-status-pill-version")?.textContent).toBe("0.2.0");
56
+ });
57
+
58
+ it("defaults to unknown state with no extras", () => {
59
+ const c = mount(h(HkStatusPill));
60
+ expect(c.querySelector(".hk-status-pill-unknown")).toBeTruthy();
61
+ expect(c.querySelector(".hk-status-pill-latency")).toBeNull();
62
+ expect(c.querySelector(".hk-status-pill-version")).toBeNull();
63
+ });
64
+ });
65
+
66
+ describe("HkShareBar", () => {
67
+ it("computes percent from value/total and clamps to 100", () => {
68
+ const c = mount(h(HkShareBar, { label: "a", value: 25, total: 100 }));
69
+ const fill = c.querySelector(".hk-share-bar-fill") as HTMLElement;
70
+ expect(fill.style.width).toBe("25%");
71
+
72
+ const over = mount(h(HkShareBar, { label: "b", value: 300, total: 100 }));
73
+ const fill2 = over.querySelector(".hk-share-bar-fill") as HTMLElement;
74
+ expect(fill2.style.width).toBe("100%");
75
+ });
76
+
77
+ it("renders zero-width for zero totals and honors captions", () => {
78
+ const c = mount(h(HkShareBar, { label: "a", value: 5, total: 0, caption: "5 tok" }));
79
+ const fill = c.querySelector(".hk-share-bar-fill") as HTMLElement;
80
+ expect(fill.style.width).toBe("0%");
81
+ expect(c.querySelector(".hk-share-bar-caption")?.textContent).toBe("5 tok");
82
+ });
83
+
84
+ it("applies the percent override", () => {
85
+ const c = mount(h(HkShareBar, { label: "a", value: 1, total: 100, percentOverride: 60 }));
86
+ const fill = c.querySelector(".hk-share-bar-fill") as HTMLElement;
87
+ expect(fill.style.width).toBe("60%");
88
+ });
89
+ });
@@ -72,7 +72,7 @@ export default defineComponent({
72
72
  <button
73
73
  class="hk-alert-close"
74
74
  type="button"
75
- aria-label={t("hk.alert.close", "Close")}
75
+ aria-label={t("hikari::alert.close", "Close")}
76
76
  onClick={onClose}
77
77
  >
78
78
  <X size={14} />
@@ -0,0 +1,55 @@
1
+ /* HkAltSignIn — alternative sign-in trigger row + entry panel.
2
+ Token-driven only; mirrors the .s-auth-footer type/color grammar so the
3
+ row sits inside an auth card without breaking its rhythm. */
4
+
5
+ .hk-alt-signin {
6
+ display: flex;
7
+ flex-direction: column;
8
+ align-items: stretch;
9
+ width: 100%;
10
+ }
11
+
12
+ .hk-alt-signin__trigger {
13
+ display: flex;
14
+ align-items: center;
15
+ gap: var(--space-8, 0.5rem);
16
+ width: 100%;
17
+ padding: var(--space-4, 0.25rem) 0;
18
+ border: 0;
19
+ background: transparent;
20
+ cursor: pointer;
21
+ font-size: var(--text-xs, 0.75rem);
22
+ color: var(--hi-color-muted, #666);
23
+ transition:
24
+ color var(--duration-normal, 0.3s) ease,
25
+ opacity var(--duration-normal, 0.3s) ease;
26
+ }
27
+
28
+ .hk-alt-signin__trigger:hover:not(:disabled) {
29
+ color: var(--hi-color-text-secondary, #999);
30
+ }
31
+
32
+ .hk-alt-signin__trigger:disabled {
33
+ opacity: 0.5;
34
+ cursor: not-allowed;
35
+ }
36
+
37
+ /* Decorative rules flanking the label give the collapsed row the
38
+ "section divider" read while staying a single focusable button. */
39
+ .hk-alt-signin__rule {
40
+ flex: 1 1 auto;
41
+ height: 1px;
42
+ background: var(--hi-color-border-subtle, rgba(0, 0, 0, 0.08));
43
+ }
44
+
45
+ .hk-alt-signin__label {
46
+ flex: 0 0 auto;
47
+ }
48
+
49
+ .hk-alt-signin__panel {
50
+ display: flex;
51
+ flex-direction: column;
52
+ gap: var(--space-2, 0.125rem);
53
+ min-width: 12rem;
54
+ padding: var(--space-4, 0.25rem) var(--space-4, 0.25rem);
55
+ }
@@ -0,0 +1,88 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { createApp, h, nextTick } from "vue";
3
+
4
+ import HkAltSignIn from "./HkAltSignIn";
5
+
6
+ const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
7
+
8
+ function mount(node: ReturnType<typeof h>) {
9
+ const container = document.createElement("div");
10
+ document.body.appendChild(container);
11
+ const app = createApp({ render: () => node });
12
+ app.mount(container);
13
+ mounts.push({ app, container });
14
+ return container;
15
+ }
16
+
17
+ afterEach(() => {
18
+ for (const { app, container } of mounts.splice(0)) {
19
+ app.unmount();
20
+ container.remove();
21
+ }
22
+ });
23
+
24
+ const entries = [
25
+ { key: "passkey", label: "使用 passkey 登录" },
26
+ { key: "feishu", label: "飞书登录" },
27
+ ];
28
+
29
+ function trigger(c: HTMLElement): HTMLButtonElement {
30
+ const el = c.querySelector<HTMLButtonElement>(".hk-alt-signin__trigger");
31
+ if (!el) throw new Error("trigger button not rendered");
32
+ return el;
33
+ }
34
+
35
+ describe("HkAltSignIn", () => {
36
+ it("renders the trigger label and decorative rules", () => {
37
+ const c = mount(
38
+ h(HkAltSignIn, {
39
+ label: "其他方式登录",
40
+ entries,
41
+ onSelect: () => undefined,
42
+ }),
43
+ );
44
+ expect(trigger(c).textContent).toContain("其他方式登录");
45
+ expect(c.querySelectorAll(".hk-alt-signin__rule").length).toBe(2);
46
+ });
47
+
48
+ it("opens the entry panel and emits select with the entry key", async () => {
49
+ const selected: string[] = [];
50
+ const c = mount(
51
+ h(HkAltSignIn, {
52
+ label: "更多",
53
+ entries,
54
+ onSelect: (k: string) => selected.push(k),
55
+ }),
56
+ );
57
+ trigger(c).click();
58
+ // The popover manager + flip coords resolve over a couple of frames.
59
+ for (let i = 0; i < 5 && !document.querySelector(".hk-alt-signin__panel"); i++) {
60
+ await nextTick();
61
+ await new Promise((r) => setTimeout(r, 10));
62
+ }
63
+ const panel = document.querySelector(".hk-alt-signin__panel");
64
+ expect(panel).not.toBeNull();
65
+ const items = [...document.querySelectorAll<HTMLButtonElement>(".hk-menu-action-item")];
66
+ expect(items.map((b) => b.textContent?.trim())).toEqual([
67
+ "使用 passkey 登录",
68
+ "飞书登录",
69
+ ]);
70
+ items[1]!.click();
71
+ expect(selected).toEqual(["feishu"]);
72
+ });
73
+
74
+ it("does not open while disabled", async () => {
75
+ const c = mount(
76
+ h(HkAltSignIn, {
77
+ label: "更多",
78
+ entries,
79
+ disabled: true,
80
+ onSelect: () => undefined,
81
+ }),
82
+ );
83
+ trigger(c).click();
84
+ await nextTick();
85
+ expect(trigger(c).disabled).toBe(true);
86
+ expect(c.querySelector(".hk-alt-signin__panel")).toBeNull();
87
+ });
88
+ });