@celestia-island/hikari 0.55.96 → 0.55.97

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.96",
3
+ "version": "0.55.97",
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",
@@ -1,4 +1,20 @@
1
+ /* Scroll end-cap: trailing air BELOW the last row. When the nav list
2
+ overflows, the old 4px tail let the final item kiss the bottom edge at
3
+ full scroll — indistinguishable from "there is more below" (user
4
+ screenshot on the chest admin sidebar, 2026-10-01: the last item sat
5
+ flush against the panel's bottom boundary). One option-sized tail now
6
+ rides every sidebar body — desktop rail and mobile drawer alike — so
7
+ a fully scrolled list visibly ends ABOVE the edge.
8
+ The token is declared on the RAIL, not on the body: an element's own
9
+ custom-property declaration always beats an inherited value, so a
10
+ body-level declaration would silently kill the knob for hosts (the
11
+ same trap the menu-item sheet tokens document). Declared here, a host
12
+ retunes (or zeroes) it on ANY ancestor up to the rail — or straight
13
+ on the body for a one-off. Keep the fallback in step with the token
14
+ it mirrors. */
1
15
  .hk-sidebar {
16
+ --hk-sidebar-scroll-tail: var(--space-24, 1.5rem);
17
+
2
18
  flex-shrink: 0;
3
19
  height: 100%;
4
20
  transition: width var(--duration-normal, 0.3s) ease;
@@ -40,7 +56,10 @@
40
56
  flex: 1;
41
57
  min-height: 0;
42
58
  overflow-y: auto;
43
- padding-block: var(--space-20) var(--space-4);
59
+ /* Spends the rail's --hk-sidebar-scroll-tail (see top of file) — the
60
+ * body deliberately does NOT re-declare it, or host retunes could
61
+ * never reach the tail through inheritance. */
62
+ padding-block: var(--space-20) var(--hk-sidebar-scroll-tail);
44
63
  padding-inline: var(--space-12);
45
64
 
46
65
  /* Overlay scrollbar (useOverlayScrollbar) — the native chrome is
@@ -65,7 +84,10 @@
65
84
  }
66
85
 
67
86
  .hk-sidebar-body {
68
- padding-block: var(--space-12) var(--space-4);
87
+ /* Same end-cap as the expanded body — collapsed rows are icon-only
88
+ * but the tail's job (announcing the end of the scroll range) is
89
+ * identical, so only the leading air shrinks here. */
90
+ padding-block: var(--space-12) var(--hk-sidebar-scroll-tail);
69
91
  }
70
92
 
