@celestia-island/hikari 0.55.78 → 0.55.79

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.78",
3
+ "version": "0.55.79",
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",
@@ -98,6 +98,11 @@ export interface BoardEdgeInput {
98
98
  width?: number;
99
99
  }
100
100
 
101
+ // Ink colors ride INLINE STYLES beside the stroke presentation attribute:
102
+ // var() references in a presentation attribute are not substituted on every
103
+ // engine (Firefox drops the whole declaration → stroke:none → invisible
104
+ // edges; svgwg#1031, 2025-11: browsers still disagree). Same contract as
105
+ // HkGaugeRing/HkProgressRing.
101
106
  const DEFAULT_INK = "rgb(var(--color-primary) / 38%)";
102
107
 
103
108
  export default defineComponent({
@@ -426,6 +431,7 @@ export default defineComponent({
426
431
  class="hk-board-edge"
427
432
  d={e.d}
428
433
  stroke={e.ink}
434
+ style={{ stroke: e.ink }}
429
435
  stroke-width={e.width}
430
436
  stroke-dasharray={e.dashed ? "6 5" : undefined}
431
437
  fill="none"
@@ -269,6 +269,7 @@ export default defineComponent({
269
269
  height={b.bounds.h * s}
270
270
  rx={2}
271
271
  fill={b.color}
272
+ style={{ fill: b.color, stroke: b.color }}
272
273
  opacity="0.28"
273
274
  stroke={b.color}
274
275
  stroke-width="0.6"
@@ -305,6 +306,7 @@ export default defineComponent({
305
306
  cy={hubP[1]}
306
307
  r={3}
307
308
  fill="rgb(var(--color-primary))"
309
+ style={{ fill: "rgb(var(--color-primary))" }}
308
310
  filter="drop-shadow(0 0 2px rgb(var(--color-primary) / 0.6))"
309
311
  />
310
312
  )}
@@ -315,6 +317,7 @@ export default defineComponent({
315
317
  height={Math.max(1, vr.h)}
316
318
  fill="none"
317
319
  stroke="rgb(var(--color-primary))"
320
+ style={{ stroke: "rgb(var(--color-primary))" }}
318
321
  stroke-width="1"
319
322
  stroke-dasharray="3 2"
320
323
  rx="2"
@@ -1293,27 +1293,35 @@ export default defineComponent({
1293
1293
  data-edge-id={edge.id}
1294
1294
  data-edge-type={edge.type}
1295
1295
  >
1296
- {props.interactiveEdges && strokes.map((sp, i) => (
1297
- <path
1298
- key={`halo-${i}`}
1299
- class="hk-node-canvas-edge-halo"
1300
- d={sp.d}
1301
- fill="none"
1302
- stroke={edge.color ?? "var(--hk-border-3, #555)"}
1303
- stroke-width={sp.width * 3}
1304
- stroke-dasharray={sp.dashed ? "6 4" : undefined}
1305
- />
1306
- ))}
1307
- {strokes.map((sp, i) => (
1308
- <path
1309
- key={`ink-${i}`}
1310
- d={sp.d}
1311
- fill="none"
1312
- stroke={edge.color ?? "var(--hk-border-3, #555)"}
1313
- stroke-width={sp.width}
1314
- stroke-dasharray={sp.dashed ? "6 4" : undefined}
1315
- />
1316
- ))}
1296
+ {props.interactiveEdges && strokes.map((sp, i) => {
1297
+ const edgeInk = edge.color ?? "var(--hk-border-3, #555)";
1298
+ return (
1299
+ <path
1300
+ key={`halo-${i}`}
1301
+ class="hk-node-canvas-edge-halo"
1302
+ d={sp.d}
1303
+ fill="none"
1304
+ stroke={edgeInk}
1305
+ style={{ stroke: edgeInk }}
1306
+ stroke-width={sp.width * 3}
1307
+ stroke-dasharray={sp.dashed ? "6 4" : undefined}
1308
+ />
1309
+ );
1310
+ })}
1311
+ {strokes.map((sp, i) => {
1312
+ const edgeInk = edge.color ?? "var(--hk-border-3, #555)";
1313
+ return (
1314
+ <path
1315
+ key={`ink-${i}`}
1316
+ d={sp.d}
1317
+ fill="none"
1318
+ stroke={edgeInk}
1319
+ style={{ stroke: edgeInk }}
1320
+ stroke-width={sp.width}
1321
+ stroke-dasharray={sp.dashed ? "6 4" : undefined}
1322
+ />
1323
+ );
1324
+ })}
1317
1325
  {edge.label && (
1318
1326
  <text
1319
1327
  x={mid.x}
@@ -1321,6 +1329,7 @@ export default defineComponent({
1321
1329
  text-anchor="middle"
1322
1330
  font-size="11"
1323
1331
  fill="var(--hk-text-2, #aaa)"
1332
+ style={{ fill: "var(--hk-text-2, #aaa)" }}
1324
1333
  >
1325
1334
  {edge.label}
1326
1335
  </text>
@@ -0,0 +1,170 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+ import { createApp, h } from "vue";
7
+
8
+ import * as Hk from "../index";
9
+ import HkBoard from "./HkBoard";
10
+ import HkMinimap from "./HkMinimap";
11
+
12
+ const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
13
+
14
+ function mount(node: ReturnType<typeof h>) {
15
+ const container = document.createElement("div");
16
+ document.body.appendChild(container);
17
+ const app = createApp({ render: () => node });
18
+ app.mount(container);
19
+ mounts.push({ app, container });
20
+ return container;
21
+ }
22
+
23
+ afterEach(() => {
24
+ for (const { app, container } of mounts.splice(0)) {
25
+ app.unmount();
26
+ container.remove();
27
+ }
28
+ });
29
+
30
+ /**
31
+ * SVG presentation attributes carrying `var()` colors are NOT substituted
32
+ * on every engine — Firefox drops the whole declaration (stroke:none / fill:
33
+ * none → invisible ink; svgwg#1031, browsers still disagree). The house
34
+ * contract is therefore: paint through an inline `style` channel BESIDE the
35
+ * attribute (inline always wins, so literal colors behave identically, while
36
+ * var() colors still resolve everywhere). HkGaugeRing documents the same
37
+ * contract for its ring colors; these tests pin the remaining SVG painters.
38
+ */
39
+
40
+ describe("HkBoard edge ink dual channel", () => {
41
+ it("paints edge strokes through the inline style beside the attribute", () => {
42
+ const c = mount(
43
+ h(HkBoard, {
44
+ nodes: [
45
+ { id: "a", x: 40, y: 40, w: 120, h: 60, label: "A" },
46
+ { id: "b", x: 320, y: 40, w: 120, h: 60, label: "B" },
47
+ ],
48
+ edges: [{ id: "e1", from: "a", to: "b" }],
49
+ }),
50
+ );
51
+ const edge = c.querySelector(".hk-board-edge") as SVGPathElement;
52
+ expect(edge).toBeTruthy();
53
+ const attr = edge.getAttribute("stroke");
54
+ const inline = edge.style.stroke;
55
+ // the attribute keeps the literal value…
56
+ expect(attr).toBe("rgb(var(--color-primary) / 38%)");
57
+ // …and the inline channel carries the SAME var() form so Firefox resolves it
58
+ expect(inline).toBe("rgb(var(--color-primary) / 38%)");
59
+ });
60
+ });
61
+
62
+ describe("HkMinimap paint dual channel", () => {
63
+ it("paints box fill/stroke through the inline style beside the attributes", () => {
64
+ const c = mount(
65
+ h(HkMinimap, {
66
+ boxes: [{ id: "b1", bounds: { x: 0, y: 0, w: 100, h: 80 }, color: "rgb(var(--color-primary))" }],
67
+ hubPos: { x: 600, y: 400 },
68
+ }),
69
+ );
70
+ const box = c.querySelector(".hk-minimap-svg rect[fill]") as SVGRectElement;
71
+ expect(box).toBeTruthy();
72
+ expect(box.getAttribute("fill")).toBe("rgb(var(--color-primary))");
73
+ expect(box.style.fill).toBe("rgb(var(--color-primary))");
74
+ expect(box.style.stroke).toBe("rgb(var(--color-primary))");
75
+ });
76
+
77
+ it("paints the hub dot and viewport frame through the inline style", () => {
78
+ const c = mount(
79
+ h(HkMinimap, {
80
+ // an empty roster renders null (documented early return) — the frame
81
+ // and hub ride along with any painted box
82
+ boxes: [{ id: "b1", bounds: { x: 0, y: 0, w: 100, h: 80 }, color: "rgb(var(--color-info, #5b8def))" }],
83
+ hubPos: { x: 600, y: 400 },
84
+ }),
85
+ );
86
+ const svg = c.querySelector(".hk-minimap-svg") as SVGSVGElement;
87
+ expect(svg).toBeTruthy();
88
+ const hub = svg.querySelector("circle") as SVGCircleElement;
89
+ expect(hub.getAttribute("fill")).toBe("rgb(var(--color-primary))");
90
+ expect(hub.style.fill).toBe("rgb(var(--color-primary))");
91
+ const frame = svg.querySelector('rect[stroke-dasharray="3 2"]') as SVGRectElement;
92
+ expect(frame.getAttribute("stroke")).toBe("rgb(var(--color-primary))");
93
+ expect(frame.style.stroke).toBe("rgb(var(--color-primary))");
94
+ });
95
+ });
96
+
97
+ describe("static sweep — literal var() presentation attributes must ride an inline style", () => {
98
+ const dir = dirname(fileURLToPath(import.meta.url));
99
+
100
+ /** Returns offending `stroke="…var(…"` / `fill="…var(…"` literal lines that
101
+ * have no compensating `style={{` within the following 3 lines. */
102
+ async function violations(file: string): Promise<string[]> {
103
+ const text = await readFile(join(dir, file), "utf-8");
104
+ const lines = text.split("\n");
105
+ const bad: string[] = [];
106
+ for (let i = 0; i < lines.length; i++) {
107
+ if (/\b(stroke|fill)="[^"]*var\(--/.test(lines[i])) {
108
+ const window = lines.slice(i + 1, i + 4).join("\n");
109
+ const prop = /stroke=/.test(lines[i]) ? "stroke" : "fill";
110
+ if (!window.includes(`style={{`) || !window.includes(`${prop}:`)) {
111
+ bad.push(`${file}:${i + 1}: ${lines[i].trim()}`);
112
+ }
113
+ }
114
+ }
115
+ return bad;
116
+ }
117
+
118
+ it("the sweep self-certifies on a known-bad sample", async () => {
119
+ // Zero-产出防呆:the detector must fire on a constructed violation, or the
120
+ // sweep below proves nothing.
121
+ const badSample = [
122
+ `<circle`,
123
+ ` cx={1}`,
124
+ ` fill="rgb(var(--color-x))"`,
125
+ ` r={2}`,
126
+ `/>`,
127
+ ].join("\n");
128
+ const lines = badSample.split("\n");
129
+ let fired = false;
130
+ for (let i = 0; i < lines.length; i++) {
131
+ if (/\b(stroke|fill)="[^"]*var\(--/.test(lines[i])) {
132
+ const window = lines.slice(i + 1, i + 4).join("\n");
133
+ if (!window.includes(`style={{`)) fired = true;
134
+ }
135
+ }
136
+ expect(fired).toBe(true);
137
+ void violations;
138
+ });
139
+
140
+ it("every painted component file is clean", async () => {
141
+ const files = (await readdir(dir)).filter((f) => /^Hk.+\.tsx$/.test(f) && !f.includes(".test."));
142
+ const offenders: string[] = [];
143
+ for (const f of files) {
144
+ offenders.push(...(await violations(f)));
145
+ }
146
+ expect(offenders).toEqual([]);
147
+ });
148
+ });
149
+
150
+ describe("index export surface smoke", () => {
151
+ it("exposes the standard KPI/filter/painter surfaces under both names", () => {
152
+ for (const name of [
153
+ "HkStatCard",
154
+ "HStatCard",
155
+ "HkFilterBar",
156
+ "HFilterBar",
157
+ "HkGaugeRing",
158
+ "HGaugeRing",
159
+ "HkBoard",
160
+ "HkMinimap",
161
+ "HkNodeCanvas",
162
+ "HkEmptyState",
163
+ "HkPageHeader",
164
+ "HkTable",
165
+ ] as const) {
166
+ expect(Hk, `missing export: ${name}`).toHaveProperty(name);
167
+ }
168
+ expect(typeof Hk.statToneColor).toBe("function");
169
+ });
170
+ });