@celestia-island/hikari 0.55.71 → 0.55.72
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/package.json +1 -1
- package/src/components/HkWallpaperBackdrop.styles.contract.test.ts +2 -1
- package/src/components/HkWallpaperBackdrop.tsx +4 -3
- package/src/index.ts +160 -1
- package/src/theme/index.ts +1 -1
- package/src/theme/useBackgroundLuminance.test.ts +46 -22
- package/src/theme/useBackgroundLuminance.ts +16 -21
package/package.json
CHANGED
|
@@ -184,7 +184,8 @@ describe("HkWallpaperBackdrop.tsx — no page-global DOM writes", () => {
|
|
|
184
184
|
});
|
|
185
185
|
|
|
186
186
|
it("does not read chest's fixed element ids", () => {
|
|
187
|
-
// The registry
|
|
187
|
+
// The surface registry lives in the theme layer, not here — and since the
|
|
188
|
+
// legacy id fallback was retired it is the only way in.
|
|
188
189
|
expect(stripComments(tsx)).not.toContain("s-wallpaper-");
|
|
189
190
|
});
|
|
190
191
|
});
|
|
@@ -381,9 +381,10 @@ export default defineComponent({
|
|
|
381
381
|
});
|
|
382
382
|
|
|
383
383
|
onMounted(() => {
|
|
384
|
-
// Registered BEFORE the first sample: the sampler
|
|
385
|
-
//
|
|
386
|
-
//
|
|
384
|
+
// Registered BEFORE the first sample: registration is the sampler's
|
|
385
|
+
// only way to find a wallpaper surface — an instance that has not
|
|
386
|
+
// registered is invisible to it, and the sample falls through to the
|
|
387
|
+
// body background.
|
|
387
388
|
unregisterSources = registerWallpaperSurfaceSources({
|
|
388
389
|
canvas: () => canvasRef.value,
|
|
389
390
|
video: () => videoRef.value,
|
package/src/index.ts
CHANGED
|
@@ -219,7 +219,7 @@ export {
|
|
|
219
219
|
themeDecorVersion, isThemeDecorSlot, THEME_DECOR_SLOT_PATTERN, THEME_DECOR_WILDCARD,
|
|
220
220
|
allGroupSlots, resolveLocalizedText, parseTokenGroupConfig, registerTokenGroupConfig,
|
|
221
221
|
startLuminanceSampler, stopLuminanceSampler, sampleLuminanceNow, invalidateLuminanceCache,
|
|
222
|
-
registerWallpaperSurfaceSources, resolveWallpaperSurfaceElement,
|
|
222
|
+
registerWallpaperSurfaceSources, resolveWallpaperSurfaceElement,
|
|
223
223
|
retainLuminanceSampler, releaseLuminanceSampler, luminanceSamplerRefCount,
|
|
224
224
|
type WallpaperSurfaceKind, type WallpaperSurfaceSources,
|
|
225
225
|
getTimePeriod, getGeolocation, solarAltitude, DEFAULT_GEO_LOCATION,
|
|
@@ -603,3 +603,162 @@ export {
|
|
|
603
603
|
type DialCodeEntry,
|
|
604
604
|
type ParsedE164,
|
|
605
605
|
} from "./data/dialCodes";
|
|
606
|
+
|
|
607
|
+
// ── Canonical Hk* names (2026-09-24 unification) ─────────────────────
|
|
608
|
+
// Every public name now exists in both spellings: the historical H*
|
|
609
|
+
// aliases above (deprecated — migrate to the Hk* forms) and the Hk*
|
|
610
|
+
// canonical names below, which match the source file names. Consumers
|
|
611
|
+
// migrate at their own pace; the H* aliases are removed only after the
|
|
612
|
+
// last consumer stops using them. Components export under the name of
|
|
613
|
+
// their source file; named exports whose source identifier was already
|
|
614
|
+
// Hk-prefixed now export that identifier unaliased.
|
|
615
|
+
|
|
616
|
+
export { default as HkActionBar } from "./components/HkActionBar";
|
|
617
|
+
export { default as HkAdaptiveDialog } from "./components/HkAdaptiveDialog";
|
|
618
|
+
export { default as HkAlert } from "./components/HkAlert";
|
|
619
|
+
export { default as HkAltSignIn } from "./components/HkAltSignIn";
|
|
620
|
+
export { default as HkAvatar } from "./components/HkAvatar";
|
|
621
|
+
export { default as HkBadge } from "./components/HkBadge";
|
|
622
|
+
export { default as HkBoard } from "./components/HkBoard";
|
|
623
|
+
export { default as HkBlockingToast } from "./components/HkBlockingToast";
|
|
624
|
+
export { default as HkBreadcrumb } from "./components/HkBreadcrumb";
|
|
625
|
+
export { default as HkButton } from "./components/HkButton";
|
|
626
|
+
export { default as HkCard } from "./components/HkCard";
|
|
627
|
+
export { default as HkCheckbox } from "./components/HkCheckbox";
|
|
628
|
+
export { default as HkColorPicker } from "./components/HkColorPicker";
|
|
629
|
+
export { default as HkConfirmDialog } from "./components/HkConfirmDialog";
|
|
630
|
+
export { default as HkCrossfade } from "./components/HkCrossfade";
|
|
631
|
+
export { default as HkDivider } from "./components/HkDivider";
|
|
632
|
+
export { default as HkDockBar } from "./components/HkDockBar";
|
|
633
|
+
export { default as HkDrawer } from "./components/HkDrawer";
|
|
634
|
+
export { default as HkEmptyState } from "./components/HkEmptyState";
|
|
635
|
+
export { default as HkExpansionPanel } from "./components/HkExpansionPanel";
|
|
636
|
+
export { default as HkFab } from "./components/HkFab";
|
|
637
|
+
export { default as HkIcon } from "./components/HkIcon";
|
|
638
|
+
export { default as HkIconButton } from "./components/HkIconButton";
|
|
639
|
+
export { default as HkIconButtonGroup } from "./components/HkIconButtonGroup";
|
|
640
|
+
export { default as HkImageLightbox } from "./components/HkImageLightbox";
|
|
641
|
+
export { default as HkImagePreview } from "./components/HkImagePreview";
|
|
642
|
+
export { default as HkInput } from "./components/HkInput";
|
|
643
|
+
export { default as HkPlaceholderMarquee } from "./components/HkPlaceholderMarquee";
|
|
644
|
+
export { default as HkKbd } from "./components/HkKbd";
|
|
645
|
+
export { default as HkLabel } from "./components/HkLabel";
|
|
646
|
+
export { default as HkListTransition } from "./components/HkListTransition";
|
|
647
|
+
export { default as HkLoadMore } from "./components/HkLoadMore";
|
|
648
|
+
export { default as HkMarkdownRenderer } from "./components/HkMarkdownRenderer";
|
|
649
|
+
export { default as HkModal } from "./components/HkModal";
|
|
650
|
+
export { default as HkNavItem } from "./components/HkNavItem";
|
|
651
|
+
export { default as HkNumberInput } from "./components/HkNumberInput";
|
|
652
|
+
export { default as HkPhoneInput } from "./components/HkPhoneInput";
|
|
653
|
+
export { default as HkAffixPicker } from "./components/HkAffixPicker";
|
|
654
|
+
export { default as HkPhaseTransition } from "./components/HkPhaseTransition";
|
|
655
|
+
export { default as HkGaugeRing } from "./components/HkGaugeRing";
|
|
656
|
+
export { default as HkProgressRing } from "./components/HkProgressRing";
|
|
657
|
+
export { default as HkQrCode } from "./components/HkQrCode";
|
|
658
|
+
export { default as HkRollingNumber } from "./components/HkRollingNumber";
|
|
659
|
+
export { default as HkLocalePickerPopup } from "./components/HkLocalePickerPopup";
|
|
660
|
+
export { default as HkHoverRevealAction } from "./components/HkHoverRevealAction";
|
|
661
|
+
export { default as HkKeywordSearchModal } from "./components/HkKeywordSearchModal";
|
|
662
|
+
export { default as HkModalBreadcrumb } from "./components/HkModalBreadcrumb";
|
|
663
|
+
export { default as HkPopover } from "./components/HkPopover";
|
|
664
|
+
export { default as HkMenu } from "./components/HkMenu";
|
|
665
|
+
export { default as HkContextMenuProvider } from "./components/HkContextMenuProvider";
|
|
666
|
+
export { default as HkMenuPanel } from "./components/HkMenuPanel";
|
|
667
|
+
export { default as HkMenuActionItem } from "./components/HkMenuActionItem";
|
|
668
|
+
export { default as HkMenuIdentityItem } from "./components/HkMenuIdentityItem";
|
|
669
|
+
export { default as HkPopupSelect } from "./components/HkPopupSelect";
|
|
670
|
+
export { default as HkProgressBar } from "./components/HkProgressBar";
|
|
671
|
+
export { default as HkProgressDialog } from "./components/HkProgressDialog";
|
|
672
|
+
export { default as HkRadio } from "./components/HkRadio";
|
|
673
|
+
export { default as HkScrollContainer } from "./components/HkScrollContainer";
|
|
674
|
+
export { default as HkSearchInput } from "./components/HkSearchInput";
|
|
675
|
+
export { default as HkSplash } from "./components/HkSplash";
|
|
676
|
+
export { default as HkSelect } from "./components/HkSelect";
|
|
677
|
+
export { default as HkSelectPanel } from "./components/HkSelectPanel";
|
|
678
|
+
export { default as HkSidebar } from "./components/HkSidebar";
|
|
679
|
+
export { default as HkSkeleton } from "./components/HkSkeleton";
|
|
680
|
+
export { default as HkSkeletonList } from "./components/HkSkeletonList";
|
|
681
|
+
export { default as HkSlider } from "./components/HkSlider";
|
|
682
|
+
export { default as HkSpinner } from "./components/HkSpinner";
|
|
683
|
+
export { default as HkSwitch } from "./components/HkSwitch";
|
|
684
|
+
export { default as HkTable } from "./components/HkTable";
|
|
685
|
+
export { default as HkTabs } from "./components/HkTabs";
|
|
686
|
+
export { default as HkTag } from "./components/HkTag";
|
|
687
|
+
export { default as HkTextarea } from "./components/HkTextarea";
|
|
688
|
+
export { default as HkFileField } from "./components/HkFileField";
|
|
689
|
+
export { default as HkFileBrowserDialog } from "./components/HkFileBrowserDialog";
|
|
690
|
+
export { default as HkFilePickerField } from "./components/HkFilePickerField";
|
|
691
|
+
export { default as HkToast } from "./components/HkToast";
|
|
692
|
+
export { default as HkTooltip } from "./components/HkTooltip";
|
|
693
|
+
export { default as HkTree } from "./components/HkTree";
|
|
694
|
+
export { default as HkWindowedItem } from "./components/HkWindowedItem";
|
|
695
|
+
export { default as HkWaterfall } from "./components/HkWaterfall";
|
|
696
|
+
export { default as HkBlankCanvas } from "./components/HkBlankCanvas";
|
|
697
|
+
export { default as HkKanban } from "./components/HkKanban";
|
|
698
|
+
export { default as HkNodeCanvas } from "./components/HkNodeCanvas";
|
|
699
|
+
export { default as HkDateTimePicker } from "./components/HkDateTimePicker";
|
|
700
|
+
export { default as HkDatePicker } from "./components/HkDatePicker";
|
|
701
|
+
export { default as HkTimeline } from "./components/HkTimeline";
|
|
702
|
+
export { default as HkTitleBar } from "./components/HkTitleBar";
|
|
703
|
+
export { default as HkStepFlow } from "./components/HkStepFlow";
|
|
704
|
+
export { default as HkScrollPin } from "./components/HkScrollPin";
|
|
705
|
+
export { default as HkMediaPlayer } from "./components/HkMediaPlayer";
|
|
706
|
+
export { default as HkMediaControlBar } from "./components/HkMediaControlBar";
|
|
707
|
+
export { default as HkMediaSlider } from "./components/HkMediaSlider";
|
|
708
|
+
export { default as HkMediaVisualizer } from "./components/HkMediaVisualizer";
|
|
709
|
+
export { default as HkImageViewer } from "./components/HkImageViewer";
|
|
710
|
+
export { default as HkZoomToolbar } from "./components/HkZoomToolbar";
|
|
711
|
+
export { default as HkMinimap } from "./components/HkMinimap";
|
|
712
|
+
export { default as HkTrendChart } from "./components/HkTrendChart";
|
|
713
|
+
export { default as HkErrorBoundary } from "./components/HkErrorBoundary";
|
|
714
|
+
export { default as HkDraggableList } from "./components/HkDraggableList";
|
|
715
|
+
export { default as HkDraggableGrid } from "./components/HkDraggableGrid";
|
|
716
|
+
export { default as HkSelectionGrid } from "./components/HkSelectionGrid";
|
|
717
|
+
export { default as HkSelectionWaterfall } from "./components/HkSelectionWaterfall";
|
|
718
|
+
export { default as HkLogo } from "./components/HkLogo";
|
|
719
|
+
export { default as HkAuthMethodList } from "./components/HkAuthMethodList";
|
|
720
|
+
export { default as HkAuthSubmitButton } from "./components/HkAuthSubmitButton";
|
|
721
|
+
export { type HkDockBarAnchor as HkDockBarAnchor } from "./components/HkDockBar";
|
|
722
|
+
export { type HkDockBarSurface as HkDockBarSurface } from "./components/HkDockBar";
|
|
723
|
+
export { HkOtpInput as HkOtpInput } from "./components/HkOtpInput";
|
|
724
|
+
export { HkLocalizedInput as HkLocalizedInput } from "./components/HkLocalizedInput";
|
|
725
|
+
export { HkTagInput as HkTagInput } from "./components/HkTagInput";
|
|
726
|
+
export { HkErrorLanding as HkErrorLanding } from "./components/HkErrorLanding";
|
|
727
|
+
export { HkErrorReportingOverlay as HkErrorReportingOverlay } from "./errorReporting";
|
|
728
|
+
export { HkAdminShell as HkAdminShell } from "./components/HkAdminShell";
|
|
729
|
+
export { HkAdminHeader as HkAdminHeader } from "./components/HkAdminHeader";
|
|
730
|
+
export { HkNavSidebar as HkNavSidebar } from "./components/HkNavSidebar";
|
|
731
|
+
export { HkThemeToggle as HkThemeToggle } from "./components/HkThemeToggle";
|
|
732
|
+
export { HkAuthCard as HkAuthCard } from "./components/HkAuthCard";
|
|
733
|
+
export { HkSignInCard as HkSignInCard } from "./components/HkSignInCard";
|
|
734
|
+
export { HkMfaVerifyCard as HkMfaVerifyCard } from "./components/HkMfaVerifyCard";
|
|
735
|
+
export { type HkMfaFactor as HkMfaFactor } from "./components/HkMfaVerifyCard";
|
|
736
|
+
export { HkLocalePicker as HkLocalePicker } from "./components/HkLocalePicker";
|
|
737
|
+
export { HkAdminTablePage as HkAdminTablePage } from "./components/HkAdminTablePage";
|
|
738
|
+
export { HkPageHeader as HkPageHeader } from "./components/HkPageHeader";
|
|
739
|
+
export { HkStatCard as HkStatCard } from "./components/HkStatCard";
|
|
740
|
+
export { HkStatusPill as HkStatusPill } from "./components/HkStatusPill";
|
|
741
|
+
export { HkShareBar as HkShareBar } from "./components/HkShareBar";
|
|
742
|
+
export { HkSecretRevealModal as HkSecretRevealModal } from "./components/HkSecretRevealModal";
|
|
743
|
+
export { HkCaptchaWidget as HkCaptchaWidget } from "./components/HkCaptchaWidget";
|
|
744
|
+
export { HkCaptchaModal as HkCaptchaModal } from "./components/HkCaptchaModal";
|
|
745
|
+
export { HkProtocolModal as HkProtocolModal } from "./components/HkProtocolModal";
|
|
746
|
+
export { HkAboutModal as HkAboutModal } from "./components/HkAboutModal";
|
|
747
|
+
export { HkLogWindow as HkLogWindow } from "./components/HkLogWindow";
|
|
748
|
+
export { HkSettingsBody as HkSettingsBody } from "./components/HkSettingsDialog";
|
|
749
|
+
export { HkSettingsDialog as HkSettingsDialog } from "./components/HkSettingsDialog";
|
|
750
|
+
export { HkSettingsGroup as HkSettingsGroup } from "./components/HkSettingsDialog";
|
|
751
|
+
export { HkSettingsSub as HkSettingsSub } from "./components/HkSettingsDialog";
|
|
752
|
+
export { HkSettingsHint as HkSettingsHint } from "./components/HkSettingsDialog";
|
|
753
|
+
export { HkCookieConsent as HkCookieConsent } from "./components/HkCookieConsent";
|
|
754
|
+
export { HkAttachmentModal as HkAttachmentModal } from "./components/HkAttachmentModal";
|
|
755
|
+
export { HkStatusBar as HkStatusBar } from "./components/HkStatusBar";
|
|
756
|
+
export { HkCountdownDigit as HkCountdownDigit } from "./components/HkCountdownDigit";
|
|
757
|
+
export { HkConnectionStatus as HkConnectionStatus } from "./components/HkConnectionStatus";
|
|
758
|
+
export { HkRichInput as HkRichInput } from "./components/HkRichInput";
|
|
759
|
+
export { HkVoiceInputPopup as HkVoiceInputPopup } from "./components/HkVoiceInputPopup";
|
|
760
|
+
export { HkToolBlock as HkToolBlock } from "./components/HkToolBlock";
|
|
761
|
+
export { HkJsonTree as HkJsonTree } from "./components/HkJsonTree";
|
|
762
|
+
export { HkTokenUsageBadge as HkTokenUsageBadge } from "./components/HkTokenUsageBadge";
|
|
763
|
+
export { HkTokenUsagePanel as HkTokenUsagePanel } from "./components/HkTokenUsagePanel";
|
|
764
|
+
export { HkModelTag as HkModelTag } from "./components/HkModelTag";
|
package/src/theme/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ export type { GeoLocation, GeoLocationProvider } from "./useSolarTime";
|
|
|
32
32
|
export { refreshThemeClock, stopThemeClock } from "./useTheme";
|
|
33
33
|
export {
|
|
34
34
|
startLuminanceSampler, stopLuminanceSampler, sampleLuminanceNow, invalidateLuminanceCache,
|
|
35
|
-
registerWallpaperSurfaceSources, resolveWallpaperSurfaceElement,
|
|
35
|
+
registerWallpaperSurfaceSources, resolveWallpaperSurfaceElement,
|
|
36
36
|
retainLuminanceSampler, releaseLuminanceSampler, luminanceSamplerRefCount,
|
|
37
37
|
} from "./useBackgroundLuminance";
|
|
38
38
|
export type { WallpaperSurfaceKind, WallpaperSurfaceSources } from "./useBackgroundLuminance";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
LEGACY_WALLPAPER_SURFACE_IDS,
|
|
5
4
|
luminanceSamplerRefCount,
|
|
6
5
|
registerWallpaperSurfaceSources,
|
|
7
6
|
releaseLuminanceSampler,
|
|
@@ -23,15 +22,21 @@ import {
|
|
|
23
22
|
* derived from the body background instead of the wallpaper.
|
|
24
23
|
*
|
|
25
24
|
* Three facts are pinned here:
|
|
26
|
-
* 1. a REGISTERED surface is what gets sampled,
|
|
27
|
-
* is
|
|
28
|
-
*
|
|
29
|
-
* 2.
|
|
30
|
-
*
|
|
25
|
+
* 1. a REGISTERED surface is what gets sampled, whatever its element id
|
|
26
|
+
* is (the assertion that would have failed for every consumer but
|
|
27
|
+
* chest);
|
|
28
|
+
* 2. registration is the WHOLE contract: the retired ids are inert, and
|
|
29
|
+
* "nothing registered" resolves to null so the sampler takes its
|
|
30
|
+
* documented body-background path instead of reading a stranger's
|
|
31
|
+
* element;
|
|
31
32
|
* 3. the shared loop is reference-counted, so an unmounting backdrop
|
|
32
33
|
* releases only its own claim.
|
|
33
34
|
*/
|
|
34
35
|
|
|
36
|
+
/** The ids the sampler used to look up before the registry existed. Still
|
|
37
|
+
* spelled out here, because the point of case 2 is that they mean nothing. */
|
|
38
|
+
const RETIRED_IDS = { canvas: "s-wallpaper-canvas", video: "s-wallpaper-video" } as const;
|
|
39
|
+
|
|
35
40
|
/** A WebGL2 context stub just real enough for the PBO round trip. */
|
|
36
41
|
function makeGl(pixel: [number, number, number, number]) {
|
|
37
42
|
return {
|
|
@@ -105,20 +110,21 @@ afterEach(() => {
|
|
|
105
110
|
|
|
106
111
|
describe("wallpaper surface registry", () => {
|
|
107
112
|
it("proves the detector can tell two canvases apart (positive control)", () => {
|
|
108
|
-
makeCanvas(
|
|
113
|
+
makeCanvas("hk-wallpaper-canvas-decoy", makeGl(BRIGHT));
|
|
109
114
|
const mine = makeCanvas("hk-wallpaper-canvas-instance-1", makeGl(DARK));
|
|
110
115
|
stubCanvasContexts();
|
|
111
116
|
|
|
112
|
-
expect(
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
expect(
|
|
118
|
+
resolveWallpaperSurfaceElement("canvas"),
|
|
119
|
+
"an unregistered element is not the sampler's source",
|
|
120
|
+
).toBeNull();
|
|
115
121
|
|
|
116
122
|
disposers.push(registerWallpaperSurfaceSources({ canvas: () => mine }));
|
|
117
123
|
expect(resolveWallpaperSurfaceElement("canvas")).toBe(mine);
|
|
118
124
|
});
|
|
119
125
|
|
|
120
|
-
it("samples the REGISTERED surface, not
|
|
121
|
-
const
|
|
126
|
+
it("samples the REGISTERED surface, not the retired ids", async () => {
|
|
127
|
+
const retired = makeCanvas(RETIRED_IDS.canvas, makeGl(BRIGHT));
|
|
122
128
|
// A different id AND a different class: nothing about this element
|
|
123
129
|
// matches what the sampler used to look for.
|
|
124
130
|
const registered = makeCanvas("hk-wallpaper-canvas-7f3a", makeGl(DARK));
|
|
@@ -133,31 +139,49 @@ describe("wallpaper surface registry", () => {
|
|
|
133
139
|
await frame();
|
|
134
140
|
|
|
135
141
|
expect(probed, "only the registered element is probed").toContain(registered);
|
|
136
|
-
expect(probed, "
|
|
142
|
+
expect(probed, "the retired id is never touched").not.toContain(retired);
|
|
137
143
|
// A DARK surface ⇒ white floating text (the registered element's pixels).
|
|
138
144
|
expect(luminanceVar()).toBe("255, 255, 255");
|
|
139
145
|
});
|
|
140
146
|
|
|
141
|
-
it("
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
document.body.appendChild(
|
|
147
|
+
it("does not resolve the retired ids when nothing is registered", () => {
|
|
148
|
+
const retiredCanvas = makeCanvas(RETIRED_IDS.canvas, makeGl(BRIGHT));
|
|
149
|
+
const retiredVideo = document.createElement("video");
|
|
150
|
+
retiredVideo.id = RETIRED_IDS.video;
|
|
151
|
+
document.body.appendChild(retiredVideo);
|
|
146
152
|
stubCanvasContexts();
|
|
147
153
|
|
|
148
|
-
expect(
|
|
149
|
-
expect(resolveWallpaperSurfaceElement("
|
|
154
|
+
expect(document.getElementById(RETIRED_IDS.canvas)).toBe(retiredCanvas);
|
|
155
|
+
expect(resolveWallpaperSurfaceElement("canvas")).toBeNull();
|
|
156
|
+
expect(resolveWallpaperSurfaceElement("video")).toBeNull();
|
|
150
157
|
});
|
|
151
158
|
|
|
152
159
|
it("stops using a surface as soon as its owner disposes the registration", () => {
|
|
153
|
-
const
|
|
160
|
+
const retired = makeCanvas(RETIRED_IDS.canvas, null);
|
|
154
161
|
const registered = makeCanvas("hk-wallpaper-canvas-transient", null);
|
|
155
162
|
stubCanvasContexts();
|
|
156
163
|
|
|
157
164
|
const dispose = registerWallpaperSurfaceSources({ canvas: () => registered });
|
|
158
165
|
expect(resolveWallpaperSurfaceElement("canvas")).toBe(registered);
|
|
159
166
|
dispose();
|
|
160
|
-
expect(
|
|
167
|
+
expect(document.getElementById(RETIRED_IDS.canvas)).toBe(retired);
|
|
168
|
+
expect(resolveWallpaperSurfaceElement("canvas")).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("keeps the other owner's surface when one registration disposes", () => {
|
|
172
|
+
const mine = makeCanvas("hk-wallpaper-canvas-my-owner", null);
|
|
173
|
+
const other = makeCanvas("hk-wallpaper-canvas-other-owner", null);
|
|
174
|
+
stubCanvasContexts();
|
|
175
|
+
|
|
176
|
+
const disposeMine = registerWallpaperSurfaceSources({ canvas: () => mine });
|
|
177
|
+
disposers.push(registerWallpaperSurfaceSources({ canvas: () => other }));
|
|
178
|
+
expect(resolveWallpaperSurfaceElement("canvas")).toBe(other);
|
|
179
|
+
|
|
180
|
+
disposeMine();
|
|
181
|
+
// Disposal is per registration, not a sweep: the survivor must keep
|
|
182
|
+
// answering, or the second backdrop (chest mounts one per layout) loses
|
|
183
|
+
// its contrast sampling the moment the first unmounts.
|
|
184
|
+
expect(resolveWallpaperSurfaceElement("canvas")).toBe(other);
|
|
161
185
|
});
|
|
162
186
|
|
|
163
187
|
it("prefers the newest registration that resolves", () => {
|
|
@@ -12,14 +12,18 @@ const CVAR_BADGE = "--float-badge-color";
|
|
|
12
12
|
// null, `sampleFrame` falls through to the body background, and text
|
|
13
13
|
// contrast is computed from the wrong surface with no error anywhere.
|
|
14
14
|
//
|
|
15
|
-
// The contract is
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
15
|
+
// The contract is explicit, in one direction only: whoever owns a wallpaper
|
|
16
|
+
// surface REGISTERS it, as a getter, because a renderer may re-create the
|
|
17
|
+
// element — hikari's HkWallpaperBackdrop re-renders its layers from state,
|
|
18
|
+
// so a captured element reference would go stale on the first wallpaper
|
|
19
|
+
// switch.
|
|
20
|
+
//
|
|
21
|
+
// The id fallback kept smaller hosts working while they migrated, and both
|
|
22
|
+
// had landed by 2026-09-24 (chest #1123, erp.celestia.world #123): every
|
|
23
|
+
// surface owner now registers, none of them renders those ids, and a
|
|
24
|
+
// fallback that matches nothing is exactly the silent wrong-surface read it
|
|
25
|
+
// was meant to avoid. So it is gone, and "no registration" means null —
|
|
26
|
+
// `sampleFrame` then takes its documented body-background path.
|
|
23
27
|
|
|
24
28
|
/** The two wallpaper surfaces the sampler can read. */
|
|
25
29
|
export type WallpaperSurfaceKind = "canvas" | "video";
|
|
@@ -32,15 +36,6 @@ export interface WallpaperSurfaceSources {
|
|
|
32
36
|
video?: () => HTMLVideoElement | null | undefined;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
/**
|
|
36
|
-
* Element ids the sampler assumed before the registry existed. Read-only
|
|
37
|
-
* fallbacks for a host that has not registered a source; never written.
|
|
38
|
-
*/
|
|
39
|
-
export const LEGACY_WALLPAPER_SURFACE_IDS: Readonly<Record<WallpaperSurfaceKind, string>> = {
|
|
40
|
-
canvas: "s-wallpaper-canvas",
|
|
41
|
-
video: "s-wallpaper-video",
|
|
42
|
-
};
|
|
43
|
-
|
|
44
39
|
const surfaceSources = new Map<symbol, WallpaperSurfaceSources>();
|
|
45
40
|
|
|
46
41
|
/**
|
|
@@ -61,8 +56,9 @@ export function registerWallpaperSurfaceSources(sources: WallpaperSurfaceSources
|
|
|
61
56
|
};
|
|
62
57
|
}
|
|
63
58
|
|
|
64
|
-
/** Resolve the live element for `kind`: newest registered source that
|
|
65
|
-
* resolves, else the
|
|
59
|
+
/** Resolve the live element for `kind`: the newest registered source that
|
|
60
|
+
* resolves, else null. Registration is the whole contract — there is no id
|
|
61
|
+
* to fall back to. */
|
|
66
62
|
export function resolveWallpaperSurfaceElement(kind: WallpaperSurfaceKind): HTMLElement | null {
|
|
67
63
|
const tokens = [...surfaceSources.keys()];
|
|
68
64
|
for (let i = tokens.length - 1; i >= 0; i -= 1) {
|
|
@@ -70,8 +66,7 @@ export function resolveWallpaperSurfaceElement(kind: WallpaperSurfaceKind): HTML
|
|
|
70
66
|
const el = source?.[kind]?.();
|
|
71
67
|
if (el) return el;
|
|
72
68
|
}
|
|
73
|
-
|
|
74
|
-
return document.getElementById(LEGACY_WALLPAPER_SURFACE_IDS[kind]);
|
|
69
|
+
return null;
|
|
75
70
|
}
|
|
76
71
|
|
|
77
72
|
let handle: AnimationHandle | null = null;
|