@celestia-island/hikari 0.55.283 → 0.55.284
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/HkAmbientLayer.styles.test.ts +19 -0
- package/src/components/HkCalendarHeatmap.scss +13 -6
- package/src/components/HkScreenIdentifyCard.styles.test.ts +28 -5
- package/src/components/calendarHeatmapRail.contract.test.ts +63 -0
- package/src/theme/useSolarTime.test.ts +14 -0
- package/src/theme/useSolarTime.ts +23 -9
package/package.json
CHANGED
|
@@ -35,6 +35,25 @@ describe("HkAmbientLayer pointer contract", () => {
|
|
|
35
35
|
const blockStart = declarations.lastIndexOf('[data-pointer-events="auto"]', autoAt);
|
|
36
36
|
expect(blockStart).toBeGreaterThan(-1);
|
|
37
37
|
expect(declarations.slice(blockStart, autoAt)).toContain("> *");
|
|
38
|
+
// EVERY ruleset is checked by SCOPE, not proximity: any block that
|
|
39
|
+
// states pointer-events:auto must be the child-scoped opt-in, and any
|
|
40
|
+
// other block stating pointer-events at all must say none (an earlier
|
|
41
|
+
// round flagged both the first-only and the proximity-based shapes).
|
|
42
|
+
const rulesets = [...declarations.matchAll(/([^{}]+)\{([^{}]*)\}/g)].map(
|
|
43
|
+
([, selector, body]) => ({ selector: selector.trim(), body }),
|
|
44
|
+
);
|
|
45
|
+
expect(rulesets.length).toBeGreaterThan(0);
|
|
46
|
+
for (const { selector, body } of rulesets) {
|
|
47
|
+
if (!/pointer-events/.test(body)) continue;
|
|
48
|
+
if (/pointer-events:\s*auto/.test(body)) {
|
|
49
|
+
expect(selector).toContain('[data-pointer-events="auto"]');
|
|
50
|
+
expect(selector).toContain("> *");
|
|
51
|
+
} else {
|
|
52
|
+
expect(body).toMatch(/pointer-events:\s*none/);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// The wrapper's own block states the ghost rule exactly once.
|
|
56
|
+
expect(declarations.match(/pointer-events:\s*none/g)).toHaveLength(1);
|
|
38
57
|
});
|
|
39
58
|
|
|
40
59
|
it("sits on the HUD ordinal band, below its root's document elevation", () => {
|
|
@@ -42,12 +42,20 @@
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// Weekday rail: the first (Monday) and last (Sunday) rows carry the
|
|
45
|
-
// labels (the wowsp convention)
|
|
46
|
-
//
|
|
45
|
+
// labels (the wowsp convention). `align-self: flex-end` rows-aligns
|
|
46
|
+
// the rail with the GRID, not with the -cells box: -cells is one
|
|
47
|
+
// marker strip taller than the grid (the month labels ride above
|
|
48
|
+
// it), so a stretched rail started its seven rows on the markers and
|
|
49
|
+
// every label sat one strip too high (2026-11-05 user report from
|
|
50
|
+
// the chest quota card). The rail's natural height (7 cells + 6
|
|
51
|
+
// gaps) equals the grid's exactly, so end-alignment is row-exact.
|
|
52
|
+
// No sticky mask on purpose: a hard-coded surface can't match every
|
|
53
|
+
// host card (it painted a visible stripe behind the labels), and
|
|
54
|
+
// sticky without a mask lets cells slide beneath the labels during
|
|
55
|
+
// horizontal scroll — the rail scrolls with the calendar instead
|
|
56
|
+
// (the GitHub convention).
|
|
47
57
|
&-rail {
|
|
48
|
-
|
|
49
|
-
left: 0;
|
|
50
|
-
z-index: 1;
|
|
58
|
+
align-self: flex-end;
|
|
51
59
|
display: flex;
|
|
52
60
|
flex-direction: column;
|
|
53
61
|
flex-shrink: 0;
|
|
@@ -55,7 +63,6 @@
|
|
|
55
63
|
font-size: 10px;
|
|
56
64
|
line-height: 14px;
|
|
57
65
|
color: var(--hi-color-muted);
|
|
58
|
-
background: var(--hi-color-surface);
|
|
59
66
|
|
|
60
67
|
> span {
|
|
61
68
|
white-space: nowrap;
|
|
@@ -54,12 +54,35 @@ describe("HkScreenIdentifyCard layout contract", () => {
|
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
it("parks the card in every corner through placement attributes", () => {
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
// Each non-default corner must OVERRIDE the exact edges that move it
|
|
58
|
+
// there — an earlier round flagged this guard as presence-only, so a
|
|
59
|
+
// placement block that forgot its edge rule stayed green.
|
|
60
|
+
const corners: Array<[string, RegExp[]]> = [
|
|
61
|
+
// End-side corners must ALSO release the logical start edge — an
|
|
62
|
+
// over-constrained inset would resolve to the start edge and leave
|
|
63
|
+
// the card bottom-left no matter the placement (R1 V-3 probe).
|
|
64
|
+
["bottom-right", [/inset-inline-end:\s*var\(--space-16/, /inset-inline-start:\s*auto/]],
|
|
65
|
+
["top-left", [/top:\s*var\(--space-16/, /bottom:\s*auto/]],
|
|
66
|
+
[
|
|
67
|
+
"top-right",
|
|
68
|
+
[
|
|
69
|
+
/inset-inline-end:\s*var\(--space-16/,
|
|
70
|
+
/inset-inline-start:\s*auto/,
|
|
71
|
+
/top:\s*var\(--space-16[\s\S]*bottom:\s*auto/,
|
|
72
|
+
],
|
|
73
|
+
],
|
|
74
|
+
];
|
|
75
|
+
for (const [placement, edges] of corners) {
|
|
76
|
+
const block = ruleBlock(`&[data-placement="${placement}"]`);
|
|
77
|
+
for (const edge of edges) {
|
|
78
|
+
expect(block).toMatch(edge);
|
|
79
|
+
}
|
|
59
80
|
}
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
81
|
+
// The default corner's own block must not have been turned into a
|
|
82
|
+
// catch-all: it keeps the bottom-left seat via the logical edge only.
|
|
83
|
+
const card = ruleBlock(".hk-screen-identify");
|
|
84
|
+
expect(card).toMatch(/inset-inline-start:\s*var\(--space-16/);
|
|
85
|
+
expect(card).toMatch(/bottom:\s*var\(--space-16/);
|
|
63
86
|
});
|
|
64
87
|
|
|
65
88
|
it("rides hikari text tokens for every font size except the numeral", () => {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source contract for the HkCalendarHeatmap weekday rail geometry
|
|
3
|
+
* (2026-11-05 user report from the chest quota card: the Monday/Sunday
|
|
4
|
+
* labels sat one marker strip too high, and a hard-coded surface fill
|
|
5
|
+
* painted a visible stripe behind them).
|
|
6
|
+
*
|
|
7
|
+
* jsdom does no layout, so the row alignment itself cannot be asserted
|
|
8
|
+
* here; these pins hold the two sheet decisions the fix rests on:
|
|
9
|
+
*
|
|
10
|
+
* 1. `align-self: flex-end` — the -cells box is one marker strip taller
|
|
11
|
+
* than the grid (the month labels ride above it); a stretched rail
|
|
12
|
+
* starts its seven rows on the markers. The rail's natural height
|
|
13
|
+
* (7 cells + 6 gaps) equals the grid's exactly, so end-alignment is
|
|
14
|
+
* row-exact. This also assumes the grid stays the LAST child of
|
|
15
|
+
* -cells with no trailing padding — the third pin below.
|
|
16
|
+
* 2. no sticky mask — a hard-coded surface can't match every host
|
|
17
|
+
* card, and sticky without a mask lets cells slide beneath the
|
|
18
|
+
* labels during horizontal scroll. The rail scrolls with the
|
|
19
|
+
* calendar (the GitHub convention).
|
|
20
|
+
*/
|
|
21
|
+
import { describe, expect, it } from "vitest";
|
|
22
|
+
import { readFileSync } from "node:fs";
|
|
23
|
+
import { dirname, join } from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
|
|
26
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const sheet = readFileSync(join(here, "HkCalendarHeatmap.scss"), "utf-8");
|
|
28
|
+
|
|
29
|
+
// The `&-rail` rule up to its closing brace (two-space indent inside
|
|
30
|
+
// the `.hk-calendar-heatmap` block) — keeps the assertions scoped to
|
|
31
|
+
// the rail (a background elsewhere in the sheet is legitimate).
|
|
32
|
+
const rail = sheet.match(/&-rail \{[\s\S]*?\n \}/)?.[0] ?? "";
|
|
33
|
+
// Same for `-cells`, whose last-child geometry the alignment relies on.
|
|
34
|
+
const cells = sheet.match(/&-cells \{[\s\S]*?\n \}/)?.[0] ?? "";
|
|
35
|
+
|
|
36
|
+
describe("HkCalendarHeatmap weekday rail contract", () => {
|
|
37
|
+
it("extracts the rail rule from the sheet", () => {
|
|
38
|
+
expect(rail, "the &-rail rule must exist").toContain("&-rail {");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("bottom-aligns the rail with the grid, not the markers row", () => {
|
|
42
|
+
expect(rail, "rail must carry align-self: flex-end").toContain(
|
|
43
|
+
"align-self: flex-end",
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("carries no sticky mask (no hard-coded surface fill)", () => {
|
|
48
|
+
expect(rail, "rail must not paint a hard-coded surface").not.toContain(
|
|
49
|
+
"background:",
|
|
50
|
+
);
|
|
51
|
+
expect(rail, "rail must not go sticky").not.toContain("position: sticky");
|
|
52
|
+
expect(rail, "sticky's scroll pin has no business left behind").not.toContain(
|
|
53
|
+
"z-index",
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("keeps the grid the flush last child of -cells", () => {
|
|
58
|
+
expect(cells, "the &-cells rule must exist").toContain("&-cells {");
|
|
59
|
+
expect(cells, "trailing padding would break the end-alignment").not.toContain(
|
|
60
|
+
"padding",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -514,6 +514,20 @@ describe("solar IP insight (the strategy dialog's readout)", () => {
|
|
|
514
514
|
expect(mod.solarIpInsightFresh(insight.fetchedAt + 11 * 60 * 1000)).toBe(false);
|
|
515
515
|
});
|
|
516
516
|
|
|
517
|
+
it("a probe and the ip cascade's geo resolution racing share one lookup GET", async () => {
|
|
518
|
+
mod.setSolarLocationStrategy("ip");
|
|
519
|
+
mod.setGeolocationProvider(async () => null);
|
|
520
|
+
const fetchMock = lookupFetch();
|
|
521
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
522
|
+
// The dialog probe and the theme clock's resolution race the same
|
|
523
|
+
// lookup — the body fetch dedupes them into one network request and
|
|
524
|
+
// both consumers converge on the same insight.
|
|
525
|
+
await Promise.all([mod.probeSolarIpInsight(), mod.getGeolocation()]);
|
|
526
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
527
|
+
expect(mod.solarIpInsight.value?.ip).toBe("203.0.113.7");
|
|
528
|
+
expect(mod.solarIpProbeState.value).toBe("ok");
|
|
529
|
+
});
|
|
530
|
+
|
|
517
531
|
it("the ip strategy's own geo resolution records the insight from the same body", async () => {
|
|
518
532
|
mod.setSolarLocationStrategy("ip");
|
|
519
533
|
mod.setGeolocationProvider(async () => null);
|
|
@@ -330,16 +330,30 @@ const GEO_API_TIMEOUT_MS = 4000;
|
|
|
330
330
|
/** One fetch of the lookup body with the shared timeout — the single
|
|
331
331
|
* network path for both consumers (the "ip" strategy's geo resolution
|
|
332
332
|
* and the strategy dialog's insight readout), so narrowing, abort
|
|
333
|
-
* handling and the no-store contract live in exactly one place.
|
|
333
|
+
* handling and the no-store contract live in exactly one place.
|
|
334
|
+
* Concurrent callers SHARE the in-flight GET (a probe and an apply's
|
|
335
|
+
* geo resolution racing each other used to issue two identical
|
|
336
|
+
* requests); failures propagate to every joiner. */
|
|
337
|
+
let ipBodyInflight: Promise<unknown> | null = null;
|
|
338
|
+
|
|
334
339
|
async function fetchIpLookupBody(): Promise<unknown> {
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
340
|
+
if (ipBodyInflight) return ipBodyInflight;
|
|
341
|
+
const inflight = (async () => {
|
|
342
|
+
const controller = new AbortController();
|
|
343
|
+
const timer = scheduleCronAfter(() => controller.abort(), GEO_API_TIMEOUT_MS);
|
|
344
|
+
return fetch(GEO_API_URL, {
|
|
345
|
+
signal: controller.signal,
|
|
346
|
+
cache: "no-store",
|
|
347
|
+
})
|
|
348
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
349
|
+
.finally(() => timer.disconnect());
|
|
350
|
+
})();
|
|
351
|
+
ipBodyInflight = inflight;
|
|
352
|
+
try {
|
|
353
|
+
return await inflight;
|
|
354
|
+
} finally {
|
|
355
|
+
ipBodyInflight = null;
|
|
356
|
+
}
|
|
343
357
|
}
|
|
344
358
|
|
|
345
359
|
/** The trimmed-string guard for the untrusted lookup body: only a
|