@celestia-island/hikari 0.55.98 → 0.55.99

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.98",
3
+ "version": "0.55.99",
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",
@@ -208,38 +208,32 @@
208
208
  user-select: none;
209
209
  }
210
210
 
211
- // Content-sized details pane under a hard ceiling: the pane hugs its
212
- // payload — folding the JSON tree down to one summary row shrinks the
213
- // frame with it (a ~20vh empty box behind a single line read as dead
214
- // space, the 2026-10-01 screenshot report) — while a long stack trace
215
- // still caps at the same ceiling and scrolls inside the frame instead of
216
- // stretching the card. The ceiling term keeps the 9rem alternative so
217
- // short viewports (20vh < 9rem) still grant tall stacks nine rem of
218
- // scroll area; a small payload is allowed BELOW that — hugging is the
219
- // point. The pane div is the
220
- // track's positioning context and wraps EXACTLY the scrolling body
221
- // (useOverlayScrollbar host contract); the family overlay scrollbar's
222
- // observers re-read thumb geometry on every fold/expand, so the
223
- // follow-the-content height rides the same update path.
211
+ // Standing-frame details pane: the raw payload keeps a STABLE height —
212
+ // the larger of a 9rem floor and ~20vh — so the frame pre-elevates a
213
+ // little but NEVER breathes with the content. The 2026-10-01
214
+ // content-hugging frame shipped and was reverted the same day after
215
+ // mobile review: folding the tree shrank the whole error card with it
216
+ // and the jumping height read as broken. Folding only changes what is
217
+ // INSIDE the frame; a long stack trace scrolls inside it instead of
218
+ // stretching the card. The 9rem term keeps short viewports (20vh < 9rem)
219
+ // at nine rem of scroll area. The pane div is the track's positioning
220
+ // context and wraps EXACTLY the scrolling body (useOverlayScrollbar
221
+ // host contract); the family overlay scrollbar's observers keep the
222
+ // thumb geometry fresh on fold/expand.
224
223
  .hk-error-landing__details-pane {
225
224
  position: relative;
226
225
  margin-top: var(--space-6, 0.375rem);
227
- max-height: max(9rem, 20vh);
228
- max-height: max(9rem, 20dvh);
226
+ height: max(9rem, 20vh);
227
+ height: max(9rem, 20dvh);
229
228
  border: 1px solid rgb(var(--hel-border) / 70%);
230
229
  border-radius: var(--radius-md, 10px);
231
230
  background: rgb(var(--hel-muted) / 4%);
232
231
  }
233
232
 
