@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,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HkAdaptiveDialog — one dialog contract, two shells.
|
|
3
|
+
*
|
|
4
|
+
* Renders HkModal on desktop viewports and HkDrawer side="bottom" on
|
|
5
|
+
* mobile (switched by `useBreakpoint().isMobile`, i.e. width < 768), so
|
|
6
|
+
* one `v-model` component gives a centered dialog on wide screens and a
|
|
7
|
+
* slide-up sheet on phones. The shell only picks which inner component
|
|
8
|
+
* renders and normalizes props/slots — overlay, teleport, z-index
|
|
9
|
+
* stacking, body scroll lock, focus handling and Escape-to-close all
|
|
10
|
+
* come from the inner components via usePopupManager and are never
|
|
11
|
+
* re-implemented here.
|
|
12
|
+
*
|
|
13
|
+
* Slot mapping:
|
|
14
|
+
* default -> body of whichever shell renders
|
|
15
|
+
* header -> HkModal sub-header (below the title bar); on mobile the
|
|
16
|
+
* drawer header is composed so the title stays in the title
|
|
17
|
+
* bar and the slot content rides under it (visual parity
|
|
18
|
+
* with the modal)
|
|
19
|
+
* footer -> footer slot of either shell; wins over footerActions,
|
|
20
|
+
* matching HkModal's own precedence
|
|
21
|
+
*
|
|
22
|
+
* footerActions pass straight into HkModal's prop on desktop; HkDrawer
|
|
23
|
+
* has no footerActions prop, so on mobile the shell renders the
|
|
24
|
+
* equivalent HButton row inside the drawer's footer slot (same order,
|
|
25
|
+
* variants, loading, disabled and onClick handlers).
|
|
26
|
+
*
|
|
27
|
+
* Caveat: if the viewport crosses the mobile breakpoint while a leave
|
|
28
|
+
* transition is still in flight, the inner Transition unmounts without
|
|
29
|
+
* firing its after-leave hook — the forwarded `afterLeave` emit and
|
|
30
|
+
* HkModal's focus restore are skipped for that close (no resource leak:
|
|
31
|
+
* popup handle and scroll lock are released on unmount). Consumers
|
|
32
|
+
* relying on afterLeave (e.g. focus hand-off) should not depend on it
|
|
33
|
+
* firing during a live shell flip.
|
|
34
|
+
*/
|
|
35
|
+
import { defineComponent, type PropType } from "vue";
|
|
36
|
+
|
|
37
|
+
import { useBreakpoint } from "../runtime/useBreakpoint";
|
|
38
|
+
import "./HkAdaptiveDialog.scss";
|
|
39
|
+
import HButton from "./HkButton";
|
|
40
|
+
import HDrawer from "./HkDrawer";
|
|
41
|
+
import HModal, { type ModalAction } from "./HkModal";
|
|
42
|
+
|
|
43
|
+
export default defineComponent({
|
|
44
|
+
name: "HkAdaptiveDialog",
|
|
45
|
+
props: {
|
|
46
|
+
modelValue: { type: Boolean, required: true },
|
|
47
|
+
title: { type: String, default: undefined },
|
|
48
|
+
closable: { type: Boolean, default: true },
|
|
49
|
+
/** Desktop modal width (HkModal `width`). */
|
|
50
|
+
width: { type: String, default: "32rem" },
|
|
51
|
+
/** Mobile drawer height (HkDrawer `size`). HkDrawer's bottom-side CSS
|
|
52
|
+
* clamps panels at maxHeight 70vh, so the default equals the cap;
|
|
53
|
+
* pass `panelClass` with a custom max-height for taller sheets. */
|
|
54
|
+
mobileSize: { type: String, default: "70vh" },
|
|
55
|
+
footerActions: {
|
|
56
|
+
type: Array as PropType<ModalAction[]>,
|
|
57
|
+
default: undefined,
|
|
58
|
+
},
|
|
59
|
+
/** Extra classes for the mobile drawer panel — same escape hatch as
|
|
60
|
+
* HkDrawer.panelClass (attrs fallthrough cannot reach a teleported
|
|
61
|
+
* panel). Desktop has no equivalent hook on HkModal. */
|
|
62
|
+
panelClass: { type: String, default: undefined },
|
|
63
|
+
},
|
|
64
|
+
emits: {
|
|
65
|
+
"update:modelValue": (_value: boolean) => true,
|
|
66
|
+
afterLeave: () => true,
|
|
67
|
+
},
|
|
68
|
+
setup(props, { emit, slots }) {
|
|
69
|
+
const { isMobile } = useBreakpoint();
|
|
70
|
+
|
|
71
|
+
function onUpdate(v: boolean) {
|
|
72
|
+
emit("update:modelValue", v);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function onAfterLeave() {
|
|
76
|
+
emit("afterLeave");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Drawer footer: the footer slot wins over footerActions (same
|
|
80
|
+
* precedence HkModal applies); with no slot, the actions render as
|
|
81
|
+
* the same HButton row the modal would show. Returns undefined when
|
|
82
|
+
* there is nothing to render so the drawer skips its footer band. */
|
|
83
|
+
function renderDrawerFooter() {
|
|
84
|
+
if (slots.footer) return slots.footer();
|
|
85
|
+
if (props.footerActions && props.footerActions.length > 0) {
|
|
86
|
+
return (
|
|
87
|
+
<div class="hk-adaptive-dialog-footer">
|
|
88
|
+
{props.footerActions.map((action, i) => (
|
|
89
|
+
<HButton
|
|
90
|
+
key={i}
|
|
91
|
+
variant={action.variant ?? "secondary"}
|
|
92
|
+
size="sm"
|
|
93
|
+
loading={action.loading}
|
|
94
|
+
disabled={action.disabled}
|
|
95
|
+
onClick={action.onClick}
|
|
96
|
+
>
|
|
97
|
+
{action.label}
|
|
98
|
+
</HButton>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** True when the drawer should render a footer band at all. */
|
|
107
|
+
function hasDrawerFooter() {
|
|
108
|
+
return Boolean(slots.footer) || Boolean(props.footerActions && props.footerActions.length > 0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** HkDrawer's header slot replaces the title span, so on mobile the
|
|
112
|
+
* shell composes title + slot content itself — keeping the title in
|
|
113
|
+
* the title bar with the slot under it, like the modal sub-header. */
|
|
114
|
+
function renderDrawerHeader() {
|
|
115
|
+
if (!slots.header) return undefined;
|
|
116
|
+
return () => (
|
|
117
|
+
<div class="hk-adaptive-dialog-drawer-header">
|
|
118
|
+
{props.title ? (
|
|
119
|
+
<span class="hk-drawer-title">{props.title}</span>
|
|
120
|
+
) : null}
|
|
121
|
+
{slots.header!()}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return () => {
|
|
127
|
+
if (isMobile.value) {
|
|
128
|
+
return (
|
|
129
|
+
<HDrawer
|
|
130
|
+
modelValue={props.modelValue}
|
|
131
|
+
onUpdate:modelValue={onUpdate}
|
|
132
|
+
onAfterLeave={onAfterLeave}
|
|
133
|
+
side="bottom"
|
|
134
|
+
size={props.mobileSize}
|
|
135
|
+
title={props.title}
|
|
136
|
+
closable={props.closable}
|
|
137
|
+
panelClass={props.panelClass}
|
|
138
|
+
>
|
|
139
|
+
{{
|
|
140
|
+
default: () => slots.default?.(),
|
|
141
|
+
header: renderDrawerHeader(),
|
|
142
|
+
footer: hasDrawerFooter() ? () => renderDrawerFooter() : undefined,
|
|
143
|
+
}}
|
|
144
|
+
</HDrawer>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<HModal
|
|
150
|
+
modelValue={props.modelValue}
|
|
151
|
+
onUpdate:modelValue={onUpdate}
|
|
152
|
+
onAfterLeave={onAfterLeave}
|
|
153
|
+
title={props.title}
|
|
154
|
+
closable={props.closable}
|
|
155
|
+
width={props.width}
|
|
156
|
+
footerActions={props.footerActions}
|
|
157
|
+
>
|
|
158
|
+
{{
|
|
159
|
+
default: () => slots.default?.(),
|
|
160
|
+
header: slots.header ? () => slots.header!() : undefined,
|
|
161
|
+
footer: slots.footer ? () => slots.footer!() : undefined,
|
|
162
|
+
}}
|
|
163
|
+
</HModal>
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createApp, h } from "vue";
|
|
3
|
+
|
|
4
|
+
// HPopover teleports to body — stubbing Teleport stringifies its vnode
|
|
5
|
+
// children. Replace just HPopover with an inline passthrough that renders
|
|
6
|
+
// its default slot while open; the rest of hikari stays real.
|
|
7
|
+
vi.mock("@celestia-island/hikari", async (importOriginal) => {
|
|
8
|
+
const actual = await importOriginal<typeof import("@celestia-island/hikari")>();
|
|
9
|
+
const { defineComponent, h } = await import("vue");
|
|
10
|
+
const HPopoverStub = defineComponent({
|
|
11
|
+
name: "HPopover",
|
|
12
|
+
props: {
|
|
13
|
+
modelValue: { type: Boolean, default: false },
|
|
14
|
+
placement: { type: String, default: "bottom" },
|
|
15
|
+
},
|
|
16
|
+
setup(props, { slots }) {
|
|
17
|
+
return () =>
|
|
18
|
+
props.modelValue
|
|
19
|
+
? h("div", { class: "popover-stub" }, slots.default?.())
|
|
20
|
+
: null;
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
return { ...actual, HPopover: HPopoverStub };
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
import { HkAdminHeader } from "./HkAdminHeader";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* HkAdminHeader contract tests for the generalized avatar/identity
|
|
30
|
+
* behaviors ported from the chest plana-legacy fork:
|
|
31
|
+
* - avatarAction "menu" (default) toggles the user dropdown; "drawer"
|
|
32
|
+
* emits avatarClick so the shell can open its nav drawer.
|
|
33
|
+
* - the identity block (name/email/groups) leads the menu; an empty
|
|
34
|
+
* identity renders the signing-in placeholder + force-sign-out escape.
|
|
35
|
+
* - an empty title hides the context-title node entirely (pages carry
|
|
36
|
+
* their own in-page title in the HPageHeader convention).
|
|
37
|
+
*
|
|
38
|
+
* (Repo test convention: raw createApp + container queries, no
|
|
39
|
+
* @vue/test-utils dependency.)
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
43
|
+
|
|
44
|
+
function mount(node: ReturnType<typeof h>) {
|
|
45
|
+
const container = document.createElement("div");
|
|
46
|
+
document.body.appendChild(container);
|
|
47
|
+
const app = createApp({ render: () => node });
|
|
48
|
+
app.mount(container);
|
|
49
|
+
mounts.push({ app, container });
|
|
50
|
+
return container;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
for (const { app, container } of mounts.splice(0)) {
|
|
55
|
+
app.unmount();
|
|
56
|
+
container.remove();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
type HeaderProps = Record<string, unknown>;
|
|
61
|
+
|
|
62
|
+
function headerNode(props: HeaderProps = {}, slots?: Record<string, unknown>) {
|
|
63
|
+
return h(HkAdminHeader, { username: "alice", ...props } as never, slots as never);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const avatarButton = (c: HTMLElement) =>
|
|
67
|
+
c.querySelector('button[aria-haspopup]') as HTMLButtonElement | null;
|
|
68
|
+
|
|
69
|
+
async function click(el: Element | null) {
|
|
70
|
+
el?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
71
|
+
await Promise.resolve();
|
|
72
|
+
await Promise.resolve();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("HkAdminHeader", () => {
|
|
76
|
+
it("toggles the user dropdown on avatar click in menu mode", async () => {
|
|
77
|
+
const c = mount(headerNode({ logoutLabel: "Log out" }));
|
|
78
|
+
const btn = avatarButton(c);
|
|
79
|
+
expect(btn?.getAttribute("aria-haspopup")).toBe("menu");
|
|
80
|
+
|
|
81
|
+
await click(btn);
|
|
82
|
+
const menu = c.querySelector(".popover-stub");
|
|
83
|
+
// The dropdown leads with the identity block and ends with logout.
|
|
84
|
+
expect(menu?.textContent).toContain("alice");
|
|
85
|
+
expect(c.querySelector(".s-user-header")).toBeTruthy();
|
|
86
|
+
expect(menu?.textContent).toContain("Log out");
|
|
87
|
+
|
|
88
|
+
await click(btn);
|
|
89
|
+
expect(c.querySelector(".s-user-header")).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("emits avatarClick instead of the dropdown in drawer mode", async () => {
|
|
93
|
+
const clicks: number[] = [];
|
|
94
|
+
const c = mount(headerNode({
|
|
95
|
+
avatarAction: "drawer",
|
|
96
|
+
logoutLabel: "Log out",
|
|
97
|
+
onAvatarClick: () => clicks.push(1),
|
|
98
|
+
}));
|
|
99
|
+
const btn = avatarButton(c);
|
|
100
|
+
expect(btn?.getAttribute("aria-haspopup")).toBe("dialog");
|
|
101
|
+
|
|
102
|
+
await click(btn);
|
|
103
|
+
expect(clicks).toHaveLength(1);
|
|
104
|
+
// The dropdown must NOT open in drawer mode, and the username stays
|
|
105
|
+
// hidden — the identity block lives in the drawer footer.
|
|
106
|
+
expect(c.querySelector(".s-user-header")).toBeNull();
|
|
107
|
+
expect(c.textContent).not.toContain("alice");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("renders the signing-in placeholder with the force-sign-out escape when the identity is empty", async () => {
|
|
111
|
+
const onForceSignOut = vi.fn();
|
|
112
|
+
const c = mount(headerNode({
|
|
113
|
+
username: "",
|
|
114
|
+
userEmail: "",
|
|
115
|
+
signingInLabel: "Signing in…",
|
|
116
|
+
forceSignOutLabel: "Sign out now",
|
|
117
|
+
onForceSignOut,
|
|
118
|
+
}));
|
|
119
|
+
await click(avatarButton(c));
|
|
120
|
+
|
|
121
|
+
expect(c.querySelector(".s-user-header--pending")).toBeTruthy();
|
|
122
|
+
expect(c.textContent).toContain("Signing in…");
|
|
123
|
+
// No action items above an absent identity.
|
|
124
|
+
expect(c.textContent).not.toContain("Avatar");
|
|
125
|
+
|
|
126
|
+
await click(c.querySelector(".s-popup-menu-item"));
|
|
127
|
+
expect(onForceSignOut).toHaveBeenCalledTimes(1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("hides the context title entirely when it is empty", () => {
|
|
131
|
+
const withTitle = mount(headerNode({ title: "Dashboard" }));
|
|
132
|
+
expect(withTitle.querySelector("h2")?.textContent).toBe("Dashboard");
|
|
133
|
+
|
|
134
|
+
const withoutTitle = mount(headerNode());
|
|
135
|
+
expect(withoutTitle.querySelector("h2")).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("renders no emergency-stop control by default", () => {
|
|
139
|
+
const c = mount(headerNode({ username: "alice" }));
|
|
140
|
+
expect(c.textContent.toLowerCase()).not.toContain("emergency");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("renders the goToFrontend row above logout and emits goToFrontend", async () => {
|
|
144
|
+
const onGoToFrontend = vi.fn();
|
|
145
|
+
const c = mount(headerNode({
|
|
146
|
+
logoutLabel: "Log out",
|
|
147
|
+
goToFrontendLabel: "Go to Frontend",
|
|
148
|
+
onGoToFrontend,
|
|
149
|
+
}));
|
|
150
|
+
// Opt-in: absent without the label prop…
|
|
151
|
+
const bare = mount(headerNode({ logoutLabel: "Log out" }));
|
|
152
|
+
await click(avatarButton(bare));
|
|
153
|
+
expect(bare.textContent).not.toContain("Go to Frontend");
|
|
154
|
+
|
|
155
|
+
// …and present (directly above logout, mirroring the drawer user
|
|
156
|
+
// panel's row order) with the label prop.
|
|
157
|
+
await click(avatarButton(c));
|
|
158
|
+
const rows = [...c.querySelectorAll(".s-popup-menu-item")];
|
|
159
|
+
const labels = rows.map((r) => r.textContent?.trim());
|
|
160
|
+
expect(labels.indexOf("Go to Frontend")).toBeGreaterThan(-1);
|
|
161
|
+
expect(labels.indexOf("Log out")).toBe(labels.indexOf("Go to Frontend") + 1);
|
|
162
|
+
|
|
163
|
+
await click(rows[labels.indexOf("Go to Frontend")]);
|
|
164
|
+
expect(onGoToFrontend).toHaveBeenCalledTimes(1);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("passes the locale trigger ref OBJECT through the slot so the picker anchors to the button", async () => {
|
|
168
|
+
let captured: unknown = null;
|
|
169
|
+
const c = mount(headerNode(
|
|
170
|
+
{ username: "alice" },
|
|
171
|
+
{
|
|
172
|
+
"locale-picker": (scope: { triggerRef: unknown }) => {
|
|
173
|
+
captured = scope.triggerRef;
|
|
174
|
+
return null;
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
));
|
|
178
|
+
await click(avatarButton(c));
|
|
179
|
+
// The slot must receive the ref object itself (reactive), not its
|
|
180
|
+
// frozen .value snapshot from the first render (null before the
|
|
181
|
+
// ref attached).
|
|
182
|
+
expect(captured).toBeTruthy();
|
|
183
|
+
expect((captured as { value?: unknown }).value).toBeInstanceOf(HTMLElement);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { defineComponent, ref, type PropType, type VNode } from "vue";
|
|
2
|
+
import { Camera, ExternalLink, Languages, LogOut, Menu } from "lucide-vue-next";
|
|
3
|
+
import { HBadge, HButton, HPopover, HSpinner } from "@celestia-island/hikari";
|
|
4
|
+
import { useI18n } from "../i18n/context";
|
|
5
|
+
|
|
6
|
+
export interface LocaleOption {
|
|
7
|
+
code: string;
|
|
8
|
+
label: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const HkAdminHeader = defineComponent({
|
|
12
|
+
name: "HkAdminHeader",
|
|
13
|
+
props: {
|
|
14
|
+
/** Optional context title — empty (the default) hides the node
|
|
15
|
+
* entirely, for layouts whose pages carry their own in-page title
|
|
16
|
+
* (the HPageHeader convention). */
|
|
17
|
+
title: { type: String, default: "" },
|
|
18
|
+
showHamburger: { type: Boolean, default: false },
|
|
19
|
+
compact: { type: Boolean, default: false },
|
|
20
|
+
actions: { type: Array as PropType<VNode[]>, default: () => [] },
|
|
21
|
+
username: { type: String, default: "" },
|
|
22
|
+
avatarUrl: { type: String, default: "" },
|
|
23
|
+
userEmail: { type: String, default: "" },
|
|
24
|
+
userGroups: { type: Array as PropType<{ id: string; name: string }[]>, default: () => [] },
|
|
25
|
+
/** What the avatar trigger does:
|
|
26
|
+
* - "menu" (default, desktop): toggle the user dropdown popover
|
|
27
|
+
* (identity, avatar edit, language, logout).
|
|
28
|
+
* - "drawer" (mobile): emit `avatarClick` so the shell opens its nav
|
|
29
|
+
* drawer, whose `userPanel` footer carries the same user content.
|
|
30
|
+
* The username stays hidden in this mode — the identity block
|
|
31
|
+
* lives in the drawer, so a header username would read as a
|
|
32
|
+
* duplicated stray control. */
|
|
33
|
+
avatarAction: { type: String as PropType<"menu" | "drawer">, default: "menu" },
|
|
34
|
+
/** Placeholder row shown while the identity is still loading (a
|
|
35
|
+
* fetchUser race on hard refresh) — the action items stay hidden
|
|
36
|
+
* until there is an identity to act on. */
|
|
37
|
+
signingInLabel: { type: String, default: undefined },
|
|
38
|
+
/** Pending-state escape hatch: typically wired to logout (clears
|
|
39
|
+
* cookies and returns to the login page) when the network wedges
|
|
40
|
+
* the session restore and the user wants to break out manually. */
|
|
41
|
+
onForceSignOut: { type: Function, default: undefined },
|
|
42
|
+
forceSignOutLabel: { type: String, default: undefined },
|
|
43
|
+
showEmergencyStop: { type: Boolean, default: false },
|
|
44
|
+
emergencyStopActive: { type: Boolean, default: false },
|
|
45
|
+
emergencyStopActiveLabel: { type: String, default: "" },
|
|
46
|
+
emergencyStopActiveTitle: { type: String, default: "" },
|
|
47
|
+
emergencyStopLabel: { type: String, default: "" },
|
|
48
|
+
emergencyStopTitle: { type: String, default: "" },
|
|
49
|
+
emergencyStopLoading: { type: Boolean, default: false },
|
|
50
|
+
avatarMenuLabel: { type: String, default: undefined },
|
|
51
|
+
/** Accessible label for the avatar trigger button. */
|
|
52
|
+
avatarTriggerLabel: { type: String, default: undefined },
|
|
53
|
+
localeMenuLabel: { type: String, default: undefined },
|
|
54
|
+
logoutLabel: { type: String, default: undefined },
|
|
55
|
+
adminGroupLabel: { type: String, default: undefined },
|
|
56
|
+
/** "Go to frontend" menu row (external-face link, rendered directly
|
|
57
|
+
* above logout). Hidden unless a label is provided — admin-only
|
|
58
|
+
* panels without a consumer-facing face simply omit the prop and
|
|
59
|
+
* keep the menu at avatar/language/logout. */
|
|
60
|
+
goToFrontendLabel: { type: String, default: undefined },
|
|
61
|
+
},
|
|
62
|
+
emits: {
|
|
63
|
+
logout: () => true,
|
|
64
|
+
goToFrontend: () => true,
|
|
65
|
+
hamburger: () => true,
|
|
66
|
+
avatarClick: () => true,
|
|
67
|
+
emergencyStop: () => true,
|
|
68
|
+
},
|
|
69
|
+
setup(props, { emit, slots }) {
|
|
70
|
+
const { t } = useI18n();
|
|
71
|
+
const userMenuOpen = ref(false);
|
|
72
|
+
const userTriggerRef = ref<HTMLElement>();
|
|
73
|
+
const avatarModalOpen = ref(false);
|
|
74
|
+
const avatarFailed = ref(false);
|
|
75
|
+
const localeMenuOpen = ref(false);
|
|
76
|
+
const localeTriggerRef = ref<HTMLElement | null>(null);
|
|
77
|
+
|
|
78
|
+
function onAvatarClick(e: MouseEvent) {
|
|
79
|
+
e.stopPropagation();
|
|
80
|
+
if (props.avatarAction === "drawer") {
|
|
81
|
+
emit("avatarClick");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
userMenuOpen.value = !userMenuOpen.value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return () => (
|
|
88
|
+
<header
|
|
89
|
+
class={[
|
|
90
|
+
"s-glass-header",
|
|
91
|
+
props.compact ? "px-4 gap-2" : "px-6 gap-3",
|
|
92
|
+
]}
|
|
93
|
+
>
|
|
94
|
+
{props.showHamburger && (
|
|
95
|
+
<HButton
|
|
96
|
+
variant="ghost"
|
|
97
|
+
size="sm"
|
|
98
|
+
onClick={() => emit("hamburger")}
|
|
99
|
+
>
|
|
100
|
+
<Menu size={20} class="w-5 h-5" />
|
|
101
|
+
</HButton>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
<div ref={userTriggerRef} class="flex items-center gap-2 min-w-0">
|
|
105
|
+
<button
|
|
106
|
+
class={[
|
|
107
|
+
"w-7 h-7 rounded-full overflow-hidden shrink-0 cursor-pointer transition-opacity relative group p-0 border-0",
|
|
108
|
+
props.avatarUrl
|
|
109
|
+
? ""
|
|
110
|
+
: "bg-primary/10 border-2 border-primary/15 hover:border-primary/30",
|
|
111
|
+
]}
|
|
112
|
+
aria-label={props.avatarTriggerLabel ?? t("hikari::adminHeader.avatarTrigger", "Account menu")}
|
|
113
|
+
aria-haspopup={props.avatarAction === "drawer" ? "dialog" : "menu"}
|
|
114
|
+
aria-expanded={props.avatarAction === "menu" ? userMenuOpen.value : undefined}
|
|
115
|
+
onClick={onAvatarClick}
|
|
116
|
+
>
|
|
117
|
+
{props.avatarUrl && !avatarFailed.value ? (
|
|
118
|
+
<img
|
|
119
|
+
src={props.avatarUrl}
|
|
120
|
+
alt={props.username}
|
|
121
|
+
class="w-full h-full object-cover"
|
|
122
|
+
onError={() => { avatarFailed.value = true; }}
|
|
123
|
+
/>
|
|
124
|
+
) : (
|
|
125
|
+
<span class="text-xs font-bold text-primary/70">
|
|
126
|
+
{props.username?.charAt(0).toUpperCase() || "?"}
|
|
127
|
+
</span>
|
|
128
|
+
)}
|
|
129
|
+
{props.avatarAction === "menu" && (
|
|
130
|
+
<div class="absolute inset-0 rounded-full bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
|
131
|
+
<Camera size={10} class="text-white" />
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</button>
|
|
135
|
+
{props.avatarAction === "menu" && (
|
|
136
|
+
<span class="text-sm font-semibold text-text truncate max-w-[8rem]">
|
|
137
|
+
{props.username}
|
|
138
|
+
</span>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<HPopover
|
|
143
|
+
modelValue={userMenuOpen.value}
|
|
144
|
+
onUpdate:modelValue={(v: boolean) => {
|
|
145
|
+
userMenuOpen.value = v;
|
|
146
|
+
// Closing the outer menu must also close the locale submenu,
|
|
147
|
+
// or it stays orphaned for the next open.
|
|
148
|
+
if (!v) localeMenuOpen.value = false;
|
|
149
|
+
}}
|
|
150
|
+
placement="bottom-start"
|
|
151
|
+
anchorRef={userTriggerRef.value ?? null}
|
|
152
|
+
class="w-56"
|
|
153
|
+
sheetOnMobile
|
|
154
|
+
title={t("hikari::adminHeader.avatarTrigger")}
|
|
155
|
+
>
|
|
156
|
+
{/* Empty identity (fetchUser race on a hard refresh): render a
|
|
157
|
+
single subtle placeholder row instead of the action items —
|
|
158
|
+
"Change Avatar"/"Logout" floating above no identity read as a
|
|
159
|
+
broken menu. The force-sign-out escape hatch lets the user
|
|
160
|
+
break out of a wedged session restore. */}
|
|
161
|
+
{!props.username && !props.userEmail ? (
|
|
162
|
+
<div>
|
|
163
|
+
<div class="s-user-header s-user-header--pending">
|
|
164
|
+
<HSpinner size="sm" />
|
|
165
|
+
<div class="s-user-header-email">{props.signingInLabel ?? t("hikari::adminHeader.signingIn", "Signing in…")}</div>
|
|
166
|
+
</div>
|
|
167
|
+
<button
|
|
168
|
+
class="s-popup-menu-item"
|
|
169
|
+
onClick={() => props.onForceSignOut?.()}
|
|
170
|
+
>
|
|
171
|
+
<LogOut size={14} />
|
|
172
|
+
{props.forceSignOutLabel ?? t("hikari::adminHeader.forceSignOut", "Sign out")}
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
) : (
|
|
176
|
+
<div>
|
|
177
|
+
{/* Identity header FIRST — nickname, login email, permission
|
|
178
|
+
badges — so every account surface reads identically. */}
|
|
179
|
+
<div class="s-user-header">
|
|
180
|
+
{props.username && <div class="s-user-header-name">{props.username}</div>}
|
|
181
|
+
{props.userEmail && <div class="s-user-header-email">{props.userEmail}</div>}
|
|
182
|
+
{props.userGroups && props.userGroups.length > 0 && (
|
|
183
|
+
<div class="s-user-header-groups">
|
|
184
|
+
{props.userGroups.map((g: { id: string; name: string }) => (
|
|
185
|
+
<HBadge
|
|
186
|
+
key={g.id}
|
|
187
|
+
variant={g.name === "Administrators" ? "error" : "primary"}
|
|
188
|
+
size="sm"
|
|
189
|
+
>
|
|
190
|
+
{g.name === "Administrators"
|
|
191
|
+
? (props.adminGroupLabel ?? t("hikari::adminHeader.adminGroup", "Administrators"))
|
|
192
|
+
: g.name}
|
|
193
|
+
</HBadge>
|
|
194
|
+
))}
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
<button
|
|
199
|
+
class="s-popup-menu-item"
|
|
200
|
+
onClick={() => {
|
|
201
|
+
userMenuOpen.value = false;
|
|
202
|
+
avatarModalOpen.value = true;
|
|
203
|
+
}}
|
|
204
|
+
>
|
|
205
|
+
<Camera size={14} />
|
|
206
|
+
{props.avatarMenuLabel ?? t("hikari::adminHeader.avatar", "Avatar")}
|
|
207
|
+
</button>
|
|
208
|
+
{/* The language trigger owns the locale anchor: the ref sits
|
|
209
|
+
on the BUTTON itself (not a wrapper div) so the picker
|
|
210
|
+
popup anchors to it instead of falling back to inline
|
|
211
|
+
rendering. The ref OBJECT is passed through the slot —
|
|
212
|
+
reading .value here would freeze null from the first
|
|
213
|
+
render before the ref attached. */}
|
|
214
|
+
<button
|
|
215
|
+
ref={localeTriggerRef}
|
|
216
|
+
class="s-popup-menu-item"
|
|
217
|
+
onClick={() => (localeMenuOpen.value = !localeMenuOpen.value)}
|
|
218
|
+
>
|
|
219
|
+
<Languages size={14} />
|
|
220
|
+
{props.localeMenuLabel ?? t("hikari::adminHeader.language", "Language")}
|
|
221
|
+
</button>
|
|
222
|
+
{slots["locale-picker"]?.({
|
|
223
|
+
open: localeMenuOpen.value,
|
|
224
|
+
onUpdateOpen: (v: boolean) => {
|
|
225
|
+
localeMenuOpen.value = v;
|
|
226
|
+
if (!v) userMenuOpen.value = false;
|
|
227
|
+
},
|
|
228
|
+
triggerRef: localeTriggerRef,
|
|
229
|
+
})}
|
|
230
|
+
{slots["user-menu-extra"]?.()}
|
|
231
|
+
{/* Frontend link — mirrors the drawer user panel's
|
|
232
|
+
"go to frontend" row (same icon/position: directly
|
|
233
|
+
above logout). Opt-in via the label prop so admin-only
|
|
234
|
+
panels keep the menu at avatar/language/logout. */}
|
|
235
|
+
{props.goToFrontendLabel ? (
|
|
236
|
+
<button
|
|
237
|
+
class="s-popup-menu-item"
|
|
238
|
+
onClick={() => {
|
|
239
|
+
userMenuOpen.value = false;
|
|
240
|
+
emit("goToFrontend");
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
<ExternalLink size={14} />
|
|
244
|
+
{props.goToFrontendLabel}
|
|
245
|
+
</button>
|
|
246
|
+
) : null}
|
|
247
|
+
<button
|
|
248
|
+
class="s-popup-menu-item"
|
|
249
|
+
onClick={() => {
|
|
250
|
+
userMenuOpen.value = false;
|
|
251
|
+
emit("logout");
|
|
252
|
+
}}
|
|
253
|
+
>
|
|
254
|
+
<LogOut size={14} />
|
|
255
|
+
{props.logoutLabel ?? t("hikari::adminHeader.logout", "Logout")}
|
|
256
|
+
</button>
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
</HPopover>
|
|
260
|
+
|
|
261
|
+
{/* The page title lives INSIDE each page in the HPageHeader
|
|
262
|
+
convention (big title left, tool buttons right). The bar
|
|
263
|
+
renders an optional context title only when one is given —
|
|
264
|
+
empty hides the node, keeping the bar to identity + theme
|
|
265
|
+
controls. */}
|
|
266
|
+
{props.title && (
|
|
267
|
+
<h2 class="text-sm font-semibold text-text truncate min-w-0">
|
|
268
|
+
{props.title}
|
|
269
|
+
</h2>
|
|
270
|
+
)}
|
|
271
|
+
<div class="ml-auto flex items-center gap-1.5 shrink-0">
|
|
272
|
+
{props.showEmergencyStop && (
|
|
273
|
+
<button
|
|
274
|
+
class={[
|
|
275
|
+
"px-3 py-1 rounded-md text-xs font-bold border transition-all",
|
|
276
|
+
props.emergencyStopLoading ? "opacity-50 cursor-wait" : "cursor-pointer",
|
|
277
|
+
props.emergencyStopActive
|
|
278
|
+
? "bg-red-600 text-white border-red-700 animate-pulse"
|
|
279
|
+
: "bg-red-600/10 text-red-500 border-red-500/40 hover:bg-red-600/25",
|
|
280
|
+
]}
|
|
281
|
+
disabled={props.emergencyStopLoading}
|
|
282
|
+
title={props.emergencyStopActive
|
|
283
|
+
? props.emergencyStopActiveTitle
|
|
284
|
+
: props.emergencyStopTitle}
|
|
285
|
+
onClick={() => emit("emergencyStop")}
|
|
286
|
+
>
|
|
287
|
+
{props.emergencyStopActive
|
|
288
|
+
? props.emergencyStopActiveLabel
|
|
289
|
+
: props.emergencyStopLabel}
|
|
290
|
+
</button>
|
|
291
|
+
)}
|
|
292
|
+
{slots["emergency-stop-extra"]?.()}
|
|
293
|
+
{(props.actions || []).map((vnode, i) => (
|
|
294
|
+
<span key={i} class="flex items-center gap-1">
|
|
295
|
+
{vnode}
|
|
296
|
+
</span>
|
|
297
|
+
))}
|
|
298
|
+
{slots["theme-toggle"]?.()}
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
{slots["avatar-modal"]?.({
|
|
302
|
+
open: avatarModalOpen.value,
|
|
303
|
+
onUpdateOpen: (v: boolean) => { avatarModalOpen.value = v; },
|
|
304
|
+
})}
|
|
305
|
+
</header>
|
|
306
|
+
);
|
|
307
|
+
},
|
|
308
|
+
});
|