@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.
Files changed (385) hide show
  1. package/README.md +1 -1
  2. package/package.json +22 -7
  3. package/src/DemoApp.tsx +491 -0
  4. package/src/animation/registerAnimations.test.ts +94 -0
  5. package/src/animation/registerAnimations.ts +103 -0
  6. package/src/components/HkAboutModal.scss +95 -0
  7. package/src/components/HkAboutModal.tsx +114 -0
  8. package/src/components/HkAdaptiveDialog.scss +22 -0
  9. package/src/components/HkAdaptiveDialog.test.tsx +271 -0
  10. package/src/components/HkAdaptiveDialog.tsx +167 -0
  11. package/src/components/HkAdminHeader.test.tsx +185 -0
  12. package/src/components/HkAdminHeader.tsx +308 -0
  13. package/src/components/HkAdminShell.test.tsx +237 -0
  14. package/src/components/HkAdminShell.tsx +129 -0
  15. package/src/components/HkAdminTablePage.tsx +70 -0
  16. package/src/components/HkAdminWidgets.test.ts +89 -0
  17. package/src/components/HkAlert.tsx +1 -1
  18. package/src/components/HkAltSignIn.scss +55 -0
  19. package/src/components/HkAltSignIn.test.tsx +88 -0
  20. package/src/components/HkAltSignIn.tsx +92 -0
  21. package/src/components/HkAttachmentModal.scss +49 -0
  22. package/src/components/HkAttachmentModal.tsx +318 -0
  23. package/src/components/HkAuthCard.tsx +50 -0
  24. package/src/components/HkAuthSubmitButton.scss +12 -0
  25. package/src/components/HkAuthSubmitButton.tsx +94 -0
  26. package/src/components/HkAvatar.scss +93 -0
  27. package/src/components/HkAvatar.tsx +2 -2
  28. package/src/components/HkBlockingToast.scss +123 -0
  29. package/src/components/HkBlockingToast.test.tsx +221 -0
  30. package/src/components/HkBlockingToast.tsx +182 -0
  31. package/src/components/HkBreadcrumb.scss +624 -0
  32. package/src/components/HkBreadcrumb.tsx +2 -2
  33. package/src/components/HkButton.scss +16 -2
  34. package/src/components/HkCaptchaModal.scss +14 -0
  35. package/src/components/HkCaptchaModal.tsx +62 -0
  36. package/src/components/HkCaptchaWidget.scss +30 -0
  37. package/src/components/HkCaptchaWidget.tsx +181 -0
  38. package/src/components/HkCard.scss +1 -1
  39. package/src/components/HkChatTypes.ts +65 -0
  40. package/src/components/HkCheckbox.scss +4 -9
  41. package/src/components/HkCheckbox.tsx +18 -6
  42. package/src/components/HkColorPicker.scss +120 -3
  43. package/src/components/HkColorPicker.tsx +130 -19
  44. package/src/components/HkColorPickerTouch.test.tsx +145 -0
  45. package/src/components/HkColorSchemeDialog.scss +52 -0
  46. package/src/components/HkColorSchemeDialog.test.ts +201 -0
  47. package/src/components/HkColorSchemeDialog.tsx +96 -0
  48. package/src/components/HkColorSchemeEditor.test.ts +270 -0
  49. package/src/components/HkColorSchemeEditor.tsx +410 -0
  50. package/src/components/HkConfirmDialog.scss +43 -3
  51. package/src/components/HkConfirmDialog.tsx +7 -4
  52. package/src/components/HkConnectionInfo.ts +80 -0
  53. package/src/components/HkConnectionStatus.tsx +158 -0
  54. package/src/components/HkCookieConsent.scss +22 -0
  55. package/src/components/HkCookieConsent.test.tsx +70 -0
  56. package/src/components/HkCookieConsent.tsx +56 -0
  57. package/src/components/HkCountdownDigit.tsx +105 -0
  58. package/src/components/HkDatePicker.scss +361 -0
  59. package/src/components/HkDatePicker.test.ts +459 -0
  60. package/src/components/HkDatePicker.tsx +568 -0
  61. package/src/components/HkDateTimePicker.scss +65 -37
  62. package/src/components/HkDateTimePicker.test.ts +344 -0
  63. package/src/components/HkDateTimePicker.tsx +249 -82
  64. package/src/components/HkDraggableGrid.scss +4 -1
  65. package/src/components/HkDraggableList.scss +4 -1
  66. package/src/components/HkDrawer.scss +27 -13
  67. package/src/components/HkDrawer.test.tsx +73 -0
  68. package/src/components/HkDrawer.tsx +157 -10
  69. package/src/components/HkErrorBoundary.scss +5 -2
  70. package/src/components/HkErrorBoundary.tsx +7 -4
  71. package/src/components/HkExpansionPanel.scss +143 -0
  72. package/src/components/HkExpansionPanel.test.tsx +109 -0
  73. package/src/components/HkExpansionPanel.tsx +101 -0
  74. package/src/components/HkFab.scss +162 -0
  75. package/src/components/HkFab.test.tsx +217 -0
  76. package/src/components/HkFab.tsx +214 -0
  77. package/src/components/HkHoverRevealAction.scss +6 -0
  78. package/src/components/HkHoverRevealAction.test.ts +68 -0
  79. package/src/components/HkHoverRevealAction.tsx +84 -6
  80. package/src/components/HkIcon.scss +123 -0
  81. package/src/components/HkIcon.tsx +7 -23
  82. package/src/components/HkIconButton.scss +170 -0
  83. package/src/components/HkIconButton.test.tsx +75 -0
  84. package/src/components/HkIconButton.tsx +4 -3
  85. package/src/components/HkIconButtonVars.scss +180 -0
  86. package/src/components/HkInput.affix.test.ts +253 -0
  87. package/src/components/HkInput.enter.test.ts +88 -0
  88. package/src/components/HkInput.scss +127 -8
  89. package/src/components/HkInput.tsx +245 -13
  90. package/src/components/HkKeywordSearchModal.scss +13 -0
  91. package/src/components/HkKeywordSearchModal.tsx +46 -9
  92. package/src/components/HkLabel.scss +34 -0
  93. package/src/components/HkLabel.test.tsx +61 -0
  94. package/src/components/HkLabel.tsx +45 -0
  95. package/src/components/HkListTransition.scss +26 -0
  96. package/src/components/HkListTransition.tsx +57 -0
  97. package/src/components/HkLocalePicker.scss +37 -0
  98. package/src/components/HkLocalePicker.tsx +133 -0
  99. package/src/components/HkLocalePickerPopup.tsx +35 -19
  100. package/src/components/HkLocalizedInput.scss +277 -0
  101. package/src/components/HkLocalizedInput.test.tsx +693 -0
  102. package/src/components/HkLocalizedInput.tsx +495 -0
  103. package/src/components/HkLogWindow.scss +121 -0
  104. package/src/components/HkLogWindow.tsx +195 -0
  105. package/src/components/HkLogo.scss +1 -1
  106. package/src/components/HkLogo.tsx +1 -1
  107. package/src/components/HkMarkdownRenderer.scss +22 -2
  108. package/src/components/HkMarkdownRenderer.tsx +33 -3
  109. package/src/components/HkMediaControlBar.tsx +20 -13
  110. package/src/components/HkMenu.scss +200 -0
  111. package/src/components/HkMenu.test.ts +779 -0
  112. package/src/components/HkMenu.tsx +770 -0
  113. package/src/components/HkMenuActionItem.scss +58 -0
  114. package/src/components/HkMenuActionItem.tsx +41 -0
  115. package/src/components/HkMenuIdentityItem.scss +70 -0
  116. package/src/components/HkMenuIdentityItem.tsx +42 -0
  117. package/src/components/HkMenuPanel.scss +20 -0
  118. package/src/components/HkMenuPanel.test.tsx +118 -0
  119. package/src/components/HkMenuPanel.tsx +32 -0
  120. package/src/components/HkMinimap.tsx +16 -8
  121. package/src/components/HkModal.autofollow.test.ts +44 -0
  122. package/src/components/HkModal.scss +170 -53
  123. package/src/components/HkModal.sheetgap.test.ts +82 -0
  124. package/src/components/HkModal.test.tsx +334 -0
  125. package/src/components/HkModal.tsx +314 -68
  126. package/src/components/HkModalBreadcrumb.scss +4 -1
  127. package/src/components/HkModalBreadcrumb.test.tsx +111 -0
  128. package/src/components/HkModalBreadcrumb.tsx +34 -11
  129. package/src/components/HkModelCatalog.ts +98 -0
  130. package/src/components/HkModelTag.scss +130 -0
  131. package/src/components/HkModelTag.test.tsx +111 -0
  132. package/src/components/HkModelTag.tsx +190 -0
  133. package/src/components/HkNavItem.scss +13 -2
  134. package/src/components/HkNavItem.tsx +2 -1
  135. package/src/components/HkNavSidebar.tsx +99 -0
  136. package/src/components/HkNumberInput.scss +12 -0
  137. package/src/components/HkNumberInput.tsx +15 -8
  138. package/src/components/HkPageHeader.scss +79 -0
  139. package/src/components/HkPageHeader.test.ts +67 -0
  140. package/src/components/HkPageHeader.tsx +51 -0
  141. package/src/components/HkPasswordInput.scss +29 -17
  142. package/src/components/HkPasswordInput.test.ts +281 -0
  143. package/src/components/HkPasswordInput.tsx +337 -59
  144. package/src/components/HkPickerPane.scss +158 -0
  145. package/src/components/HkPlaceholderMarquee.scss +109 -0
  146. package/src/components/HkPlaceholderMarquee.test.ts +305 -0
  147. package/src/components/HkPlaceholderMarquee.tsx +170 -0
  148. package/src/components/HkPopover.scss +134 -10
  149. package/src/components/HkPopover.sheetdock.test.ts +34 -0
  150. package/src/components/HkPopover.surfacevars.test.ts +20 -0
  151. package/src/components/HkPopover.titleSuppress.test.tsx +265 -0
  152. package/src/components/HkPopover.tsx +225 -50
  153. package/src/components/HkPopoverSheet.test.tsx +130 -0
  154. package/src/components/HkPopupSelect.scss +30 -10
  155. package/src/components/HkPopupSelect.tsx +21 -1
  156. package/src/components/HkProgressDialog.scss +31 -0
  157. package/src/components/HkProgressDialog.tsx +37 -19
  158. package/src/components/HkProtocolModal.scss +26 -0
  159. package/src/components/HkProtocolModal.tsx +112 -0
  160. package/src/components/HkRadio.scss +1 -16
  161. package/src/components/HkRadio.tsx +5 -2
  162. package/src/components/HkRichInput.scss +162 -0
  163. package/src/components/HkRichInput.tsx +335 -0
  164. package/src/components/HkRollingNumber.test.ts +90 -0
  165. package/src/components/HkRollingNumber.tsx +39 -1
  166. package/src/components/HkScrollContainer.scss +71 -54
  167. package/src/components/HkScrollContainer.test.ts +160 -0
  168. package/src/components/HkScrollContainer.tsx +187 -160
  169. package/src/components/HkSearchInput.tsx +3 -3
  170. package/src/components/HkSecretRevealModal.scss +61 -0
  171. package/src/components/HkSecretRevealModal.tsx +91 -0
  172. package/src/components/HkSelect.scss +263 -12
  173. package/src/components/HkSelect.sheetdock.test.ts +30 -0
  174. package/src/components/HkSelect.test.tsx +88 -0
  175. package/src/components/HkSelect.tsx +59 -74
  176. package/src/components/HkSelectPanel.test.tsx +666 -0
  177. package/src/components/HkSelectPanel.tsx +595 -0
  178. package/src/components/HkSelectSheet.test.tsx +132 -0
  179. package/src/components/HkSelectionGrid.scss +1 -1
  180. package/src/components/HkSelectionGrid.tsx +2 -1
  181. package/src/components/HkSelectionWaterfall.scss +1 -1
  182. package/src/components/HkShareBar.scss +40 -0
  183. package/src/components/HkShareBar.tsx +50 -0
  184. package/src/components/HkSidebar.scss +15 -10
  185. package/src/components/HkSidebar.tsx +26 -2
  186. package/src/components/HkSignInCard.test.ts +154 -0
  187. package/src/components/HkSignInCard.tsx +155 -0
  188. package/src/components/HkSlider.scss +99 -0
  189. package/src/components/HkSlider.test.tsx +155 -0
  190. package/src/components/HkSlider.tsx +173 -0
  191. package/src/components/HkSpinner.scss +36 -1
  192. package/src/components/HkSpinner.test.tsx +63 -0
  193. package/src/components/HkSpinner.tsx +16 -1
  194. package/src/components/HkSplash.scss +1 -1
  195. package/src/components/HkSplash.tsx +3 -1
  196. package/src/components/HkStatCard.scss +73 -0
  197. package/src/components/HkStatCard.tsx +46 -0
  198. package/src/components/HkStatusBar.test.tsx +178 -0
  199. package/src/components/HkStatusBar.tsx +379 -0
  200. package/src/components/HkStatusPill.scss +73 -0
  201. package/src/components/HkStatusPill.tsx +41 -0
  202. package/src/components/HkStepFlow.scss +75 -0
  203. package/src/components/HkStepFlow.test.tsx +246 -0
  204. package/src/components/HkStepFlow.tsx +115 -0
  205. package/src/components/HkSwitch.scss +5 -17
  206. package/src/components/HkSwitch.tsx +6 -3
  207. package/src/components/HkTable.scss +20 -1
  208. package/src/components/HkTable.tsx +28 -3
  209. package/src/components/HkTabs.scss +222 -48
  210. package/src/components/HkTabs.test.ts +517 -0
  211. package/src/components/HkTabs.tsx +567 -56
  212. package/src/components/HkTag.tsx +2 -1
  213. package/src/components/HkTextarea.tsx +5 -0
  214. package/src/components/HkThemeToggle.scss +232 -0
  215. package/src/components/HkThemeToggle.test.ts +231 -0
  216. package/src/components/HkThemeToggle.tsx +289 -0
  217. package/src/components/HkTimeline.scss +134 -1
  218. package/src/components/HkTimeline.test.tsx +319 -0
  219. package/src/components/HkTimeline.tsx +303 -54
  220. package/src/components/HkToast.scss +34 -5
  221. package/src/components/HkToast.test.tsx +109 -0
  222. package/src/components/HkToast.tsx +57 -8
  223. package/src/components/HkTokenUsageBadge.scss +53 -0
  224. package/src/components/HkTokenUsageBadge.test.tsx +59 -0
  225. package/src/components/HkTokenUsageBadge.tsx +54 -0
  226. package/src/components/HkTokenUsagePanel.scss +95 -0
  227. package/src/components/HkTokenUsagePanel.tsx +140 -0
  228. package/src/components/HkToolBlock.scss +428 -0
  229. package/src/components/HkToolBlock.tsx +639 -0
  230. package/src/components/HkTooltip.scss +14 -2
  231. package/src/components/HkTooltip.tsx +25 -2
  232. package/src/components/HkTrendChart.tsx +27 -16
  233. package/src/components/HkVoiceInputPopup.scss +77 -0
  234. package/src/components/HkVoiceInputPopup.tsx +97 -0
  235. package/src/components/HkZoomToolbar.scss +4 -1
  236. package/src/components/HkZoomToolbar.tsx +7 -4
  237. package/src/components/_hikari-vars.scss +6 -0
  238. package/src/composables/highlightLanguages.ts +109 -0
  239. package/src/composables/iconRegistry.ts +127 -0
  240. package/src/composables/messaging/browserTransport.ts +96 -0
  241. package/src/composables/messaging/index.ts +204 -0
  242. package/src/composables/messaging/messaging.test.ts +256 -0
  243. package/src/composables/messaging/toastTransport.ts +26 -0
  244. package/src/composables/messaging/types.ts +115 -0
  245. package/src/composables/useActionBar.ts +66 -0
  246. package/src/composables/useAttachments.ts +154 -0
  247. package/src/composables/useConnectionProbe.ts +142 -0
  248. package/src/composables/useDeferredTransition.ts +16 -0
  249. package/src/composables/useEngineHealth.ts +69 -0
  250. package/src/composables/useHighlight.ts +124 -0
  251. package/src/composables/useMeasuredHighlight.ts +150 -0
  252. package/src/composables/useOverlayScrollbar.test.ts +221 -0
  253. package/src/composables/useOverlayScrollbar.ts +296 -0
  254. package/src/composables/usePageTitle.ts +37 -0
  255. package/src/composables/useReducedMotion.ts +34 -0
  256. package/src/composables/useResourceListModal.tsx +147 -0
  257. package/src/composables/useSafeArea.ts +137 -0
  258. package/src/composables/useSendShortcut.ts +35 -0
  259. package/src/composables/useSizeMorph.test.ts +156 -0
  260. package/src/composables/useSizeMorph.ts +149 -0
  261. package/src/composables/useSurfaceTransition.test.ts +96 -0
  262. package/src/composables/useSurfaceTransition.ts +71 -0
  263. package/src/composables/useZoomPan.ts +192 -0
  264. package/src/demo.scss +9 -1
  265. package/src/demo.ts +5 -1
  266. package/src/i18n/locales/ar/chat.json +38 -0
  267. package/src/i18n/locales/ar/components.json +141 -40
  268. package/src/i18n/locales/ar/components.toml +59 -0
  269. package/src/i18n/locales/ar/platform.json +65 -0
  270. package/src/i18n/locales/de/chat.json +38 -0
  271. package/src/i18n/locales/de/components.json +139 -38
  272. package/src/i18n/locales/de/components.toml +59 -0
  273. package/src/i18n/locales/de/platform.json +65 -0
  274. package/src/i18n/locales/en/chat.json +38 -0
  275. package/src/i18n/locales/en/components.json +104 -3
  276. package/src/i18n/locales/en/components.toml +59 -0
  277. package/src/i18n/locales/en/platform.json +65 -0
  278. package/src/i18n/locales/es/chat.json +38 -0
  279. package/src/i18n/locales/es/components.json +141 -40
  280. package/src/i18n/locales/es/components.toml +59 -0
  281. package/src/i18n/locales/es/platform.json +65 -0
  282. package/src/i18n/locales/fr/chat.json +38 -0
  283. package/src/i18n/locales/fr/components.json +137 -36
  284. package/src/i18n/locales/fr/components.toml +59 -0
  285. package/src/i18n/locales/fr/platform.json +65 -0
  286. package/src/i18n/locales/ja/chat.json +38 -0
  287. package/src/i18n/locales/ja/components.json +104 -3
  288. package/src/i18n/locales/ja/components.toml +59 -0
  289. package/src/i18n/locales/ja/platform.json +65 -0
  290. package/src/i18n/locales/ko/chat.json +38 -0
  291. package/src/i18n/locales/ko/components.json +104 -3
  292. package/src/i18n/locales/ko/components.toml +59 -0
  293. package/src/i18n/locales/ko/platform.json +65 -0
  294. package/src/i18n/locales/pt/chat.json +38 -0
  295. package/src/i18n/locales/pt/components.json +141 -40
  296. package/src/i18n/locales/pt/components.toml +59 -0
  297. package/src/i18n/locales/pt/platform.json +65 -0
  298. package/src/i18n/locales/ru/chat.json +38 -0
  299. package/src/i18n/locales/ru/components.json +141 -40
  300. package/src/i18n/locales/ru/components.toml +59 -0
  301. package/src/i18n/locales/ru/platform.json +65 -0
  302. package/src/i18n/locales/zh-Hans/chat.json +38 -0
  303. package/src/i18n/locales/zh-Hans/components.json +109 -4
  304. package/src/i18n/locales/zh-Hans/components.toml +59 -0
  305. package/src/i18n/locales/zh-Hans/platform.json +65 -0
  306. package/src/i18n/locales/zh-Hant/chat.json +38 -0
  307. package/src/i18n/locales/zh-Hant/components.json +109 -4
  308. package/src/i18n/locales/zh-Hant/components.toml +59 -0
  309. package/src/i18n/locales/zh-Hant/platform.json +65 -0
  310. package/src/index.ts +225 -6
  311. package/src/plugins/bootScriptsPlugin.ts +47 -0
  312. package/src/plugins/buildHashPlugin.ts +67 -0
  313. package/src/plugins/index.ts +2 -0
  314. package/src/render.ts +1 -1
  315. package/src/runtime/animationBus.test.ts +112 -0
  316. package/src/runtime/animationBus.ts +33 -2
  317. package/src/runtime/backStack.test.ts +334 -0
  318. package/src/runtime/backStack.ts +377 -0
  319. package/src/runtime/env.ts +4 -0
  320. package/src/runtime/index.ts +15 -3
  321. package/src/runtime/intervalBus.test.ts +118 -0
  322. package/src/runtime/intervalBus.ts +189 -0
  323. package/src/runtime/mobileViewport.test.ts +132 -0
  324. package/src/runtime/mobileViewport.ts +157 -0
  325. package/src/runtime/pageLifecycle.ts +91 -0
  326. package/src/runtime/useBlockingToast.ts +112 -0
  327. package/src/runtime/useClipboard.ts +28 -0
  328. package/src/runtime/useConfirm.ts +15 -7
  329. package/src/runtime/useMediaQuery.test.ts +67 -0
  330. package/src/runtime/useMediaQuery.ts +89 -0
  331. package/src/runtime/useOverlay.test.ts +160 -0
  332. package/src/runtime/useOverlay.ts +59 -12
  333. package/src/runtime/usePopupManager.test.ts +200 -0
  334. package/src/runtime/usePopupManager.ts +106 -10
  335. package/src/runtime/useToast.test.ts +90 -0
  336. package/src/runtime/useToast.ts +20 -2
  337. package/src/styles/admin-tokens.scss +744 -0
  338. package/src/styles/index.scss +5 -3
  339. package/src/styles/theme/_glass.scss +33 -0
  340. package/src/styles/theme/_layout.scss +123 -0
  341. package/src/styles/theme/_scrollbar.scss +73 -0
  342. package/src/styles/theme/_tokens.scss +69 -0
  343. package/src/styles/theme/base.scss +617 -0
  344. package/src/styles/theme/foundation.scss +219 -0
  345. package/src/styles/theme/mixins.scss +793 -0
  346. package/src/styles/theme/themes.scss +25 -0
  347. package/src/styles/theme/variables.scss +125 -0
  348. package/src/theme/_field.scss +116 -0
  349. package/src/theme/fontContext.test.ts +179 -0
  350. package/src/theme/fontContext.ts +169 -0
  351. package/src/theme/index.ts +19 -2
  352. package/src/theme/presets.ts +86 -18
  353. package/src/theme/theme.scss +61 -1
  354. package/src/theme/tokenGroups.test.ts +534 -0
  355. package/src/theme/tokenGroups.ts +552 -0
  356. package/src/theme/useSolarTime.test.ts +113 -0
  357. package/src/theme/useSolarTime.ts +95 -20
  358. package/src/theme/useTheme.test.ts +72 -0
  359. package/src/theme/useTheme.ts +144 -9
  360. package/src/tokens.scss +110 -0
  361. package/src/utils/base64.ts +31 -0
  362. package/src/utils/connectivity.ts +51 -0
  363. package/src/utils/dom.test.ts +131 -0
  364. package/src/utils/dom.ts +133 -0
  365. package/src/utils/download.ts +22 -0
  366. package/src/utils/fileType.ts +127 -0
  367. package/src/utils/format.ts +98 -0
  368. package/src/utils/fuzzy.ts +77 -0
  369. package/src/utils/fuzzySearch.test.ts +87 -0
  370. package/src/utils/healthProbe.ts +64 -0
  371. package/src/utils/objectPath.ts +59 -0
  372. package/src/utils/password.ts +53 -0
  373. package/src/utils/pow.test.ts +45 -0
  374. package/src/utils/pow.ts +226 -0
  375. package/src/utils/powNonce.ts +95 -0
  376. package/src/utils/validation.test.ts +78 -0
  377. package/src/DemoApp.vue +0 -400
  378. package/src/components/HkCollapse.tsx +0 -48
  379. package/src/components/HkMorphingTabs.scss +0 -141
  380. package/src/components/HkMorphingTabs.tsx +0 -113
  381. package/src/composables/useBreakpoint.ts +0 -41
  382. package/src/composables/useClipboard.ts +0 -37
  383. package/src/composables/useConfirm.ts +0 -35
  384. package/src/composables/useToast.ts +0 -2
  385. 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