71
93
  .hk-sidebar-footer {
@@ -0,0 +1,103 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { compile } from "sass";
6
+ import { describe, expect, it } from "vitest";
7
+
8
+ /**
9
+ * Scroll end-cap contract for the HkSidebar body.
10
+ *
11
+ * The sidebar body is the scroll viewport for every nav list the library
12
+ * hosts (desktop rail, mobile drawer). When the list overflows, the air
13
+ * BELOW the last row is the only "this is the end" signal a scroller
14
+ * gets: a tail the size of a hairline reads as "keep going". The 4px
15
+ * tail let the final item kiss the panel's bottom edge at full scroll
16
+ * (user screenshot on the chest admin sidebar, 2026-10-01 — the last
17
+ * item sat flush against the panel's bottom boundary), so the rail now
18
+ * declares an option-sized `--hk-sidebar-scroll-tail` and both density
19
+ * variants of the body spend it as their block-end padding.
20
+ *
21
+ * The structural assertions run on the COMPILED sheet, not the source
22
+ * text, so a declaration cannot hide behind SCSS nesting, and comments
23
+ * are stripped first so a commented-out tail cannot keep a hollow guard
24
+ * green. Two pins deliberately break that rule and read the raw source:
25
+ * the fallback literal and the single-declaration count (each explains
26
+ * why in place).
27
+ */
28
+ const componentDir = resolve(dirname(fileURLToPath(import.meta.url)));
29
+
30
+ function sidebarCss(): string {
31
+ const file = resolve(componentDir, "HkSidebar.scss");
32
+ return compile(file, { style: "expanded", loadPaths: [componentDir] }).css.replace(
33
+ /\/\*[\s\S]*?\*\//g,
34
+ "",
35
+ );
36
+ }
37
+
38
+ function ruleBody(css: string, selector: string): string | null {
39
+ const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
40
+ const m = css.match(new RegExp(`${escaped}\\s*\\{([^}]*)\\}`));
41
+ return m ? m[1]! : null;
42
+ }
43
+
44
+ /** The exact token expression the tail is declared with. */
45
+ const TAIL_DECL = "--hk-sidebar-scroll-tail: var(--space-24, 1.5rem)";
46
+
47
+ describe("HkSidebar scroll end-cap", () => {
48
+ const css = sidebarCss();
49
+
50
+ it("compiles", () => {
51
+ expect(css.length).toBeGreaterThan(0);
52
+ // Sanity anchors so a renamed file cannot hollow this guard.
53
+ expect(css).toContain(".hk-sidebar-body");
54
+ expect(css).toContain(".hk-sidebar-footer");
55
+ });
56
+
57
+ it("declares the tail token on the rail, not the body", () => {
58
+ // The rail-level declaration is what makes the knob real for hosts:
59
+ // an element's own custom-property declaration always beats an
60
+ // inherited value, so a body-level declaration would silently kill
61
+ // ancestor retunes (the menu-item sheet-token trap).
62
+ const rail = ruleBody(css, ".hk-sidebar");
63
+ expect(rail, "the rail rule exists").not.toBeNull();
64
+ expect(rail).toContain(TAIL_DECL);
65
+ const body = ruleBody(css, ".hk-sidebar-body");
66
+ expect(body, "the body rule exists").not.toBeNull();
67
+ expect(body).not.toContain("--hk-sidebar-scroll-tail:");
68
+ });
69
+
70
+ it("spends the tail as the body's block-end padding", () => {
71
+ const body = ruleBody(css, ".hk-sidebar-body");
72
+ expect(body).toContain("padding-block: var(--space-20) var(--hk-sidebar-scroll-tail)");
73
+ // The old hairline tail must not sneak back through another spelling.
74
+ expect(body).not.toMatch(/padding-block:[^;]*--space-4/);
75
+ });
76
+
77
+ it("keeps the same tail on the collapsed body", () => {
78
+ const body = ruleBody(css, ".hk-sidebar[data-collapsed] .hk-sidebar-body");
79
+ expect(body, "the collapsed body rule exists").not.toBeNull();
80
+ expect(body).toContain("padding-block: var(--space-12) var(--hk-sidebar-scroll-tail)");
81
+ });
82
+
83
+ it("reads the tail fallback from the scss source, not a drifted literal", () => {
84
+ // The fallback lives once, next to the token it mirrors — pin the
85
+ // source line so a retune of the token cannot leave a stale literal
86
+ // behind (the compiled-sheet pins above would both have to move).
87
+ const source = readFileSync(resolve(componentDir, "HkSidebar.scss"), "utf8");
88
+ expect(source).toContain("--hk-sidebar-scroll-tail: var(--space-24, 1.5rem);");
89
+ });
90
+
91
+ it("declares the tail token exactly once in the source", () => {
92
+ // The rule-body pins above inspect only specific rules, so a second
93
+ // --hk-sidebar-scroll-tail declaration sneaking into a mid-tree rule
94
+ // (.hk-sidebar-panel, .hk-sidebar-body-wrap, the collapsed block)
95
+ // would silently re-kill the host knob — nearest-ancestor
96
+ // declaration wins over the rail's — while every pin stays green.
97
+ // Counting the source occurrences closes that hollow path (and
98
+ // enforces the fallback's uniqueness for real, unlike toContain).
99
+ // The pattern takes any colon spelling, spaced or not.
100
+ const source = readFileSync(resolve(componentDir, "HkSidebar.scss"), "utf8");
101
+ expect(source.match(/--hk-sidebar-scroll-tail\s*:/g)).toHaveLength(1);
102
+ });
103
+ });