@celestia-island/hikari 0.55.106 → 0.55.107

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.106",
3
+ "version": "0.55.107",
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,13 +1,14 @@
1
1
  /**
2
- * Source contract: the inline landing sits FLUSH under its context. The
3
- * `.is-inline` variant must drop the viewport takeover's block-axis
4
- * padding entirely (the host pane already pads and a sibling header sits
5
- * immediately above — the old 1.25rem top padding floated the card a
6
- * full padding-frame below its context) and slim the card's page-grade
7
- * top padding (2.25rem desktop / 1.75rem mobile) to one space step. The
8
- * horizontal inset stays so a narrow pane never seats the card
9
- * edge-to-edge. A class-name test cannot see this; the compiled contract
10
- * lives here.
2
+ * Source contract: the inline landing CENTERS inside its host region. The
3
+ * `.is-inline` variant must fill the host container's height (so the base
4
+ * centering seats the card mid-container — the inline twin of the page
5
+ * variant centering in the viewport), carry no block-axis padding (the
6
+ * #695 lesson: a hardcoded top inset read as "floating too low" under a
7
+ * sibling header), and center the card via overflow-safe auto margins
8
+ * (collapse to top-anchored when the card is taller than the container,
9
+ * instead of a both-edge clip nothing can scroll to). The horizontal
10
+ * inset stays so a narrow pane never seats the card edge-to-edge. A
11
+ * class-name test cannot see this; the compiled contract lives here.
11
12
  */
12
13
  import { describe, expect, it } from "vitest";
13
14
  import { readFileSync } from "node:fs";
@@ -16,7 +17,32 @@ import { fileURLToPath } from "node:url";
16
17
 
17
18
  const here = dirname(fileURLToPath(import.meta.url));
18
19
 
