@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.
- package/README.md +1 -1
- package/package.json +22 -7
- package/src/DemoApp.tsx +491 -0
- package/src/animation/registerAnimations.test.ts +94 -0
- package/src/animation/registerAnimations.ts +103 -0
- package/src/components/HkAboutModal.scss +95 -0
- package/src/components/HkAboutModal.tsx +114 -0
- package/src/components/HkAdaptiveDialog.scss +22 -0
- package/src/components/HkAdaptiveDialog.test.tsx +271 -0
- package/src/components/HkAdaptiveDialog.tsx +167 -0
- package/src/components/HkAdminHeader.test.tsx +185 -0
- package/src/components/HkAdminHeader.tsx +308 -0
- package/src/components/HkAdminShell.test.tsx +237 -0
- package/src/components/HkAdminShell.tsx +129 -0
- package/src/components/HkAdminTablePage.tsx +70 -0
- package/src/components/HkAdminWidgets.test.ts +89 -0
- package/src/components/HkAlert.tsx +1 -1
- package/src/components/HkAltSignIn.scss +55 -0
- package/src/components/HkAltSignIn.test.tsx +88 -0
- package/src/components/HkAltSignIn.tsx +92 -0
- package/src/components/HkAttachmentModal.scss +49 -0
- package/src/components/HkAttachmentModal.tsx +318 -0
- package/src/components/HkAuthCard.tsx +50 -0
- package/src/components/HkAuthSubmitButton.scss +12 -0
- package/src/components/HkAuthSubmitButton.tsx +94 -0
- package/src/components/HkAvatar.scss +93 -0
- package/src/components/HkAvatar.tsx +2 -2
- package/src/components/HkBlockingToast.scss +123 -0
- package/src/components/HkBlockingToast.test.tsx +221 -0
- package/src/components/HkBlockingToast.tsx +182 -0
- package/src/components/HkBreadcrumb.scss +624 -0
- package/src/components/HkBreadcrumb.tsx +2 -2
- package/src/components/HkButton.scss +16 -2
- package/src/components/HkCaptchaModal.scss +14 -0
- package/src/components/HkCaptchaModal.tsx +62 -0
- package/src/components/HkCaptchaWidget.scss +30 -0
- package/src/components/HkCaptchaWidget.tsx +181 -0
- package/src/components/HkCard.scss +1 -1
- package/src/components/HkChatTypes.ts +65 -0
- package/src/components/HkCheckbox.scss +4 -9
- package/src/components/HkCheckbox.tsx +18 -6
- package/src/components/HkColorPicker.scss +120 -3
- package/src/components/HkColorPicker.tsx +130 -19
- package/src/components/HkColorPickerTouch.test.tsx +145 -0
- package/src/components/HkColorSchemeDialog.scss +52 -0
- package/src/components/HkColorSchemeDialog.test.ts +201 -0
- package/src/components/HkColorSchemeDialog.tsx +96 -0
- package/src/components/HkColorSchemeEditor.test.ts +270 -0
- package/src/components/HkColorSchemeEditor.tsx +410 -0
- package/src/components/HkConfirmDialog.scss +43 -3
- package/src/components/HkConfirmDialog.tsx +7 -4
- package/src/components/HkConnectionInfo.ts +80 -0
- package/src/components/HkConnectionStatus.tsx +158 -0
- package/src/components/HkCookieConsent.scss +22 -0
- package/src/components/HkCookieConsent.test.tsx +70 -0
- package/src/components/HkCookieConsent.tsx +56 -0
- package/src/components/HkCountdownDigit.tsx +105 -0
- package/src/components/HkDatePicker.scss +361 -0
- package/src/components/HkDatePicker.test.ts +459 -0
- package/src/components/HkDatePicker.tsx +568 -0
- package/src/components/HkDateTimePicker.scss +65 -37
- package/src/components/HkDateTimePicker.test.ts +344 -0
- package/src/components/HkDateTimePicker.tsx +249 -82
- package/src/components/HkDraggableGrid.scss +4 -1
- package/src/components/HkDraggableList.scss +4 -1
- package/src/components/HkDrawer.scss +27 -13
- package/src/components/HkDrawer.test.tsx +73 -0
- package/src/components/HkDrawer.tsx +157 -10
- package/src/components/HkErrorBoundary.scss +5 -2
- package/src/components/HkErrorBoundary.tsx +7 -4
- package/src/components/HkExpansionPanel.scss +143 -0
- package/src/components/HkExpansionPanel.test.tsx +109 -0
- package/src/components/HkExpansionPanel.tsx +101 -0
- package/src/components/HkFab.scss +162 -0
- package/src/components/HkFab.test.tsx +217 -0
- package/src/components/HkFab.tsx +214 -0
- package/src/components/HkHoverRevealAction.scss +6 -0
- package/src/components/HkHoverRevealAction.test.ts +68 -0
- package/src/components/HkHoverRevealAction.tsx +84 -6
- package/src/components/HkIcon.scss +123 -0
- package/src/components/HkIcon.tsx +7 -23
- package/src/components/HkIconButton.scss +170 -0
- package/src/components/HkIconButton.test.tsx +75 -0
- package/src/components/HkIconButton.tsx +4 -3
- package/src/components/HkIconButtonVars.scss +180 -0
- package/src/components/HkInput.affix.test.ts +253 -0
- package/src/components/HkInput.enter.test.ts +88 -0
- package/src/components/HkInput.scss +127 -8
- package/src/components/HkInput.tsx +245 -13
- package/src/components/HkKeywordSearchModal.scss +13 -0
- package/src/components/HkKeywordSearchModal.tsx +46 -9
- package/src/components/HkLabel.scss +34 -0
- package/src/components/HkLabel.test.tsx +61 -0
- package/src/components/HkLabel.tsx +45 -0
- package/src/components/HkListTransition.scss +26 -0
- package/src/components/HkListTransition.tsx +57 -0
- package/src/components/HkLocalePicker.scss +37 -0
- package/src/components/HkLocalePicker.tsx +133 -0
- package/src/components/HkLocalePickerPopup.tsx +35 -19
- package/src/components/HkLocalizedInput.scss +277 -0
- package/src/components/HkLocalizedInput.test.tsx +693 -0
- package/src/components/HkLocalizedInput.tsx +495 -0
- package/src/components/HkLogWindow.scss +121 -0
- package/src/components/HkLogWindow.tsx +195 -0
- package/src/components/HkLogo.scss +1 -1
- package/src/components/HkLogo.tsx +1 -1
- package/src/components/HkMarkdownRenderer.scss +22 -2
- package/src/components/HkMarkdownRenderer.tsx +33 -3
- package/src/components/HkMediaControlBar.tsx +20 -13
- package/src/components/HkMenu.scss +200 -0
- package/src/components/HkMenu.test.ts +779 -0
- package/src/components/HkMenu.tsx +770 -0
- package/src/components/HkMenuActionItem.scss +58 -0
- package/src/components/HkMenuActionItem.tsx +41 -0
- package/src/components/HkMenuIdentityItem.scss +70 -0
- package/src/components/HkMenuIdentityItem.tsx +42 -0
- package/src/components/HkMenuPanel.scss +20 -0
- package/src/components/HkMenuPanel.test.tsx +118 -0
- package/src/components/HkMenuPanel.tsx +32 -0
- package/src/components/HkMinimap.tsx +16 -8
- package/src/components/HkModal.autofollow.test.ts +44 -0
- package/src/components/HkModal.scss +170 -53
- package/src/components/HkModal.sheetgap.test.ts +82 -0
- package/src/components/HkModal.test.tsx +334 -0
- package/src/components/HkModal.tsx +314 -68
- package/src/components/HkModalBreadcrumb.scss +4 -1
- package/src/components/HkModalBreadcrumb.test.tsx +111 -0
- package/src/components/HkModalBreadcrumb.tsx +34 -11
- package/src/components/HkModelCatalog.ts +98 -0
- package/src/components/HkModelTag.scss +130 -0
- package/src/components/HkModelTag.test.tsx +111 -0
- package/src/components/HkModelTag.tsx +190 -0
- package/src/components/HkNavItem.scss +13 -2
- package/src/components/HkNavItem.tsx +2 -1
- package/src/components/HkNavSidebar.tsx +99 -0
- package/src/components/HkNumberInput.scss +12 -0
- package/src/components/HkNumberInput.tsx +15 -8
- package/src/components/HkPageHeader.scss +79 -0
- package/src/components/HkPageHeader.test.ts +67 -0
- package/src/components/HkPageHeader.tsx +51 -0
- package/src/components/HkPasswordInput.scss +29 -17
- package/src/components/HkPasswordInput.test.ts +281 -0
- package/src/components/HkPasswordInput.tsx +337 -59
- package/src/components/HkPickerPane.scss +158 -0
- package/src/components/HkPlaceholderMarquee.scss +109 -0
- package/src/components/HkPlaceholderMarquee.test.ts +305 -0
- package/src/components/HkPlaceholderMarquee.tsx +170 -0
- package/src/components/HkPopover.scss +134 -10
- package/src/components/HkPopover.sheetdock.test.ts +34 -0
- package/src/components/HkPopover.surfacevars.test.ts +20 -0
- package/src/components/HkPopover.titleSuppress.test.tsx +265 -0
- package/src/components/HkPopover.tsx +225 -50
- package/src/components/HkPopoverSheet.test.tsx +130 -0
- package/src/components/HkPopupSelect.scss +30 -10
- package/src/components/HkPopupSelect.tsx +21 -1
- package/src/components/HkProgressDialog.scss +31 -0
- package/src/components/HkProgressDialog.tsx +37 -19
- package/src/components/HkProtocolModal.scss +26 -0
- package/src/components/HkProtocolModal.tsx +112 -0
- package/src/components/HkRadio.scss +1 -16
- package/src/components/HkRadio.tsx +5 -2
- package/src/components/HkRichInput.scss +162 -0
- package/src/components/HkRichInput.tsx +335 -0
- package/src/components/HkRollingNumber.test.ts +90 -0
- package/src/components/HkRollingNumber.tsx +39 -1
- package/src/components/HkScrollContainer.scss +71 -54
- package/src/components/HkScrollContainer.test.ts +160 -0
- package/src/components/HkScrollContainer.tsx +187 -160
- package/src/components/HkSearchInput.tsx +3 -3
- package/src/components/HkSecretRevealModal.scss +61 -0
- package/src/components/HkSecretRevealModal.tsx +91 -0
- package/src/components/HkSelect.scss +263 -12
- package/src/components/HkSelect.sheetdock.test.ts +30 -0
- package/src/components/HkSelect.test.tsx +88 -0
- package/src/components/HkSelect.tsx +59 -74
- package/src/components/HkSelectPanel.test.tsx +666 -0
- package/src/components/HkSelectPanel.tsx +595 -0
- package/src/components/HkSelectSheet.test.tsx +132 -0
- package/src/components/HkSelectionGrid.scss +1 -1
- package/src/components/HkSelectionGrid.tsx +2 -1
- package/src/components/HkSelectionWaterfall.scss +1 -1
- package/src/components/HkShareBar.scss +40 -0
- package/src/components/HkShareBar.tsx +50 -0
- package/src/components/HkSidebar.scss +15 -10
- package/src/components/HkSidebar.tsx +26 -2
- package/src/components/HkSignInCard.test.ts +154 -0
- package/src/components/HkSignInCard.tsx +155 -0
- package/src/components/HkSlider.scss +99 -0
- package/src/components/HkSlider.test.tsx +155 -0
- package/src/components/HkSlider.tsx +173 -0
- package/src/components/HkSpinner.scss +36 -1
- package/src/components/HkSpinner.test.tsx +63 -0
- package/src/components/HkSpinner.tsx +16 -1
- package/src/components/HkSplash.scss +1 -1
- package/src/components/HkSplash.tsx +3 -1
- package/src/components/HkStatCard.scss +73 -0
- package/src/components/HkStatCard.tsx +46 -0
- package/src/components/HkStatusBar.test.tsx +178 -0
- package/src/components/HkStatusBar.tsx +379 -0
- package/src/components/HkStatusPill.scss +73 -0
- package/src/components/HkStatusPill.tsx +41 -0
- package/src/components/HkStepFlow.scss +75 -0
- package/src/components/HkStepFlow.test.tsx +246 -0
- package/src/components/HkStepFlow.tsx +115 -0
- package/src/components/HkSwitch.scss +5 -17
- package/src/components/HkSwitch.tsx +6 -3
- package/src/components/HkTable.scss +20 -1
- package/src/components/HkTable.tsx +28 -3
- package/src/components/HkTabs.scss +222 -48
- package/src/components/HkTabs.test.ts +517 -0
- package/src/components/HkTabs.tsx +567 -56
- package/src/components/HkTag.tsx +2 -1
- package/src/components/HkTextarea.tsx +5 -0
- package/src/components/HkThemeToggle.scss +232 -0
- package/src/components/HkThemeToggle.test.ts +231 -0
- package/src/components/HkThemeToggle.tsx +289 -0
- package/src/components/HkTimeline.scss +134 -1
- package/src/components/HkTimeline.test.tsx +319 -0
- package/src/components/HkTimeline.tsx +303 -54
- package/src/components/HkToast.scss +34 -5
- package/src/components/HkToast.test.tsx +109 -0
- package/src/components/HkToast.tsx +57 -8
- package/src/components/HkTokenUsageBadge.scss +53 -0
- package/src/components/HkTokenUsageBadge.test.tsx +59 -0
- package/src/components/HkTokenUsageBadge.tsx +54 -0
- package/src/components/HkTokenUsagePanel.scss +95 -0
- package/src/components/HkTokenUsagePanel.tsx +140 -0
- package/src/components/HkToolBlock.scss +428 -0
- package/src/components/HkToolBlock.tsx +639 -0
- package/src/components/HkTooltip.scss +14 -2
- package/src/components/HkTooltip.tsx +25 -2
- package/src/components/HkTrendChart.tsx +27 -16
- package/src/components/HkVoiceInputPopup.scss +77 -0
- package/src/components/HkVoiceInputPopup.tsx +97 -0
- package/src/components/HkZoomToolbar.scss +4 -1
- package/src/components/HkZoomToolbar.tsx +7 -4
- package/src/components/_hikari-vars.scss +6 -0
- package/src/composables/highlightLanguages.ts +109 -0
- package/src/composables/iconRegistry.ts +127 -0
- package/src/composables/messaging/browserTransport.ts +96 -0
- package/src/composables/messaging/index.ts +204 -0
- package/src/composables/messaging/messaging.test.ts +256 -0
- package/src/composables/messaging/toastTransport.ts +26 -0
- package/src/composables/messaging/types.ts +115 -0
- package/src/composables/useActionBar.ts +66 -0
- package/src/composables/useAttachments.ts +154 -0
- package/src/composables/useConnectionProbe.ts +142 -0
- package/src/composables/useDeferredTransition.ts +16 -0
- package/src/composables/useEngineHealth.ts +69 -0
- package/src/composables/useHighlight.ts +124 -0
- package/src/composables/useMeasuredHighlight.ts +150 -0
- package/src/composables/useOverlayScrollbar.test.ts +221 -0
- package/src/composables/useOverlayScrollbar.ts +296 -0
- package/src/composables/usePageTitle.ts +37 -0
- package/src/composables/useReducedMotion.ts +34 -0
- package/src/composables/useResourceListModal.tsx +147 -0
- package/src/composables/useSafeArea.ts +137 -0
- package/src/composables/useSendShortcut.ts +35 -0
- package/src/composables/useSizeMorph.test.ts +156 -0
- package/src/composables/useSizeMorph.ts +149 -0
- package/src/composables/useSurfaceTransition.test.ts +96 -0
- package/src/composables/useSurfaceTransition.ts +71 -0
- package/src/composables/useZoomPan.ts +192 -0
- package/src/demo.scss +9 -1
- package/src/demo.ts +5 -1
- package/src/i18n/locales/ar/chat.json +38 -0
- package/src/i18n/locales/ar/components.json +141 -40
- package/src/i18n/locales/ar/components.toml +59 -0
- package/src/i18n/locales/ar/platform.json +65 -0
- package/src/i18n/locales/de/chat.json +38 -0
- package/src/i18n/locales/de/components.json +139 -38
- package/src/i18n/locales/de/components.toml +59 -0
- package/src/i18n/locales/de/platform.json +65 -0
- package/src/i18n/locales/en/chat.json +38 -0
- package/src/i18n/locales/en/components.json +104 -3
- package/src/i18n/locales/en/components.toml +59 -0
- package/src/i18n/locales/en/platform.json +65 -0
- package/src/i18n/locales/es/chat.json +38 -0
- package/src/i18n/locales/es/components.json +141 -40
- package/src/i18n/locales/es/components.toml +59 -0
- package/src/i18n/locales/es/platform.json +65 -0
- package/src/i18n/locales/fr/chat.json +38 -0
- package/src/i18n/locales/fr/components.json +137 -36
- package/src/i18n/locales/fr/components.toml +59 -0
- package/src/i18n/locales/fr/platform.json +65 -0
- package/src/i18n/locales/ja/chat.json +38 -0
- package/src/i18n/locales/ja/components.json +104 -3
- package/src/i18n/locales/ja/components.toml +59 -0
- package/src/i18n/locales/ja/platform.json +65 -0
- package/src/i18n/locales/ko/chat.json +38 -0
- package/src/i18n/locales/ko/components.json +104 -3
- package/src/i18n/locales/ko/components.toml +59 -0
- package/src/i18n/locales/ko/platform.json +65 -0
- package/src/i18n/locales/pt/chat.json +38 -0
- package/src/i18n/locales/pt/components.json +141 -40
- package/src/i18n/locales/pt/components.toml +59 -0
- package/src/i18n/locales/pt/platform.json +65 -0
- package/src/i18n/locales/ru/chat.json +38 -0
- package/src/i18n/locales/ru/components.json +141 -40
- package/src/i18n/locales/ru/components.toml +59 -0
- package/src/i18n/locales/ru/platform.json +65 -0
- package/src/i18n/locales/zh-Hans/chat.json +38 -0
- package/src/i18n/locales/zh-Hans/components.json +109 -4
- package/src/i18n/locales/zh-Hans/components.toml +59 -0
- package/src/i18n/locales/zh-Hans/platform.json +65 -0
- package/src/i18n/locales/zh-Hant/chat.json +38 -0
- package/src/i18n/locales/zh-Hant/components.json +109 -4
- package/src/i18n/locales/zh-Hant/components.toml +59 -0
- package/src/i18n/locales/zh-Hant/platform.json +65 -0
- package/src/index.ts +225 -6
- package/src/plugins/bootScriptsPlugin.ts +47 -0
- package/src/plugins/buildHashPlugin.ts +67 -0
- package/src/plugins/index.ts +2 -0
- package/src/render.ts +1 -1
- package/src/runtime/animationBus.test.ts +112 -0
- package/src/runtime/animationBus.ts +33 -2
- package/src/runtime/backStack.test.ts +334 -0
- package/src/runtime/backStack.ts +377 -0
- package/src/runtime/env.ts +4 -0
- package/src/runtime/index.ts +15 -3
- package/src/runtime/intervalBus.test.ts +118 -0
- package/src/runtime/intervalBus.ts +189 -0
- package/src/runtime/mobileViewport.test.ts +132 -0
- package/src/runtime/mobileViewport.ts +157 -0
- package/src/runtime/pageLifecycle.ts +91 -0
- package/src/runtime/useBlockingToast.ts +112 -0
- package/src/runtime/useClipboard.ts +28 -0
- package/src/runtime/useConfirm.ts +15 -7
- package/src/runtime/useMediaQuery.test.ts +67 -0
- package/src/runtime/useMediaQuery.ts +89 -0
- package/src/runtime/useOverlay.test.ts +160 -0
- package/src/runtime/useOverlay.ts +59 -12
- package/src/runtime/usePopupManager.test.ts +200 -0
- package/src/runtime/usePopupManager.ts +106 -10
- package/src/runtime/useToast.test.ts +90 -0
- package/src/runtime/useToast.ts +20 -2
- package/src/styles/admin-tokens.scss +744 -0
- package/src/styles/index.scss +5 -3
- package/src/styles/theme/_glass.scss +33 -0
- package/src/styles/theme/_layout.scss +123 -0
- package/src/styles/theme/_scrollbar.scss +73 -0
- package/src/styles/theme/_tokens.scss +69 -0
- package/src/styles/theme/base.scss +617 -0
- package/src/styles/theme/foundation.scss +219 -0
- package/src/styles/theme/mixins.scss +793 -0
- package/src/styles/theme/themes.scss +25 -0
- package/src/styles/theme/variables.scss +125 -0
- package/src/theme/_field.scss +116 -0
- package/src/theme/fontContext.test.ts +179 -0
- package/src/theme/fontContext.ts +169 -0
- package/src/theme/index.ts +19 -2
- package/src/theme/presets.ts +86 -18
- package/src/theme/theme.scss +61 -1
- package/src/theme/tokenGroups.test.ts +534 -0
- package/src/theme/tokenGroups.ts +552 -0
- package/src/theme/useSolarTime.test.ts +113 -0
- package/src/theme/useSolarTime.ts +95 -20
- package/src/theme/useTheme.test.ts +72 -0
- package/src/theme/useTheme.ts +144 -9
- package/src/tokens.scss +110 -0
- package/src/utils/base64.ts +31 -0
- package/src/utils/connectivity.ts +51 -0
- package/src/utils/dom.test.ts +131 -0
- package/src/utils/dom.ts +133 -0
- package/src/utils/download.ts +22 -0
- package/src/utils/fileType.ts +127 -0
- package/src/utils/format.ts +98 -0
- package/src/utils/fuzzy.ts +77 -0
- package/src/utils/fuzzySearch.test.ts +87 -0
- package/src/utils/healthProbe.ts +64 -0
- package/src/utils/objectPath.ts +59 -0
- package/src/utils/password.ts +53 -0
- package/src/utils/pow.test.ts +45 -0
- package/src/utils/pow.ts +226 -0
- package/src/utils/powNonce.ts +95 -0
- package/src/utils/validation.test.ts +78 -0
- package/src/DemoApp.vue +0 -400
- package/src/components/HkCollapse.tsx +0 -48
- package/src/components/HkMorphingTabs.scss +0 -141
- package/src/components/HkMorphingTabs.tsx +0 -113
- package/src/composables/useBreakpoint.ts +0 -41
- package/src/composables/useClipboard.ts +0 -37
- package/src/composables/useConfirm.ts +0 -35
- package/src/composables/useToast.ts +0 -2
- 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
|
+
});
|
|
@@ -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
|
+
});
|