@celestia-island/hikari 0.51.1 → 0.51.3

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.51.1",
3
+ "version": "0.51.3",
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",
@@ -31,7 +31,7 @@
31
31
  // handle hasn't resolved yet — zIndex briefly starts at 0).
32
32
  z-index: 0;
33
33
  background: var(--hk-modal-overlay-bg, rgba(0, 0, 0, 0.45));
34
- backdrop-filter: var(--hk-modal-overlay-blur, blur(6px));
34
+ backdrop-filter: var(--hk-modal-overlay-blur, blur(3px));
35
35
  pointer-events: auto;
36
36
  }
37
37
 
@@ -40,10 +40,10 @@
40
40
  // viewport in place, never sliding or scaling with the content frame.
41
41
  @include sf.scrim-fade(
42
42
  "hk-modal-overlay",
43
- $enter-duration: var(--hk-modal-duration, 0.3s),
44
- $enter-ease: var(--hk-modal-ease-out, cubic-bezier(0.16, 1, 0.3, 1)),
45
- $leave-duration: var(--hk-modal-duration, 0.3s),
46
- $leave-ease: var(--hk-modal-ease-in, cubic-bezier(0.4, 0, 1, 1))
43
+ $enter-duration: var(--hk-modal-duration, 0.42s),
44
+ $enter-ease: var(--hk-modal-ease-out, cubic-bezier(0.33, 1, 0.68, 1)),
45
+ $leave-duration: var(--hk-modal-duration, 0.42s),
46
+ $leave-ease: var(--hk-modal-ease-in, cubic-bezier(0.45, 0, 0.55, 1))
47
47
  );
48
48
 
49
49
  // ------
@@ -72,7 +72,7 @@
72
72
  height var(--duration-fast, 0.15s) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1)),
73
73
  max-height var(--duration-fast, 0.15s) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
74
74
  background: var(--hk-modal-bg, var(--hi-color-surface, rgba(240,244,248,0.95)));
75
- backdrop-filter: var(--hk-modal-blur, blur(12px));
75
+ backdrop-filter: var(--hk-modal-blur, none);
76
76
  border: 1px solid var(--hk-modal-border-color, var(--hi-color-border, rgba(100,100,100,0.08)));
77
77
  border-radius: var(--hk-modal-radius, var(--radius-lg, 12px));
78
78
  box-shadow:
