@shipload/item-renderer 0.2.1 → 0.2.3
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/.claude/settings.local.json +6 -0
- package/bun.lock +2 -2
- package/package.json +8 -4
- package/scripts/check-bundle-size.ts +21 -21
- package/scripts/copy-fonts.ts +19 -19
- package/scripts/preview.ts +13 -15
- package/src/assets/stardust-base64.ts +1 -1
- package/src/errors.ts +8 -8
- package/src/fonts/index.ts +25 -26
- package/src/fonts/load-bun.ts +9 -9
- package/src/index.ts +21 -21
- package/src/links.ts +11 -11
- package/src/meta.ts +16 -16
- package/src/payload/base64url.ts +16 -16
- package/src/payload/codec.ts +13 -13
- package/src/primitives/category-icon.ts +69 -48
- package/src/primitives/compact-row.ts +13 -13
- package/src/primitives/divider.ts +9 -9
- package/src/primitives/icon-hex.ts +18 -16
- package/src/primitives/module-slot.ts +73 -73
- package/src/primitives/panel.ts +10 -10
- package/src/primitives/quantity-badge.ts +13 -13
- package/src/primitives/span-paragraph.ts +48 -50
- package/src/primitives/stat-bar.ts +24 -24
- package/src/primitives/svg.ts +13 -13
- package/src/primitives/text.ts +25 -25
- package/src/primitives/wrap.ts +12 -12
- package/src/render.ts +15 -19
- package/src/templates/_shared.ts +6 -7
- package/src/templates/component.ts +68 -63
- package/src/templates/index.ts +17 -17
- package/src/templates/item-cell.ts +48 -41
- package/src/templates/module.ts +84 -83
- package/src/templates/packed-entity.ts +12 -14
- package/src/templates/resource.ts +63 -65
- package/src/templates/ship-panel.ts +67 -72
- package/src/templates/social-card.ts +27 -25
- package/src/tokens/colors.ts +29 -29
- package/src/tokens/index.ts +6 -6
- package/src/tokens/spacing.ts +1 -1
- package/src/tokens/typography.ts +1 -1
- package/test/__image_snapshots__/component-hull-plates.diff.png +0 -0
- package/test/__image_snapshots__/module-engine-t1.diff.png +0 -0
- package/test/__image_snapshots__/module-storage-t1.diff.png +0 -0
- package/test/__image_snapshots__/packed-entity-ship-t1-only-engine.diff.png +0 -0
- package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.diff.png +0 -0
- package/test/__image_snapshots__/resource-ore-t1.diff.png +0 -0
- package/test/base64url.test.ts +22 -22
- package/test/codec.test.ts +26 -35
- package/test/errors.test.ts +21 -21
- package/test/fixtures/cargo-items.ts +43 -43
- package/test/fonts.test.ts +23 -23
- package/test/links-meta.test.ts +37 -37
- package/test/pixel.test.ts +44 -41
- package/test/primitives-category-icon.test.ts +74 -67
- package/test/primitives-compact-row.test.ts +29 -29
- package/test/primitives-domain.test.ts +61 -50
- package/test/primitives-layout.test.ts +47 -47
- package/test/primitives-module-slot.test.ts +58 -58
- package/test/render.test.ts +38 -35
- package/test/sanity.test.ts +5 -5
- package/test/sdk-link.test.ts +13 -13
- package/test/svg.test.ts +24 -22
- package/test/templates-component.test.ts +32 -32
- package/test/templates-dispatch.test.ts +29 -29
- package/test/templates-item-cell.test.ts +79 -79
- package/test/templates-module.test.ts +52 -52
- package/test/templates-packed-entity.test.ts +42 -42
- package/test/templates-resource.test.ts +61 -61
- package/test/templates-ship-panel.test.ts +69 -65
- package/test/tokens.test.ts +28 -26
|
@@ -1,79 +1,86 @@
|
|
|
1
|
-
import { test, expect } from
|
|
2
|
-
import { categoryIconSvg, categoryIconPath } from
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { categoryIconSvg, categoryIconPath } from "../src/primitives/category-icon.ts";
|
|
3
3
|
|
|
4
|
-
test(
|
|
5
|
-
for (const shape of [
|
|
6
|
-
const svg = categoryIconSvg(shape)
|
|
7
|
-
expect(svg.startsWith(
|
|
8
|
-
expect(svg.endsWith(
|
|
9
|
-
expect(svg).toContain(
|
|
4
|
+
test("categoryIconSvg returns self-contained <svg> for each shape", () => {
|
|
5
|
+
for (const shape of ["hex", "diamond", "star", "circle", "square"] as const) {
|
|
6
|
+
const svg = categoryIconSvg(shape);
|
|
7
|
+
expect(svg.startsWith("<svg ")).toBe(true);
|
|
8
|
+
expect(svg.endsWith("</svg>")).toBe(true);
|
|
9
|
+
expect(svg).toContain("viewBox=");
|
|
10
10
|
}
|
|
11
|
-
})
|
|
11
|
+
});
|
|
12
12
|
|
|
13
|
-
test(
|
|
14
|
-
const svg = categoryIconSvg(
|
|
15
|
-
expect(svg).toContain('width="24"')
|
|
16
|
-
expect(svg).toContain('height="24"')
|
|
17
|
-
expect(svg).toContain('viewBox="0 0 24 24"')
|
|
18
|
-
})
|
|
13
|
+
test("categoryIconSvg respects size option", () => {
|
|
14
|
+
const svg = categoryIconSvg("hex", { size: 24 });
|
|
15
|
+
expect(svg).toContain('width="24"');
|
|
16
|
+
expect(svg).toContain('height="24"');
|
|
17
|
+
expect(svg).toContain('viewBox="0 0 24 24"');
|
|
18
|
+
});
|
|
19
19
|
|
|
20
|
-
test(
|
|
21
|
-
const svg = categoryIconSvg(
|
|
22
|
-
expect(svg).toContain(
|
|
23
|
-
})
|
|
20
|
+
test("categoryIconSvg respects color option", () => {
|
|
21
|
+
const svg = categoryIconSvg("hex", { color: "#ff0000" });
|
|
22
|
+
expect(svg).toContain("#ff0000");
|
|
23
|
+
});
|
|
24
24
|
|
|
25
|
-
test(
|
|
26
|
-
const path = categoryIconPath({ shape:
|
|
27
|
-
expect(path.startsWith(
|
|
28
|
-
expect(path).toMatch(/^<(polygon|rect|circle)\b/)
|
|
29
|
-
expect(path).toContain(
|
|
30
|
-
})
|
|
25
|
+
test("categoryIconPath returns inline element at given coordinates without svg wrapper", () => {
|
|
26
|
+
const path = categoryIconPath({ shape: "diamond", cx: 50, cy: 50, size: 20, color: "#00ff00" });
|
|
27
|
+
expect(path.startsWith("<svg")).toBe(false);
|
|
28
|
+
expect(path).toMatch(/^<(polygon|rect|circle)\b/);
|
|
29
|
+
expect(path).toContain("#00ff00");
|
|
30
|
+
});
|
|
31
31
|
|
|
32
|
-
test(
|
|
33
|
-
const path = categoryIconPath({ shape:
|
|
34
|
-
expect(path).toContain(
|
|
35
|
-
const match = path.match(/points="([^"]+)"/)
|
|
36
|
-
expect(match).not.toBeNull()
|
|
37
|
-
const pointCount = match![1].trim().split(/\s+/).length
|
|
38
|
-
expect(pointCount).toBe(6)
|
|
39
|
-
})
|
|
32
|
+
test("hex shape produces a 6-point polygon", () => {
|
|
33
|
+
const path = categoryIconPath({ shape: "hex", cx: 50, cy: 50, size: 40, color: "#fff" });
|
|
34
|
+
expect(path).toContain("<polygon");
|
|
35
|
+
const match = path.match(/points="([^"]+)"/);
|
|
36
|
+
expect(match).not.toBeNull();
|
|
37
|
+
const pointCount = match![1].trim().split(/\s+/).length;
|
|
38
|
+
expect(pointCount).toBe(6);
|
|
39
|
+
});
|
|
40
40
|
|
|
41
|
-
test(
|
|
42
|
-
const path = categoryIconPath({ shape:
|
|
43
|
-
const match = path.match(/points="([^"]+)"/)
|
|
44
|
-
expect(match).not.toBeNull()
|
|
45
|
-
const pointCount = match![1].trim().split(/\s+/).length
|
|
46
|
-
expect(pointCount).toBe(10)
|
|
47
|
-
})
|
|
41
|
+
test("star shape produces a 10-vertex polygon", () => {
|
|
42
|
+
const path = categoryIconPath({ shape: "star", cx: 50, cy: 50, size: 40, color: "#fff" });
|
|
43
|
+
const match = path.match(/points="([^"]+)"/);
|
|
44
|
+
expect(match).not.toBeNull();
|
|
45
|
+
const pointCount = match![1].trim().split(/\s+/).length;
|
|
46
|
+
expect(pointCount).toBe(10);
|
|
47
|
+
});
|
|
48
48
|
|
|
49
|
-
test(
|
|
50
|
-
const path = categoryIconPath({ shape:
|
|
51
|
-
expect(path).toMatch(/^<circle\b/)
|
|
52
|
-
expect(path).toContain('cx="50"')
|
|
53
|
-
expect(path).toContain('cy="50"')
|
|
54
|
-
expect(path).toContain('r="20"')
|
|
55
|
-
})
|
|
49
|
+
test("circle shape produces a <circle> element", () => {
|
|
50
|
+
const path = categoryIconPath({ shape: "circle", cx: 50, cy: 50, size: 40, color: "#abc" });
|
|
51
|
+
expect(path).toMatch(/^<circle\b/);
|
|
52
|
+
expect(path).toContain('cx="50"');
|
|
53
|
+
expect(path).toContain('cy="50"');
|
|
54
|
+
expect(path).toContain('r="20"');
|
|
55
|
+
});
|
|
56
56
|
|
|
57
|
-
test(
|
|
58
|
-
const path = categoryIconPath({ shape:
|
|
59
|
-
expect(path).toMatch(/^<rect\b/)
|
|
60
|
-
expect(path).toContain('width="40"')
|
|
61
|
-
expect(path).toContain('height="40"')
|
|
62
|
-
expect(path).toContain('x="30"')
|
|
63
|
-
expect(path).toContain('y="30"')
|
|
64
|
-
})
|
|
57
|
+
test("square shape produces a <rect> element", () => {
|
|
58
|
+
const path = categoryIconPath({ shape: "square", cx: 50, cy: 50, size: 40, color: "#abc" });
|
|
59
|
+
expect(path).toMatch(/^<rect\b/);
|
|
60
|
+
expect(path).toContain('width="40"');
|
|
61
|
+
expect(path).toContain('height="40"');
|
|
62
|
+
expect(path).toContain('x="30"');
|
|
63
|
+
expect(path).toContain('y="30"');
|
|
64
|
+
});
|
|
65
65
|
|
|
66
|
-
test(
|
|
67
|
-
for (const shape of [
|
|
68
|
-
const path = categoryIconPath({
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
test("strokeWidth renders outline mode: fill=none, stroke=color", () => {
|
|
67
|
+
for (const shape of ["hex", "diamond", "star", "circle", "square"] as const) {
|
|
68
|
+
const path = categoryIconPath({
|
|
69
|
+
shape,
|
|
70
|
+
cx: 50,
|
|
71
|
+
cy: 50,
|
|
72
|
+
size: 40,
|
|
73
|
+
color: "#ff0000",
|
|
74
|
+
strokeWidth: 2,
|
|
75
|
+
});
|
|
76
|
+
expect(path).toContain('fill="none"');
|
|
77
|
+
expect(path).toContain('stroke="#ff0000"');
|
|
78
|
+
expect(path).toContain('stroke-width="2"');
|
|
72
79
|
}
|
|
73
|
-
})
|
|
80
|
+
});
|
|
74
81
|
|
|
75
|
-
test(
|
|
76
|
-
const path = categoryIconPath({ shape:
|
|
77
|
-
expect(path).toContain('fill="#ff0000"')
|
|
78
|
-
expect(path).not.toContain(
|
|
79
|
-
})
|
|
82
|
+
test("fill mode (no strokeWidth) does not produce stroke attributes", () => {
|
|
83
|
+
const path = categoryIconPath({ shape: "hex", cx: 50, cy: 50, size: 40, color: "#ff0000" });
|
|
84
|
+
expect(path).toContain('fill="#ff0000"');
|
|
85
|
+
expect(path).not.toContain("stroke=");
|
|
86
|
+
});
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
import { test, expect } from
|
|
2
|
-
import { compactRow } from
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { compactRow } from "../src/primitives/compact-row.ts";
|
|
3
3
|
|
|
4
|
-
test(
|
|
4
|
+
test("compactRow emits left-anchored label and right-anchored value", () => {
|
|
5
5
|
const svg = compactRow({
|
|
6
6
|
x: 14,
|
|
7
7
|
y: 80,
|
|
8
8
|
width: 252,
|
|
9
|
-
label:
|
|
10
|
-
value:
|
|
11
|
-
})
|
|
12
|
-
expect(svg).toContain(
|
|
13
|
-
expect(svg).toContain(
|
|
14
|
-
expect(svg).toContain(
|
|
15
|
-
expect(svg).toContain('x="14"')
|
|
16
|
-
expect(svg).toContain('x="266"')
|
|
17
|
-
expect(svg).toContain('text-anchor="end"')
|
|
18
|
-
})
|
|
9
|
+
label: "Thrust",
|
|
10
|
+
value: "700",
|
|
11
|
+
});
|
|
12
|
+
expect(svg).toContain("<text");
|
|
13
|
+
expect(svg).toContain("Thrust");
|
|
14
|
+
expect(svg).toContain("700");
|
|
15
|
+
expect(svg).toContain('x="14"');
|
|
16
|
+
expect(svg).toContain('x="266"');
|
|
17
|
+
expect(svg).toContain('text-anchor="end"');
|
|
18
|
+
});
|
|
19
19
|
|
|
20
|
-
test(
|
|
20
|
+
test("compactRow applies supplied label and value colors", () => {
|
|
21
21
|
const svg = compactRow({
|
|
22
22
|
x: 14,
|
|
23
23
|
y: 80,
|
|
24
24
|
width: 252,
|
|
25
|
-
label:
|
|
26
|
-
value:
|
|
27
|
-
labelColor:
|
|
28
|
-
valueColor:
|
|
29
|
-
})
|
|
30
|
-
expect(svg).toContain('fill="#aabbcc"')
|
|
31
|
-
expect(svg).toContain('fill="#ddeeff"')
|
|
32
|
-
})
|
|
25
|
+
label: "Drain",
|
|
26
|
+
value: "50",
|
|
27
|
+
labelColor: "#aabbcc",
|
|
28
|
+
valueColor: "#ddeeff",
|
|
29
|
+
});
|
|
30
|
+
expect(svg).toContain('fill="#aabbcc"');
|
|
31
|
+
expect(svg).toContain('fill="#ddeeff"');
|
|
32
|
+
});
|
|
33
33
|
|
|
34
|
-
test(
|
|
34
|
+
test("compactRow escapes HTML entities in label and value", () => {
|
|
35
35
|
const svg = compactRow({
|
|
36
36
|
x: 14,
|
|
37
37
|
y: 80,
|
|
38
38
|
width: 252,
|
|
39
|
-
label:
|
|
40
|
-
value:
|
|
41
|
-
})
|
|
42
|
-
expect(svg).toContain(
|
|
43
|
-
expect(svg).toContain(
|
|
44
|
-
})
|
|
39
|
+
label: "A & B",
|
|
40
|
+
value: "<5",
|
|
41
|
+
});
|
|
42
|
+
expect(svg).toContain("A & B");
|
|
43
|
+
expect(svg).toContain("<5");
|
|
44
|
+
});
|
|
@@ -1,72 +1,83 @@
|
|
|
1
|
-
import { expect, test } from
|
|
2
|
-
import { iconHex } from
|
|
3
|
-
import { statBar } from
|
|
4
|
-
import { moduleSlot } from
|
|
5
|
-
import { quantityBadge } from
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { iconHex } from "../src/primitives/icon-hex.ts";
|
|
3
|
+
import { statBar } from "../src/primitives/stat-bar.ts";
|
|
4
|
+
import { moduleSlot } from "../src/primitives/module-slot.ts";
|
|
5
|
+
import { quantityBadge } from "../src/primitives/quantity-badge.ts";
|
|
6
6
|
|
|
7
|
-
test(
|
|
8
|
-
const svg = iconHex({ x: 14, y: 14, color:
|
|
9
|
-
expect(svg).toContain(
|
|
10
|
-
expect(svg).toContain('stroke="#58d08c"')
|
|
11
|
-
expect(svg).toContain(
|
|
12
|
-
})
|
|
7
|
+
test("iconHex draws a hexagon with the category color and 2-char code", () => {
|
|
8
|
+
const svg = iconHex({ x: 14, y: 14, color: "#58d08c", code: "FE" });
|
|
9
|
+
expect(svg).toContain("<polygon");
|
|
10
|
+
expect(svg).toContain('stroke="#58d08c"');
|
|
11
|
+
expect(svg).toContain(">FE<");
|
|
12
|
+
});
|
|
13
13
|
|
|
14
|
-
test(
|
|
14
|
+
test("statBar emits a labeled bar with value ∈ [0, 1023]", () => {
|
|
15
15
|
const svg = statBar({
|
|
16
16
|
x: 14,
|
|
17
17
|
y: 100,
|
|
18
18
|
width: 252,
|
|
19
|
-
label:
|
|
20
|
-
abbreviation:
|
|
19
|
+
label: "Strength",
|
|
20
|
+
abbreviation: "STR",
|
|
21
21
|
value: 342,
|
|
22
|
-
color:
|
|
23
|
-
})
|
|
24
|
-
expect(svg).toContain(
|
|
25
|
-
expect(svg).toContain(
|
|
26
|
-
expect(svg).toContain(
|
|
22
|
+
color: "#58d08c",
|
|
23
|
+
});
|
|
24
|
+
expect(svg).toContain("STR");
|
|
25
|
+
expect(svg).toContain("Strength");
|
|
26
|
+
expect(svg).toContain("342");
|
|
27
27
|
// Fill width proportional to value/1023:
|
|
28
28
|
// expected filled width ≈ 252 * 342 / 1023 ≈ 84.2
|
|
29
|
-
expect(svg).toContain('width="84"')
|
|
30
|
-
})
|
|
29
|
+
expect(svg).toContain('width="84"');
|
|
30
|
+
});
|
|
31
31
|
|
|
32
|
-
test(
|
|
32
|
+
test("statBar inverts the visual fill when inverted is true", () => {
|
|
33
33
|
const hi = statBar({
|
|
34
|
-
x: 0,
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
x: 0,
|
|
35
|
+
y: 0,
|
|
36
|
+
width: 100,
|
|
37
|
+
label: "X",
|
|
38
|
+
abbreviation: "X",
|
|
39
|
+
value: 900,
|
|
40
|
+
color: "#fff",
|
|
41
|
+
});
|
|
37
42
|
const lo = statBar({
|
|
38
|
-
x: 0,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
x: 0,
|
|
44
|
+
y: 0,
|
|
45
|
+
width: 100,
|
|
46
|
+
label: "X",
|
|
47
|
+
abbreviation: "X",
|
|
48
|
+
value: 900,
|
|
49
|
+
color: "#fff",
|
|
50
|
+
inverted: true,
|
|
51
|
+
});
|
|
52
|
+
expect(hi).not.toBe(lo);
|
|
53
|
+
});
|
|
43
54
|
|
|
44
55
|
test('moduleSlot renders an empty state with "Empty module" label', () => {
|
|
45
|
-
const svg = moduleSlot({ x: 14, y: 200, width: 252, installed: false })
|
|
46
|
-
expect(svg).toContain(
|
|
47
|
-
})
|
|
56
|
+
const svg = moduleSlot({ x: 14, y: 200, width: 252, installed: false });
|
|
57
|
+
expect(svg).toContain("Empty module");
|
|
58
|
+
});
|
|
48
59
|
|
|
49
|
-
test(
|
|
60
|
+
test("moduleSlot renders an installed state with capability description", () => {
|
|
50
61
|
const svg = moduleSlot({
|
|
51
62
|
x: 14,
|
|
52
63
|
y: 200,
|
|
53
64
|
width: 252,
|
|
54
65
|
installed: true,
|
|
55
|
-
capability:
|
|
56
|
-
description:
|
|
57
|
-
accentColor:
|
|
58
|
-
})
|
|
59
|
-
expect(svg).toContain(
|
|
60
|
-
expect(svg).toContain(
|
|
61
|
-
expect(svg).toContain(
|
|
62
|
-
})
|
|
66
|
+
capability: "Engine",
|
|
67
|
+
description: "generates 757 thrust for travel while draining 41 energy per distance travelled",
|
|
68
|
+
accentColor: "#58d08c",
|
|
69
|
+
});
|
|
70
|
+
expect(svg).toContain("Engine");
|
|
71
|
+
expect(svg).toContain("757");
|
|
72
|
+
expect(svg).toContain("thrust");
|
|
73
|
+
});
|
|
63
74
|
|
|
64
|
-
test(
|
|
65
|
-
expect(quantityBadge({ x: 0, y: 0, quantity: 1 })).toBe(
|
|
66
|
-
expect(quantityBadge({ x: 0, y: 0, quantity: 0 })).toBe(
|
|
67
|
-
})
|
|
75
|
+
test("quantityBadge is empty string when quantity <= 1", () => {
|
|
76
|
+
expect(quantityBadge({ x: 0, y: 0, quantity: 1 })).toBe("");
|
|
77
|
+
expect(quantityBadge({ x: 0, y: 0, quantity: 0 })).toBe("");
|
|
78
|
+
});
|
|
68
79
|
|
|
69
|
-
test(
|
|
70
|
-
const svg = quantityBadge({ x: 250, y: 8, quantity: 50 })
|
|
71
|
-
expect(svg).toContain(
|
|
72
|
-
})
|
|
80
|
+
test("quantityBadge renders ×N chip when quantity > 1", () => {
|
|
81
|
+
const svg = quantityBadge({ x: 250, y: 8, quantity: 50 });
|
|
82
|
+
expect(svg).toContain("×50");
|
|
83
|
+
});
|
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
import { expect, test } from
|
|
2
|
-
import { panel } from
|
|
3
|
-
import { text } from
|
|
4
|
-
import { divider } from
|
|
5
|
-
import { wrapText } from
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { panel } from "../src/primitives/panel.ts";
|
|
3
|
+
import { text } from "../src/primitives/text.ts";
|
|
4
|
+
import { divider } from "../src/primitives/divider.ts";
|
|
5
|
+
import { wrapText } from "../src/primitives/wrap.ts";
|
|
6
6
|
|
|
7
|
-
test(
|
|
8
|
-
const svg = panel({ width: 280, height: 120 })
|
|
9
|
-
expect(svg).toContain(
|
|
10
|
-
expect(svg).toContain('x="0.5"')
|
|
11
|
-
expect(svg).toContain('y="0.5"')
|
|
12
|
-
expect(svg).toContain('width="279"')
|
|
13
|
-
expect(svg).toContain('height="119"')
|
|
14
|
-
expect(svg).toContain('rx="10"')
|
|
15
|
-
})
|
|
7
|
+
test("panel renders a rect inset by 0.5px so the 1px stroke stays inside the viewBox", () => {
|
|
8
|
+
const svg = panel({ width: 280, height: 120 });
|
|
9
|
+
expect(svg).toContain("<rect");
|
|
10
|
+
expect(svg).toContain('x="0.5"');
|
|
11
|
+
expect(svg).toContain('y="0.5"');
|
|
12
|
+
expect(svg).toContain('width="279"');
|
|
13
|
+
expect(svg).toContain('height="119"');
|
|
14
|
+
expect(svg).toContain('rx="10"');
|
|
15
|
+
});
|
|
16
16
|
|
|
17
|
-
test(
|
|
18
|
-
const svg = panel({ width: 280, height: 120, borderColor:
|
|
19
|
-
expect(svg).toContain('stroke="#6cb9ff"')
|
|
20
|
-
})
|
|
17
|
+
test("panel accepts an optional tier border color", () => {
|
|
18
|
+
const svg = panel({ width: 280, height: 120, borderColor: "#6cb9ff" });
|
|
19
|
+
expect(svg).toContain('stroke="#6cb9ff"');
|
|
20
|
+
});
|
|
21
21
|
|
|
22
|
-
test(
|
|
23
|
-
const svg = text({ x: 10, y: 20, value: `Ship "T1"`, size: 14, weight: 600 })
|
|
24
|
-
expect(svg).toContain(
|
|
25
|
-
expect(svg).toContain('x="10"')
|
|
26
|
-
expect(svg).toContain('y="20"')
|
|
27
|
-
expect(svg).toContain(
|
|
28
|
-
expect(svg).toContain('font-size="14"')
|
|
29
|
-
expect(svg).toContain('font-weight="600"')
|
|
30
|
-
})
|
|
22
|
+
test("text emits a <text> element with escaped content", () => {
|
|
23
|
+
const svg = text({ x: 10, y: 20, value: `Ship "T1"`, size: 14, weight: 600 });
|
|
24
|
+
expect(svg).toContain("<text");
|
|
25
|
+
expect(svg).toContain('x="10"');
|
|
26
|
+
expect(svg).toContain('y="20"');
|
|
27
|
+
expect(svg).toContain("Ship "T1"");
|
|
28
|
+
expect(svg).toContain('font-size="14"');
|
|
29
|
+
expect(svg).toContain('font-weight="600"');
|
|
30
|
+
});
|
|
31
31
|
|
|
32
|
-
test(
|
|
33
|
-
const svg = divider({ x: 14, y: 40, width: 252 })
|
|
34
|
-
expect(svg).toContain(
|
|
35
|
-
expect(svg).toContain('x1="14"')
|
|
36
|
-
expect(svg).toContain('x2="266"')
|
|
37
|
-
expect(svg).toContain('y1="40"')
|
|
38
|
-
expect(svg).toContain('y2="40"')
|
|
39
|
-
})
|
|
32
|
+
test("divider is a horizontal line at y", () => {
|
|
33
|
+
const svg = divider({ x: 14, y: 40, width: 252 });
|
|
34
|
+
expect(svg).toContain("<line");
|
|
35
|
+
expect(svg).toContain('x1="14"');
|
|
36
|
+
expect(svg).toContain('x2="266"');
|
|
37
|
+
expect(svg).toContain('y1="40"');
|
|
38
|
+
expect(svg).toContain('y2="40"');
|
|
39
|
+
});
|
|
40
40
|
|
|
41
|
-
test(
|
|
41
|
+
test("wrapText splits a string into lines that fit within a char budget", () => {
|
|
42
42
|
const lines = wrapText({
|
|
43
|
-
value:
|
|
43
|
+
value: "generates 757 thrust for travel while draining 41 energy per distance travelled",
|
|
44
44
|
charsPerLine: 40,
|
|
45
|
-
})
|
|
46
|
-
for (const line of lines) expect(line.length).toBeLessThanOrEqual(40)
|
|
45
|
+
});
|
|
46
|
+
for (const line of lines) expect(line.length).toBeLessThanOrEqual(40);
|
|
47
47
|
// No word is broken mid-word:
|
|
48
|
-
expect(lines.join(
|
|
49
|
-
|
|
50
|
-
)
|
|
51
|
-
})
|
|
48
|
+
expect(lines.join(" ")).toBe(
|
|
49
|
+
"generates 757 thrust for travel while draining 41 energy per distance travelled",
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
52
|
|
|
53
|
-
test(
|
|
54
|
-
const lines = wrapText({ value:
|
|
55
|
-
expect(lines).toEqual([
|
|
56
|
-
})
|
|
53
|
+
test("wrapText preserves a single unbreakable long token", () => {
|
|
54
|
+
const lines = wrapText({ value: "supercalifragilisticexpialidocious", charsPerLine: 10 });
|
|
55
|
+
expect(lines).toEqual(["supercalifragilisticexpialidocious"]);
|
|
56
|
+
});
|
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
import { test, expect } from
|
|
2
|
-
import { moduleSlot } from
|
|
3
|
-
import type { TextSpan } from
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { moduleSlot } from "../src/primitives/module-slot.ts";
|
|
3
|
+
import type { TextSpan } from "@shipload/sdk";
|
|
4
4
|
|
|
5
|
-
test(
|
|
5
|
+
test("string description flows inline with the bold capability label", () => {
|
|
6
6
|
const svg = moduleSlot({
|
|
7
7
|
x: 14,
|
|
8
8
|
y: 40,
|
|
9
9
|
width: 252,
|
|
10
10
|
installed: true,
|
|
11
|
-
capability:
|
|
12
|
-
description:
|
|
13
|
-
accentColor:
|
|
14
|
-
})
|
|
15
|
-
expect(svg).toContain(
|
|
16
|
-
expect(svg).toContain(
|
|
17
|
-
expect(svg).toContain('font-weight="600"')
|
|
18
|
-
expect(svg).toContain(
|
|
19
|
-
})
|
|
11
|
+
capability: "Engine",
|
|
12
|
+
description: "generates 700 thrust for travel",
|
|
13
|
+
accentColor: "#2fd6d1",
|
|
14
|
+
});
|
|
15
|
+
expect(svg).toContain("<polygon");
|
|
16
|
+
expect(svg).toContain("Engine: ");
|
|
17
|
+
expect(svg).toContain('font-weight="600"');
|
|
18
|
+
expect(svg).toContain("generates 700 thrust for");
|
|
19
|
+
});
|
|
20
20
|
|
|
21
|
-
test(
|
|
21
|
+
test("TextSpan[] description renders each span; highlighted spans get accent color", () => {
|
|
22
22
|
const spans: TextSpan[] = [
|
|
23
|
-
{ text:
|
|
24
|
-
{ text:
|
|
25
|
-
{ text:
|
|
26
|
-
{ text:
|
|
27
|
-
{ text:
|
|
28
|
-
]
|
|
23
|
+
{ text: "generates " },
|
|
24
|
+
{ text: "700", highlight: true },
|
|
25
|
+
{ text: " thrust for travel while draining " },
|
|
26
|
+
{ text: "45", highlight: true },
|
|
27
|
+
{ text: " energy per distance travelled" },
|
|
28
|
+
];
|
|
29
29
|
const svg = moduleSlot({
|
|
30
30
|
x: 14,
|
|
31
31
|
y: 40,
|
|
32
32
|
width: 252,
|
|
33
33
|
installed: true,
|
|
34
|
-
capability:
|
|
34
|
+
capability: "Engine",
|
|
35
35
|
description: spans,
|
|
36
|
-
accentColor:
|
|
37
|
-
})
|
|
38
|
-
expect(svg).toContain(
|
|
39
|
-
expect(svg).toContain(
|
|
40
|
-
expect(svg).toContain(
|
|
41
|
-
expect(svg).toContain(
|
|
42
|
-
expect(svg).toMatch(/fill="#[0-9A-Fa-f]{6}"[^>]*>700</)
|
|
43
|
-
})
|
|
36
|
+
accentColor: "#2fd6d1",
|
|
37
|
+
});
|
|
38
|
+
expect(svg).toContain("Engine:");
|
|
39
|
+
expect(svg).toContain(">generates <");
|
|
40
|
+
expect(svg).toContain(">700<");
|
|
41
|
+
expect(svg).toContain(">45<");
|
|
42
|
+
expect(svg).toMatch(/fill="#[0-9A-Fa-f]{6}"[^>]*>700</);
|
|
43
|
+
});
|
|
44
44
|
|
|
45
|
-
test(
|
|
45
|
+
test("TextSpan[] description wraps across lines preserving highlight boundaries", () => {
|
|
46
46
|
const spans: TextSpan[] = [
|
|
47
|
-
{ text:
|
|
48
|
-
{ text:
|
|
49
|
-
{ text:
|
|
50
|
-
{ text:
|
|
51
|
-
{ text:
|
|
52
|
-
{ text:
|
|
53
|
-
{ text:
|
|
54
|
-
{ text:
|
|
55
|
-
{ text:
|
|
56
|
-
]
|
|
47
|
+
{ text: "mines resources at " },
|
|
48
|
+
{ text: "880", highlight: true },
|
|
49
|
+
{ text: " speed to a max depth of " },
|
|
50
|
+
{ text: "248", highlight: true },
|
|
51
|
+
{ text: " with " },
|
|
52
|
+
{ text: "100", highlight: true },
|
|
53
|
+
{ text: " gather speed while draining " },
|
|
54
|
+
{ text: "1,250", highlight: true },
|
|
55
|
+
{ text: " energy per second" },
|
|
56
|
+
];
|
|
57
57
|
const svg = moduleSlot({
|
|
58
58
|
x: 14,
|
|
59
59
|
y: 40,
|
|
60
60
|
width: 252,
|
|
61
61
|
installed: true,
|
|
62
|
-
capability:
|
|
62
|
+
capability: "Gatherer",
|
|
63
63
|
description: spans,
|
|
64
|
-
accentColor:
|
|
65
|
-
})
|
|
66
|
-
expect(svg).toContain(
|
|
67
|
-
expect(svg).toContain(
|
|
68
|
-
expect(svg).toContain(
|
|
69
|
-
expect(svg).toContain(
|
|
70
|
-
})
|
|
64
|
+
accentColor: "#f59e0b",
|
|
65
|
+
});
|
|
66
|
+
expect(svg).toContain("880");
|
|
67
|
+
expect(svg).toContain("248");
|
|
68
|
+
expect(svg).toContain("100");
|
|
69
|
+
expect(svg).toContain("1,250");
|
|
70
|
+
});
|
|
71
71
|
|
|
72
|
-
test(
|
|
72
|
+
test("Empty description array renders headline only", () => {
|
|
73
73
|
const svg = moduleSlot({
|
|
74
74
|
x: 14,
|
|
75
75
|
y: 40,
|
|
76
76
|
width: 252,
|
|
77
77
|
installed: true,
|
|
78
|
-
capability:
|
|
78
|
+
capability: "Engine",
|
|
79
79
|
description: [],
|
|
80
|
-
accentColor:
|
|
81
|
-
})
|
|
82
|
-
expect(svg).toContain(
|
|
83
|
-
})
|
|
80
|
+
accentColor: "#2fd6d1",
|
|
81
|
+
});
|
|
82
|
+
expect(svg).toContain("Engine:");
|
|
83
|
+
});
|
|
84
84
|
|
|
85
|
-
test(
|
|
86
|
-
const svg = moduleSlot({ x: 14, y: 40, width: 252, installed: false })
|
|
87
|
-
expect(svg).toContain(
|
|
88
|
-
})
|
|
85
|
+
test("Empty slot renders unchanged", () => {
|
|
86
|
+
const svg = moduleSlot({ x: 14, y: 40, width: 252, installed: false });
|
|
87
|
+
expect(svg).toContain("Empty module");
|
|
88
|
+
});
|