- function timezoneFallback(): { lat: number; lng: number } {
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
- export function getGeolocation(): Promise<{ lat: number; lng: number }> {
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
- return fetch(GEO_API_URL, { signal: controller.signal, cache: "no-store" })
134
+ const d = await fetch(GEO_API_URL, { signal: controller.signal, cache: "no-store" })
82
135
  .then((r) => (r.ok ? r.json() : null))
83
- .then(
84
- (d: { latitude?: number; longitude?: number } | null) => {
85
- timer.disconnect();
86
- if (
87
- d &&
88
- typeof d.latitude === "number" &&
89
- typeof d.longitude === "number" &&
90
- (Math.abs(d.latitude) > 0.01 || Math.abs(d.longitude) > 0.01)
91
- ) {
92
- return { lat: d.latitude, lng: d.longitude };
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
- return timezoneFallback();
95
- },
96
- () => {
97
- timer.disconnect();
98
- return timezoneFallback();
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
+ });
@@ -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, DEFAULT_GEO_LOCATION, type TimePeriod } from "./useSolarTime";
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 STORAGE_MODE_KEY = "hikari-theme-mode";
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
- getTimePeriod(DEFAULT_GEO_LOCATION.lat, DEFAULT_GEO_LOCATION.lng),
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
- const vars = tokensToCSSVars(tokens);
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(DEFAULT_THEME);
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
+ }