@@ -109,12 +109,12 @@
109
109
  @media (min-width: 768px) {
110
110
  .hk-modal-content-enter-active {
111
111
  transition:
112
- opacity var(--hk-modal-duration, 0.3s)
113
- var(--hk-modal-ease-out, cubic-bezier(0.16, 1, 0.3, 1)),
114
- transform var(--hk-modal-duration, 0.3s)
115
- var(--hk-modal-ease-out, cubic-bezier(0.16, 1, 0.3, 1)),
116
- clip-path var(--hk-modal-duration, 0.3s)
117
- var(--hk-modal-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
112
+ opacity var(--hk-modal-duration, 0.42s)
113
+ var(--hk-modal-ease-out, cubic-bezier(0.33, 1, 0.68, 1)),
114
+ transform var(--hk-modal-duration, 0.42s)
115
+ var(--hk-modal-ease-out, cubic-bezier(0.33, 1, 0.68, 1)),
116
+ clip-path var(--hk-modal-duration, 0.42s)
117
+ var(--hk-modal-ease-out, cubic-bezier(0.33, 1, 0.68, 1));
118
118
  will-change: transform, opacity, clip-path;
119
119
  }
120
120
 
@@ -125,16 +125,11 @@
125
125
  // (user report 2026-09-14).
126
126
  .hk-modal-content-leave-active {
127
127
  transition:
128
- opacity var(--hk-modal-duration, 0.25s)
129
- var(--hk-modal-ease-in, cubic-bezier(0.4, 0, 1, 1)),
130
- transform var(--hk-modal-duration, 0.25s)
131
- var(--hk-modal-ease-in, cubic-bezier(0.4, 0, 1, 1));
128
+ opacity var(--hk-modal-duration, 0.32s)
129
+ var(--hk-modal-ease-in, cubic-bezier(0.45, 0, 0.55, 1)),
130
+ transform var(--hk-modal-duration, 0.32s)
131
+ var(--hk-modal-ease-in, cubic-bezier(0.45, 0, 0.55, 1));
132
132
  will-change: transform, opacity;
133
- // Collapse toward the frame's own top edge: the height shrink reads as
134
- // the box folding upward (the unveil's reverse), not as a centered
135
- // shrink — and the top-of-content (header) keeps its anchor while the
136
- // rest eases down and out with the fade.
137
- transform-origin: 50% 0;
138
133
  }
139
134
 
140
135
  .hk-modal-content-enter-from {
@@ -153,12 +148,12 @@
153
148
 
154
149
  .hk-modal-content-leave-from {
155
150
  opacity: 1;
156
- transform: translate(-50%, -50%) scaleY(1);
151
+ transform: translate(-50%, -50%);
157
152
  }
158
153
 
159
154
  .hk-modal-content-leave-to {
160
155
  opacity: 0;
161
- transform: translate(-50%, -50%) scaleY(0.9);
156
+ transform: translate(-50%, -50%) translateY(5%);
162
157
  }
163
158
  }
164
159
 
@@ -445,17 +440,17 @@
445
440
  // Sheet slide reveal replaces the height-clip reveal on mobile.
446
441
  .hk-modal-content-enter-active {
447
442
  transition:
448
- opacity var(--hk-modal-duration, 0.3s) ease-out,
449
- transform var(--hk-modal-duration, 0.3s)
450
- var(--hk-modal-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
443
+ opacity var(--hk-modal-duration, 0.42s) ease-out,
444
+ transform var(--hk-modal-duration, 0.42s)
445
+ var(--hk-modal-ease-out, cubic-bezier(0.33, 1, 0.68, 1));
451
446
  will-change: transform, opacity;
452
447
  }
453
448
 
454
449
  .hk-modal-content-leave-active {
455
450
  transition:
456
- opacity var(--hk-modal-duration, 0.25s) ease-in,
457
- transform var(--hk-modal-duration, 0.25s)
458
- var(--hk-modal-ease-in, cubic-bezier(0.4, 0, 1, 1));
451
+ opacity var(--hk-modal-duration, 0.32s) ease-in,
452
+ transform var(--hk-modal-duration, 0.32s)
453
+ var(--hk-modal-ease-in, cubic-bezier(0.45, 0, 0.55, 1));
459
454
  will-change: transform, opacity;
460
455
  }
461
456
 
@@ -474,13 +469,8 @@
474
469
  padding: 0.625rem 1rem 0.625rem;
475
470
  }
476
471
 
477
- // The gutter contract retunes here too — the covers and the inner's
478
- // padding both consume the vars, so one override moves both.
479
- .hk-modal-body {
480
- --hk-scroll-pad-top: 1rem;
481
- --hk-scroll-pad-right: 1rem;
482
- --hk-scroll-pad-bottom: 1rem;
483
- --hk-scroll-pad-left: 1rem;
472
+ .hk-modal-body-inner {
473
+ padding: 1rem;
484
474
  }
485
475
 
486
476
  // Lift the desktop 70vh body cap on phones: with a docked sheet the cap
@@ -491,6 +481,13 @@
491
481
  // scrolls, and the footer always sits on the sheet's bottom edge.
492
482
  .hk-modal-body {
493
483
  max-height: none;
484
+
485
+ // The gutter contract retunes here too — the covers and the inner's
486
+ // padding both consume the vars, so one override moves both.
487
+ --hk-scroll-pad-top: 1rem;
488
+ --hk-scroll-pad-right: 1rem;
489
+ --hk-scroll-pad-bottom: 1rem;
490
+ --hk-scroll-pad-left: 1rem;
494
491
  }
495
492
 
496
493
  .hk-modal-footer {
@@ -0,0 +1,92 @@
1
+ // Dock-slot contract for HkScrollContainer.
2
+ //
3
+ // A dock (dockTop / dockBottom named slot) renders as a NON-scrolling
4
+ // flex sibling of the viewport: pinned to its edge, occupying layout
5
+ // space (the flex: 1 viewport shrinks by the dock's height — content
6
+ // can never slide under it). The container measures each mounted dock
7
+ // and publishes --hk-scroll-dock-top / --hk-scroll-dock-bottom on the
8
+ // host so consumers size their content end-padding against the dock
9
+ // without hand-rolled px. The dock-edge fade rides on data-dock-fade.
10
+ //
11
+ // House style: raw createApp mounts on shared containers (no
12
+ // @vue/test-utils).
13
+ import { afterEach, describe, expect, it } from "vitest";
14
+ import { createApp, h, defineComponent } from "vue";
15
+
16
+ import HkScrollContainer from "./HkScrollContainer";
17
+
18
+ const mounts: ReturnType<typeof createApp>[] = [];
19
+ const containers: HTMLElement[] = [];
20
+
21
+ function mountDock(slots: Record<string, () => any>, props: Record<string, unknown> = {}) {
22
+ const container = document.createElement("div");
23
+ document.body.appendChild(container);
24
+ containers.push(container);
25
+ const app = createApp(defineComponent({
26
+ setup() {
27
+ return () => h(HkScrollContainer, props, slots);
28
+ },
29
+ }));
30
+ mounts.push(app);
31
+ return app.mount(container);
32
+ }
33
+
34
+ afterEach(() => {
35
+ for (const app of mounts) app.unmount();
36
+ mounts.length = 0;
37
+ for (const el of containers) el.remove();
38
+ containers.length = 0;
39
+ });
40
+
41
+ describe("HkScrollContainer dock slots", () => {
42
+ it("renders a bottom dock as a non-scrolling sibling with the fade marker", () => {
43
+ const vm = mountDock({
44
+ default: () => h("div", { class: "content" }, "rows"),
45
+ dockBottom: () => h("div", { class: "the-dock" }, "dock"),
46
+ });
47
+ const host = vm.$el as HTMLElement;
48
+ const dock = host.querySelector(".hk-scroll-dock[data-side='bottom']");
49
+ expect(dock).toBeTruthy();
50
+ // Sibling of the viewport, not inside it — the dock never scrolls.
51
+ const viewport = host.querySelector(".hk-scroll-container-viewport")!;
52
+ expect(viewport.contains(dock as Node)).toBe(false);
53
+ // The dock-edge fade engages by default for a bottom dock.
54
+ expect(host.getAttribute("data-dock-fade")).toBe("bottom");
55
+ // The measured-height vars are published on the host (0px in
56
+ // happy-dom — no layout; the contract is the vars exist + RO wired).
57
+ expect(host.style.getPropertyValue("--hk-scroll-dock-bottom")).toBe("0px");
58
+ expect(host.style.getPropertyValue("--hk-scroll-dock-top")).toBe("0px");
59
+ });
60
+
61
+ it("renders a top dock and fades the top edge instead", () => {
62
+ const vm = mountDock({
63
+ default: () => h("div", { class: "content" }, "rows"),
64
+ dockTop: () => h("div", { class: "the-dock" }, "progress"),
65
+ });
66
+ const host = vm.$el as HTMLElement;
67
+ expect(host.querySelector(".hk-scroll-dock[data-side='top']")).toBeTruthy();
68
+ expect(host.getAttribute("data-dock-fade")).toBe("top");
69
+ expect(host.querySelector(".hk-scroll-dock[data-side='bottom']")).toBeNull();
70
+ });
71
+
72
+ it("omits the dock fade marker when dockFade is off", () => {
73
+ const vm = mountDock(
74
+ {
75
+ default: () => h("div", { class: "content" }, "rows"),
76
+ dockBottom: () => h("div", { class: "the-dock" }, "dock"),
77
+ },
78
+ { dockFade: false },
79
+ );
80
+ const host = vm.$el as HTMLElement;
81
+ expect(host.querySelector(".hk-scroll-dock[data-side='bottom']")).toBeTruthy();
82
+ expect(host.getAttribute("data-dock-fade")).toBeNull();
83
+ });
84
+
85
+ it("publishes no dock marker without dock slots", () => {
86
+ const vm = mountDock({ default: () => h("div", { class: "content" }, "rows") });
87
+ const host = vm.$el as HTMLElement;
88
+ expect(host.querySelector(".hk-scroll-dock")).toBeNull();
89
+ expect(host.getAttribute("data-dock-fade")).toBeNull();
90
+ expect(host.style.getPropertyValue("--hk-scroll-dock-bottom")).toBe("");
91
+ });
92
+ });
@@ -53,6 +53,38 @@
53
53
  mask-image: linear-gradient(to left, transparent 0, #000 var(--hk-scroll-fade-size));
54
54
  }
55
55
 
56
+ /* ── Anchored docks (dockTop / dockBottom slots) ─────────────────
57
+ A dock renders as a NON-scrolling flex sibling of the viewport: it
58
+ stays pinned to its edge, never scrolls, and occupies layout space —
59
+ the flex: 1 viewport shrinks by the dock's height, so the content's
60
+ scroll end lands exactly at the dock's edge and can never slide
61
+ under it. The container measures each mounted dock (ResizeObserver)
62
+ and publishes --hk-scroll-dock-top / --hk-scroll-dock-bottom on the
63
+ host, so a consumer's content end-padding clears the dock with
64
+ breathing room (padding-bottom: calc(var(--hk-scroll-dock-bottom)
65
+ + 2rem)) instead of hand-rolled px guesses that go stale when the
66
+ dock reflows. */
67
+ .hk-scroll-dock {
68
+ flex: none;
69
+ min-height: 0;
70
+ }
71
+
72
+ /* Dock-edge fade: mid-scroll content dissolving at the dock's edge
73
+ (opacity 100 -> 0 across --hk-scroll-dock-fade) reads as intentional
74
+ instead of a hard clip against the dock's top edge. Content never
75
+ sits under the dock, so this fades the viewport's last few pixels,
76
+ not the dock itself. */
77
+ .hk-scroll-container[data-dock-fade="bottom"] > .hk-scroll-container-viewport {
78
+ -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - var(--hk-scroll-dock-fade, 2rem)), transparent);
79
+ mask-image: linear-gradient(to bottom, #000 calc(100% - var(--hk-scroll-dock-fade, 2rem)), transparent);
80
+ }
81
+
82
+ .hk-scroll-container[data-dock-fade="top"] > .hk-scroll-container-viewport {
83
+ -webkit-mask-image: linear-gradient(to bottom, transparent, #000 var(--hk-scroll-dock-fade, 2rem));
84
+ mask-image: linear-gradient(to bottom, transparent, #000 var(--hk-scroll-dock-fade, 2rem));
85
+ }
86
+
87
+
56
88
  .hk-scroll-container[data-fade="true"][data-h-overflow="start"] > .hk-scroll-container-viewport {
57
89
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--hk-scroll-fade-size));
58
90
  mask-image: linear-gradient(to right, transparent 0, #000 var(--hk-scroll-fade-size));
@@ -80,12 +80,21 @@ export default defineComponent({
80
80
  * approaching (`approachEnd`). Read live, so runtime changes need
81
81
  * no remount. */
82
82
  approachDistance: { type: Number, default: 160 },
83
+ /** Fade the viewport's bottom edge (CSS mask, --hk-scroll-dock-fade
84
+ * tall, opacity 100 -> 0) while a dock slot is mounted, so mid-
85
+ * scroll content dissolving at the dock's edge reads as
86
+ * intentional instead of a hard clip. */
87
+ dockFade: { type: Boolean, default: true },
83
88
  },
84
89
  emits: { approachEnd: () => true },
85
90
  setup(props, { slots, expose, emit }) {
86
91
  const { t } = useI18n();
87
92
  const viewportRef = ref<HTMLElement>();
93
+ const hostRef = ref<HTMLElement>();
94
+ const dockTopRef = ref<HTMLElement>();
95
+ const dockBottomRef = ref<HTMLElement>();
88
96
  let ro: ResizeObserver | null = null;
97
+ let dockRO: ResizeObserver | null = null;
89
98
  let scheduled: AnimationHandle | null = null;
90
99
  // Overlay track/thumb machinery lives in the shared composable;
91
100
  // this component keeps overflow sensing, autoFollow, the aligner,
@@ -292,10 +301,40 @@ export default defineComponent({
292
301
  }
293
302
  }
294
303
 
304
+ /** Publish each mounted dock's measured height as a host-level custom
305
+ * property (`--hk-scroll-dock-top` / `--hk-scroll-dock-bottom`). The
306
+ * dock is a non-scrolling flex sibling, so the viewport already
307
+ * shrinks by its height; the published var exists so a consumer's
308
+ * content end-padding can clear the dock with breathing room
309
+ * (padding-bottom: calc(var(--hk-scroll-dock-bottom, 0px) + 2rem))
310
+ * instead of hand-rolled px guesses that go stale when the dock
311
+ * reflows. ResizeObserver keeps it live across dock reflows. */
312
+ function publishDockSizes() {
313
+ const host = hostRef.value;
314
+ if (!host) return;
315
+ const top = dockTopRef.value;
316
+ const bottom = dockBottomRef.value;
317
+ host.style.setProperty(
318
+ "--hk-scroll-dock-top",
319
+ top ? `${Math.round(top.getBoundingClientRect().height)}px` : "0px",
320
+ );
321
+ host.style.setProperty(
322
+ "--hk-scroll-dock-bottom",
323
+ bottom ? `${Math.round(bottom.getBoundingClientRect().height)}px` : "0px",
324
+ );
325
+ }
326
+
295
327
  onMounted(() => {
296
328
  const vp = viewportRef.value;
297
329
  if (!vp) return;
298
330
 
331
+ if (dockTopRef.value || dockBottomRef.value) {
332
+ dockRO = new ResizeObserver(publishDockSizes);
333
+ if (dockTopRef.value) dockRO.observe(dockTopRef.value);
334
+ if (dockBottomRef.value) dockRO.observe(dockBottomRef.value);
335
+ publishDockSizes();
336
+ }
337
+
299
338
  if (props.scrollbar) {
300
339
  mountScrollbars();
301
340
  }
@@ -340,6 +379,8 @@ export default defineComponent({
340
379
  settleHandle = null;
341
380
  ro?.disconnect();
342
381
  ro = null;
382
+ dockRO?.disconnect();
383
+ dockRO = null;
343
384
  followRO?.disconnect();
344
385
  followRO = null;
345
386
  alignRO?.disconnect();
@@ -431,13 +472,34 @@ export default defineComponent({
431
472
  if (alignCenter()) {
432
473
  content = <div ref={setAligner} class="hk-scroll-container-aligner">{content}</div>;
433
474
  }
475
+ // Anchored dock slots: a dock renders as a NON-scrolling flex
476
+ // sibling of the viewport — it stays pinned to its edge, never
477
+ // scrolls, and occupies layout space (the flex: 1 viewport shrinks
478
+ // by the dock's height). The container measures each mounted dock
479
+ // and publishes --hk-scroll-dock-top/bottom so consumers can size
480
+ // their content end-padding against the dock without guessing px.
481
+ const dockTopSlot = slots.dockTop;
482
+ const dockBottomSlot = slots.dockBottom;
434
483
  return (
435
484
  <Tag
485
+ ref={hostRef}
436
486
  class="hk-scroll-container"
437
487
  data-axis={props.axis}
438
488
  data-align={alignCenter() ? "center" : undefined}
439
489
  data-fade={props.fade ? "true" : undefined}
490
+ data-dock-fade={
491
+ props.dockFade && dockBottomSlot
492
+ ? "bottom"
493
+ : props.dockFade && dockTopSlot
494
+ ? "top"
495
+ : undefined
496
+ }
440
497
  >
498
+ {dockTopSlot && (
499
+ <div ref={dockTopRef} class="hk-scroll-dock" data-side="top">
500
+ {dockTopSlot()}
501
+ </div>
502
+ )}
441
503
  {/* Scroll-pin host marker: the viewport participates in the pin
442
504
  contract with its live axis; generic containers declare no
443
505
  standard padding, so pins inside resolve their strategy from
@@ -449,6 +511,11 @@ export default defineComponent({
449
511
  >
450
512
  {content}
451
513
  </div>
514
+ {dockBottomSlot && (
515
+ <div ref={dockBottomRef} class="hk-scroll-dock" data-side="bottom">
516
+ {dockBottomSlot()}
517
+ </div>
518
+ )}
452
519
  {showAutoTag.value && (
453
520
  <span class="hk-scroll-container-autotag" aria-hidden="true">{t("hikari::scrollContainer.auto", "Auto")}</span>
454
521
  )}