@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
|
@@ -66,36 +66,111 @@ export function getTimePeriod(
|
|
|
66
66
|
|
|
67
67
|
export const DEFAULT_GEO_LOCATION = { lat: 31.23, lng: 121.47 };
|
|
68
68
|
|
|
69
|
+
/** Resolved geographic coordinates in degrees. */
|
|
70
|
+
export interface GeoLocation {
|
|
71
|
+
lat: number;
|
|
72
|
+
lng: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Host-supplied geolocation source. Native shells (Tauri 2's geolocation
|
|
77
|
+
* plugin, Electron IPC, a saved user preference, …) register one with
|
|
78
|
+
* `setGeolocationProvider` so the theme clock uses the device's real fix
|
|
79
|
+
* instead of hikari's web cascade. Return the coordinates, or return
|
|
80
|
+
* null / throw to fall through to the standard cascade (already-granted
|
|
81
|
+
* browser geolocation → IP lookup → timezone estimate).
|
|
82
|
+
*/
|
|
83
|
+
export type GeoLocationProvider = () => Promise<GeoLocation | null>;
|
|
84
|
+
|
|
85
|
+
let geoProvider: GeoLocationProvider | null = null;
|
|
86
|
+
|
|
87
|
+
/** One successful resolution per page is shared by every consumer (theme
|
|
88
|
+
* clock, theme toggle readout, wallpaper clock). Reset by
|
|
89
|
+
* `setGeolocationProvider` so a late registration is honored on the next
|
|
90
|
+
* call; the timezone estimate is never memoized, so a transient offline
|
|
91
|
+
* boot can heal on the next `getGeolocation` / `refreshThemeClock`. */
|
|
92
|
+
let geoMemo: Promise<GeoLocation> | null = null;
|
|
93
|
+
|
|
94
|
+
export function setGeolocationProvider(provider: GeoLocationProvider | null): void {
|
|
95
|
+
geoProvider = provider;
|
|
96
|
+
geoMemo = null;
|
|
97
|
+
}
|
|
98
|
+
|
|
69
99
|
const GEO_API_URL = "https://ipapi.co/json/";
|
|
70
100
|
const GEO_API_TIMEOUT_MS = 4000;
|
|
71
101
|
|
|
72
|
-
|
|
102
|
+
/** Synchronous coarse estimate: default latitude + the timezone-offset
|
|
103
|
+
* longitude. No fetch, no permission — the first-paint guess. */
|
|
104
|
+
export function timezoneFallback(): GeoLocation {
|
|
73
105
|
const offsetMin = -new Date().getTimezoneOffset();
|
|
74
106
|
const lng = offsetMin / 4;
|
|
75
107
|
return { lat: DEFAULT_GEO_LOCATION.lat, lng };
|
|
76
108
|
}
|
|
77
109
|
|
|
78
|
-
|
|
110
|
+
async function browserGeolocation(): Promise<GeoLocation> {
|
|
111
|
+
// Standard web geolocation — but ONLY when the site already holds the
|
|
112
|
+
// permission: a theme must never be the thing that pops the browser's
|
|
113
|
+
// location prompt. Prompt/denied/unavailable all fall through to the IP
|
|
114
|
+
// lookup.
|
|
115
|
+
if (typeof navigator === "undefined" || !navigator.geolocation) {
|
|
116
|
+
throw new Error("navigator.geolocation unavailable");
|
|
117
|
+
}
|
|
118
|
+
const perm = await navigator.permissions?.query({ name: "geolocation" as PermissionName });
|
|
119
|
+
if (!perm || perm.state !== "granted") {
|
|
120
|
+
throw new Error("geolocation permission not granted");
|
|
121
|
+
}
|
|
122
|
+
return new Promise<GeoLocation>((resolve, reject) => {
|
|
123
|
+
navigator.geolocation.getCurrentPosition(
|
|
124
|
+
(pos) => resolve({ lat: pos.coords.latitude, lng: pos.coords.longitude }),
|
|
125
|
+
(err) => reject(err),
|
|
126
|
+
{ timeout: GEO_API_TIMEOUT_MS, maximumAge: 6 * 60 * 60 * 1000 },
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function ipGeolocation(): Promise<GeoLocation> {
|
|
79
132
|
const controller = new AbortController();
|
|
80
133
|
const timer = scheduleCronAfter(() => controller.abort(), GEO_API_TIMEOUT_MS);
|
|
81
|
-
|
|
134
|
+
const d = await fetch(GEO_API_URL, { signal: controller.signal, cache: "no-store" })
|
|
82
135
|
.then((r) => (r.ok ? r.json() : null))
|
|
83
|
-
.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
136
|
+
.finally(() => timer.disconnect()) as { latitude?: number; longitude?: number } | null;
|
|
137
|
+
if (
|
|
138
|
+
d &&
|
|
139
|
+
typeof d.latitude === "number" &&
|
|
140
|
+
typeof d.longitude === "number" &&
|
|
141
|
+
(Math.abs(d.latitude) > 0.01 || Math.abs(d.longitude) > 0.01)
|
|
142
|
+
) {
|
|
143
|
+
return { lat: d.latitude, lng: d.longitude };
|
|
144
|
+
}
|
|
145
|
+
throw new Error("ip geolocation unavailable");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function getGeolocation(): Promise<GeoLocation> {
|
|
149
|
+
geoMemo ??= (async () => {
|
|
150
|
+
if (geoProvider) {
|
|
151
|
+
try {
|
|
152
|
+
const provided = await geoProvider();
|
|
153
|
+
if (provided && Number.isFinite(provided.lat) && Number.isFinite(provided.lng)) {
|
|
154
|
+
return { lat: provided.lat, lng: provided.lng };
|
|
93
155
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
156
|
+
} catch {
|
|
157
|
+
// Provider failed — fall through to the standard cascade.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
return await browserGeolocation();
|
|
162
|
+
} catch {
|
|
163
|
+
// No API / not granted / denied — never prompt for a theme.
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
return await ipGeolocation();
|
|
167
|
+
} catch {
|
|
168
|
+
// Offline or the lookup service is down.
|
|
169
|
+
}
|
|
170
|
+
// Don't pin the estimate for the whole session — drop the memo so the
|
|
171
|
+
// next call retries the cascade.
|
|
172
|
+
geoMemo = null;
|
|
173
|
+
return timezoneFallback();
|
|
174
|
+
})();
|
|
175
|
+
return geoMemo;
|
|
101
176
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// The theme engine keeps singleton state (mode/theme refs + clock flag), so
|
|
4
|
+
// every test gets a fresh module instance via vi.resetModules + dynamic import.
|
|
5
|
+
type ThemeModule = typeof import("./useTheme");
|
|
6
|
+
type SolarModule = typeof import("./useSolarTime");
|
|
7
|
+
|
|
8
|
+
describe("useTheme theme clock", () => {
|
|
9
|
+
let theme: ThemeModule;
|
|
10
|
+
let solar: SolarModule;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
vi.resetModules();
|
|
14
|
+
vi.unstubAllGlobals();
|
|
15
|
+
localStorage.clear();
|
|
16
|
+
document.documentElement.style.cssText = "";
|
|
17
|
+
document.documentElement.removeAttribute("data-theme");
|
|
18
|
+
document.documentElement.removeAttribute("data-mode");
|
|
19
|
+
// Nothing here may touch the real network.
|
|
20
|
+
vi.stubGlobal("fetch", vi.fn(async () => {
|
|
21
|
+
throw new Error("offline");
|
|
22
|
+
}));
|
|
23
|
+
solar = await import("./useSolarTime");
|
|
24
|
+
theme = await import("./useTheme");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
theme.stopThemeClock();
|
|
29
|
+
vi.unstubAllGlobals();
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("initTheme starts the clock; the geo fix feeds geo/period on useTheme()", async () => {
|
|
34
|
+
solar.setGeolocationProvider(async () => ({ lat: 40.71, lng: -74.01 }));
|
|
35
|
+
theme.initTheme();
|
|
36
|
+
|
|
37
|
+
const th = theme.useTheme();
|
|
38
|
+
// Synchronous first paint: the timezone estimate, not the default
|
|
39
|
+
// Shanghai constant.
|
|
40
|
+
expect(th.geo.value.lng).toBeCloseTo(-new Date().getTimezoneOffset() / 4);
|
|
41
|
+
|
|
42
|
+
await vi.waitFor(() => {
|
|
43
|
+
expect(th.geo.value).toEqual({ lat: 40.71, lng: -74.01 });
|
|
44
|
+
});
|
|
45
|
+
expect(["day", "dusk", "night"]).toContain(th.period.value);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("refreshThemeClock honors a provider registered after initTheme", async () => {
|
|
49
|
+
theme.initTheme();
|
|
50
|
+
// No provider yet → the cascade bottoms out at the timezone estimate,
|
|
51
|
+
// which must NOT be pinned.
|
|
52
|
+
const first = await theme.refreshThemeClock();
|
|
53
|
+
expect(first.lng).toBeCloseTo(-new Date().getTimezoneOffset() / 4);
|
|
54
|
+
|
|
55
|
+
solar.setGeolocationProvider(async () => ({ lat: -33.87, lng: 151.21 }));
|
|
56
|
+
const second = await theme.refreshThemeClock();
|
|
57
|
+
expect(second).toEqual({ lat: -33.87, lng: 151.21 });
|
|
58
|
+
expect(theme.useTheme().geo.value).toEqual({ lat: -33.87, lng: 151.21 });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("stopThemeClock allows initTheme to restart the clock", async () => {
|
|
62
|
+
solar.setGeolocationProvider(async () => ({ lat: 1, lng: 2 }));
|
|
63
|
+
theme.initTheme();
|
|
64
|
+
await vi.waitFor(() => {
|
|
65
|
+
expect(theme.useTheme().geo.value).toEqual({ lat: 1, lng: 2 });
|
|
66
|
+
});
|
|
67
|
+
theme.stopThemeClock();
|
|
68
|
+
// Restarting the clock keeps the engine consistent (idempotent lifecycle).
|
|
69
|
+
theme.initTheme();
|
|
70
|
+
expect(theme.useTheme().geo.value).toEqual({ lat: 1, lng: 2 });
|
|
71
|
+
});
|
|
72
|
+
});
|
package/src/theme/useTheme.ts
CHANGED
|
@@ -1,26 +1,141 @@
|
|
|
1
1
|
import { computed, ref, watch } from "vue";
|
|
2
2
|
|
|
3
3
|
import { scheduleCronAfter, type CronHandle } from "../runtime/cronBus";
|
|
4
|
+
import { scheduleInterval, type IntervalHandle } from "../runtime/intervalBus";
|
|
4
5
|
import { addCustomTheme as addCustomThemeToStorage, loadCustomThemes, removeCustomTheme as removeCustomThemeFromStorage, themePresets, tokensToCSSVars, type CustomThemePreset, type ThemeId, type ThemeMode, type ThemePreset } from "./presets";
|
|
6
|
+
import { groupTokensToCSSVars, resolveGroupTokens, setTokenGroupsReapply } from "./tokenGroups";
|
|
5
7
|
import { invalidateLuminanceCache } from "./useBackgroundLuminance";
|
|
6
|
-
import { getTimePeriod,
|
|
8
|
+
import { getGeolocation, getTimePeriod, timezoneFallback, type GeoLocation, type TimePeriod } from "./useSolarTime";
|
|
7
9
|
|
|
8
10
|
const DEFAULT_THEME = "synthwave84";
|
|
9
11
|
const STORAGE_THEME_KEY = "hikari-theme";
|
|
10
|
-
const
|
|
12
|
+
export const THEME_MODE_STORAGE_KEY = "hikari-theme-mode";
|
|
13
|
+
const STORAGE_MODE_KEY = THEME_MODE_STORAGE_KEY;
|
|
14
|
+
|
|
15
|
+
// Page-declared brand themes (optional): a site may declare
|
|
16
|
+
// window.__celestiaThemes (extra theme ids) and window.__celestiaDefaultTheme
|
|
17
|
+
// in its index.html before this module loads. Honor them when resolving the
|
|
18
|
+
// stored theme id so a brand id like "endfield" is not dropped as unknown.
|
|
19
|
+
// Both helpers are no-ops when the globals are absent, keeping every other
|
|
20
|
+
// consumer's behavior unchanged.
|
|
21
|
+
interface PageDeclaredThemeGlobals {
|
|
22
|
+
__celestiaThemes?: Record<string, unknown>;
|
|
23
|
+
__celestiaDefaultTheme?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function pageDeclaredThemes(): Record<string, unknown> {
|
|
27
|
+
if (typeof window === "undefined") return {};
|
|
28
|
+
return (window as Window & PageDeclaredThemeGlobals).__celestiaThemes ?? {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function pageDeclaredDefaultTheme(): string | null {
|
|
32
|
+
if (typeof window === "undefined") return null;
|
|
33
|
+
const declared = (window as Window & PageDeclaredThemeGlobals).__celestiaDefaultTheme;
|
|
34
|
+
return typeof declared === "string" ? declared : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveDefaultTheme(): ThemeId {
|
|
38
|
+
const declared = pageDeclaredDefaultTheme();
|
|
39
|
+
if (
|
|
40
|
+
declared &&
|
|
41
|
+
(declared in themePresets || declared in pageDeclaredThemes())
|
|
42
|
+
) {
|
|
43
|
+
return declared as ThemeId;
|
|
44
|
+
}
|
|
45
|
+
return DEFAULT_THEME;
|
|
46
|
+
}
|
|
11
47
|
|
|
12
|
-
const currentTheme = ref<ThemeId>(
|
|
13
|
-
localStorage.getItem(STORAGE_THEME_KEY) || DEFAULT_THEME,
|
|
14
|
-
);
|
|
15
48
|
const currentMode = ref<ThemeMode>(
|
|
16
49
|
(localStorage.getItem(STORAGE_MODE_KEY) as ThemeMode) || "system",
|
|
17
50
|
);
|
|
18
51
|
const customThemes = ref<CustomThemePreset[]>(loadCustomThemes());
|
|
19
52
|
|
|
53
|
+
function storedThemeId(): ThemeId {
|
|
54
|
+
const stored = localStorage.getItem(STORAGE_THEME_KEY);
|
|
55
|
+
if (stored) {
|
|
56
|
+
const known = new Set<string>([
|
|
57
|
+
...Object.keys(themePresets),
|
|
58
|
+
...Object.keys(pageDeclaredThemes()),
|
|
59
|
+
...customThemes.value.map((c: CustomThemePreset) => c.id),
|
|
60
|
+
]);
|
|
61
|
+
if (known.has(stored)) return stored as ThemeId;
|
|
62
|
+
// Stale/invalid theme id (e.g. written by an older build): drop it so
|
|
63
|
+
// applyTheme() never silently bails and leaves the page unthemed.
|
|
64
|
+
localStorage.removeItem(STORAGE_THEME_KEY);
|
|
65
|
+
}
|
|
66
|
+
return resolveDefaultTheme();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const currentTheme = ref<ThemeId>(storedThemeId());
|
|
70
|
+
|
|
20
71
|
const currentPeriod = ref<TimePeriod>(
|
|
21
|
-
|
|
72
|
+
// First paint uses the synchronous timezone-offset estimate; the theme
|
|
73
|
+
// clock below refines it with a real geolocation fix once that lands.
|
|
74
|
+
getTimePeriod(timezoneFallback().lat, timezoneFallback().lng),
|
|
22
75
|
);
|
|
23
76
|
|
|
77
|
+
/**
|
|
78
|
+
* The theme clock. `system` mode resolves day/night from the sun, so it
|
|
79
|
+
* needs (a) a real geolocation fix and (b) re-evaluation as time passes —
|
|
80
|
+
* previously the period was computed once at module init from
|
|
81
|
+
* DEFAULT_GEO_LOCATION and never refreshed, pinning auto mode to Shanghai's
|
|
82
|
+
* daylight at load time.
|
|
83
|
+
*/
|
|
84
|
+
const themeGeo = ref<GeoLocation>(timezoneFallback());
|
|
85
|
+
|
|
86
|
+
/** How often the solar period is recomputed from the cached coordinates
|
|
87
|
+
* (dawn/dusk flips while the page stays open). Visibility-aware: the
|
|
88
|
+
* interval bus pauses while hidden and catches up once on return. */
|
|
89
|
+
const THEME_CLOCK_TICK_MS = 5 * 60 * 1000;
|
|
90
|
+
|
|
91
|
+
function updatePeriod() {
|
|
92
|
+
currentPeriod.value = getTimePeriod(themeGeo.value.lat, themeGeo.value.lng);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let clockStarted = false;
|
|
96
|
+
let clockInterval: IntervalHandle | null = null;
|
|
97
|
+
|
|
98
|
+
function startThemeClock() {
|
|
99
|
+
if (clockStarted) return;
|
|
100
|
+
clockStarted = true;
|
|
101
|
+
// Fire-and-forget: a failed fix keeps the timezone estimate silently.
|
|
102
|
+
void getGeolocation()
|
|
103
|
+
.then((geo) => {
|
|
104
|
+
themeGeo.value = geo;
|
|
105
|
+
updatePeriod();
|
|
106
|
+
if (currentMode.value === "system") applyTheme();
|
|
107
|
+
})
|
|
108
|
+
.catch(() => {});
|
|
109
|
+
clockInterval = scheduleInterval(() => {
|
|
110
|
+
const before = currentPeriod.value;
|
|
111
|
+
updatePeriod();
|
|
112
|
+
// Re-apply only on an actual day↔night flip while in system mode —
|
|
113
|
+
// manual modes ignore the period entirely.
|
|
114
|
+
if (currentMode.value === "system" && currentPeriod.value !== before) {
|
|
115
|
+
applyTheme();
|
|
116
|
+
}
|
|
117
|
+
}, THEME_CLOCK_TICK_MS);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Re-run the geolocation resolution (and re-apply in system mode). For
|
|
121
|
+
* hosts that registered a geolocation provider after initTheme(), or that
|
|
122
|
+
* want a manual refresh (network reconnect, wake-from-sleep). */
|
|
123
|
+
export function refreshThemeClock(): Promise<GeoLocation> {
|
|
124
|
+
return getGeolocation().then((geo) => {
|
|
125
|
+
themeGeo.value = geo;
|
|
126
|
+
updatePeriod();
|
|
127
|
+
if (currentMode.value === "system") applyTheme();
|
|
128
|
+
return geo;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Stop the theme clock. Host teardown / tests; initTheme() restarts it. */
|
|
133
|
+
export function stopThemeClock() {
|
|
134
|
+
clockInterval?.disconnect();
|
|
135
|
+
clockInterval = null;
|
|
136
|
+
clockStarted = false;
|
|
137
|
+
}
|
|
138
|
+
|
|
24
139
|
let initialized = false;
|
|
25
140
|
|
|
26
141
|
function resolveEffectiveMode(mode: ThemeMode): "dark" | "light" {
|
|
@@ -43,7 +158,13 @@ function applyTheme() {
|
|
|
43
158
|
|
|
44
159
|
const effectiveMode = resolveEffectiveMode(currentMode.value);
|
|
45
160
|
const tokens = effectiveMode === "dark" ? preset.dark : preset.light;
|
|
46
|
-
|
|
161
|
+
// Extension token groups always emit their cssvars: preset/custom-theme
|
|
162
|
+
// overrides win, registry defaults are the final fallback, so consumers
|
|
163
|
+
// can rely on `--<group>-<slot>` existing once the group is registered.
|
|
164
|
+
const groupVars = groupTokensToCSSVars(
|
|
165
|
+
resolveGroupTokens(effectiveMode, preset.groups?.[effectiveMode]),
|
|
166
|
+
);
|
|
167
|
+
const vars = tokensToCSSVars(tokens, groupVars);
|
|
47
168
|
|
|
48
169
|
invalidateLuminanceCache();
|
|
49
170
|
|
|
@@ -61,6 +182,14 @@ function applyTheme() {
|
|
|
61
182
|
}, THEME_TRANSITION_DURATION);
|
|
62
183
|
}
|
|
63
184
|
|
|
185
|
+
// Late token-group registrations re-apply the current theme so their
|
|
186
|
+
// cssvars land immediately (microtask-coalesced) instead of waiting for
|
|
187
|
+
// the next theme/mode switch. The registry lives in tokenGroups.ts and
|
|
188
|
+
// must not import this module (that would be a cycle), so it calls back
|
|
189
|
+
// through this injected hook. Hikari wires it once on module load;
|
|
190
|
+
// applications never call setTokenGroupsReapply themselves.
|
|
191
|
+
setTokenGroupsReapply(() => applyTheme());
|
|
192
|
+
|
|
64
193
|
function getAllThemePresets(): Record<string, ThemePreset> {
|
|
65
194
|
const result: Record<string, ThemePreset> = { ...themePresets };
|
|
66
195
|
for (const ct of customThemes.value) {
|
|
@@ -77,6 +206,7 @@ export function initTheme() {
|
|
|
77
206
|
currentMode.value = storedMode;
|
|
78
207
|
}
|
|
79
208
|
applyTheme();
|
|
209
|
+
startThemeClock();
|
|
80
210
|
}
|
|
81
211
|
|
|
82
212
|
export function useTheme() {
|
|
@@ -88,7 +218,7 @@ export function useTheme() {
|
|
|
88
218
|
name: themePresets[id as keyof typeof themePresets].name,
|
|
89
219
|
isCustom: false,
|
|
90
220
|
}));
|
|
91
|
-
const custom = customThemes.value.map((ct) => ({
|
|
221
|
+
const custom = customThemes.value.map((ct: CustomThemePreset) => ({
|
|
92
222
|
id: ct.id,
|
|
93
223
|
name: ct.name,
|
|
94
224
|
isCustom: true,
|
|
@@ -121,7 +251,7 @@ export function useTheme() {
|
|
|
121
251
|
removeCustomThemeFromStorage(id);
|
|
122
252
|
customThemes.value = loadCustomThemes();
|
|
123
253
|
if (currentTheme.value === id) {
|
|
124
|
-
setTheme(
|
|
254
|
+
setTheme(resolveDefaultTheme());
|
|
125
255
|
}
|
|
126
256
|
}
|
|
127
257
|
|
|
@@ -129,6 +259,11 @@ export function useTheme() {
|
|
|
129
259
|
currentTheme,
|
|
130
260
|
currentMode,
|
|
131
261
|
effectiveMode,
|
|
262
|
+
/** The theme clock's resolved coordinates (timezone estimate until a
|
|
263
|
+
* real fix lands). Read-only; refreshThemeClock() updates it. */
|
|
264
|
+
geo: computed(() => themeGeo.value),
|
|
265
|
+
/** The current solar period driving system mode. */
|
|
266
|
+
period: computed(() => currentPeriod.value),
|
|
132
267
|
setTheme,
|
|
133
268
|
setMode,
|
|
134
269
|
toggleMode,
|
package/src/tokens.scss
CHANGED
|
@@ -23,4 +23,114 @@
|
|
|
23
23
|
--border-input: rgb(var(--color-border, 100 100 100) / 15%);
|
|
24
24
|
--border-faint: rgb(var(--color-border, 100 100 100) / 10%);
|
|
25
25
|
--border-subtle: rgb(var(--color-border, 100 100 100) / 12%);
|
|
26
|
+
|
|
27
|
+
/* Icon-button state bridge — HkIconButtonVars references this vocabulary
|
|
28
|
+
but nothing ever defined it, so every var() without an in-file fallback
|
|
29
|
+
was invalid at computed-value time: non-ghost variants had no active
|
|
30
|
+
background, focus rings and glow vanished, and one missing item even
|
|
31
|
+
invalidated the whole transition shorthand (all icon buttons lost their
|
|
32
|
+
hover/press animation). Defined once here as live expressions over the
|
|
33
|
+
theme triplets so they track whichever palette is active; the -dark
|
|
34
|
+
variants are the pressed-step of their base slot. */
|
|
35
|
+
--hi-icon-color: rgb(var(--color-text, 30 30 30));
|
|
36
|
+
--hi-icon-size-xs: 0.75rem;
|
|
37
|
+
--hi-icon-size-sm: 1rem;
|
|
38
|
+
--hi-icon-size-md: 1.25rem;
|
|
39
|
+
--hi-icon-size-lg: 1.5rem;
|
|
40
|
+
--hi-duration-instant: var(--duration-instant, 0.1s);
|
|
41
|
+
--hi-border-color-focus: rgb(var(--color-primary, 122 162 247));
|
|
42
|
+
--hi-glow-color: transparent;
|
|
43
|
+
--hi-bg-surface-dark: rgb(var(--color-muted, 108 108 108) / 12%);
|
|
44
|
+
--hi-color-primary-dark: color-mix(in srgb, rgb(var(--color-primary, 122 162 247)) 82%, #000);
|
|
45
|
+
--hi-color-secondary-dark: color-mix(in srgb, rgb(var(--color-secondary, 81 154 115)) 82%, #000);
|
|
46
|
+
--hi-color-danger-dark: color-mix(in srgb, rgb(var(--color-error, 200 50 50)) 82%, #000);
|
|
47
|
+
--hi-color-success-dark: color-mix(in srgb, rgb(var(--color-success, 0 150 0)) 82%, #000);
|
|
48
|
+
--hi-color-text-on-secondary: rgb(var(--color-on-solid, 255 255 255));
|
|
49
|
+
--hi-color-text-on-danger: rgb(var(--color-on-solid, 255 255 255));
|
|
50
|
+
--hi-color-text-on-success: rgb(var(--color-on-solid, 255 255 255));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Static default light theme — renders correctly even without initTheme().
|
|
54
|
+
initTheme() overrides these at runtime via inline styles on
|
|
55
|
+
documentElement, which beat stylesheet :root declarations. */
|
|
56
|
+
:root {
|
|
57
|
+
--color-primary: 122 162 247;
|
|
58
|
+
--color-on-primary: 255 255 255;
|
|
59
|
+
--color-on-solid: 255 255 255;
|
|
60
|
+
--color-on-solid-text: 255 255 255;
|
|
61
|
+
--color-on-solid-icon: 255 255 255;
|
|
62
|
+
--color-secondary: 81 154 115;
|
|
63
|
+
--color-accent: 247 181 0;
|
|
64
|
+
--color-text: 30 30 30;
|
|
65
|
+
--color-text-secondary: 102 102 102;
|
|
66
|
+
--color-text-tertiary: 140 140 140;
|
|
67
|
+
--color-muted: 108 108 108;
|
|
68
|
+
--color-border: 100 100 100;
|
|
69
|
+
--color-focused-border: 122 162 247;
|
|
70
|
+
--color-background: 214 236 240;
|
|
71
|
+
--color-surface: 240 244 248;
|
|
72
|
+
--color-success: 14 184 64;
|
|
73
|
+
--color-error: 247 118 142;
|
|
74
|
+
--color-warning: 238 214 119;
|
|
75
|
+
--color-info: 106 190 214;
|
|
76
|
+
|
|
77
|
+
--hi-color-primary: rgb(var(--color-primary));
|
|
78
|
+
--hi-color-secondary: rgb(var(--color-secondary));
|
|
79
|
+
--hi-color-surface: rgb(var(--color-surface));
|
|
80
|
+
--hi-color-background: rgb(var(--color-background));
|
|
81
|
+
--hi-color-border: rgb(var(--color-border));
|
|
82
|
+
--hi-color-text-primary: rgb(var(--color-text));
|
|
83
|
+
--hi-color-text-secondary: rgb(var(--color-text-secondary));
|
|
84
|
+
--hi-color-muted: rgb(var(--color-muted));
|
|
85
|
+
--hi-color-on-primary: rgb(var(--color-on-primary));
|
|
86
|
+
/* Deprecated in-tree: luminance-computed contrast, kept only for external
|
|
87
|
+
consumers. In-tree components use the on-solid slots instead. */
|
|
88
|
+
--hi-color-text-on-primary: rgb(var(--color-on-primary));
|
|
89
|
+
--hi-color-text-on-solid: rgb(var(--color-on-solid-text, 255 255 255));
|
|
90
|
+
--hi-color-icon-on-solid: rgb(var(--color-on-solid-icon, 255 255 255));
|
|
91
|
+
--hi-color-success: rgb(var(--color-success));
|
|
92
|
+
--hi-color-error: rgb(var(--color-error));
|
|
93
|
+
--hi-color-warning: rgb(var(--color-warning));
|
|
94
|
+
--hi-color-info: rgb(var(--color-info));
|
|
95
|
+
|
|
96
|
+
/* Design tokens consumed by hikari components. These used to live only in
|
|
97
|
+
plana-ui's token set; without them the background/backdrop declarations
|
|
98
|
+
of fields and surfaces resolve to nothing. Values align with plana-ui. */
|
|
99
|
+
--space-2: 0.125rem;
|
|
100
|
+
--space-4: 0.25rem;
|
|
101
|
+
--space-6: 0.375rem;
|
|
102
|
+
--space-8: 0.5rem;
|
|
103
|
+
--space-10: 0.625rem;
|
|
104
|
+
--space-12: 0.75rem;
|
|
105
|
+
--space-14: 0.875rem;
|
|
106
|
+
--space-16: 1rem;
|
|
107
|
+
--space-20: 1.25rem;
|
|
108
|
+
--space-24: 1.5rem;
|
|
109
|
+
--space-28: 1.75rem;
|
|
110
|
+
--space-32: 2rem;
|
|
111
|
+
--space-40: 2.5rem;
|
|
112
|
+
--text-2xs: 0.625rem;
|
|
113
|
+
--text-xs: 0.75rem;
|
|
114
|
+
--text-sm: 0.8125rem;
|
|
115
|
+
--text-base: 0.875rem;
|
|
116
|
+
--text-md: 1rem;
|
|
117
|
+
--text-lg: 1.125rem;
|
|
118
|
+
--radius-sm: 4px;
|
|
119
|
+
--radius-md: 8px;
|
|
120
|
+
--opacity-half: 0.92;
|
|
121
|
+
--blur-sm: 8px;
|
|
122
|
+
--blur-md: 16px;
|
|
123
|
+
--blur-lg: 24px;
|
|
124
|
+
--duration-fast: 0.15s;
|
|
125
|
+
--duration-short: 0.15s;
|
|
126
|
+
--duration-normal: 0.3s;
|
|
127
|
+
--ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
|
|
128
|
+
--ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);
|
|
129
|
+
--ease-in-expo: cubic-bezier(0.95, 0.05, 0.795, 0.035);
|
|
130
|
+
--s-footer-height: 3rem;
|
|
131
|
+
--z-header: 30;
|
|
132
|
+
--z-sidebar: 40;
|
|
133
|
+
/* Apple-style stack + CJK UI tail — keep in sync with packages/vue/src/theme/fontContext.ts */
|
|
134
|
+
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
|
|
135
|
+
--font-reading: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
|
|
26
136
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Encode a binary buffer as a base64 string. */
|
|
2
|
+
export function bytesToBase64(bytes: ArrayBuffer | Uint8Array): string {
|
|
3
|
+
const buf = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
4
|
+
let binary = "";
|
|
5
|
+
const chunk = 0x8000;
|
|
6
|
+
for (let i = 0; i < buf.length; i += chunk) {
|
|
7
|
+
binary += String.fromCharCode(...buf.subarray(i, i + chunk));
|
|
8
|
+
}
|
|
9
|
+
return btoa(binary);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Decode a base64 string into raw bytes. */
|
|
13
|
+
export function base64ToBytes(b64: string): Uint8Array {
|
|
14
|
+
const binary = atob(b64);
|
|
15
|
+
const bytes = new Uint8Array(binary.length);
|
|
16
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
17
|
+
return bytes;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Read a Blob/File and return its base64 payload (no data: prefix). */
|
|
21
|
+
export function blobToBase64(blob: Blob): Promise<string> {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const reader = new FileReader();
|
|
24
|
+
reader.onload = () => {
|
|
25
|
+
const result = String(reader.result ?? "");
|
|
26
|
+
resolve(result.includes(",") ? result.slice(result.indexOf(",") + 1) : result);
|
|
27
|
+
};
|
|
28
|
+
reader.onerror = () => reject(reader.error);
|
|
29
|
+
reader.readAsDataURL(blob);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser CORS reachability probe (upstreamed from shittim-chest).
|
|
3
|
+
*
|
|
4
|
+
* Distinguishes "host unreachable" from "not allowed by CORS": a
|
|
5
|
+
* credentialed fetch first; on failure a no-cors probe — if that also
|
|
6
|
+
* fails the origin is unreachable, otherwise the host responded but the
|
|
7
|
+
* CORS policy rejected the credentialed request.
|
|
8
|
+
*/
|
|
9
|
+
export type OriginProbe =
|
|
10
|
+
| { ok: true }
|
|
11
|
+
| { ok: false; reason: "unreachable" | "cors-blocked" };
|
|
12
|
+
|
|
13
|
+
export async function probeOrigin(
|
|
14
|
+
url: string,
|
|
15
|
+
opts?: { fetchFn?: typeof fetch },
|
|
16
|
+
): Promise<OriginProbe> {
|
|
17
|
+
const fetchFn = opts?.fetchFn ?? fetch;
|
|
18
|
+
try {
|
|
19
|
+
// A successful credentialed fetch means reachable + CORS-whitelisted,
|
|
20
|
+
// regardless of the HTTP status code (4xx/5xx still prove the origin).
|
|
21
|
+
await fetchFn(url, { credentials: "include", mode: "cors" });
|
|
22
|
+
return { ok: true };
|
|
23
|
+
} catch {
|
|
24
|
+
// no-cors probe: still reports success if the server is reachable,
|
|
25
|
+
// just without reading the response.
|
|
26
|
+
try {
|
|
27
|
+
await fetchFn(url, { mode: "no-cors" });
|
|
28
|
+
return { ok: false, reason: "cors-blocked" };
|
|
29
|
+
} catch {
|
|
30
|
+
return { ok: false, reason: "unreachable" };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Like `probeOrigin` but captures the response body when the credentialed
|
|
37
|
+
* fetch succeeds — for identity assertions (e.g. checking a product field)
|
|
38
|
+
* without re-implementing the probe ladder.
|
|
39
|
+
*/
|
|
40
|
+
export async function probeOriginWithBody(
|
|
41
|
+
url: string,
|
|
42
|
+
opts?: { fetchFn?: typeof fetch },
|
|
43
|
+
): Promise<{ ok: boolean; body?: string }> {
|
|
44
|
+
const fetchFn = opts?.fetchFn ?? fetch;
|
|
45
|
+
try {
|
|
46
|
+
const resp = await fetchFn(url, { credentials: "include", mode: "cors" });
|
|
47
|
+
return { ok: true, body: await resp.text().catch(() => undefined) };
|
|
48
|
+
} catch {
|
|
49
|
+
return { ok: false };
|
|
50
|
+
}
|
|
51
|
+
}
|