234
233
  .hk-error-landing__details-body {
235
- // The pane's height IS the body's content height now, so the body can
236
- // not reference it (a percentage height against an auto parent is
237
- // circular) — instead it inherits the pane's cap and scrolls against
238
- // it: small content hugs, tall content clamps at the ceiling and the
239
- // overlay scrollbar carries the overflow. A max cap (not a fixed
240
- // height) keeps the shrink direction free; overflow != visible is what
241
- // lets the block actually clamp.
242
- max-height: inherit;
234
+ // The frame's height is fixed, so the body simply fills it; a fold or
235
+ // an expand re-renders INSIDE the frame and never moves the card.
236
+ height: 100%;
243
237
  overflow: auto;
244
238
  border-radius: inherit;
245
239
  padding: var(--space-4, 0.25rem);
@@ -0,0 +1,80 @@
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 error landing's raw-details pane is a STANDING
8
+ * frame — the larger of 9rem and ~20vh, fixed. The frame must never
9
+ * breathe with its content: the 2026-10-01 content-hugging experiment
10
+ * (max-height cap + inheriting body) shipped and was reverted the same
11
+ * day after mobile review — folding the tree shrank the whole error card
12
+ * and the jumping height read as broken. These pins keep BOTH regression
13
+ * classes out: the breathing class (max-height instead of height on the
14
+ * pane, an inheriting/max-capped body) and the late-re-declaration class
15
+ * (a later same-file override re-pinning either box).
16
+ */
17
+ const here = dirname(fileURLToPath(import.meta.url));
18
+ const scss = readFileSync(join(here, "HkErrorLanding.scss"), "utf-8");
19
+
20
+ describe("HkErrorLanding standing frame contract", () => {
21
+ const paneAt = scss.indexOf(".hk-error-landing__details-pane {");
22
+ const bodyAt = scss.indexOf(".hk-error-landing__details-body {");
23
+
24
+ it("declares the pane and body exactly once — no late re-declaration", () => {
25
+ // Whole-file site count: the slice-scoped sweeps below read only up
26
+ // to the nested tree rule, so a LATER `.hk-error-landing__details-
27
+ // body { height: ... }` in this file would cascade over the frame
28
+ // while every slice pin stays green (the R3-M4 mutation class).
29
+ // Pristine file has exactly two sites (pane + body); the descendant
30
+ // `.s-tool-json-tree` rule and the label/toggle selectors don't match.
31
+ // Keep this pin at strict equality — softening it to >= silently
32
+ // reopens the late-re-declaration class.
33
+ const sites = scss.match(/\.hk-error-landing__details-(?:pane|body)\s*[,{]/g) ?? [];
34
+ expect(sites.length, `details pane/body selector sites = ${sites.join(" | ")}`).toBe(2);
35
+ });
36
+
37
+ it("fixes the pane at the standing frame, never a content-driven cap", () => {
38
+ expect(paneAt, "pane rule present").toBeGreaterThanOrEqual(0);
39
+ const pane = scss.slice(paneAt, bodyAt);
40
+ // Both viewport lines: the vh fallback pairs with the dvh override,
41
+ // matching the family pattern.
42
+ expect(pane).toContain("height: max(9rem, 20vh)");
43
+ expect(pane).toContain("height: max(9rem, 20dvh)");
44
+ // Every height declaration must BE the frame pair — a later `height:
45
+ // auto` (or an extra one) would let the frame breathe again.
46
+ const heights = [...pane.matchAll(/^\s*height:.*$/gm)].map((m) => m[0].trim());
47
+ expect(heights.length, `pane height declarations = ${JSON.stringify(heights)}`).toBe(2);
48
+ for (const value of heights) expect(value).toMatch(/^height: max\(9rem, 20d?vh\);$/);
49
+ // A content-driven cap is EXACTLY the reverted breathing frame.
50
+ expect(pane).not.toContain("max-height:");
51
+ expect(pane).not.toContain("min-height:");
52
+ });
53
+
54
+ it("fills the fixed frame instead of hugging the payload", () => {
55
+ expect(bodyAt, "body rule present").toBeGreaterThanOrEqual(0);
56
+ const nextAt = scss.indexOf(".hk-error-landing__details-body .s-tool-json-tree");
57
+ // The slice boundary is load-bearing: the sweeps below must never
58
+ // silently extend to EOF (where unrelated rules would trip them).
59
+ expect(nextAt, "tree-neutering rule present").toBeGreaterThanOrEqual(0);
60
+ const body = scss.slice(bodyAt, nextAt);
61
+ // The body fills the fixed frame — its ONLY height declaration is
62
+ // the 100% fill; any other height (auto/inherit/max cap) is the
63
+ // reverted content-hugging regression (the R2-M-D mutation class).
64
+ const bodyHeights = [...body.matchAll(/^\s*height:.*$/gm)].map((m) => m[0].trim());
65
+ expect(bodyHeights, `body height declarations = ${JSON.stringify(bodyHeights)}`).toEqual(["height: 100%;"]);
66
+ expect(body).not.toContain("max-height:");
67
+ expect(body).not.toContain("min-height:");
68
+ // Long content still scrolls inside the frame.
69
+ expect(body).toContain("overflow: auto");
70
+ });
71
+
72
+ it("keeps the mobile dvh override after its vh fallback", () => {
73
+ // The pair is order-sensitive by convention (the later dvh line wins
74
+ // where dvh is supported); nothing may reorder or deduplicate it.
75
+ const vhAt = scss.indexOf("height: max(9rem, 20vh)");
76
+ const dvhAt = scss.indexOf("height: max(9rem, 20dvh)");
77
+ expect(vhAt, "vh fallback present").toBeGreaterThanOrEqual(0);
78
+ expect(dvhAt, "dvh override present").toBeGreaterThan(vhAt);
79
+ });
80
+ });
@@ -95,8 +95,8 @@ describe("HkErrorLanding", () => {
95
95
  const el = mountLanding({
96
96
  details: () => h("pre", { class: "s-tool-json-tree" }, "raw"),
97
97
  });
98
- // No collapse affordance — the pane is always open; its frame now
99
- // follows the payload under the ceiling cap (fold contract in scss).
98
+ // No collapse affordance — the pane is a standing frame; folding
99
+ // changes only its content (standing-frame contract in scss).
100
100
  expect(el.querySelector(".hk-error-landing__details-toggle")).toBeNull();
101
101
  expect(el.querySelector(".hk-error-landing__details-label")!.textContent).toContain("Raw error details");
102
102
  expect(el.querySelector(".hk-error-landing__details-pane")).not.toBeNull();
@@ -44,14 +44,14 @@ function normalizeSlotNodes(nodes: VNode | VNode[] | undefined): VNode[] {
44
44
  * carrying a tone icon, a (pre-translated) title and description, the wire
45
45
  * error code / HTTP status as HkBadge chips above the headline, an
46
46
  * always-open raw-details pane (the default slot — hosts render HkJsonTree
47
- * there) that hugs its payload under a hard ~20vh ceiling carried by the
48
- * family overlay scrollbar, and an actions slot.
47
+ * there) with a STABLE standing frame — the larger of 9rem and ~20vh,
48
+ * carried by the family overlay scrollbar — and an actions slot.
49
49
  *
50
- * The details pane never grows past its frame, but it follows its content
51
- * downward: folding every JSON node shrinks the pane to the surviving
52
- * summary row (a standing ~20vh empty box behind one line reads as dead
53
- * space), and a long stack trace scrolls inside the capped frame instead
54
- * of stretching the card.
50
+ * The pane never breathes with its content: folding every JSON node only
51
+ * changes what renders INSIDE the standing frame, and a long stack trace
52
+ * scrolls inside it instead of stretching the card. (The 2026-10-01
53
+ * content-hugging frame was reverted the same day: on mobile the whole
54
+ * error card visibly jumped between fold states, which read as broken.)
55
55
  *
56
56
  * The component is presentation-only and route-agnostic: it never touches
57
57
  * the router and can be mounted by an SPA overlay, a modal, or a standalone
@@ -111,12 +111,11 @@ export const HkErrorLanding = defineComponent({
111
111
 
112
112
  const detailsBodyRef = ref<HTMLElement | null>(null);
113
113
  let detailsScrollbars: OverlayScrollbarHandle | null = null;
114
- // The composable's own viewport ResizeObserver covers the pane cap
115
- // moving with the viewport, and a fold/expand DOES resize the
116
- // content-sized body — but a fold re-renders HkJsonTree internally
117
- // without touching the landing's own render, so observing the CONTENT
118
- // element (the tree root) too keeps the thumb geometry re-read on
119
- // every fold/expand by whichever observer sees it first.
114
+ // The pane's viewport box is fixed, so the composable's own viewport
115
+ // ResizeObserver never fires when the slot content changes size — and
116
+ // folding a JSON node re-renders HkJsonTree internally, so the landing
117
+ // itself does not re-render either. Observe the CONTENT element (the
118
+ // tree root) so every fold/expand re-reads the thumb geometry.
120
119
  let contentResizeObserver: ResizeObserver | null = null;
121
120
  let observedContent: Element | null = null;
122
121
 
@@ -1,63 +0,0 @@
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 error landing's raw-details pane follows its
8
- * content under a hard ceiling (2026-10-01 screenshot round). The frame
9
- * used to stand at a fixed ~20vh — folding the tree then left a tall
10
- * empty box behind a single summary row. The pane now hugs the payload
11
- * (max-height cap + inheriting body) while tall stack traces still clamp
12
- * and scroll inside the frame. These pins keep the standing-frame
13
- * regression class (any `height:` declaration here re-pins the box) and
14
- * the unbounded-growth class (a missing body cap) out.
15
- */
16
- const here = dirname(fileURLToPath(import.meta.url));
17
- const scss = readFileSync(join(here, "HkErrorLanding.scss"), "utf-8");
18
-
19
- describe("HkErrorLanding details fold contract", () => {
20
- const paneAt = scss.indexOf(".hk-error-landing__details-pane {");
21
- const bodyAt = scss.indexOf(".hk-error-landing__details-body {");
22
-
23
- it("declares the pane and body exactly once — no late re-declaration", () => {
24
- // Whole-file site count: the slice-scoped sweeps below read only up
25
- // to the nested tree rule, so a LATER `.hk-error-landing__details-
26
- // body { height: ... }` in this file would cascade over the fold
27
- // while every slice pin stays green (R3's M4 mutation proved it).
28
- // Pristine file has exactly two sites (pane + body); the descendant
29
- // `.s-tool-json-tree` rule and the label/toggle selectors don't match.
30
- const sites = scss.match(/\.hk-error-landing__details-(?:pane|body)\s*[,{]/g) ?? [];
31
- expect(sites.length, `details pane/body selector sites = ${sites.join(" | ")}`).toBe(2);
32
- });
33
-
34
- it("sizes the pane by cap, never by fixed height", () => {
35
- expect(paneAt, "pane rule present").toBeGreaterThanOrEqual(0);
36
- const pane = scss.slice(paneAt, bodyAt);
37
- // Both viewport lines: the vh fallback pairs with the dvh override,
38
- // matching the family pattern.
39
- expect(pane).toContain("max-height: max(9rem, 20vh)");
40
- expect(pane).toContain("max-height: max(9rem, 20dvh)");
41
- // ANY height/min-height declaration re-pins the standing frame —
42
- // exactly the regression this fold fixes (a min-height would force
43
- // the folded pane back up; R2 F3) — so the pane carries none.
44
- const heights = [...pane.matchAll(/^\s*(?:min-)?height:.*$/gm)].map((m) => m[0].trim());
45
- expect(heights, `pane height declarations = ${JSON.stringify(heights)}`).toEqual([]);
46
- });
47
-
48
- it("clamps the scrolling body with the inherited pane cap", () => {
49
- expect(bodyAt, "body rule present").toBeGreaterThanOrEqual(0);
50
- const nextAt = scss.indexOf(".hk-error-landing__details-body .s-tool-json-tree");
51
- const body = scss.slice(bodyAt, nextAt);
52
- // Percentage height against an auto-height parent is circular; the
53
- // body must take the pane's cap itself.
54
- expect(body).toContain("max-height: inherit");
55
- // A fixed body height re-pins the standing frame through the back
56
- // door even with the inherit present (R2 F2) — same sweep as the
57
- // pane: no height/min-height declarations at all.
58
- const bodyHeights = [...body.matchAll(/^\s*(?:min-)?height:.*$/gm)].map((m) => m[0].trim());
59
- expect(bodyHeights, `body height declarations = ${JSON.stringify(bodyHeights)}`).toEqual([]);
60
- // The clamp only bites when the body may actually shrink and scroll.
61
- expect(body).toContain("overflow: auto");
62
- });
63
- });