@celestia-island/hikari 0.55.326 → 0.55.328

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 (78) hide show
  1. package/package.json +1 -1
  2. package/src/DemoApp.tsx +3 -3
  3. package/src/animation/registerAnimations.ts +6 -0
  4. package/src/components/HkAffixPicker.scss +2 -2
  5. package/src/components/HkBreadcrumb.scss +1 -1
  6. package/src/components/HkButton.scss +1 -1
  7. package/src/components/HkCalendarHeatmap.scss +43 -6
  8. package/src/components/HkCalendarHeatmap.test.tsx +136 -0
  9. package/src/components/HkCalendarHeatmap.tsx +73 -6
  10. package/src/components/HkCard.scss +1 -1
  11. package/src/components/HkCardList.scss +2 -2
  12. package/src/components/HkCheckbox.scss +207 -0
  13. package/src/components/HkColorPicker.scss +1 -1
  14. package/src/components/HkCornerBadge.scss +1 -1
  15. package/src/components/HkDatePicker.scss +5 -5
  16. package/src/components/HkDateTag.scss +1 -1
  17. package/src/components/HkDateTimePicker.scss +8 -8
  18. package/src/components/HkDrawer.scss +4 -4
  19. package/src/components/HkEdgeGlow.barrel.test.ts +27 -0
  20. package/src/components/HkEdgeGlow.scss +132 -0
  21. package/src/components/HkEdgeGlow.test.tsx +244 -0
  22. package/src/components/HkEdgeGlow.tsx +130 -0
  23. package/src/components/HkErrorLanding.scss +2 -2
  24. package/src/components/HkFileField.scss +1 -1
  25. package/src/components/HkFilePickerField.scss +1 -1
  26. package/src/components/HkFilterPanel.scss +14 -1
  27. package/src/components/HkIconButtonGroup.scss +1 -1
  28. package/src/components/HkIconButtonVars.scss +1 -1
  29. package/src/components/HkInput.scss +1 -1
  30. package/src/components/HkKanban.scss +8 -0
  31. package/src/components/HkLoadingVeil.scss +1 -1
  32. package/src/components/HkLogWindow.scss +2 -2
  33. package/src/components/HkMarkdownRenderer.scss +1 -1
  34. package/src/components/HkModal.scss +3 -3
  35. package/src/components/HkPageHeader.scss +1 -1
  36. package/src/components/HkPasswordSurface.scss +1 -1
  37. package/src/components/HkPersistentToast.scss +2 -2
  38. package/src/components/HkPersistentToastGroup.scss +1 -1
  39. package/src/components/HkPopover.scss +2 -2
  40. package/src/components/HkProgressDialog.scss +1 -1
  41. package/src/components/HkRadio.scss +49 -0
  42. package/src/components/HkScrollContainer.fade.contract.test.ts +9 -7
  43. package/src/components/HkScrollContainer.scss +39 -62
  44. package/src/components/HkScrollContainer.selectionHint.test.ts +106 -87
  45. package/src/components/HkScrollContainer.test.ts +291 -0
  46. package/src/components/HkScrollContainer.tsx +328 -7
  47. package/src/components/HkScrollProgressDock.test.tsx +108 -2
  48. package/src/components/HkSearchInput.scss +1 -1
  49. package/src/components/HkSelect.scss +2 -2
  50. package/src/components/HkSelectionControls.styles.test.ts +4242 -92
  51. package/src/components/HkSelectionGrid.scss +1 -1
  52. package/src/components/HkSelectionWaterfall.scss +2 -2
  53. package/src/components/HkSidebarRail.scss +1 -1
  54. package/src/components/HkSlider.scss +2 -2
  55. package/src/components/HkSplash.scss +1 -1
  56. package/src/components/HkStarSystem.scss +1 -1
  57. package/src/components/HkStatCard.scss +2 -2
  58. package/src/components/HkStatusPill.scss +1 -1
  59. package/src/components/HkStatusRow.scss +3 -3
  60. package/src/components/HkStepGuide.scss +2 -2
  61. package/src/components/HkSwitch.scss +1 -1
  62. package/src/components/HkTemplateField.scss +1 -1
  63. package/src/components/HkTextarea.scss +1 -1
  64. package/src/components/HkThemeToggle.scss +1 -1
  65. package/src/components/HkTimeZoneGlobe.scss +12 -3
  66. package/src/components/HkTitleBar.scss +2 -2
  67. package/src/components/HkToast.scss +1 -1
  68. package/src/components/HkTooltip.scss +1 -1
  69. package/src/components/HkTreemap.scss +2 -2
  70. package/src/components/HkTreemapStage.scss +1 -1
  71. package/src/components/HkWindowControls.scss +1 -1
  72. package/src/components/nativeScrollbar.contract.test.ts +603 -0
  73. package/src/index.ts +8 -0
  74. package/src/plugins/bootLandingCss.mjs +1 -1
  75. package/src/runtime/touchHint.scss +1 -1
  76. package/src/scale.scss +1 -0
  77. package/src/styles/admin-tokens.scss +4 -4
  78. package/src/styles/theme/scale.scss +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.326",
