@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
|
@@ -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
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
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
|
+
});
|