@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,200 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { watch } from "vue";
|
|
3
|
+
|
|
4
|
+
import { POPUP_Z_BANDS, POPUP_Z_STEP, usePopupManager } from "./usePopupManager";
|
|
5
|
+
|
|
6
|
+
const managers: ReturnType<typeof usePopupManager>[] = [];
|
|
7
|
+
|
|
8
|
+
function freshManager() {
|
|
9
|
+
const m = usePopupManager();
|
|
10
|
+
managers.push(m);
|
|
11
|
+
return m;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
// The registry is a module singleton shared across usePopupManager()
|
|
16
|
+
// instances — unregister everything so tests do not leak z state.
|
|
17
|
+
for (const m of managers.splice(0)) {
|
|
18
|
+
for (const entry of [...m.registry.value.values()]) {
|
|
19
|
+
m.unregister(entry.id);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
document.body.style.overflow = "";
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("usePopupManager z bands", () => {
|
|
26
|
+
it("bands are ordered overlay < dropdown < tooltip < toast", () => {
|
|
27
|
+
expect(POPUP_Z_BANDS.modal).toBe(POPUP_Z_BANDS.drawer);
|
|
28
|
+
expect(POPUP_Z_BANDS.modal).toBeLessThan(POPUP_Z_BANDS.dropdown);
|
|
29
|
+
expect(POPUP_Z_BANDS.dropdown).toBeLessThan(POPUP_Z_BANDS.tooltip);
|
|
30
|
+
expect(POPUP_Z_BANDS.tooltip).toBeLessThan(POPUP_Z_BANDS.toast);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("keeps a toast registered first above overlays opened later", () => {
|
|
34
|
+
// The mobile regression: HkToast mounts once at shell level, so the
|
|
35
|
+
// old registration-order ladder gave every later modal a higher z and
|
|
36
|
+
// buried the toast under the phone bottom sheet. Bands must hold the
|
|
37
|
+
// toast on top regardless of registration order.
|
|
38
|
+
const m = freshManager();
|
|
39
|
+
const toast = m.register("toast", false);
|
|
40
|
+
expect(toast.zIndex).toBe(POPUP_Z_BANDS.toast);
|
|
41
|
+
|
|
42
|
+
const modal = m.register("modal", true);
|
|
43
|
+
const drawer = m.register("drawer", true);
|
|
44
|
+
const dropdown = m.register("dropdown", false);
|
|
45
|
+
expect(modal.zIndex).toBeLessThan(toast.zIndex);
|
|
46
|
+
expect(drawer.zIndex).toBeLessThan(toast.zIndex);
|
|
47
|
+
expect(dropdown.zIndex).toBeLessThan(toast.zIndex);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("stacks in open order within a band and reclaims freed slots", () => {
|
|
51
|
+
const m = freshManager();
|
|
52
|
+
const a = m.register("modal", true);
|
|
53
|
+
const b = m.register("modal", true);
|
|
54
|
+
expect(a.zIndex).toBe(POPUP_Z_BANDS.modal);
|
|
55
|
+
expect(b.zIndex).toBe(a.zIndex + POPUP_Z_STEP);
|
|
56
|
+
|
|
57
|
+
m.unregister(b.id);
|
|
58
|
+
// C must reclaim B's slot — NOT drift upward past it.
|
|
59
|
+
const c = m.register("modal", true);
|
|
60
|
+
expect(c.zIndex).toBe(b.zIndex);
|
|
61
|
+
|
|
62
|
+
m.unregister(a.id);
|
|
63
|
+
m.unregister(c.id);
|
|
64
|
+
// Band empty again → back to the band base.
|
|
65
|
+
const d = m.register("modal", true);
|
|
66
|
+
expect(d.zIndex).toBe(POPUP_Z_BANDS.modal);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("shares the overlay band between modal and drawer in open order", () => {
|
|
70
|
+
const m = freshManager();
|
|
71
|
+
const modal = m.register("modal", true);
|
|
72
|
+
const drawer = m.register("drawer", true);
|
|
73
|
+
// Same band: the drawer opened over the modal paints above it, and a
|
|
74
|
+
// replacement drawer reclaims the freed slot instead of drifting.
|
|
75
|
+
expect(modal.zIndex).toBe(POPUP_Z_BANDS.modal);
|
|
76
|
+
expect(drawer.zIndex).toBe(modal.zIndex + POPUP_Z_STEP);
|
|
77
|
+
|
|
78
|
+
m.unregister(drawer.id);
|
|
79
|
+
const drawer2 = m.register("drawer", true);
|
|
80
|
+
expect(drawer2.zIndex).toBe(drawer.zIndex);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("keeps a dropdown opened from within an overlay above it", () => {
|
|
84
|
+
// Select panels portal to <body>; opened from inside a modal they
|
|
85
|
+
// must paint above the modal that contains them — the whole reason
|
|
86
|
+
// the dropdown band sits above the overlay band.
|
|
87
|
+
const m = freshManager();
|
|
88
|
+
const modal = m.register("modal", true);
|
|
89
|
+
const panel = m.register("dropdown", false);
|
|
90
|
+
expect(panel.zIndex).toBeGreaterThan(modal.zIndex);
|
|
91
|
+
|
|
92
|
+
m.unregister(panel.id);
|
|
93
|
+
m.unregister(modal.id);
|
|
94
|
+
|
|
95
|
+
// Same guarantee inverted: with the dropdown registered FIRST, the
|
|
96
|
+
// later modal still lands on its own (lower) overlay band — band
|
|
97
|
+
// membership, not registration order, decides.
|
|
98
|
+
const dropdown = m.register("dropdown", false);
|
|
99
|
+
const modal2 = m.register("modal", true);
|
|
100
|
+
expect(dropdown.zIndex).toBe(POPUP_Z_BANDS.dropdown);
|
|
101
|
+
expect(modal2.zIndex).toBe(POPUP_Z_BANDS.modal);
|
|
102
|
+
expect(modal2.zIndex).toBeLessThan(dropdown.zIndex);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("keeps tooltips above overlays and below toasts", () => {
|
|
106
|
+
const m = freshManager();
|
|
107
|
+
const modal = m.register("modal", true);
|
|
108
|
+
const tooltip = m.register("tooltip", false);
|
|
109
|
+
const toast = m.register("toast", false);
|
|
110
|
+
expect(tooltip.zIndex).toBe(POPUP_Z_BANDS.tooltip);
|
|
111
|
+
expect(tooltip.zIndex).toBeGreaterThan(modal.zIndex);
|
|
112
|
+
expect(toast.zIndex).toBeGreaterThan(tooltip.zIndex);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("keeps the scroll lock counter balanced across register/unregister", () => {
|
|
116
|
+
const m = freshManager();
|
|
117
|
+
const modal = m.register("modal", true);
|
|
118
|
+
expect(document.body.style.overflow).toBe("hidden");
|
|
119
|
+
|
|
120
|
+
const dropdown = m.register("dropdown", false);
|
|
121
|
+
m.unregister(dropdown.id);
|
|
122
|
+
// The modal still locks scroll.
|
|
123
|
+
expect(document.body.style.overflow).toBe("hidden");
|
|
124
|
+
|
|
125
|
+
m.unregister(modal.id);
|
|
126
|
+
expect(document.body.style.overflow).toBe("");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("usePopupManager blocking flag (breadcrumb levels)", () => {
|
|
131
|
+
it("defaults to non-blocking and stores explicit blocking", () => {
|
|
132
|
+
const m = freshManager();
|
|
133
|
+
const anchored = m.register("dropdown", false);
|
|
134
|
+
const sheet = m.register("dropdown", false, "Sheet", true);
|
|
135
|
+
const modal = m.register("modal", true, "Window");
|
|
136
|
+
expect(m.registry.value.get(anchored.id)!.blocking).toBe(false);
|
|
137
|
+
expect(m.registry.value.get(sheet.id)!.blocking).toBe(true);
|
|
138
|
+
// Window kinds list by kind alone; the flag stays false.
|
|
139
|
+
expect(m.registry.value.get(modal.id)!.blocking).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("setBlocking flips the flag reactively (anchor ↔ sheet morph)", () => {
|
|
143
|
+
const m = freshManager();
|
|
144
|
+
const popup = m.register("dropdown", false, "Menu");
|
|
145
|
+
// Vue reactivity: the reassigned Map must notify registry watchers.
|
|
146
|
+
const flips: boolean[] = [];
|
|
147
|
+
const stop = watch(
|
|
148
|
+
() => m.registry.value.get(popup.id)?.blocking,
|
|
149
|
+
(v) => { if (v !== undefined) flips.push(v); },
|
|
150
|
+
{ flush: "sync" },
|
|
151
|
+
);
|
|
152
|
+
m.setBlocking(popup.id, true);
|
|
153
|
+
expect(m.registry.value.get(popup.id)!.blocking).toBe(true);
|
|
154
|
+
m.setBlocking(popup.id, false);
|
|
155
|
+
expect(m.registry.value.get(popup.id)!.blocking).toBe(false);
|
|
156
|
+
expect(flips).toEqual([true, false]);
|
|
157
|
+
stop();
|
|
158
|
+
// Flipping to the same value is a no-op (no map churn).
|
|
159
|
+
const before = m.registry.value;
|
|
160
|
+
m.setBlocking(popup.id, false);
|
|
161
|
+
expect(m.registry.value).toBe(before);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("dev-warns when a breadcrumb-visible popup registers untitled", () => {
|
|
165
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
166
|
+
try {
|
|
167
|
+
const m = freshManager();
|
|
168
|
+
m.register("dropdown", false); // anchored popover: fine, hidden level
|
|
169
|
+
expect(warn).not.toHaveBeenCalled();
|
|
170
|
+
|
|
171
|
+
m.register("dropdown", false, undefined, true); // blocking sheet: unnamed
|
|
172
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
173
|
+
expect(warn.mock.calls[0][0]).toMatch(/without a title/);
|
|
174
|
+
|
|
175
|
+
m.register("modal", true); // window kind: unnamed
|
|
176
|
+
m.register("drawer", true); // window kind: unnamed
|
|
177
|
+
expect(warn).toHaveBeenCalledTimes(3);
|
|
178
|
+
|
|
179
|
+
warn.mockClear();
|
|
180
|
+
m.register("modal", true, "Named");
|
|
181
|
+
m.register("dropdown", false, "Named sheet", true);
|
|
182
|
+
expect(warn).not.toHaveBeenCalled();
|
|
183
|
+
} finally {
|
|
184
|
+
warn.mockRestore();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("dev-warns when setBlocking promotes an untitled popup", () => {
|
|
189
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
190
|
+
try {
|
|
191
|
+
const m = freshManager();
|
|
192
|
+
const popup = m.register("dropdown", false);
|
|
193
|
+
expect(warn).not.toHaveBeenCalled();
|
|
194
|
+
m.setBlocking(popup.id, true);
|
|
195
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
196
|
+
} finally {
|
|
197
|
+
warn.mockRestore();
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -2,16 +2,93 @@ import { readonly, ref } from "vue";
|
|
|
2
2
|
|
|
3
3
|
export type PopupKind = "dropdown" | "modal" | "drawer" | "tooltip" | "toast";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Kind-priority z bands — the single source of truth for popup stacking.
|
|
7
|
+
*
|
|
8
|
+
* Every registered popup lands in its kind's band, so layering is decided
|
|
9
|
+
* by WHAT a surface is, never by WHEN it happened to register. Bands are
|
|
10
|
+
* ordered low → high:
|
|
11
|
+
*
|
|
12
|
+
* modal (1000) centered dialogs + phone bottom sheets
|
|
13
|
+
* drawer (1000) edge drawers share the overlay band with modals —
|
|
14
|
+
* inside the band they stack in open order, so a
|
|
15
|
+
* drawer opened over a modal still paints above it
|
|
16
|
+
* dropdown (2000) anchor-attached popovers, select panels, menus —
|
|
17
|
+
* ABOVE the overlay band on purpose: a select panel
|
|
18
|
+
* opened from inside a modal portals to <body> and
|
|
19
|
+
* must paint above the modal that contains it (the
|
|
20
|
+
* common in-modal form flow). A page-level dropdown
|
|
21
|
+
* can only coexist with a modal programmatically (the
|
|
22
|
+
* modal overlay intercepts pointers), so the rare
|
|
23
|
+
* stale-dropdown-over-modal case is accepted, matching
|
|
24
|
+
* Ant Design
|
|
25
|
+
* tooltip (3000) transient annotations must stay visible above the
|
|
26
|
+
* surfaces they annotate
|
|
27
|
+
* toast (4000) ALWAYS the topmost surface — a toast must never be
|
|
28
|
+
* buried under a modal/sheet, no matter which opened
|
|
29
|
+
* first (the long-lived HkToast stack registers once
|
|
30
|
+
* at shell mount and must keep its band forever)
|
|
31
|
+
*
|
|
32
|
+
* Within a band, entries stack in open order (max live slot + step), and
|
|
33
|
+
* freed slots are reclaimed automatically because the next z derives from
|
|
34
|
+
* the CURRENT live entries of that band only — no monotonic counter, no
|
|
35
|
+
* cross-band coupling (a persistent toast/tooltip registration can no
|
|
36
|
+
* longer push later modals up the ladder; that ordering bug is what this
|
|
37
|
+
* band scheme replaces). Overlay roots are spaced one Z_STEP apart so each
|
|
38
|
+
* overlay's +1 content/panel layer always has a free slot above its own
|
|
39
|
+
* root and below the next overlay.
|
|
40
|
+
*
|
|
41
|
+
* Keep the SCSS fallbacks (`--hi-z-*` / `--hk-z-*` in theme.scss and the
|
|
42
|
+
* component sheets) in sync with these numbers.
|
|
43
|
+
*/
|
|
44
|
+
export const POPUP_Z_BANDS: Record<PopupKind, number> = {
|
|
45
|
+
modal: 1000,
|
|
46
|
+
drawer: 1000,
|
|
47
|
+
dropdown: 2000,
|
|
48
|
+
tooltip: 3000,
|
|
49
|
+
toast: 4000,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** In-band stacking step. Even numbers leave the odd slot free for the
|
|
53
|
+
* +1 content/panel layer each overlay puts above its own root. */
|
|
54
|
+
export const POPUP_Z_STEP = 2;
|
|
55
|
+
|
|
56
|
+
export interface PopupEntry {
|
|
6
57
|
id: string;
|
|
7
58
|
kind: PopupKind;
|
|
8
59
|
locksScroll: boolean;
|
|
9
60
|
zIndex: number;
|
|
10
61
|
title?: string;
|
|
62
|
+
/**
|
|
63
|
+
* True while the popup is a blocking window the user "navigates" —
|
|
64
|
+
* a mobile bottom sheet that rose from a dropdown-kind surface. The
|
|
65
|
+
* modal-stack breadcrumb lists window kinds (modal/drawer) always and
|
|
66
|
+
* dropdown kinds only while they block like this; an anchored desktop
|
|
67
|
+
* popover stays a hidden level. Named surfaces only: a blocking popup
|
|
68
|
+
* without a title is a naming bug (dev warn at registration).
|
|
69
|
+
*/
|
|
70
|
+
blocking: boolean;
|
|
11
71
|
}
|
|
12
72
|
|
|
13
|
-
|
|
14
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Dev-time naming enforcement for the window-stack breadcrumb. Any popup
|
|
75
|
+
* that participates in the breadcrumb (window kinds, or a dropdown that
|
|
76
|
+
* currently blocks as a sheet) must carry a real, i18n-resolved title —
|
|
77
|
+
* a bare "Layer N" style fallback is exactly what the strip must never
|
|
78
|
+
* show. Production builds stay silent; the breadcrumb falls back to a
|
|
79
|
+
* generic localized label there.
|
|
80
|
+
*/
|
|
81
|
+
function warnUntitled(kind: PopupKind, blocking: boolean, title?: string): void {
|
|
82
|
+
const stacks = kind === "modal" || kind === "drawer" || blocking;
|
|
83
|
+
if (!stacks || title) return;
|
|
84
|
+
if (import.meta.env?.DEV) {
|
|
85
|
+
console.warn(
|
|
86
|
+
`[hikari] popup registered without a title (kind: ${kind}). ` +
|
|
87
|
+
"Window surfaces must pass an i18n-resolved title so the " +
|
|
88
|
+
"modal-stack breadcrumb can label their layer.",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
15
92
|
|
|
16
93
|
function uid(): string {
|
|
17
94
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
@@ -21,7 +98,6 @@ function uid(): string {
|
|
|
21
98
|
}
|
|
22
99
|
|
|
23
100
|
const registry = ref<Map<string, PopupEntry>>(new Map());
|
|
24
|
-
let nextZ = Z_BASE;
|
|
25
101
|
let scrollLockCount = 0;
|
|
26
102
|
|
|
27
103
|
function updateBodyScroll() {
|
|
@@ -42,12 +118,21 @@ export function usePopupManager() {
|
|
|
42
118
|
kind: PopupKind,
|
|
43
119
|
locksScroll = false,
|
|
44
120
|
title?: string,
|
|
121
|
+
blocking = false,
|
|
45
122
|
): PopupHandle {
|
|
46
123
|
const id = uid();
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
124
|
+
const band = POPUP_Z_BANDS[kind];
|
|
125
|
+
// Next slot = one step above the highest LIVE entry of the same band.
|
|
126
|
+
let maxSlot = -1;
|
|
127
|
+
for (const entry of registry.value.values()) {
|
|
128
|
+
if (POPUP_Z_BANDS[entry.kind] !== band) continue;
|
|
129
|
+
const slot = (entry.zIndex - band) / POPUP_Z_STEP;
|
|
130
|
+
if (slot > maxSlot) maxSlot = slot;
|
|
131
|
+
}
|
|
132
|
+
const zIndex = band + (maxSlot + 1) * POPUP_Z_STEP;
|
|
133
|
+
const entry: PopupEntry = { id, kind, locksScroll, zIndex, title, blocking };
|
|
50
134
|
registry.value.set(id, entry);
|
|
135
|
+
warnUntitled(kind, blocking, title);
|
|
51
136
|
if (locksScroll) {
|
|
52
137
|
scrollLockCount++;
|
|
53
138
|
updateBodyScroll();
|
|
@@ -62,6 +147,19 @@ export function usePopupManager() {
|
|
|
62
147
|
registry.value = new Map(registry.value);
|
|
63
148
|
}
|
|
64
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Flip the blocking flag while the popup stays open — a dropdown that
|
|
152
|
+
* docks as a bottom sheet when the viewport crosses the mobile
|
|
153
|
+
* breakpoint becomes a breadcrumb level mid-flight (and back).
|
|
154
|
+
*/
|
|
155
|
+
function setBlocking(id: string, blocking: boolean) {
|
|
156
|
+
const entry = registry.value.get(id);
|
|
157
|
+
if (!entry || entry.blocking === blocking) return;
|
|
158
|
+
entry.blocking = blocking;
|
|
159
|
+
registry.value = new Map(registry.value);
|
|
160
|
+
if (blocking) warnUntitled(entry.kind, true, entry.title);
|
|
161
|
+
}
|
|
162
|
+
|
|
65
163
|
function unregister(id: string) {
|
|
66
164
|
const entry = registry.value.get(id);
|
|
67
165
|
if (!entry) return;
|
|
@@ -70,9 +168,6 @@ export function usePopupManager() {
|
|
|
70
168
|
scrollLockCount = Math.max(0, scrollLockCount - 1);
|
|
71
169
|
updateBodyScroll();
|
|
72
170
|
}
|
|
73
|
-
if (registry.value.size === 0) {
|
|
74
|
-
nextZ = Z_BASE;
|
|
75
|
-
}
|
|
76
171
|
}
|
|
77
172
|
|
|
78
173
|
function isOpen(id: string): boolean {
|
|
@@ -83,6 +178,7 @@ export function usePopupManager() {
|
|
|
83
178
|
registry: readonly(registry),
|
|
84
179
|
register,
|
|
85
180
|
setTitle,
|
|
181
|
+
setBlocking,
|
|
86
182
|
unregister,
|
|
87
183
|
isOpen,
|
|
88
184
|
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
MAX_TOASTS_PER_SLOT,
|
|
5
|
+
useToast,
|
|
6
|
+
} from "./useToast";
|
|
7
|
+
|
|
8
|
+
function clearAll() {
|
|
9
|
+
const { toasts, remove } = useToast();
|
|
10
|
+
for (const t of [...toasts]) remove(t.id);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("useToast stacking and dedupe", () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.useFakeTimers();
|
|
16
|
+
clearAll();
|
|
17
|
+
});
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
clearAll();
|
|
20
|
+
vi.useRealTimers();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("dedupes identical consecutive messages in the same slot", () => {
|
|
24
|
+
const toast = useToast();
|
|
25
|
+
const firstId = toast.error("backend unreachable");
|
|
26
|
+
const secondId = toast.error("backend unreachable");
|
|
27
|
+
|
|
28
|
+
expect(toast.toasts).toHaveLength(1);
|
|
29
|
+
expect(toast.toasts[0]!.messages).toHaveLength(1);
|
|
30
|
+
// Replace semantics: the second push returns the existing entry id.
|
|
31
|
+
expect(secondId).toBe(firstId);
|
|
32
|
+
expect(toast.toasts[0]!.messages[0]!.text).toBe("backend unreachable");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("keeps distinct consecutive messages", () => {
|
|
36
|
+
const toast = useToast();
|
|
37
|
+
toast.error("first");
|
|
38
|
+
toast.error("second");
|
|
39
|
+
|
|
40
|
+
expect(toast.toasts[0]!.messages.map((m) => m.text)).toEqual([
|
|
41
|
+
"first",
|
|
42
|
+
"second",
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("dedupes only consecutive duplicates", () => {
|
|
47
|
+
const toast = useToast();
|
|
48
|
+
toast.error("a");
|
|
49
|
+
toast.error("b");
|
|
50
|
+
toast.error("b");
|
|
51
|
+
|
|
52
|
+
expect(toast.toasts[0]!.messages.map((m) => m.text)).toEqual(["a", "b"]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("caps messages per slot at MAX_TOASTS_PER_SLOT, dropping the oldest", () => {
|
|
56
|
+
const toast = useToast();
|
|
57
|
+
for (let i = 0; i < MAX_TOASTS_PER_SLOT + 3; i++) {
|
|
58
|
+
toast.error(`msg-${i}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
expect(toast.toasts[0]!.messages).toHaveLength(MAX_TOASTS_PER_SLOT);
|
|
62
|
+
const texts = toast.toasts[0]!.messages.map((m) => m.text);
|
|
63
|
+
expect(texts[0]).toBe("msg-3"); // 0..2 dropped (oldest)
|
|
64
|
+
expect(texts[texts.length - 1]).toBe(`msg-${MAX_TOASTS_PER_SLOT + 2}`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("caps each slot independently", () => {
|
|
68
|
+
const toast = useToast();
|
|
69
|
+
for (let i = 0; i < MAX_TOASTS_PER_SLOT + 2; i++) toast.error(`e-${i}`);
|
|
70
|
+
for (let i = 0; i < MAX_TOASTS_PER_SLOT + 2; i++) toast.success(`s-${i}`);
|
|
71
|
+
|
|
72
|
+
const errorSlot = toast.toasts.find((t) => t.type === "error")!;
|
|
73
|
+
const successSlot = toast.toasts.find((t) => t.type === "success")!;
|
|
74
|
+
expect(errorSlot.messages).toHaveLength(MAX_TOASTS_PER_SLOT);
|
|
75
|
+
expect(successSlot.messages).toHaveLength(MAX_TOASTS_PER_SLOT);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("dedupe still restarts the auto-dismiss clock (loading never dismisses)", () => {
|
|
79
|
+
const toast = useToast();
|
|
80
|
+
toast.info("pulse");
|
|
81
|
+
const first = toast.toasts[0]!;
|
|
82
|
+
// info duration is 3000ms — after 1000ms a duplicate push keeps a
|
|
83
|
+
// single entry; the slot is still present at 2500ms (timer restarted).
|
|
84
|
+
vi.advanceTimersByTime(1000);
|
|
85
|
+
toast.info("pulse");
|
|
86
|
+
expect(toast.toasts[0]!.messages).toHaveLength(1);
|
|
87
|
+
vi.advanceTimersByTime(2499);
|
|
88
|
+
expect(toast.toasts.some((t) => t.type === "info")).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|
package/src/runtime/useToast.ts
CHANGED
|
@@ -33,6 +33,11 @@ export interface ToastItem {
|
|
|
33
33
|
copyable?: boolean;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/** Hard cap on stacked messages per toast slot. Beyond it the oldest
|
|
37
|
+
* messages are dropped so a noisy caller cannot bury the surface in
|
|
38
|
+
* unbounded history. */
|
|
39
|
+
export const MAX_TOASTS_PER_SLOT = 5;
|
|
40
|
+
|
|
36
41
|
const state = reactive<{ toasts: ToastItem[] }>({ toasts: [] });
|
|
37
42
|
|
|
38
43
|
let nextSlotId = 0;
|
|
@@ -86,8 +91,21 @@ function push(
|
|
|
86
91
|
} else if (options.copyable === true && !slot.copyable) {
|
|
87
92
|
slot.copyable = true;
|
|
88
93
|
}
|
|
89
|
-
|
|
90
|
-
slot.messages.
|
|
94
|
+
let msgId: number;
|
|
95
|
+
const last = slot.messages[slot.messages.length - 1];
|
|
96
|
+
if (last && last.text === text) {
|
|
97
|
+
// Dedupe: an identical consecutive message (same slot + same text in
|
|
98
|
+
// the current stack) replaces the previous entry instead of appending —
|
|
99
|
+
// the surface keeps a single entry and the auto-dismiss clock restarts.
|
|
100
|
+
msgId = last.id;
|
|
101
|
+
} else {
|
|
102
|
+
msgId = ++nextMsgId;
|
|
103
|
+
slot.messages.push({ id: msgId, text });
|
|
104
|
+
}
|
|
105
|
+
// Cap: drop the oldest messages beyond the per-slot cap.
|
|
106
|
+
while (slot.messages.length > MAX_TOASTS_PER_SLOT) {
|
|
107
|
+
slot.messages.shift();
|
|
108
|
+
}
|
|
91
109
|
scheduleAutoDismiss(slot);
|
|
92
110
|
return msgId;
|
|
93
111
|
}
|