3
+ "version": "0.55.328",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari UI library, Vue 3 runtime — one design system for Vue 3 and Rust/WASM applications",
package/src/DemoApp.tsx CHANGED
@@ -620,11 +620,11 @@ export default defineComponent({
620
620
  <h2>HTimeline</h2>
621
621
  <HTimeline steps={timelineSteps} currentKey="v2" />
622
622
  <h3>Narrow host → auto window (prev / current / next, edges fade)</h3>
623
- <div style="max-width:280px;padding:8px;border:1px dashed var(--hi-color-border, #888);border-radius:8px">
623
+ <div style="max-width:280px;padding:8px;border:1px dashed var(--hi-color-border, #888);border-radius:var(--radius-md, 8px)">
624
624
  <HTimeline steps={wizardSteps} currentKey="vendor" />
625
625
  </div>
626
626
  <h3>collapse="always" at the first step</h3>
627
- <div style="max-width:280px;padding:8px;border:1px dashed var(--hi-color-border, #888);border-radius:8px">
627
+ <div style="max-width:280px;padding:8px;border:1px dashed var(--hi-color-border, #888);border-radius:var(--radius-md, 8px)">
628
628
  <HTimeline steps={wizardSteps} currentKey="type" collapse="always" />
629
629
  </div>
630
630
  </section>
@@ -658,7 +658,7 @@ export default defineComponent({
658
658
 
659
659
  <section>
660
660
  <h2>HZoomToolbar / HMinimap</h2>
661
- <div style="position:relative;height:200px;border-radius:8px;overflow:hidden;background:rgba(0,0,0,0.35)">
661
+ <div style="position:relative;height:200px;border-radius:var(--radius-md, 8px);overflow:hidden;background:rgba(0,0,0,0.35)">
662
662
  <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:0.75rem;opacity:0.5">
663
663
  Host surface — zoom {zoomState.value.zoom.toFixed(2)}x, pan ({Math.round(zoomState.value.panX)}, {Math.round(zoomState.value.panY)})
664
664
  </div>
@@ -145,3 +145,9 @@ registerCssAnimation("hk-range-ribbon-pulse", { infinite: true });
145
145
  // prop's dot breathes while its state is in-flight — a settled dot sits
146
146
  // static. Reduced motion drops the loop (house pattern, see HkSkeleton).
147
147
  registerCssAnimation("hk-badge-dot-pulse", { infinite: true });
148
+ // components/HkEdgeGlow.scss — the shared edge-glow breathe (loops
149
+ // forever): the one painter behind HkScrollContainer's overflow glow and
150
+ // any host-mounted band (the static selection hint keeps animate off).
151
+ // Reduced motion keeps the band and drops the loop (house pattern, see
152
+ // HkSkeleton); the global CSS-animation switch pauses it as usual.
153
+ registerCssAnimation("hk-edge-glow-breathe", { infinite: true });
@@ -21,7 +21,7 @@
21
21
  padding-block: 4px;
22
22
  padding-inline: 9px 11px;
23
23
  border: none;
24
- border-radius: var(--radius-sm, 6px);
24
+ border-radius: var(--radius-md, 8px);
25
25
  background: transparent;
26
26
  color: rgb(var(--color-text));
27
27
  cursor: pointer;
@@ -196,7 +196,7 @@
196
196
  height: 1.5rem;
197
197
  padding: 0;
198
198
  border: 0;
199
- border-radius: var(--radius-full, 999px);
199
+ border-radius: var(--radius-full, 9999px);
200
200
  background: transparent;
201
201
  color: var(--hi-icon-button-icon-color, rgb(var(--color-muted)));
202
202
  cursor: pointer;
@@ -341,7 +341,7 @@
341
341
  .hk-breadcrumb-item {
342
342
  background: var(--hi-card-bg);
343
343
  padding: 0.375rem 0.875rem;
344
- border-radius: 999px;
344
+ border-radius: var(--radius-full);
345
345
  border: 1px solid var(--hi-color-border);
346
346
 
347
347
  &:not(:last-child):hover {
@@ -11,7 +11,7 @@
11
11
  font-weight: 600;
12
12
  line-height: 1;
13
13
  border: 1px solid transparent;
14
- border-radius: var(--radius-sm);
14
+ border-radius: var(--radius-md, 8px);
15
15
  cursor: pointer;
16
16
  /* Longhand on purpose: the former comma-separated shorthand
17
17
  * `transition: background-color, ..., filter var(--duration-normal) ...`
@@ -24,20 +24,57 @@
24
24
  color: var(--hi-color-muted);
25
25
  }
26
26
 
27
- // w-max + auto margins center the whole calendar (rail included) on
28
- // wide cards while letting narrow screens scroll from the left edge —
29
- // auto margins resolve to zero when the content overflows, so mobile
30
- // keeps its natural scroll origin.
27
+ // The strip's scroll host is HkScrollContainer — the root element carries
28
+ // .hk-scroll-container PLUS this class. The native horizontal bar is gone
29
+ // (2026-11-08: the battle heatmap's platform bar was the last one in the
30
+ // chart family and read as an embedded widget); hikari's auto-hiding
31
+ // overlay scrollbar is the only bar, and the container's align="center"
32
+ // centers the calendar while it fits and lets it scroll when it does not
33
+ // (the old auto-margin recipe now lives there — do NOT re-add overflow
34
+ // here: a native bar would come back and the overlay chrome would stack
35
+ // under it).
36
+ //
37
+ // padding-bottom is the OVERLAY TRACK's strip, not breathing room: the
38
+ // shared chrome is positioned against this root (bottom: 2px, height:
39
+ // 6px), so 8px of padding keeps the bar BELOW the Sunday rail instead of
40
+ // painting over it while the strip scrolls.
31
41
  &-scroll {
32
42
  margin-top: 12px;
43
+ padding-bottom: 8px;
44
+ }
45
+
46
+ // ── Legacy plain-form fallback (Rust/WASM renderer) ──────────────────
47
+ // The Vue form mounted the strip on HkScrollContainer (above), but the
48
+ // Rust/WASM CalendarHeatmap (packages/components/src/data/calendar_heatmap.rs)
49
+ // still renders the pre-migration plain -.hk-calendar-heatmap-scroll div and
50
+ // reads THIS sheet through the generated vue-mirror. The mirror sheet no
51
+ // longer owns the plain form's overflow/centering, so restore the previous
52
+ // horizontal scrolling here for the plain form ONLY: a Vue strip carries
53
+ // .hk-scroll-container on the same element and never matches, so the
54
+ // overlay chrome can never be re-claimed by this rule. The platform bar is
55
+ // suppressed (the chart family's no-native-bar invariant) — until the Rust
56
+ // renderer migrates onto the shared scroll-container script this form
57
+ // scrolls by wheel/touch/keyboard without a visible bar, the same
58
+ // bar-less trade the suppression-only scrollers take.
59
+ &-scroll:not(.hk-scroll-container) {
33
60
  overflow-x: auto;
34
- padding-bottom: 4px;
61
+ scrollbar-width: none;
62
+
63
+ &::-webkit-scrollbar {
64
+ display: none;
65
+ }
66
+
67
+ > .hk-calendar-heatmap-body {
68
+ margin-inline: auto;
69
+ }
35
70
  }
36
71
 
72
+ // width: max-content keeps the strip's natural extent (the container's
73
+ // aligner owns the centering, so no auto margins here — they would be
74
+ // resolved inside a box sized exactly to this element's content).
37
75
  &-body {
38
76
  display: flex;
39
77
  width: max-content;
40
- margin-inline: auto;
41
78
  gap: 8px;
42
79
  }
43
80
 
@@ -1,9 +1,23 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
1
5
  import { afterEach, describe, expect, it, vi } from "vitest";
2
6
  import { createApp, defineComponent, h, nextTick, reactive } from "vue";
3
7
 
4
8
  import HkCalendarHeatmap from "./HkCalendarHeatmap";
5
9
  import { HEATMAP_LEVEL_OPACITY, shiftIso } from "./calendarHeatmap";
6
10
 
11
+ const here = dirname(fileURLToPath(import.meta.url));
12
+
13
+ /** Resolve after two animation frames: the scroll container coalesces its
14
+ * sensing pass through the shared animationBus, whose rAF is scheduled one
15
+ * frame ahead of the awaited callback. */
16
+ const flushFrames = async () => {
17
+ await new Promise<void>((r) => requestAnimationFrame(() => r()));
18
+ await new Promise<void>((r) => requestAnimationFrame(() => r()));
19
+ };
20
+
7
21
  const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
8
22
 
9
23
  function mount(node: ReturnType<typeof h>) {
@@ -381,3 +395,125 @@ describe("HkCalendarHeatmap", () => {
381
395
  }
382
396
  });
383
397
  });
398
+ // ── Overlay scroll strip (2026-11-08) ────────────────────────────────────
399
+ // The calendar body rides HkScrollContainer now: hikari's auto-hiding
400
+ // overlay bar is the only bar, the strip opens focused on the NEWEST day
401
+ // (anchor="end"), a host can opt back to the start, and a series
402
+ // replacement re-parks at the end. happy-dom performs no layout, so the
403
+ // strip's extent is stubbed and a dispatched scroll event runs the
404
+ // container's coalesced sensing pass.
405
+ describe("HkCalendarHeatmap overlay scroll strip", () => {
406
+ const stripOf = (c: HTMLElement): HTMLElement =>
407
+ c.querySelector<HTMLElement>(".hk-calendar-heatmap-scroll")!;
408
+ const viewportOf = (c: HTMLElement): HTMLElement =>
409
+ c.querySelector<HTMLElement>(".hk-scroll-container-viewport")!;
410
+
411
+ /** Mount a live render function (reactive props re-render — the range
412
+ * switch test replaces the series). */
413
+ function mountLive(render: () => ReturnType<typeof h>): HTMLElement {
414
+ const container = document.createElement("div");
415
+ document.body.appendChild(container);
416
+ const app = createApp({ render });
417
+ app.mount(container);
418
+ mounts.push({ app, container });
419
+ return container;
420
+ }
421
+
422
+ /** Give the viewport a scrollable extent. Only the SIZES are stubbed:
423
+ * the position stays happy-dom's settable scrollLeft, so the park's
424
+ * write is observable exactly as a reader's scroll would be. */
425
+ function stubStrip(el: HTMLElement, scrollWidth: number, clientWidth: number) {
426
+ Object.defineProperties(el, {
427
+ scrollWidth: { configurable: true, get: () => scrollWidth },
428
+ clientWidth: { configurable: true, get: () => clientWidth },
429
+ });
430
+ }
431
+
432
+ it("mounts the overlay chrome over the strip, with the body still inside", () => {
433
+ const c = mount(
434
+ h(HkCalendarHeatmap, { days: daysFrom(CLEAN_HEAD_MON, 7), first: null }),
435
+ );
436
+ const strip = stripOf(c);
437
+ // The class contract survives (consumers style .hk-calendar-heatmap-scroll)
438
+ // and the element IS the scroll container root.
439
+ expect(strip.classList.contains("hk-scroll-container")).toBe(true);
440
+ // hikari's auto-hiding overlay track is the only bar…
441
+ expect(
442
+ c.querySelector('.hk-scrollbar-track[data-axis="horizontal"]'),
443
+ ).not.toBeNull();
444
+ // …the overflow glow is opted in (the strip hides days at its edges)…
445
+ expect(strip.getAttribute("data-overflow-glow")).toBe("true");
446
+ // …and the body still rides inside the scrolling viewport.
447
+ expect(viewportOf(c).querySelector(".hk-calendar-heatmap-body")).not.toBeNull();
448
+ });
449
+
450
+ it("keeps native overflow out of the Vue form (source contract)", () => {
451
+ const sheet = readFileSync(join(here, "HkCalendarHeatmap.scss"), "utf8");
452
+ // The -rail/-cells contract is pinned by calendarHeatmapRail.contract;
453
+ // this pin owns the STRIP rule the migration rewrote.
454
+ const scrollRule = sheet.match(/&-scroll \{[\s\S]*?\n \}/)?.[0] ?? "";
455
+ expect(scrollRule, "the &-scroll rule still owns the strip box").toContain("&-scroll {");
456
+ // The Vue strip IS the HkScrollContainer root. Any overflow declaration
457
+ // in this rule would bring a NATIVE bar back on top of the overlay chrome
458
+ // (the 2026-11-08 regression this migration fixed).
459
+ expect(scrollRule).not.toMatch(/overflow/);
460
+ // The Rust/WASM renderer still renders the plain div, so the sheet keeps
461
+ // a fallback scroller for it — but ONLY off the container form, and the
462
+ // platform bar stays suppressed (the chart family's native-bar invariant;
463
+ // the Rust form is bar-less until it migrates onto the shared script).
464
+ const fallback =
465
+ sheet.match(/&-scroll:not\(\.hk-scroll-container\) \{[\s\S]*?\n \}/)?.[0] ?? "";
466
+ expect(fallback, "the plain-form fallback must exist for the Rust mirror").not.toBe("");
467
+ expect(fallback).toContain("overflow-x: auto");
468
+ expect(fallback).toContain("scrollbar-width: none");
469
+ expect(fallback).toMatch(/&::-webkit-scrollbar\s*\{[\s\S]*?display:\s*none/);
470
+ expect(fallback, "the plain form keeps its centering").toContain("margin-inline: auto");
471
+ });
472
+
473
+ it("opens focused on the newest end (anchor=end parks the strip)", async () => {
474
+ const c = mount(
475
+ h(HkCalendarHeatmap, { days: daysFrom(CLEAN_HEAD_MON, 7), first: null }),
476
+ );
477
+ const viewport = viewportOf(c);
478
+ stubStrip(viewport, 300, 100);
479
+ viewport.dispatchEvent(new Event("scroll"));
480
+ await flushFrames();
481
+ expect(viewport.scrollLeft, "parked at max = 300 - 100").toBe(200);
482
+ });
483
+
484
+ it("forwards anchor=start so a host can opt back to the oldest end", async () => {
485
+ const c = mount(
486
+ h(HkCalendarHeatmap, {
487
+ days: daysFrom(CLEAN_HEAD_MON, 7),
488
+ first: null,
489
+ anchor: "start",
490
+ }),
491
+ );
492
+ const viewport = viewportOf(c);
493
+ stubStrip(viewport, 300, 100);
494
+ viewport.dispatchEvent(new Event("scroll"));
495
+ await flushFrames();
496
+ expect(viewport.scrollLeft).toBe(0);
497
+ });
498
+
499
+ it("re-parks at the end when the series is replaced (range switch)", async () => {
500
+ const state = reactive({ days: daysFrom(CLEAN_HEAD_MON, 7) });
501
+ const c = mountLive(() => h(HkCalendarHeatmap, { days: state.days, first: null }));
502
+ const viewport = viewportOf(c);
503
+ stubStrip(viewport, 300, 100);
504
+ viewport.dispatchEvent(new Event("scroll"));
505
+ await flushFrames();
506
+ expect(viewport.scrollLeft, "initial end focus").toBe(200);
507
+
508
+ // A reader scrolls back to the oldest day…
509
+ viewport.scrollLeft = 0;
510
+ // …then switches range: the new series must land on ITS newest end
511
+ // (the container's sticky anchor deliberately would not, since the
512
+ // reader is no longer parked — the days watcher is the deterministic
513
+ // re-park).
514
+ state.days = daysFrom("2025-11-01", 30);
515
+ await nextTick();
516
+ await nextTick();
517
+ expect(viewport.scrollLeft).toBe(200);
518
+ });
519
+ });
@@ -1,6 +1,7 @@
1
- import { computed, defineComponent, type PropType } from "vue";
1
+ import { computed, defineComponent, ref, watch, type PropType } from "vue";
2
2
 
3
3
  import { useI18n } from "../i18n/context";
4
+ import HkScrollContainer from "./HkScrollContainer";
4
5
  import {
5
6
  calendarGrid,
6
7
  heatmapCellFill,
@@ -53,6 +54,18 @@ function fill(template: string, vars: Record<string, string | number>): string {
53
54
  * null = never). All math lives in ./calendarHeatmap (pure, pinned by
54
55
  * its own tests). The `toolbar` slot rides right of the legend — the
55
56
  * consumer's range tabs / controls belong there.
57
+ *
58
+ * Scrolling (2026-11-08): the calendar body rides HkScrollContainer
59
+ * (axis="horizontal", align="center") so the strip uses hikari's own
60
+ * auto-hiding overlay scrollbar and NEVER a native bar — a chart that
61
+ * paints a platform scrollbar reads as an embedded widget, and the
62
+ * battle heatmap's bar was the last one in the family. The container's
63
+ * `anchor` prop defaults to "end" here: the newest day is the reading
64
+ * interest, so the strip opens focused on it (and the `anchor` prop lets
65
+ * a host opt back to "start"), while the container's overflow glow marks
66
+ * the side that still hides days. A series replacement (range switch,
67
+ * refetch across midnight) re-parks at the end explicitly — see the
68
+ * days watcher.
56
69
  */
57
70
  export default defineComponent({
58
71
  name: "HkCalendarHeatmap",
@@ -72,6 +85,15 @@ export default defineComponent({
72
85
  /** Square-cell scale (GitHub-scale: a 53-week grid stays compact). */
73
86
  cellSize: { type: Number, default: 14 },
74
87
  cellGap: { type: Number, default: 3 },
88
+ /** Which end of the horizontal strip is focused first: the default
89
+ * "end" opens on the NEWEST day (the reading interest — a year-wide
90
+ * series otherwise starts at the oldest, faded edge, which is exactly
91
+ * the "why is the chart empty-looking?" report this default answers).
92
+ * Forwarded to HkScrollContainer's `anchor`; the days/range watcher
93
+ * still re-parks at the end on a data change, so this prop changes the
94
+ * INITIAL focus (and the sticky behaviour) rather than the re-park
95
+ * contract. Pass "start" to restore the old GitHub-style origin. */
96
+ anchor: { type: String as PropType<"start" | "end">, default: "end" },
75
97
  },
76
98
  setup(props, { slots }) {
77
99
  const { t } = useI18n();
@@ -160,6 +182,38 @@ export default defineComponent({
160
182
  : undefined;
161
183
  });
162
184
 
185
+ // The strip's scroll host. Only the two handle members the heatmap
186
+ // uses are named (structural typing, the consumers' house convention):
187
+ // the end park and the sensing refresh.
188
+ const scroller = ref<{
189
+ scrollToEnd: (behavior?: ScrollBehavior) => void;
190
+ refresh: () => void;
191
+ } | null>(null);
192
+
193
+ /** Re-park at the newest day whenever the caller replaces the series —
194
+ * a range switch (the toolbar tabs), a refetch across UTC midnight, a
195
+ * totalled window. The container's `anchor="end"` only keeps a
196
+ * viewport that ALREADY sits at the end, deliberately (it must never
197
+ * fight a reader who scrolled back); switching range from mid-strip is
198
+ * a data replacement, so "land on the newest" is made unconditional
199
+ * here instead.
200
+ *
201
+ * flush: "post" is load-bearing — the grid must have re-rendered (new
202
+ * scrollWidth, possibly a narrower strip) before the target is
203
+ * measured, or the park would aim at the previous range's extent.
204
+ * refresh() then re-derives the overlay chrome from the same new
205
+ * extent: a range that shrinks the series from overflowing to fitting
206
+ * fires no scroll event, so without it the auto-hiding track could
207
+ * stay painted over content that no longer overflows. */
208
+ watch(
209
+ () => props.days,
210
+ () => {
211
+ scroller.value?.scrollToEnd("auto");
212
+ scroller.value?.refresh();
213
+ },
214
+ { flush: "post" },
215
+ );
216
+
163
217
  return () => (
164
218
  <div class="hk-calendar-heatmap">
165
219
  {/* The legend row: the scale in the caller's one unit on the
@@ -191,10 +245,23 @@ export default defineComponent({
191
245
  {slots.toolbar?.({})}
192
246
  </div>
193
247
 
194
- {/* The scroll wrapper centers the whole calendar (rail included)
195
- * on wide cards while letting narrow screens scroll from the
196
- * left edge (see the SCSS). */}
197
- <div class="hk-calendar-heatmap-scroll">
248
+ {/* The strip rides HkScrollContainer: hikari's own auto-hiding
249
+ * overlay scrollbar is the ONLY bar (a native one would read as an
250
+ * embedded platform widget — the battle heatmap's last regression),
251
+ * align="center" keeps the whole calendar (rail included) centered
252
+ * while it fits and lets it scroll once it does not, and
253
+ * anchor="end" opens it focused on the newest day. The class stays
254
+ * on the container root, so consumers styling
255
+ * .hk-calendar-heatmap-scroll keep working; overflowGlow lights the
256
+ * side that still hides days. */}
257
+ <HkScrollContainer
258
+ ref={scroller}
259
+ class="hk-calendar-heatmap-scroll"
260
+ axis="horizontal"
261
+ align="center"
262
+ anchor={props.anchor}
263
+ overflowGlow
264
+ >
198
265
  <div class="hk-calendar-heatmap-body">
199
266
  {/* Weekday rail: the first (Monday) and last (Sunday) rows
200
267
  * carry the labels, the wowsp convention. */}
@@ -271,7 +338,7 @@ export default defineComponent({
271
338
  </div>
272
339
  </div>
273
340
  </div>
274
- </div>
341
+ </HkScrollContainer>
275
342
  </div>
276
343
  );
277
344
  },
@@ -10,7 +10,7 @@
10
10
  background: color-mix(in srgb, rgb(var(--color-surface)) 70%, transparent);
11
11
  @include backdrop-filter(blur(var(--blur-md)));
12
12
  border: 1px solid var(--border-subtle);
13
- border-radius: var(--radius-md);
13
+ border-radius: var(--radius-lg, 12px);
14
14
  box-shadow: var(--shadow-elevated);
15
15
  }
16
16
 
@@ -105,7 +105,7 @@
105
105
  top: 50%;
106
106
  transform: translateY(-50%);
107
107
  padding: 0.125rem;
108
- border-radius: var(--radius-sm, 0.375rem);
108
+ border-radius: var(--radius-sm, 4px);
109
109
  background: rgb(var(--color-surface, 255 255 255));
110
110
  box-shadow: 0 2px 10px rgb(var(--color-border, 100 100 100) / 22%);
111
111
  opacity: 0;
@@ -137,7 +137,7 @@
137
137
  height: 1.625rem;
138
138
  padding: 0;
139
139
  border: 1px solid transparent;
140
- border-radius: var(--radius-xs, 0.25rem);
140
+ border-radius: var(--radius-md, 8px);
141
141
  background: transparent;
142
142
  color: rgb(var(--color-muted, 108 108 108));
143
143
  cursor: pointer;