@celestia-island/hikari 0.55.70 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.70",
3
+ "version": "0.55.72",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari Vue 3 component library — production-grade UI components based on shittim-chest design system",
@@ -441,10 +441,9 @@
441
441
  }
442
442
 
443
443
  // ── Navigation motion (2026-09-23 user directive, round 16) ──────────
444
- // The stepper never hard-cuts: status colors crossfade, the full-mode
445
- // active halo slides between step nodes, and the window-mode cells wipe
446
- // in from the navigation side. All of it rides the family's standard
447
- // duration/ease and collapses under reduced motion.
444
+ // The stepper never hard-cuts: status colors crossfade and the window-mode
445
+ // cells wipe in from the navigation side. All of it rides the family's
446
+ // standard duration/ease and collapses under reduced motion.
448
447
 
449
448
  .hk-timeline {
450
449
  --hk-timeline-motion: 0.3s;
@@ -462,41 +461,17 @@
462
461
  border-color var(--hk-timeline-motion) var(--hk-timeline-motion-ease);
463
462
  }
464
463
 
465
- // Full mode: the active halo — a measured pill behind the active step
466
- // (the component positions it via transform/size), sliding between
467
- // nodes with the same clock. The first placement is instant (data-placed
468
- // gates the transition) so opening the sheet does not animate it.
464
+ // The full-mode active halo was removed here (2026-09-24 user directive):
465
+ // a measured pill behind the active step, sliding between nodes, which
466
+ // read as an unexplained primary blob in every wide multi-step flow and
467
+ // duplicated what the filled indicator and the label weight already say.
468
+ // Window mode never had one. The containing-block rule below stays — it is
469
+ // the timeline's anchor for any absolutely positioned consumer content, and
470
+ // the vertical rail anchors to the step rather than to this box.
469
471
  .hk-timeline[data-mode="full"]:not(.hk-scroll-pin) {
470
472
  position: relative;
471
473
  }
472
474
 
473
- .hk-timeline-halo {
474
- position: absolute;
475
- left: 0;
476
- top: 50%;
477
- translate: 0 -50%;
478
- border-radius: 999px;
479
- background: color-mix(in srgb, var(--hi-color-primary) 10%, transparent);
480
- pointer-events: none;
481
- height: 100%;
482
- box-sizing: border-box;
483
- }
484
-
485
- .hk-timeline[data-orientation="vertical"] .hk-timeline-halo {
486
- top: 0;
487
- left: 50%;
488
- translate: -50% 0;
489
- width: 100%;
490
- height: auto;
491
- }
492
-
493
- .hk-timeline-halo[data-placed] {
494
- transition:
495
- transform var(--hk-timeline-motion) var(--hk-timeline-motion-ease),
496
- width var(--hk-timeline-motion) var(--hk-timeline-motion-ease),
497
- height var(--hk-timeline-motion) var(--hk-timeline-motion-ease);
498
- }
499
-
500
475
  // Window mode: each cell's step content is keyed by its step key, so a
501
476
  // navigation remounts it — wipe in from the navigation side. data-dir is
502
477
  // absent on the very first render, so opening does not animate.
@@ -562,8 +537,4 @@ html[data-css-animations="0"] .hk-timeline[data-mode="window"][data-dir] .hk-tim
562
537
  .hk-timeline[data-mode="window"][data-dir] .hk-timeline-step {
563
538
  animation: none;
564
539
  }
565
-
566
- .hk-timeline-halo[data-placed] {
567
- transition: none;
568
- }
569
540
  }
