@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,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central back-guard service — window-first back navigation.
|
|
3
|
+
*
|
|
4
|
+
* Priority rule (user directive): the back gesture — the mobile
|
|
5
|
+
* back button/gesture and the desktop browser back button alike —
|
|
6
|
+
* must be consumed by the topmost open WINDOW first (modal, drawer,
|
|
7
|
+
* menu sheet stack); only when every window has closed does the
|
|
8
|
+
* gesture reach page history and actually navigate back.
|
|
9
|
+
*
|
|
10
|
+
* Mechanism (a generalization of HkMenu's battle-tested Android
|
|
11
|
+
* modal-navigation mode): every window-like surface pushes marked
|
|
12
|
+
* history entries while open, so a back gesture lands on an entry
|
|
13
|
+
* below the window instead of leaving the page. ONE module-level
|
|
14
|
+
* popstate listener dispatches each gesture to the guard that should
|
|
15
|
+
* consume it, and programmatic closes (X / Escape / overlay tap)
|
|
16
|
+
* rewind the entries the window pushed so the page's own history
|
|
17
|
+
* stays clean.
|
|
18
|
+
*
|
|
19
|
+
* Traversal discipline (the subtle part):
|
|
20
|
+
* - pushes are SYNCHRONOUS (marker depth must stack immediately);
|
|
21
|
+
* - rewinds (pop / release) are DEFERRED to a macrotask and
|
|
22
|
+
* re-validated at execution time. A same-tick "close A, open B"
|
|
23
|
+
* (select a menu leaf → closeAll → modal opens) must not fire a
|
|
24
|
+
* go(-n) computed from a history top that B's push already
|
|
25
|
+
* replaced: at flush time a guard that no longer owns the current
|
|
26
|
+
* entry simply drops its claim (its markers stay buried and inert)
|
|
27
|
+
* instead of yanking B's live entry to the forward stack;
|
|
28
|
+
* - a popstate while a suppressed traversal is in flight is ours;
|
|
29
|
+
* any other is a user gesture. Listener teardown resets the
|
|
30
|
+
* expectation so a stale counter can never swallow a later,
|
|
31
|
+
* genuine gesture after the guard set changed.
|
|
32
|
+
*
|
|
33
|
+
* Dead markers (documented trade-off): when a window closes while a
|
|
34
|
+
* foreign entry sits above it (a router pushed during the window's
|
|
35
|
+
* lifetime), its markers are buried mid-stack and cannot be removed —
|
|
36
|
+
* the history API has no delete-entry primitive. They are inert and
|
|
37
|
+
* self-releasing: the next back that lands on one is treated as plain
|
|
38
|
+
* navigation (the router re-syncs from the URL) and the marker is
|
|
39
|
+
* de-marked in place. A back through such a marker can therefore cost
|
|
40
|
+
* one extra press before the URL actually changes — the unavoidable
|
|
41
|
+
* price of keeping the router's live state intact.
|
|
42
|
+
*
|
|
43
|
+
* State markers stamped into history.state:
|
|
44
|
+
* { __hkBack: <guardId>, __hkBackDepth: <0-based level> }
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import type { ShallowRef } from "vue";
|
|
48
|
+
import { shallowRef } from "vue";
|
|
49
|
+
|
|
50
|
+
/** history.state key holding the owning guard id. */
|
|
51
|
+
export const BACK_GUARD_MARKER = "__hkBack";
|
|
52
|
+
/** history.state key holding the 0-based level of the pushed entry. */
|
|
53
|
+
export const BACK_GUARD_DEPTH = "__hkBackDepth";
|
|
54
|
+
|
|
55
|
+
interface BackState {
|
|
56
|
+
[BACK_GUARD_MARKER]?: string;
|
|
57
|
+
[BACK_GUARD_DEPTH]?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface BackGuardOptions {
|
|
61
|
+
/**
|
|
62
|
+
* A user back gesture arrived while this guard is the topmost window.
|
|
63
|
+
* `depth` is the landed entry's level when it belongs to this guard
|
|
64
|
+
* (multi-level surfaces — menu sheet stacks — truncate to it);
|
|
65
|
+
* `null` means the landing was outside this guard's stack and the
|
|
66
|
+
* surface must collapse entirely. The service has already updated
|
|
67
|
+
* its entry bookkeeping before the call.
|
|
68
|
+
*/
|
|
69
|
+
onBack: (depth: number | null) => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface BackGuard {
|
|
73
|
+
readonly id: string;
|
|
74
|
+
/** History entries this guard currently owns above the page base. */
|
|
75
|
+
readonly entries: number;
|
|
76
|
+
/** Push one marked entry (opening a window / adding a level). */
|
|
77
|
+
push(): void;
|
|
78
|
+
/**
|
|
79
|
+
* In-app one-level back (a sheet's own back button): a suppressed
|
|
80
|
+
* history.back() deferred like every rewind. The caller truncates
|
|
81
|
+
* its own visual stack; the service only maintains history.
|
|
82
|
+
*/
|
|
83
|
+
pop(): void;
|
|
84
|
+
/**
|
|
85
|
+
* Programmatic full close (X / Escape / overlay / unmount): rewind
|
|
86
|
+
* every entry this guard pushed via one suppressed traversal. When a
|
|
87
|
+
* foreign entry sits on top (a router or another window pushed while
|
|
88
|
+
* open) the live state is left untouched — the buried markers simply
|
|
89
|
+
* become inert and are released if the user ever lands on them.
|
|
90
|
+
*/
|
|
91
|
+
release(): void;
|
|
92
|
+
/**
|
|
93
|
+
* Drop every entry claim AND cancel any pending rewind WITHOUT
|
|
94
|
+
* touching history: for a close that is itself the consequence of an
|
|
95
|
+
* in-page action started from the surface — a menu leaf whose select
|
|
96
|
+
* handler opens a modal or drives an async router navigation. The
|
|
97
|
+
* deferred rewind would otherwise win the race (its flush runs on the
|
|
98
|
+
* next macrotask, before an async navigation commits its pushState)
|
|
99
|
+
* and yank the page back onto the closed surface's marker entry,
|
|
100
|
+
* silently discarding the action's navigation. The pushed markers
|
|
101
|
+
* stay where they lie as inert dead markers — released in place by
|
|
102
|
+
* the popstate cleanup if a later traversal ever lands on them.
|
|
103
|
+
*/
|
|
104
|
+
abandon(): void;
|
|
105
|
+
/**
|
|
106
|
+
* Drop every entry claim WITHOUT touching history: for a surface
|
|
107
|
+
* that is already closed yet finds itself owning the current (spent)
|
|
108
|
+
* marker — de-marks it so a closed window never owns live history.
|
|
109
|
+
*/
|
|
110
|
+
forget(): void;
|
|
111
|
+
/** True when the current history entry carries this guard's marker. */
|
|
112
|
+
ownsCurrent(): boolean;
|
|
113
|
+
/** Release and unregister from dispatch. */
|
|
114
|
+
destroy(): void;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface GuardRecord {
|
|
118
|
+
id: string;
|
|
119
|
+
options: BackGuardOptions;
|
|
120
|
+
/** Live entry count — kept reactive for devtool-friendly consumers. */
|
|
121
|
+
count: ShallowRef<number>;
|
|
122
|
+
/** Entry count the surface WANTS (drives deferred rewinds). */
|
|
123
|
+
desired: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const hasWindow = typeof window !== "undefined";
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Dispatch stack, ordered by LAST ACTIVATION (last = topmost window).
|
|
130
|
+
* Guards register at component setup — mount order, not open order —
|
|
131
|
+
* so every 0→1 entry transition re-stamps the activation order; a
|
|
132
|
+
* window opened later must consume the back gesture first.
|
|
133
|
+
*/
|
|
134
|
+
const guards: GuardRecord[] = [];
|
|
135
|
+
/**
|
|
136
|
+
* Records with a pending rewind claim — INCLUDING destroyed ones: a
|
|
137
|
+
* guard unmounted between release() and the flush must still rewind
|
|
138
|
+
* its entries (its markers live in history regardless), or the dead
|
|
139
|
+
* marker strands as the current entry.
|
|
140
|
+
*/
|
|
141
|
+
const rewindQueue = new Set<GuardRecord>();
|
|
142
|
+
/** Popstate events produced by our own suppressed traversals. */
|
|
143
|
+
let suppressCount = 0;
|
|
144
|
+
/** A suppressed traversal has been issued and expects one popstate. */
|
|
145
|
+
let traversalInFlight = false;
|
|
146
|
+
/** Macrotask-scheduled rewind flush (single-flight). */
|
|
147
|
+
let flushScheduled = false;
|
|
148
|
+
let listening = false;
|
|
149
|
+
|
|
150
|
+
function readState(): BackState | null {
|
|
151
|
+
if (!hasWindow) return null;
|
|
152
|
+
const st = window.history.state;
|
|
153
|
+
return st != null && typeof st === "object" ? (st as BackState) : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function onPopState(e: PopStateEvent): void {
|
|
157
|
+
if (suppressCount > 0) {
|
|
158
|
+
// Our own rewind traversal landing — not a user gesture.
|
|
159
|
+
suppressCount--;
|
|
160
|
+
traversalInFlight = false;
|
|
161
|
+
maybeDropListener();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// Window priority: the topmost guard still owning entries consumes
|
|
165
|
+
// the gesture. Only when no window owns history does the gesture
|
|
166
|
+
// fall through to plain page navigation (handled by the router /
|
|
167
|
+
// the browser, not by us).
|
|
168
|
+
for (let i = guards.length - 1; i >= 0; i--) {
|
|
169
|
+
const g = guards[i];
|
|
170
|
+
if (g.count.value <= 0) continue;
|
|
171
|
+
const st =
|
|
172
|
+
e.state != null && typeof e.state === "object" ? (e.state as BackState) : null;
|
|
173
|
+
const own = st?.[BACK_GUARD_MARKER] === g.id;
|
|
174
|
+
const depth =
|
|
175
|
+
own && typeof st?.[BACK_GUARD_DEPTH] === "number" ? st[BACK_GUARD_DEPTH] : null;
|
|
176
|
+
g.count.value = depth === null ? 0 : depth + 1;
|
|
177
|
+
g.desired = g.count.value;
|
|
178
|
+
g.options.onBack(depth);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// No window owns history: a forward gesture onto one of our spent
|
|
182
|
+
// marker entries releases the marker so closed surfaces never keep
|
|
183
|
+
// owning live history.
|
|
184
|
+
const st =
|
|
185
|
+
e.state != null && typeof e.state === "object" ? (e.state as BackState) : null;
|
|
186
|
+
if (typeof st?.[BACK_GUARD_MARKER] === "string") {
|
|
187
|
+
window.history.replaceState(null, "");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function ensureListener(): void {
|
|
192
|
+
if (!hasWindow || listening) return;
|
|
193
|
+
listening = true;
|
|
194
|
+
window.addEventListener("popstate", onPopState);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Drop the listener when nothing needs it — but NEVER while a
|
|
199
|
+
* suppressed traversal is still in flight (its landing popstate must
|
|
200
|
+
* be consumed here, or the expectation leaks into the next listener
|
|
201
|
+
* generation and swallows a genuine gesture). Teardown also clears a
|
|
202
|
+
* spent expectation: a landing nobody observes is harmless, a stale
|
|
203
|
+
* counter is not.
|
|
204
|
+
*/
|
|
205
|
+
function maybeDropListener(): void {
|
|
206
|
+
if (!hasWindow || !listening) return;
|
|
207
|
+
if (guards.length > 0 || traversalInFlight) return;
|
|
208
|
+
listening = false;
|
|
209
|
+
window.removeEventListener("popstate", onPopState);
|
|
210
|
+
suppressCount = 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Deferred rewind flush, re-validated at execution time. Only the
|
|
215
|
+
* record that still OWNS the current entry may traverse — a foreign or
|
|
216
|
+
* newer window push above it makes its count unreliable, so it DROPS
|
|
217
|
+
* the claim instead (its markers stay buried, inert, self-releasing).
|
|
218
|
+
* Destroyed records keep their place in the queue: their markers live
|
|
219
|
+
* in history regardless of the component tree.
|
|
220
|
+
*/
|
|
221
|
+
function scheduleRewind(record: GuardRecord): void {
|
|
222
|
+
if (!hasWindow) return;
|
|
223
|
+
rewindQueue.add(record);
|
|
224
|
+
if (flushScheduled) return;
|
|
225
|
+
flushScheduled = true;
|
|
226
|
+
setTimeout(() => {
|
|
227
|
+
flushScheduled = false;
|
|
228
|
+
// A record can sit in the rewind queue AND still be registered (the
|
|
229
|
+
// normalizer's release-then-push), so collect into a Set to visit
|
|
230
|
+
// each exactly once.
|
|
231
|
+
const candidates = new Set<GuardRecord>(rewindQueue);
|
|
232
|
+
for (const g of guards) {
|
|
233
|
+
if (g.count.value > g.desired) candidates.add(g);
|
|
234
|
+
}
|
|
235
|
+
rewindQueue.clear();
|
|
236
|
+
for (const g of candidates) {
|
|
237
|
+
if (g.count.value <= g.desired) continue;
|
|
238
|
+
const st = readState();
|
|
239
|
+
if (st?.[BACK_GUARD_MARKER] === g.id) {
|
|
240
|
+
const n = g.count.value - g.desired;
|
|
241
|
+
g.count.value = g.desired;
|
|
242
|
+
if (n > 0) {
|
|
243
|
+
if (listening) {
|
|
244
|
+
suppressCount++;
|
|
245
|
+
traversalInFlight = true;
|
|
246
|
+
}
|
|
247
|
+
// With no listener attached (every guard unmounted before
|
|
248
|
+
// this flush ran), the landing popstate goes unobserved —
|
|
249
|
+
// issuing suppression bookkeeping anyway would leak the
|
|
250
|
+
// expectation into the next listener generation and eat a
|
|
251
|
+
// genuine gesture.
|
|
252
|
+
window.history.go(-n);
|
|
253
|
+
}
|
|
254
|
+
return; // one traversal per flush; nothing else can own the top
|
|
255
|
+
}
|
|
256
|
+
// Not ours to rewind (a router or newer window pushed above):
|
|
257
|
+
// abandon the entries where they lie — dead markers are
|
|
258
|
+
// released by the landing cleanup whenever the user reaches
|
|
259
|
+
// them, and forward-stack entries simply die with the session.
|
|
260
|
+
g.count.value = g.desired;
|
|
261
|
+
}
|
|
262
|
+
}, 0);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Create a back-guard for one window-like surface. The guard must be
|
|
267
|
+
* `destroy()`ed on unmount; `onBack` fires only for user gestures that
|
|
268
|
+
* this guard — as the topmost window — should consume.
|
|
269
|
+
*/
|
|
270
|
+
export function createBackGuard(options: BackGuardOptions): BackGuard {
|
|
271
|
+
const id = `hk-back-${Math.random().toString(36).slice(2, 10)}`;
|
|
272
|
+
const record: GuardRecord = { id, options, count: shallowRef(0), desired: 0 };
|
|
273
|
+
guards.push(record);
|
|
274
|
+
ensureListener();
|
|
275
|
+
|
|
276
|
+
function ownsCurrent(): boolean {
|
|
277
|
+
return readState()?.[BACK_GUARD_MARKER] === id;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
id,
|
|
282
|
+
get entries(): number {
|
|
283
|
+
return record.count.value;
|
|
284
|
+
},
|
|
285
|
+
push(): void {
|
|
286
|
+
if (!hasWindow) return;
|
|
287
|
+
window.history.pushState(
|
|
288
|
+
{ [BACK_GUARD_MARKER]: id, [BACK_GUARD_DEPTH]: record.count.value },
|
|
289
|
+
"",
|
|
290
|
+
);
|
|
291
|
+
record.count.value++;
|
|
292
|
+
// desired advances by ONE level, never snaps to count: a pending
|
|
293
|
+
// release() (desired = 0, rewind deferred) followed by a same-tick
|
|
294
|
+
// push — the HkMenu normalizer's "desktop → mobile" flip — must
|
|
295
|
+
// keep the rewind claim, or the flush sees count == desired and
|
|
296
|
+
// strands the stale entry (N1). Absolute writes (pop / release /
|
|
297
|
+
// dispatch landing) remain absolute: they assert a state, not a delta.
|
|
298
|
+
record.desired++;
|
|
299
|
+
if (record.count.value === 1) {
|
|
300
|
+
// Open order wins over mount order for dispatch priority.
|
|
301
|
+
const i = guards.indexOf(record);
|
|
302
|
+
if (i >= 0 && i !== guards.length - 1) {
|
|
303
|
+
guards.splice(i, 1);
|
|
304
|
+
guards.push(record);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
pop(): void {
|
|
309
|
+
if (!hasWindow || record.count.value <= 0) return;
|
|
310
|
+
// A foreign entry above ours means the history top is not ours to
|
|
311
|
+
// pop — the caller still truncates its visual stack locally.
|
|
312
|
+
if (!ownsCurrent()) return;
|
|
313
|
+
record.desired = record.count.value - 1;
|
|
314
|
+
scheduleRewind(record);
|
|
315
|
+
},
|
|
316
|
+
release(): void {
|
|
317
|
+
record.desired = 0;
|
|
318
|
+
if (!hasWindow) {
|
|
319
|
+
record.count.value = 0;
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (record.count.value > 0) {
|
|
323
|
+
scheduleRewind(record);
|
|
324
|
+
} else {
|
|
325
|
+
record.count.value = 0;
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
abandon(): void {
|
|
329
|
+
// Snap both counters to zero and retract any queued rewind claim —
|
|
330
|
+
// the flush's `count > desired` re-check then skips this record, so
|
|
331
|
+
// no go(-n) fires and the markers are left buried (inert).
|
|
332
|
+
record.desired = 0;
|
|
333
|
+
record.count.value = 0;
|
|
334
|
+
rewindQueue.delete(record);
|
|
335
|
+
},
|
|
336
|
+
forget(): void {
|
|
337
|
+
record.count.value = 0;
|
|
338
|
+
record.desired = 0;
|
|
339
|
+
if (hasWindow && ownsCurrent()) {
|
|
340
|
+
// Spent marker on the live entry — release ownership in place.
|
|
341
|
+
window.history.replaceState(null, "");
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
ownsCurrent,
|
|
345
|
+
destroy(): void {
|
|
346
|
+
this.release();
|
|
347
|
+
// A destroyed guard's rewind cannot wait for the deferred flush:
|
|
348
|
+
// later mounts (the next view, a router swap) stack entries above
|
|
349
|
+
// ours before the macrotask runs, and the deferred candidate can
|
|
350
|
+
// then only abandon — stranding our dead markers mid-stack, where
|
|
351
|
+
// every later rewind stops one short of the page base. The
|
|
352
|
+
// unmount sweep runs before sibling mounts within one patch, so
|
|
353
|
+
// rewinding synchronously here is safe.
|
|
354
|
+
if (hasWindow && record.count.value > 0 && ownsCurrent()) {
|
|
355
|
+
const n = record.count.value;
|
|
356
|
+
record.count.value = 0;
|
|
357
|
+
record.desired = 0;
|
|
358
|
+
rewindQueue.delete(record);
|
|
359
|
+
suppressCount++;
|
|
360
|
+
traversalInFlight = true;
|
|
361
|
+
window.history.go(-n);
|
|
362
|
+
}
|
|
363
|
+
const i = guards.indexOf(record);
|
|
364
|
+
if (i >= 0) guards.splice(i, 1);
|
|
365
|
+
maybeDropListener();
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Test hook: how many guards are registered (leak detection). */
|
|
371
|
+
export function __registeredBackGuards(): number {
|
|
372
|
+
return guards.length;
|
|
373
|
+
}
|
|
374
|
+
/** Test hook: does the module popstate listener currently exist? */
|
|
375
|
+
export function __backListenerActive(): boolean {
|
|
376
|
+
return listening;
|
|
377
|
+
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
export * from "./animationBus";
|
|
2
2
|
export * from "./cronBus";
|
|
3
|
+
export * from "./intervalBus";
|
|
4
|
+
export * from "./pageLifecycle";
|
|
5
|
+
export * from "./mobileViewport";
|
|
6
|
+
export {
|
|
7
|
+
createBackGuard,
|
|
8
|
+
BACK_GUARD_MARKER,
|
|
9
|
+
BACK_GUARD_DEPTH,
|
|
10
|
+
type BackGuard,
|
|
11
|
+
type BackGuardOptions,
|
|
12
|
+
} from "./backStack";
|
|
13
|
+
export { useMediaQuery, releaseMediaQuery } from "./useMediaQuery";
|
|
3
14
|
export { useOverlay, closeAll, isOverlayOpen, type OverlayHandle, type UseOverlayOptions } from "./useOverlay";
|
|
4
|
-
export { usePopupManager, type PopupHandle, type PopupKind } from "./usePopupManager";
|
|
15
|
+
export { usePopupManager, POPUP_Z_BANDS, POPUP_Z_STEP, type PopupHandle, type PopupKind } from "./usePopupManager";
|
|
5
16
|
export { useToast, TOAST_DURATION, type ToastItem, type ToastMessage, type ToastType } from "./useToast";
|
|
6
17
|
export { useConfirm } from "./useConfirm";
|
|
18
|
+
export { useBlockingToast, showBlockingToast, resolveBlockingToast, clearBlockingToasts, type BlockingToastItem, type BlockingToastOptions, type BlockingToastVariant } from "./useBlockingToast";
|
|
7
19
|
export { useBreakpoint } from "./useBreakpoint";
|
|
8
|
-
export { useClipboard } from "./useClipboard";
|
|
9
|
-
export { useAsyncData, type UseAsyncDataReturn } from "./useAsyncData";
|
|
20
|
+
export { useClipboard, useClipboardWithToast } from "./useClipboard";
|
|
10
21
|
export { provideScrollWindow, useScrollWindow, type ScrollWindowContext } from "../composables/useScrollWindow";
|
|
11
22
|
export { useReportedTransition, type ReportedTransition, type ReportedTransitionTrack } from "../composables/useReportedTransition";
|
|
12
23
|
export { showProgressDialog, useProgressDialog, type ProgressDialogHandle, type ProgressDialogState } from "../composables/useProgressDialog";
|
|
24
|
+
export { useSafeArea, type SafeAreaInsets } from "../composables/useSafeArea";
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
scheduleInterval,
|
|
5
|
+
scheduleIntervalAfter,
|
|
6
|
+
} from "./intervalBus";
|
|
7
|
+
import { onPageLifecycle, pageLifecycleState } from "./pageLifecycle";
|
|
8
|
+
|
|
9
|
+
describe("pageLifecycle", () => {
|
|
10
|
+
it("reports initial state from document", () => {
|
|
11
|
+
const state = pageLifecycleState();
|
|
12
|
+
expect(state.visible).toBe(!document.hidden);
|
|
13
|
+
expect(typeof state.online).toBe("boolean");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("notifies listeners on visibilitychange", () => {
|
|
17
|
+
const seen: boolean[] = [];
|
|
18
|
+
const unsub = onPageLifecycle((s) => seen.push(s.visible));
|
|
19
|
+
const initial = document.hidden;
|
|
20
|
+
Object.defineProperty(document, "hidden", { value: !initial, configurable: true });
|
|
21
|
+
document.dispatchEvent(new Event("visibilitychange"));
|
|
22
|
+
expect(seen[seen.length - 1]).toBe(initial); // flipped
|
|
23
|
+
Object.defineProperty(document, "hidden", { value: initial, configurable: true });
|
|
24
|
+
document.dispatchEvent(new Event("visibilitychange"));
|
|
25
|
+
unsub();
|
|
26
|
+
const count = seen.length;
|
|
27
|
+
document.dispatchEvent(new Event("visibilitychange"));
|
|
28
|
+
expect(seen.length).toBe(count); // unsubscribed
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("scheduleInterval", () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.useFakeTimers();
|
|
35
|
+
});
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.useRealTimers();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("fires on cadence and stops on disconnect", () => {
|
|
41
|
+
const cb = vi.fn();
|
|
42
|
+
const handle = scheduleInterval(cb, 100);
|
|
43
|
+
vi.advanceTimersByTime(350);
|
|
44
|
+
expect(cb.mock.calls.length).toBe(3);
|
|
45
|
+
handle.disconnect();
|
|
46
|
+
vi.advanceTimersByTime(1000);
|
|
47
|
+
expect(cb.mock.calls.length).toBe(3);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("parks while hidden and catches up once on visibility", () => {
|
|
51
|
+
const cb = vi.fn();
|
|
52
|
+
const handle = scheduleInterval(cb, 100);
|
|
53
|
+
vi.advanceTimersByTime(100);
|
|
54
|
+
expect(cb.mock.calls.length).toBe(1);
|
|
55
|
+
|
|
56
|
+
// Hide — pending timer parked.
|
|
57
|
+
Object.defineProperty(document, "hidden", { value: true, configurable: true });
|
|
58
|
+
document.dispatchEvent(new Event("visibilitychange"));
|
|
59
|
+
vi.advanceTimersByTime(1000);
|
|
60
|
+
expect(cb.mock.calls.length).toBe(1); // no background firing
|
|
61
|
+
|
|
62
|
+
// Visible again — elapsed schedule fires once (catch-up), no burst.
|
|
63
|
+
Object.defineProperty(document, "hidden", { value: false, configurable: true });
|
|
64
|
+
document.dispatchEvent(new Event("visibilitychange"));
|
|
65
|
+
expect(cb.mock.calls.length).toBe(2);
|
|
66
|
+
vi.advanceTimersByTime(100);
|
|
67
|
+
expect(cb.mock.calls.length).toBe(3);
|
|
68
|
+
handle.disconnect();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("stays independent of the reduced-motion switch (data polling is not animation)", () => {
|
|
72
|
+
// Interval bus is intentionally NOT wired to setReducedMotion — data
|
|
73
|
+
// polling must survive motion preferences. Guard against accidental
|
|
74
|
+
// coupling:
|
|
75
|
+
const cb = vi.fn();
|
|
76
|
+
const handle = scheduleInterval(cb, 50);
|
|
77
|
+
vi.advanceTimersByTime(150);
|
|
78
|
+
expect(cb.mock.calls.length).toBe(3);
|
|
79
|
+
handle.disconnect();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("scheduleIntervalAfter", () => {
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
vi.useFakeTimers();
|
|
86
|
+
});
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
vi.useRealTimers();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("fires once after the delay", () => {
|
|
92
|
+
const cb = vi.fn();
|
|
93
|
+
const handle = scheduleIntervalAfter(cb, 100);
|
|
94
|
+
vi.advanceTimersByTime(99);
|
|
95
|
+
expect(cb).not.toHaveBeenCalled();
|
|
96
|
+
vi.advanceTimersByTime(1);
|
|
97
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
98
|
+
vi.advanceTimersByTime(500);
|
|
99
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
100
|
+
handle.disconnect();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("holds the countdown while hidden", () => {
|
|
104
|
+
const cb = vi.fn();
|
|
105
|
+
const handle = scheduleIntervalAfter(cb, 200);
|
|
106
|
+
vi.advanceTimersByTime(100);
|
|
107
|
+
Object.defineProperty(document, "hidden", { value: true, configurable: true });
|
|
108
|
+
document.dispatchEvent(new Event("visibilitychange"));
|
|
109
|
+
vi.advanceTimersByTime(1000); // hidden: timer parked, no fire
|
|
110
|
+
expect(cb).not.toHaveBeenCalled();
|
|
111
|
+
Object.defineProperty(document, "hidden", { value: false, configurable: true });
|
|
112
|
+
document.dispatchEvent(new Event("visibilitychange"));
|
|
113
|
+
// nextDue was 100ms away at park time; on resume it fires after that.
|
|
114
|
+
vi.advanceTimersByTime(101);
|
|
115
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
116
|
+
handle.disconnect();
|
|
117
|
+
});
|
|
118
|
+
});
|