@celestia-island/hikari 0.55.106 → 0.55.108
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 +1 -1
- package/src/components/HkButton.scss +29 -1
- package/src/components/HkButton.test.tsx +17 -0
- package/src/components/HkButton.tsx +1 -1
- package/src/components/HkErrorLanding.inline.contract.test.ts +89 -12
- package/src/components/HkErrorLanding.scss +32 -11
- package/src/components/HkErrorLanding.tsx +4 -2
- package/src/components/HkStatusBar.test.tsx +3 -0
- package/src/components/HkStatusBar.tsx +6 -1
package/package.json
CHANGED
|
@@ -44,7 +44,18 @@
|
|
|
44
44
|
text and toolbar rows. Font size does NOT scale with the size
|
|
45
45
|
(2026-08-30 user report, Material-Design convention): md and lg share
|
|
46
46
|
--text-base and lg only grows padding/min-height for a larger touch
|
|
47
|
-
target; sm keeps its compact --text-xs.
|
|
47
|
+
target; sm keeps its compact --text-xs. xs (2026-10-02, connection
|
|
48
|
+
popover action row) shrinks the footprint further for inline popover
|
|
49
|
+
footers: same --text-xs, tighter padding, 1.375rem min-height, and a
|
|
50
|
+
13px glyph (HkButton.tsx scales icon/suffix with the size) inside a
|
|
51
|
+
0.8125rem fixed box so the square-glyph contract keeps its rhythm at
|
|
52
|
+
the smaller scale. */
|
|
53
|
+
|
|
54
|
+
.hk-btn-xs {
|
|
55
|
+
padding: var(--space-4) var(--space-8);
|
|
56
|
+
font-size: var(--text-xs);
|
|
57
|
+
min-height: 1.375rem;
|
|
58
|
+
}
|
|
48
59
|
|
|
49
60
|
.hk-btn-sm {
|
|
50
61
|
padding: var(--space-6) var(--space-12);
|
|
@@ -73,6 +84,19 @@
|
|
|
73
84
|
consistent margin from the adjacent label via the button's own gap.
|
|
74
85
|
The 1rem box matches the 16px glyph HkButton renders for the
|
|
75
86
|
icon/suffix props. */
|
|
87
|
+
.hk-btn-xs .hk-btn-icon,
|
|
88
|
+
.hk-btn-xs .hk-btn-suffix {
|
|
89
|
+
inline-size: 0.8125rem;
|
|
90
|
+
block-size: 0.8125rem;
|
|
91
|
+
|
|
92
|
+
/* HIcon's smallest span is 1rem; the box scales the span (and the svg
|
|
93
|
+
fills the span), so the glyph lands at 13px inside the 22px button. */
|
|
94
|
+
.hk-icon {
|
|
95
|
+
inline-size: 100%;
|
|
96
|
+
block-size: 100%;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
76
100
|
.hk-btn-icon,
|
|
77
101
|
.hk-btn-suffix {
|
|
78
102
|
display: inline-flex;
|
|
@@ -121,6 +145,10 @@
|
|
|
121
145
|
box-sizing: border-box;
|
|
122
146
|
}
|
|
123
147
|
|
|
148
|
+
.hk-btn-xs.hk-btn-icon-only {
|
|
149
|
+
width: 1.375rem;
|
|
150
|
+
}
|
|
151
|
+
|
|
124
152
|
.hk-btn-sm.hk-btn-icon-only {
|
|
125
153
|
width: 1.75rem;
|
|
126
154
|
}
|
|
@@ -62,6 +62,15 @@ describe("HkButton class mapping", () => {
|
|
|
62
62
|
expect(cls).not.toContain("hk-btn-md");
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
it("maps the xs size with its leading icon box", () => {
|
|
66
|
+
const xs = mount(h(HkButton, { size: "xs", icon: "RotateCw" }, () => "Reconnect"));
|
|
67
|
+
const xsBtn = button(xs);
|
|
68
|
+
expect(xsBtn.className).toContain("hk-btn-xs");
|
|
69
|
+
const box = xsBtn.querySelector(".hk-btn-icon");
|
|
70
|
+
expect(box, "xs renders the leading icon box").toBeTruthy();
|
|
71
|
+
expect(box!.querySelector(".hk-icon"), "HIcon span renders").toBeTruthy();
|
|
72
|
+
});
|
|
73
|
+
|
|
65
74
|
it("adds the block modifier only when block is set", () => {
|
|
66
75
|
const blocked = mount(h(HkButton, { block: true }, () => "wide"));
|
|
67
76
|
expect(button(blocked).className).toContain("hk-btn-block");
|
|
@@ -207,8 +216,16 @@ describe("HkButton icon-only SCSS contract", () => {
|
|
|
207
216
|
expect(rule).toContain("width: 100%");
|
|
208
217
|
});
|
|
209
218
|
|
|
219
|
+
it("shrinks the xs glyph box and scales the HIcon span with it", () => {
|
|
220
|
+
const rule = scss.match(/\.hk-btn-xs \.hk-btn-icon,\n\.hk-btn-xs \.hk-btn-suffix\s*{[^}]*}/)?.[0] ?? "";
|
|
221
|
+
expect(rule, "xs glyph-box rule must exist").toBeTruthy();
|
|
222
|
+
expect(rule).toContain("inline-size: 0.8125rem");
|
|
223
|
+
expect(rule).toContain("inline-size: 100%");
|
|
224
|
+
});
|
|
225
|
+
|
|
210
226
|
it("locks each size variant width to its min-height", () => {
|
|
211
227
|
const cases: Array<[string, string]> = [
|
|
228
|
+
["xs", "1.375rem"],
|
|
212
229
|
["sm", "1.75rem"],
|
|
213
230
|
["md", "2.5rem"],
|
|
214
231
|
["lg", "2.75rem"],
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Source contract: the inline landing
|
|
3
|
-
* `.is-inline` variant must
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
// the
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
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
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
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
|
|
29
|
-
*
|
|
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
|
|
|
@@ -345,6 +345,9 @@ describe("HkStatusBar", () => {
|
|
|
345
345
|
expect(row, "actions row renders").toBeTruthy();
|
|
346
346
|
const button = row.querySelector<HTMLButtonElement>("button")!;
|
|
347
347
|
expect(button.textContent).toContain("Reconnect now");
|
|
348
|
+
// 2026-10-02 user direction: small buttons with a leading glyph.
|
|
349
|
+
expect(button.className).toContain("hk-btn-xs");
|
|
350
|
+
expect(button.querySelector(".hk-btn-icon svg"), "leading glyph renders").toBeTruthy();
|
|
348
351
|
expect(panel.textContent).not.toContain("Click to retry");
|
|
349
352
|
button.click();
|
|
350
353
|
expect(onRetry, "popover button click retries").toHaveBeenCalledTimes(1);
|
|
@@ -458,7 +458,12 @@ export const HkStatusBar = defineComponent({
|
|
|
458
458
|
}}
|
|
459
459
|
>
|
|
460
460
|
{showRetryButton && (
|
|
461
|
-
<HkButton
|
|
461
|
+
<HkButton
|
|
462
|
+
size="xs"
|
|
463
|
+
variant="secondary"
|
|
464
|
+
icon="RotateCw"
|
|
465
|
+
onClick={onRetryButtonClick}
|
|
466
|
+
>
|
|
462
467
|
{t("hikari::statusBar.retryNow", "Reconnect now")}
|
|
463
468
|
</HkButton>
|
|
464
469
|
)}
|