@@ -424,36 +424,20 @@ describe("HkTimeline navigation motion (round 16)", () => {
424
424
  return { container, current };
425
425
  }
426
426
 
427
- it("positions the full-mode halo on the active step and slides it on navigation", async () => {
428
- const { container, current } = mountTimeline(4, "s1");
429
- // happy-dom has no layout engine: give the step nodes deterministic
430
- // offsets so the halo's measured transform is observable.
431
- const steps = () =>
432
- Array.from(container.querySelectorAll<HTMLElement>(".hk-timeline-step"));
433
- for (const [i, step] of steps().entries()) {
434
- Object.defineProperty(step, "offsetLeft", {
435
- configurable: true,
436
- get: () => i * 100,
437
- });
438
- Object.defineProperty(step, "offsetWidth", {
439
- configurable: true,
440
- get: () => 80,
441
- });
442
- }
443
- await nextTick();
444
- await nextTick();
445
- const halo = container.querySelector<HTMLElement>(".hk-timeline-halo");
446
- expect(halo).not.toBeNull();
447
- // Active = s1 (second node): translateX(100 - pad).
448
- expect(halo!.style.transform).toBe("translateX(94px)");
449
- expect(halo!.style.width).toBe("92px");
450
- expect(halo!.hasAttribute("data-placed")).toBe(true);
451
-
452
- current.value = "s3";
427
+ it("renders no full-mode halo behind the active step", async () => {
428
+ // The measured pill that used to slide behind the active step is gone
429
+ // (2026-09-24 user directive): in a wide multi-step flow it read as an
430
+ // unexplained primary blob, and the filled indicator plus the label
431
+ // weight already carry the active step. Pinned so a later "highlight"
432
+ // refactor cannot quietly reintroduce it.
433
+ const { container } = mountTimeline(4, "s1");
453
434
  await nextTick();
454
435
  await nextTick();
455
- // Slid to the fourth node.
456
- expect(halo!.style.transform).toBe("translateX(294px)");
436
+ expect(container.querySelector(".hk-timeline-halo")).toBeNull();
437
+ // The overlay the window mode owns is a different element and stays a
438
+ // window-mode concern — full mode draws its connectors inline.
439
+ expect(container.querySelector(".hk-timeline[data-mode='full']")).not.toBeNull();
440
+ expect(container.querySelector(".hk-timeline-links")).toBeNull();
457
441
  });
458
442
 
459
443
  it("marks the navigation direction for the window-mode wipe", async () => {
@@ -178,9 +178,8 @@ export default defineComponent({
178
178
 
179
179
  // ── Navigation motion (2026-09-23 user directive, round 16) ──────
180
180
  // The stepper must not hard-cut between steps: the status colors
181
- // crossfade (stylesheet), the window-mode cells wipe in from the
182
- // navigation side (keyed remount + data-dir), and the full-mode
183
- // active halo slides between step nodes (measured transform).
181
+ // crossfade (stylesheet) and the window-mode cells wipe in from the
182
+ // navigation side (keyed remount + data-dir).
184
183
  let prevIndex = currentIndex.value;
185
184
  /** Set once the first navigation revealed a direction; drives the
186
185
  * window-mode wipe's side via the host attribute. */
@@ -192,37 +191,11 @@ export default defineComponent({
192
191
  prevIndex = idx;
193
192
  });
194
193
 
195
- /** The sliding full-mode highlight. */
196
- const halo = ref<HTMLElement | null>(null);
197
- function positionHalo(): void {
198
- const hostEl = host.value;
199
- const haloEl = halo.value;
200
- if (!hostEl || !haloEl) return;
201
- const step = hostEl.querySelector<HTMLElement>(
202
- '.hk-timeline-step[data-status="active"]',
203
- );
204
- if (!step) return;
205
- const vertical = props.orientation === "vertical";
206
- const pad = 6;
207
- if (vertical) {
208
- haloEl.style.transform = `translateY(${Math.max(0, step.offsetTop - pad)}px)`;
209
- haloEl.style.width = "";
210
- haloEl.style.height = `${step.offsetHeight + pad * 2}px`;
211
- } else {
212
- haloEl.style.transform = `translateX(${Math.max(0, step.offsetLeft - pad)}px)`;
213
- haloEl.style.height = "";
214
- haloEl.style.width = `${step.offsetWidth + pad * 2}px`;
215
- }
216
- // The very first placement must not animate from nothing — only
217
- // subsequent moves ride the CSS transition.
218
- haloEl.setAttribute("data-placed", "");
219
- }
220
- watch(
221
- () => [props.currentKey, props.orientation] as const,
222
- () => {
223
- void nextTick(positionHalo);
224
- },
225
- );
194
+ // The sliding full-mode highlight is gone (2026-09-24 user directive).
195
+ // It read as an unexplained primary blob behind the active step in every
196
+ // multi-step flow that is wide enough to show all of them, and the
197
+ // active step is already carried by its filled indicator plus the label
198
+ // weight. Window mode never had one, so nothing replaces it there.
226
199
 
227
200
  function statusOf(idx: number): TimelineStepStatus {
228
201
  return idx < currentIndex.value
@@ -262,13 +235,11 @@ export default defineComponent({
262
235
  if (typeof ResizeObserver !== "undefined") {
263
236
  observer = new ResizeObserver(() => {
264
237
  measure();
265
- positionHalo();
266
238
  });
267
239
  if (host.value) observer.observe(host.value);
268
240
  }
269
241
  void nextTick(() => {
270
242
  measure();
271
- positionHalo();
272
243
  });
273
244
  });
274
245
 
@@ -428,7 +399,6 @@ export default defineComponent({
428
399
  data-mode="full"
429
400
  data-dir={navDir.value ?? undefined}
430
401
  >
431
- <div ref={halo} class="hk-timeline-halo" aria-hidden="true" />
432
402
  {props.steps.map((_step, idx) => {
433
403
  const isLast = idx === props.steps.length - 1;
434
404
  return renderStep(idx, { connector: !isLast, last: isLast });
@@ -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 + legacy fallback live in the theme layer, not here.
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 resolves its source
385
- // through this registry, and an unregistered instance falls back to
386
- // the legacy element ids (the pre-component contract).
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, LEGACY_WALLPAPER_SURFACE_IDS,
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";
@@ -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, LEGACY_WALLPAPER_SURFACE_IDS,
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, even when its element id
27
- * is nothing like chest's (the assertion that would have failed for
28
- * every consumer but chest);
29
- * 2. the legacy ids still resolve when nothing is registered (chest keeps
30
- * working until its own migration PR);
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(LEGACY_WALLPAPER_SURFACE_IDS.canvas, makeGl(BRIGHT));
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(resolveWallpaperSurfaceElement("canvas")).toBe(
113
- document.getElementById(LEGACY_WALLPAPER_SURFACE_IDS.canvas),
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 chest's fixed id", async () => {
121
- const legacy = makeCanvas(LEGACY_WALLPAPER_SURFACE_IDS.canvas, makeGl(BRIGHT));
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, "chest's element is never touched").not.toContain(legacy);
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("still resolves the legacy ids when nothing is registered", () => {
142
- const legacyCanvas = makeCanvas(LEGACY_WALLPAPER_SURFACE_IDS.canvas, makeGl(BRIGHT));
143
- const legacyVideo = document.createElement("video");
144
- legacyVideo.id = LEGACY_WALLPAPER_SURFACE_IDS.video;
145
- document.body.appendChild(legacyVideo);
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(resolveWallpaperSurfaceElement("canvas")).toBe(legacyCanvas);
149
- expect(resolveWallpaperSurfaceElement("video")).toBe(legacyVideo);
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 legacy = makeCanvas(LEGACY_WALLPAPER_SURFACE_IDS.canvas, null);
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(resolveWallpaperSurfaceElement("canvas")).toBe(legacy);
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 now explicit in both directions:
16
- // - whoever owns a wallpaper surface REGISTERS it (as a getter, because a
17
- // renderer may re-create the element — hikari's HkWallpaperBackdrop
18
- // re-renders its layers from state, so a captured element reference
19
- // would go stale on the first wallpaper switch),
20
- // - the legacy ids stay as the FALLBACK, so a host that has not migrated
21
- // (chest, until its own PR) keeps sampling exactly what it samples
22
- // today.
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 legacy id, else null. */
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
- if (typeof document === "undefined") return null;
74
- return document.getElementById(LEGACY_WALLPAPER_SURFACE_IDS[kind]);
69
+ return null;
75
70
  }
76
71
 
77
72
  let handle: AnimationHandle | null = null;