19
- /** Extract one rule's body with nesting-aware brace matching. */
20
+ /** Strip SCSS comments — line and block form — so a contract literal can
21
+ * only ever be satisfied by a REAL declaration, never by prose: a
22
+ * commented-out `margin: auto` (line-comment mutation found in R1,
23
+ * block-comment in R2) would otherwise false-green `toContain`. A `//`
24
+ * preceded by `:` is a URL scheme (`https://`) and survives. Block
25
+ * comments go first so a `//` hidden inside them can't leak through. */
26
+ function stripComments(css: string): string {
27
+ return stripLineComments(css.replace(/\/\*[\s\S]*?\*\//g, ""));
28
+ }
29
+
30
+ /** Strip SCSS `//` line comments; `://` URL schemes survive. */
31
+ function stripLineComments(css: string): string {
32
+ return css
33
+ .split("\n")
34
+ .map((line) => {
35
+ let i = line.indexOf("//");
36
+ while (i !== -1 && line[i - 1] === ":") {
37
+ i = line.indexOf("//", i + 1);
38
+ }
39
+ return i === -1 ? line : line.slice(0, i);
40
+ })
41
+ .join("\n");
42
+ }
43
+
44
+ /** Extract one rule's body with nesting-aware brace matching, prose
45
+ * stripped. */
20
46
  function ruleBody(css: string, selector: string): string {
21
47
  const start = css.indexOf(`${selector} {`);
22
48
  expect(start, `rule ${selector} present`).toBeGreaterThanOrEqual(0);
@@ -26,23 +52,57 @@ function ruleBody(css: string, selector: string): string {
26
52
  if (css[i] === "{") depth++;
27
53
  else if (css[i] === "}") {
28
54
  depth--;
29
- if (depth === 0) return css.slice(open + 1, i);
55
+ if (depth === 0) return stripComments(css.slice(open + 1, i));
30
56
  }
31
57
  }
32
58
  throw new Error(`unbalanced braces in ${selector}`);
33
59
  }
34
60
 
61
+ describe("contract-test comment stripping", () => {
62
+ it("strips comments (positive control — the guard must be able to bite)", () => {
63
+ // Without this control a broken stripper would silently turn every
64
+ // assertion above into prose-matching; 0-strip must be LOUD.
65
+ const dirty = "// min-height: 100%\n min-height: 100%;";
66
+ expect(stripComments(dirty)).not.toContain("//");
67
+ expect(stripComments(dirty).trim()).toBe("min-height: 100%;");
68
+ // Block comments are prose too (the R2 mutation shape: a commented-out
69
+ // declaration must never satisfy the contract).
70
+ const blocked = "margin: initial; /* margin: auto */";
71
+ expect(stripComments(blocked)).toContain("margin: initial;");
72
+ expect(stripComments(blocked)).not.toContain("margin: auto");
73
+ // URL schemes survive intact.
74
+ expect(stripComments('icon: url("https://cdn.example/x.svg");')).toContain("://");
75
+ });
76
+ });
77
+
35
78
  describe("HkErrorLanding inline contract", () => {
36
79
  const css = readFileSync(join(here, "HkErrorLanding.scss"), "utf-8");
37
80
  const inline = ruleBody(css, ".hk-error-landing.is-inline");
38
81
 
82
+ it("fills the host region's height so the shared centering can seat the card", () => {
83
+ // `min-height: 0` collapsed the root to its content height, which
84
+ // silently disabled the base align/justify centering and glued the
85
+ // card to the container's top edge.
86
+ expect(inline).toContain("min-height: 100%");
87
+ expect(inline).not.toContain("min-height: 0");
88
+ });
89
+
39
90
  it("drops the block-axis padding entirely and keeps only the horizontal inset", () => {
40
- expect(inline).toContain("min-height: 0");
41
91
  expect(inline).toContain("padding: 0 var(--space-12, 0.75rem)");
42
92
  // The takeover padding must not survive in any form.
43
93
  expect(inline).not.toContain("1.25rem");
44
94
  });
45
95
 
96
+ it("centers the card through overflow-safe auto margins", () => {
97
+ const card = ruleBody(inline, ".hk-error-landing__card");
98
+ // Auto margins absorb positive free space (centering) and collapse to
99
+ // 0 on overflow (no both-edge clip).
100
+ expect(card).toContain("margin: auto");
101
+ // The overflow anchor: with the margins collapsed the card must pin
102
+ // to the container's TOP edge — reachable — never float mid-clip.
103
+ expect(inline).toContain("align-items: flex-start");
104
+ });
105
+
46
106
  it("slims the card's top padding below the page-grade value", () => {
47
107
  const card = ruleBody(inline, ".hk-error-landing__card");
48
108
  // The shorthand's block-start value IS the slim step, not the
@@ -63,3 +123,20 @@ describe("HkErrorLanding multiline headline contract", () => {
63
123
  expect(title).toContain("overflow-wrap: anywhere");
64
124
  });
65
125
  });
126
+
127
+ describe("HkErrorLanding page-variant seating contract", () => {
128
+ const css = readFileSync(join(here, "HkErrorLanding.scss"), "utf-8");
129
+ const base = ruleBody(css, ".hk-error-landing");
130
+
131
+ it("fills the viewport and centers the card in it", () => {
132
+ // The page variant's whole seating model: a full-viewport backdrop
133
+ // with the card centered in it (standalone error pages, the chest
134
+ // overlay, the reporting takeover all ride this rule). R2 flipped
135
+ // align-items to flex-end and every landing test stayed green — this
136
+ // pin closes that hole.
137
+ expect(base).toContain("min-height: 100vh");
138
+ expect(base).toContain("min-height: 100dvh");
139
+ expect(base).toContain("align-items: center");
140
+ expect(base).toContain("justify-content: center");
141
+ });
142
+ });
@@ -276,21 +276,42 @@
276
276
  // in-place instead of covering the whole screen, or when a host view
277
277
  // renders it as its failed content region right under a sibling header.
278
278
  .hk-error-landing.is-inline {
279
- min-height: 0;
280
- // In-pane, the host region already pads and a sibling header usually
281
- // sits immediately above: the takeover's block-axis padding floated
282
- // the card a full padding-frame below its context. The vertical
283
- // padding is dropped entirely (the card's own slimmer top padding
284
- // below supplies the breathing room); the horizontal inset stays so a
285
- // narrow pane never seats the card edge-to-edge.
279
+ // Fill the HOST REGION's height so the base centering (the align/justify
280
+ // center on .hk-error-landing — this element matches that selector too,
281
+ // which is how the centering reaches the inline variant) seats the card
282
+ // in the middle
283
+ // of whatever container it landed in — the inline twin of the page
284
+ // variant centering in the viewport. The old zero min-height collapsed
285
+ // the root to its content height, which silently disabled that
286
+ // centering and glued the card to the container's top edge; the #695
287
+ // padding removal traded "a padding-frame too low" for "flush against
288
+ // the header". Both were symptoms of spacing-by-inset; centering in the
289
+ // actual container fixes both hosts at once. A host whose height is
290
+ // auto (a growing content column, not a framed pane) resolves the
291
+ // percentage to nothing and keeps a content-sized, top-seated landing
292
+ // — centering applies exactly as far as the container can express it.
293
+ min-height: 100%;
294
+ // The horizontal inset stays so a narrow pane never seats the card
295
+ // edge-to-edge. No block-axis padding: the gap above/below the card is
296
+ // the centering's job now — a hardcoded top inset is exactly what read
297
+ // as "floating too low" under a sibling header in #695.
286
298
  padding: 0 var(--space-12, 0.75rem);
299
+ // Explicit overflow anchor: with the card's auto margins below
300
+ // collapsed there is no free space left to center in, and the alignment
301
+ // that then applies must be the START edge — reachable top, clip only
302
+ // at the unreachable bottom. Spelled out (and pinned by the contract
303
+ // test) rather than left emergent from the inherited center; with free
304
+ // space the margins absorb it before alignment ever applies, so this
305
+ // line only ever shows on overflow.
306
+ align-items: flex-start;
287
307
  background: transparent;
288
308
 
289
- // The card's page-grade top padding (2.25rem desktop / 1.75rem mobile)
290
- // reads as a detached float inside a pane — slim it to one space step.
291
- // Specificity (0,3,0) outranks the ≤480px media rule (0,1,0), so this
292
- // holds on phones too.
309
+ // Safe centering: auto margins center the card on both axes while free
310
+ // space exists and collapse to 0 when it doesn't (the overflow case
311
+ // anchored by align-items above). Specificity (0,3,0) also keeps the
312
+ // slim padding below outranking the ≤480px media rule (0,1,0).
293
313
  .hk-error-landing__card {
314
+ margin: auto;
294
315
  padding: var(--space-16, 1rem) var(--space-28, 1.75rem) var(--space-24, 1.5rem);
295
316
  }
296
317
  }
@@ -25,8 +25,10 @@ export type HErrorTone = "error" | "warning" | "info";
25
25
  * Layout variant of the landing.
26
26
  * - `page` (default): owns a full-viewport backdrop — for overlays and
27
27
  * standalone error pages.
28
- * - `inline`: drops the backdrop and viewport height so the same card can
29
- * live inside a pane captured by HkErrorBoundary.
28
+ * - `inline`: drops the backdrop and fills its host region's height
29
+ * instead of the viewport, so the same card centers inside whatever
30
+ * pane captured it (HkErrorBoundary) or whatever content region a host
31
+ * view hands it.
30
32
  */
31
33
  export type HErrorLandingVariant = "page" | "inline";
32
34