@celestia-island/hikari 0.55.95 → 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.95",
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",
@@ -13,9 +13,11 @@
13
13
  gap: 0.3rem;
14
14
  max-width: 11rem;
15
15
  /* Logical axes: the wide side reserves caret room at inline-end and
16
- * flips together with the host input's RTL suffix/prefix side. */
17
- padding-block: 2px;
18
- padding-inline: 4px 6px;
16
+ * flips together with the host input's RTL suffix/prefix side.
17
+ * Roomy padding: the pill's fill must not hug the label — the text
18
+ * keeps clear air on both sides (user direction, round N). */
19
+ padding-block: 4px;
20
+ padding-inline: 9px 11px;
19
21
  border: none;
20
22
  border-radius: var(--radius-sm, 6px);
21
23
  background: transparent;
@@ -33,6 +33,10 @@ describe("HkJsonTree spacing contract", () => {
33
33
  const fallbacks = [...scss.matchAll(/var\(--jt-toggle,\s*([^)]+)\)/g)].map((m) => m[1].trim());
34
34
  expect(fallbacks.length, "fallback sites were scanned").toBeGreaterThan(2);
35
35
  expect(fallbacks.every((f) => f === "14px"), `fallbacks = ${fallbacks}`).toBe(true);
36
+ // The canonical definition itself: the TSX drives depth steps and
37
+ // guide lines from JT_TOGGLE = 14 — a different definition desyncs
38
+ // CSS geometry from the TS geometry with zero other red.
39
+ expect(scss).toContain("--jt-toggle: 14px;");
36
40
  });
37
41
 
38
42
  it("pins the depth step to exactly one toggle width", () => {
@@ -40,6 +44,28 @@ describe("HkJsonTree spacing contract", () => {
40
44
  expect(tsx).toContain("const JT_INDENT = JT_TOGGLE;");
41
45
  });
42
46
 
47
+ it("reads collapsed summaries one tab in, at full row size", () => {
48
+ const tsx = readFileSync(join(here, "HkJsonTree.tsx"), "utf-8");
49
+ // Both collapsed shapes (object preview and long string) carry the
50
+ // one-tab slot before the summary — the collapsed text sits in the
51
+ // child column instead of hugging the chevron.
52
+ expect(tsx.match(/<span class="s-jt-indent" \/>/g)?.length).toBe(2);
53
+ const preview = scss.slice(scss.indexOf(".s-jt-preview {"));
54
+ // A smaller preview font would shrink a keyless row's line box below
55
+ // the toggle centering assumption and float the row high.
56
+ expect(preview).toContain("font-size: inherit");
57
+ expect(preview).not.toContain("0.5rem");
58
+ });
59
+
60
+ it("keeps the collapsed summary indent slot exactly one tab wide", () => {
61
+ const indent = scss.slice(scss.indexOf(".s-jt-indent {"), scss.indexOf(".s-jt-preview {"));
62
+ // Deleting the whole rule (not just tweaking it) must also go red —
63
+ // this slot IS the second-round "collapsed text hugs the chevron" fix.
64
+ expect(indent, ".s-jt-indent rule present").not.toBe("");
65
+ expect(indent).toContain("width: var(--jt-toggle, 14px)");
66
+ expect(indent).toContain("flex-shrink: 0");
67
+ });
68
+
43
69
  it("keeps the key colon gap on one spacing unit", () => {
44
70
  const block = scss.slice(scss.indexOf(".s-jt-colon {"), scss.indexOf(".s-jv-null"));
45
71
  expect(block).toContain("margin-inline-end: var(--space-4");
@@ -178,9 +178,22 @@
178
178
  font-weight: 400;
179
179
  }
180
180
 
181
+ .s-jt-indent {
182
+ /* The collapsed summary reads one tab in — the same column the
183
+ children occupy when the node is expanded — instead of hugging the
184
+ chevron. Kept as a flex slot (not padding) so it can never shrink. */
185
+ flex-shrink: 0;
186
+ width: var(--jt-toggle, 14px);
187
+ }
188
+
181
189
  .s-jt-preview {
182
190
  color: rgb(var(--color-muted));
183
- font-size: var(--text-3xs, 0.5rem);
191
+ /* Full row size: a keyless collapsed root's ONLY text is the preview,
192
+ and a smaller font here would shrink the line box below the toggle's
193
+ centering assumption and float the whole row high (the 2026-09-30
194
+ second-round screenshot report). Quietness comes from the muted
195
+ color, not a smaller size. */
196
+ font-size: inherit;
184
197
  font-weight: 400;
185
198
  opacity: 0.7;
186
199
  }
@@ -234,6 +234,7 @@ export const HkJsonTree = defineComponent({
234
234
  </span>
235
235
  {node.key !== null && <span class="s-jt-key">{node.key}</span>}
236
236
  {node.key !== null && <span class="s-jt-colon">: </span>}
237
+ <span class="s-jt-indent" />
237
238
  <span class="s-jt-preview">{node.preview}</span>
238
239
  </div>
239
240
  );
@@ -267,6 +268,7 @@ export const HkJsonTree = defineComponent({
267
268
  </span>
268
269
  {node.key !== null && <span class="s-jt-key">{node.key}</span>}
269
270
  {node.key !== null && <span class="s-jt-colon">: </span>}
271
+ <span class="s-jt-indent" />
270
272
  <span class="s-jv-str">"{truncateStr(node.stringValue, STR_PREVIEW_LEN)}"</span>
271
273
  <span class="s-jt-type">string ({node.stringValue.length})</span>
272
274
  </div>
@@ -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
+ });