@celestia-island/hikari 0.55.94 → 0.55.96

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.94",
3
+ "version": "0.55.96",
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;
@@ -0,0 +1,85 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ /**
7
+ * Source contract: the JSON tree's spacing rhythm is token-based, not
8
+ * ad-hoc. The 2026-09-30 normalization replaced the off-scale gaps
9
+ * (2px toggle margin, 0.35em colon, 0.75em type hint) with design
10
+ * units and added the toggle's first-line centering formula — this
11
+ * contract keeps the file from drifting back to magic values, which is
12
+ * exactly the "crooked collapsed/expanded alignment" regression class
13
+ * the normalization fixed.
14
+ */
15
+ const here = dirname(fileURLToPath(import.meta.url));
16
+ const scss = readFileSync(join(here, "HkJsonTree.scss"), "utf-8");
17
+
18
+ describe("HkJsonTree spacing contract", () => {
19
+ it("keeps the toggle exactly one spacing unit from the row text", () => {
20
+ const block = scss.slice(scss.indexOf(".s-jt-toggle {"), scss.indexOf(".s-jt-toggle[data-parent]"));
21
+ expect(block).toContain("margin-inline-end: var(--space-4");
22
+ });
23
+
24
+ it("centers the toggle box on the first text line by formula", () => {
25
+ const block = scss.slice(scss.indexOf(".s-jt-toggle {"), scss.indexOf(".s-jt-toggle[data-parent]"));
26
+ expect(block).toContain("align-self: flex-start");
27
+ expect(block).toContain("margin-block-start: max(0px, calc((1.55em - var(--jt-toggle, 14px)) / 2))");
28
+ });
29
+
30
+ it("keeps every --jt-toggle fallback at the 14px the formula assumes", () => {
31
+ // Regex sweep, not a blacklist: ANY fallback other than 14px (today
32
+ // or added later) would de-center the box by the delta.
33
+ const fallbacks = [...scss.matchAll(/var\(--jt-toggle,\s*([^)]+)\)/g)].map((m) => m[1].trim());
34
+ expect(fallbacks.length, "fallback sites were scanned").toBeGreaterThan(2);
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;");
40
+ });
41
+
42
+ it("pins the depth step to exactly one toggle width", () => {
43
+ const tsx = readFileSync(join(here, "HkJsonTree.tsx"), "utf-8");
44
+ expect(tsx).toContain("const JT_INDENT = JT_TOGGLE;");
45
+ });
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
+
69
+ it("keeps the key colon gap on one spacing unit", () => {
70
+ const block = scss.slice(scss.indexOf(".s-jt-colon {"), scss.indexOf(".s-jv-null"));
71
+ expect(block).toContain("margin-inline-end: var(--space-4");
72
+ expect(block).not.toContain("0.35em");
73
+ });
74
+
75
+ it("keeps the type hint gap on two spacing units", () => {
76
+ const block = scss.slice(scss.indexOf(".s-jt-type {"), scss.indexOf(".s-jt-badge {"));
77
+ expect(block).toContain("margin-inline-start: var(--space-8");
78
+ expect(block).not.toContain("0.75em");
79
+ });
80
+
81
+ it("leaves no off-scale magic gaps anywhere in the tree stylesheet", () => {
82
+ expect(scss).not.toMatch(/margin-inline-end: 0\.\d+em/);
83
+ expect(scss).not.toMatch(/margin-inline-start: 0\.\d+em/);
84
+ });
85
+ });
@@ -69,14 +69,20 @@
69
69
  display: inline-flex;
70
70
  align-items: center;
71
71
  justify-content: center;
72
- width: var(--jt-toggle, 1rem);
73
- height: var(--jt-toggle, 1rem);
72
+ width: var(--jt-toggle, 14px);
73
+ height: var(--jt-toggle, 14px);
74
74
  flex-shrink: 0;
75
- margin-inline-end: var(--space-2, 0.125rem);
75
+ /* Toggle → text is ONE spacing unit everywhere (leaf, collapsed and
76
+ expanded parents alike) — the collapsed row and its children read
77
+ on the same rhythm instead of ad-hoc gaps. */
78
+ margin-inline-end: var(--space-4, 0.25rem);
76
79
  /* The toggle box has no text baseline of its own — pin it to the line
77
- top instead of letting it join the row's baseline group with a
78
- synthesized border-box baseline. */
80
+ top and center it on the FIRST text line by formula (line-height
81
+ 1.55 × font-size vs the fixed box), so the chevron/leaf sits on the
82
+ text's optical center in collapsed and expanded rows alike instead
83
+ of floating above the baseline group. */
79
84
  align-self: flex-start;
