@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.283",
3
+ "version": "0.55.284",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari UI library, Vue 3 runtime — one design system for Vue 3 and Rust/WASM applications",
@@ -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); sticky so it survives horizontal
46
- // scroll on narrow screens.
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
- position: sticky;
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
- for (const placement of ["bottom-right", "top-left", "top-right"]) {
58
- expect(scss).toContain(`&[data-placement="${placement}"]`);
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
- // Left/right use logical properties so RTL mirrors the pair while
61
- // the card stays at the same corner of the glass.
62
- expect(ruleBlock(".hk-screen-identify")).toMatch(/inset-inline-start:/);
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
- const controller = new AbortController();
336
- const timer = scheduleCronAfter(() => controller.abort(), GEO_API_TIMEOUT_MS);
337
- return fetch(GEO_API_URL, {
338
- signal: controller.signal,
339
- cache: "no-store",
340
- })
341
- .then((r) => (r.ok ? r.json() : null))
342
- .finally(() => timer.disconnect());
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