85
+ margin-block-start: max(0px, calc((1.55em - var(--jt-toggle, 14px)) / 2));
80
86
  }
81
87
 
82
88
  .s-jt-toggle[data-parent] {
@@ -117,7 +123,7 @@
117
123
 
118
124
  .s-jt-colon {
119
125
  color: rgb(var(--color-muted));
120
- margin-inline-end: 0.35em;
126
+ margin-inline-end: var(--space-4, 0.25rem);
121
127
  }
122
128
 
123
129
  .s-jv-null {
@@ -152,7 +158,7 @@
152
158
  font-size: var(--text-3xs, 0.5rem);
153
159
  font-style: italic;
154
160
  font-weight: 400;
155
- margin-inline-start: 0.75em;
161
+ margin-inline-start: var(--space-8, 0.5rem);
156
162
  }
157
163
 
158
164
  .s-jt-badge {
@@ -172,9 +178,22 @@
172
178
  font-weight: 400;
173
179
  }
174
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
+
175
189
  .s-jt-preview {
176
190
  color: rgb(var(--color-muted));
177
- 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;
178
197
  font-weight: 400;
179
198
  opacity: 0.7;
180
199
  }
@@ -130,14 +130,24 @@ function initialExpandedSet(root: HJsonNode): Set<number> {
130
130
  /**
131
131
  * HkJsonTree — the interactive expandable JSON tree.
132
132
  *
133
- * This is the exact renderer the chat card flow uses for tool results
134
- * (extracted verbatim from HkToolBlock, same DOM and `s-jt-*` stylesheet, so
135
- * the two can never drift). Feed it either pre-parsed `value` or a raw JSON
136
- * `text`; non-container input renders nothing.
133
+ * A first-class family component (same tier as the rich-text surfaces):
134
+ * the chat tool blocks, the chest error landing's raw-details pane and
135
+ * any host pane render through THIS component — HkToolBlock consumes it
136
+ * directly, and the `s-jt-*` stylesheet single-sources the visual
137
+ * language, so the two can never drift. Feed it either pre-parsed
138
+ * `value` or a raw JSON `text`; non-container input renders nothing.
137
139
  *
138
140
  * The root keeps the legacy `s-tool-json-tree` class: hosts (including
139
141
  * HkToolBlock's deferred scrollbar pass) target it, and the pane chrome
140
142
  * (mono font, capped height, hidden native scrollbars) lives there.
143
+ *
144
+ * Spacing contract (pinned by HkJsonTree.contract.test.ts): the toggle
145
+ * box is 14px and sits ONE `--space-4` from the row text — identical for
146
+ * leaf dots, collapsed previews and expanded parents — and is centered
147
+ * on the first text line by formula, so collapsed and expanded states
148
+ * align with the same left rhythm; `key:` gaps are one unit, type hints
149
+ * two; the depth step is one toggle width (JT_INDENT = JT_TOGGLE,
150
+ * pinned alongside the stylesheet contract).
141
151
  */
142
152
  export const HkJsonTree = defineComponent({
143
153
  name: "HkJsonTree",
@@ -224,6 +234,7 @@ export const HkJsonTree = defineComponent({
224
234
  </span>
225
235
  {node.key !== null && <span class="s-jt-key">{node.key}</span>}
226
236
  {node.key !== null && <span class="s-jt-colon">: </span>}
237
+ <span class="s-jt-indent" />
227
238
  <span class="s-jt-preview">{node.preview}</span>
228
239
  </div>
229
240
  );
@@ -257,6 +268,7 @@ export const HkJsonTree = defineComponent({
257
268
  </span>
258
269
  {node.key !== null && <span class="s-jt-key">{node.key}</span>}
259
270
  {node.key !== null && <span class="s-jt-colon">: </span>}
271
+ <span class="s-jt-indent" />
260
272
  <span class="s-jv-str">"{truncateStr(node.stringValue, STR_PREVIEW_LEN)}"</span>
261
273
  <span class="s-jt-type">string ({node.stringValue.length})</span>
262
274
  </div>