@shipload/item-renderer 0.2.2 → 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.
Files changed (67) hide show
  1. package/.claude/settings.local.json +6 -0
  2. package/bun.lock +2 -2
  3. package/package.json +2 -2
  4. package/scripts/check-bundle-size.ts +21 -21
  5. package/scripts/copy-fonts.ts +19 -19
  6. package/scripts/preview.ts +13 -15
  7. package/src/assets/stardust-base64.ts +1 -1
  8. package/src/errors.ts +8 -8
  9. package/src/fonts/index.ts +25 -26
  10. package/src/fonts/load-bun.ts +9 -9
  11. package/src/index.ts +21 -21
  12. package/src/links.ts +11 -11
  13. package/src/meta.ts +16 -16
  14. package/src/payload/base64url.ts +16 -16
  15. package/src/payload/codec.ts +13 -13
  16. package/src/primitives/category-icon.ts +69 -48
  17. package/src/primitives/compact-row.ts +13 -13
  18. package/src/primitives/divider.ts +9 -9
  19. package/src/primitives/icon-hex.ts +18 -16
  20. package/src/primitives/module-slot.ts +73 -73
  21. package/src/primitives/panel.ts +10 -10
  22. package/src/primitives/quantity-badge.ts +13 -13
  23. package/src/primitives/span-paragraph.ts +48 -50
  24. package/src/primitives/stat-bar.ts +24 -24
  25. package/src/primitives/svg.ts +13 -13
  26. package/src/primitives/text.ts +25 -25
  27. package/src/primitives/wrap.ts +12 -12
  28. package/src/render.ts +15 -19
  29. package/src/templates/_shared.ts +5 -6
  30. package/src/templates/component.ts +67 -65
  31. package/src/templates/index.ts +17 -17
  32. package/src/templates/item-cell.ts +48 -45
  33. package/src/templates/module.ts +83 -81
  34. package/src/templates/packed-entity.ts +12 -14
  35. package/src/templates/resource.ts +63 -65
  36. package/src/templates/ship-panel.ts +66 -71
  37. package/src/templates/social-card.ts +27 -25
  38. package/src/tokens/colors.ts +29 -29
  39. package/src/tokens/index.ts +6 -6
  40. package/src/tokens/spacing.ts +1 -1
  41. package/src/tokens/typography.ts +1 -1
  42. package/test/__image_snapshots__/module-storage-t1.diff.png +0 -0
  43. package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.diff.png +0 -0
  44. package/test/base64url.test.ts +22 -22
  45. package/test/codec.test.ts +26 -35
  46. package/test/errors.test.ts +21 -21
  47. package/test/fixtures/cargo-items.ts +43 -43
  48. package/test/fonts.test.ts +23 -23
  49. package/test/links-meta.test.ts +37 -37
  50. package/test/pixel.test.ts +44 -41
  51. package/test/primitives-category-icon.test.ts +74 -67
  52. package/test/primitives-compact-row.test.ts +29 -29
  53. package/test/primitives-domain.test.ts +61 -50
  54. package/test/primitives-layout.test.ts +47 -47
  55. package/test/primitives-module-slot.test.ts +58 -58
  56. package/test/render.test.ts +38 -35
  57. package/test/sanity.test.ts +5 -5
  58. package/test/sdk-link.test.ts +13 -13
  59. package/test/svg.test.ts +24 -22
  60. package/test/templates-component.test.ts +32 -32
  61. package/test/templates-dispatch.test.ts +29 -29
  62. package/test/templates-item-cell.test.ts +79 -79
  63. package/test/templates-module.test.ts +52 -52
  64. package/test/templates-packed-entity.test.ts +42 -42
  65. package/test/templates-resource.test.ts +61 -61
  66. package/test/templates-ship-panel.test.ts +65 -61
  67. package/test/tokens.test.ts +28 -26
@@ -1,47 +1,47 @@
1
- import { expect, test } from 'bun:test'
2
- import { resolveItem } from '@shipload/sdk'
3
- import { renderPackedEntity } from '../src/templates/packed-entity.ts'
4
- import { FIXTURES } from './fixtures/cargo-items.ts'
1
+ import { expect, test } from "bun:test";
2
+ import { resolveItem } from "@shipload/sdk";
3
+ import { renderPackedEntity } from "../src/templates/packed-entity.ts";
4
+ import { FIXTURES } from "./fixtures/cargo-items.ts";
5
5
 
6
- test('renders Ship with hull attributes and two modules', () => {
7
- const item = FIXTURES.shipT1TwoModules
8
- const resolved = resolveItem(item.item_id, item.stats, item.modules)
9
- const svg = renderPackedEntity(item, resolved)
10
- expect(svg).toContain('Ship (Packed)')
11
- expect(svg).toContain('HULL')
12
- expect(svg).toContain('Mass')
13
- expect(svg).toContain('Capacity')
14
- expect(svg).toContain('Engine')
15
- expect(svg).toContain('Generator')
16
- })
6
+ test("renders Ship with hull attributes and two modules", () => {
7
+ const item = FIXTURES.shipT1TwoModules;
8
+ const resolved = resolveItem(item.item_id, item.stats, item.modules);
9
+ const svg = renderPackedEntity(item, resolved);
10
+ expect(svg).toContain("Ship (Packed)");
11
+ expect(svg).toContain("HULL");
12
+ expect(svg).toContain("Mass");
13
+ expect(svg).toContain("Capacity");
14
+ expect(svg).toContain("Engine");
15
+ expect(svg).toContain("Generator");
16
+ });
17
17
 
18
- test('renders empty-module rows when slots are unfilled', () => {
19
- const item = FIXTURES.shipT1NoModules
20
- const resolved = resolveItem(item.item_id, item.stats, item.modules)
21
- const svg = renderPackedEntity(item, resolved)
22
- expect(svg.match(/Empty module/g)?.length ?? 0).toBeGreaterThanOrEqual(1)
23
- })
18
+ test("renders empty-module rows when slots are unfilled", () => {
19
+ const item = FIXTURES.shipT1NoModules;
20
+ const resolved = resolveItem(item.item_id, item.stats, item.modules);
21
+ const svg = renderPackedEntity(item, resolved);
22
+ expect(svg.match(/Empty module/g)?.length ?? 0).toBeGreaterThanOrEqual(1);
23
+ });
24
24
 
25
- test('matches the committed Ship T1 (two modules) snapshot', async () => {
26
- const item = FIXTURES.shipT1TwoModules
27
- const resolved = resolveItem(item.item_id, item.stats, item.modules)
28
- const svg = renderPackedEntity(item, resolved)
29
- expect(svg).toMatchSnapshot('packed-entity-ship-t1-two-modules.svg')
30
- })
25
+ test("matches the committed Ship T1 (two modules) snapshot", async () => {
26
+ const item = FIXTURES.shipT1TwoModules;
27
+ const resolved = resolveItem(item.item_id, item.stats, item.modules);
28
+ const svg = renderPackedEntity(item, resolved);
29
+ expect(svg).toMatchSnapshot("packed-entity-ship-t1-two-modules.svg");
30
+ });
31
31
 
32
- test('matches the committed Ship T1 (only engine) snapshot', async () => {
33
- const item = FIXTURES.shipT1OnlyEngine
34
- const resolved = resolveItem(item.item_id, item.stats, item.modules)
35
- const svg = renderPackedEntity(item, resolved)
36
- expect(svg).toMatchSnapshot('packed-entity-ship-t1-only-engine.svg')
37
- })
32
+ test("matches the committed Ship T1 (only engine) snapshot", async () => {
33
+ const item = FIXTURES.shipT1OnlyEngine;
34
+ const resolved = resolveItem(item.item_id, item.stats, item.modules);
35
+ const svg = renderPackedEntity(item, resolved);
36
+ expect(svg).toMatchSnapshot("packed-entity-ship-t1-only-engine.svg");
37
+ });
38
38
 
39
- test('ship with two modules renders SDK-sourced narrative descriptions', () => {
40
- const item = FIXTURES.shipT1TwoModules
41
- const resolved = resolveItem(item.item_id, item.stats, item.modules)
42
- const svg = renderPackedEntity(item, resolved)
43
- expect(svg).toContain('Engine: ')
44
- expect(svg).toContain('generates')
45
- expect(svg).toContain('Generator: ')
46
- expect(svg).toContain('holds')
47
- })
39
+ test("ship with two modules renders SDK-sourced narrative descriptions", () => {
40
+ const item = FIXTURES.shipT1TwoModules;
41
+ const resolved = resolveItem(item.item_id, item.stats, item.modules);
42
+ const svg = renderPackedEntity(item, resolved);
43
+ expect(svg).toContain("Engine: ");
44
+ expect(svg).toContain("generates");
45
+ expect(svg).toContain("Generator: ");
46
+ expect(svg).toContain("holds");
47
+ });
@@ -1,71 +1,71 @@
1
- import { expect, test } from 'bun:test'
2
- import { resolveItem, getStatDefinitions } from '@shipload/sdk'
3
- import { renderResource } from '../src/templates/resource.ts'
4
- import { FIXTURES } from './fixtures/cargo-items.ts'
1
+ import { expect, test } from "bun:test";
2
+ import { resolveItem, getStatDefinitions } from "@shipload/sdk";
3
+ import { renderResource } from "../src/templates/resource.ts";
4
+ import { FIXTURES } from "./fixtures/cargo-items.ts";
5
5
 
6
- test('renders Crude Ore with category, mass, and three stat bars', () => {
7
- const item = FIXTURES.oreT1
8
- const resolved = resolveItem(item.item_id, item.stats, item.modules)
9
- const svg = renderResource(item, resolved)
10
- expect(svg).toContain('Crude Ore')
11
- expect(svg).toContain('Ore') // category label
12
- expect(svg).toContain('30,000') // mass
13
- expect(svg).toContain('STR') // strength abbreviation
14
- expect(svg).toContain('TOL')
15
- expect(svg).toContain('DEN')
16
- })
6
+ test("renders Crude Ore with category, mass, and three stat bars", () => {
7
+ const item = FIXTURES.oreT1;
8
+ const resolved = resolveItem(item.item_id, item.stats, item.modules);
9
+ const svg = renderResource(item, resolved);
10
+ expect(svg).toContain("Crude Ore");
11
+ expect(svg).toContain("Ore"); // category label
12
+ expect(svg).toContain("30,000"); // mass
13
+ expect(svg).toContain("STR"); // strength abbreviation
14
+ expect(svg).toContain("TOL");
15
+ expect(svg).toContain("DEN");
16
+ });
17
17
 
18
- test('renders quantity badge when stack > 1', () => {
19
- const item = FIXTURES.oreT1StackOf50
20
- const resolved = resolveItem(item.item_id, item.stats, item.modules)
21
- const svg = renderResource(item, resolved)
22
- expect(svg).toContain('×50')
23
- })
18
+ test("renders quantity badge when stack > 1", () => {
19
+ const item = FIXTURES.oreT1StackOf50;
20
+ const resolved = resolveItem(item.item_id, item.stats, item.modules);
21
+ const svg = renderResource(item, resolved);
22
+ expect(svg).toContain("×50");
23
+ });
24
24
 
25
- test('does not render quantity badge when stack == 1', () => {
26
- const item = FIXTURES.oreT1
27
- const resolved = resolveItem(item.item_id, item.stats, item.modules)
28
- const svg = renderResource(item, resolved)
29
- expect(svg).not.toContain('×')
30
- })
25
+ test("does not render quantity badge when stack == 1", () => {
26
+ const item = FIXTURES.oreT1;
27
+ const resolved = resolveItem(item.item_id, item.stats, item.modules);
28
+ const svg = renderResource(item, resolved);
29
+ expect(svg).not.toContain("×");
30
+ });
31
31
 
32
- test('matches the committed Crude Ore snapshot', async () => {
33
- const item = FIXTURES.oreT1
34
- const resolved = resolveItem(item.item_id, item.stats, item.modules)
35
- const svg = renderResource(item, resolved)
36
- expect(svg).toMatchSnapshot('resource-ore-t1.svg')
37
- })
32
+ test("matches the committed Crude Ore snapshot", async () => {
33
+ const item = FIXTURES.oreT1;
34
+ const resolved = resolveItem(item.item_id, item.stats, item.modules);
35
+ const svg = renderResource(item, resolved);
36
+ expect(svg).toMatchSnapshot("resource-ore-t1.svg");
37
+ });
38
38
 
39
- test('matches the committed Dense Gas snapshot', async () => {
40
- const item = FIXTURES.gasT2
41
- const resolved = resolveItem(item.item_id, item.stats, item.modules)
42
- const svg = renderResource(item, resolved)
43
- expect(svg).toMatchSnapshot('resource-gas-t2.svg')
44
- })
39
+ test("matches the committed Dense Gas snapshot", async () => {
40
+ const item = FIXTURES.gasT2;
41
+ const resolved = resolveItem(item.item_id, item.stats, item.modules);
42
+ const svg = renderResource(item, resolved);
43
+ expect(svg).toMatchSnapshot("resource-gas-t2.svg");
44
+ });
45
45
 
46
- test('renderResource ranges mode shows stat abbreviations with no values', () => {
47
- const item = FIXTURES.oreT1
48
- const resolved = resolveItem(item.item_id)
49
- const svg = renderResource(item, resolved, { mode: 'ranges' })
50
- const defs = getStatDefinitions(resolved.category!)
46
+ test("renderResource ranges mode shows stat abbreviations with no values", () => {
47
+ const item = FIXTURES.oreT1;
48
+ const resolved = resolveItem(item.item_id);
49
+ const svg = renderResource(item, resolved, { mode: "ranges" });
50
+ const defs = getStatDefinitions(resolved.category!);
51
51
  for (const def of defs) {
52
- expect(svg).toContain(def.abbreviation)
52
+ expect(svg).toContain(def.abbreviation);
53
53
  }
54
- expect(svg).not.toMatch(/>\d{3}<\/text>/)
55
- expect(svg).toContain('Category')
56
- expect(svg).toContain('Mass')
57
- })
54
+ expect(svg).not.toMatch(/>\d{3}<\/text>/);
55
+ expect(svg).toContain("Category");
56
+ expect(svg).toContain("Mass");
57
+ });
58
58
 
59
- test('renderResource values mode (default) still shows concrete numbers', () => {
60
- const item = FIXTURES.oreT1
61
- const resolved = resolveItem(item.item_id, item.stats)
62
- const svg = renderResource(item, resolved)
63
- expect(svg).toMatch(/>\d+<\/text>/)
64
- })
59
+ test("renderResource values mode (default) still shows concrete numbers", () => {
60
+ const item = FIXTURES.oreT1;
61
+ const resolved = resolveItem(item.item_id, item.stats);
62
+ const svg = renderResource(item, resolved);
63
+ expect(svg).toMatch(/>\d+<\/text>/);
64
+ });
65
65
 
66
- test('renderResource ranges mode matches snapshot', () => {
67
- const item = FIXTURES.oreT1
68
- const resolved = resolveItem(item.item_id)
69
- const svg = renderResource(item, resolved, { mode: 'ranges' })
70
- expect(svg).toMatchSnapshot('resource-ranges')
71
- })
66
+ test("renderResource ranges mode matches snapshot", () => {
67
+ const item = FIXTURES.oreT1;
68
+ const resolved = resolveItem(item.item_id);
69
+ const svg = renderResource(item, resolved, { mode: "ranges" });
70
+ expect(svg).toMatchSnapshot("resource-ranges");
71
+ });
@@ -1,87 +1,91 @@
1
- import { test, expect } from 'bun:test'
2
- import { renderShipPanel } from '../src/templates/ship-panel.ts'
1
+ import { test, expect } from "bun:test";
2
+ import { renderShipPanel } from "../src/templates/ship-panel.ts";
3
3
 
4
- test('renderShipPanel with empty slots renders empty module rows', () => {
4
+ test("renderShipPanel with empty slots renders empty module rows", () => {
5
5
  const svg = renderShipPanel({
6
- name: 'Ship T1 (Packed)',
6
+ name: "Ship T1 (Packed)",
7
7
  tier: 1,
8
- attributes: [{
9
- capability: 'Hull',
10
- attributes: [
11
- { label: 'Mass', value: 100 },
12
- { label: 'Capacity', value: 5000 },
13
- ],
14
- }],
15
- slots: [
16
- { installed: false },
17
- { installed: false },
18
- { installed: false },
8
+ attributes: [
9
+ {
10
+ capability: "Hull",
11
+ attributes: [
12
+ { label: "Mass", value: 100 },
13
+ { label: "Capacity", value: 5000 },
14
+ ],
15
+ },
19
16
  ],
20
- })
21
- expect(svg).toContain('Ship T1 (Packed)')
22
- expect(svg).toContain('HULL')
23
- expect(svg).toContain('Mass')
24
- expect(svg).toContain('Capacity')
25
- expect((svg.match(/Empty module/g) ?? []).length).toBe(3)
26
- })
17
+ slots: [{ installed: false }, { installed: false }, { installed: false }],
18
+ });
19
+ expect(svg).toContain("Ship T1 (Packed)");
20
+ expect(svg).toContain("HULL");
21
+ expect(svg).toContain("Mass");
22
+ expect(svg).toContain("Capacity");
23
+ expect((svg.match(/Empty module/g) ?? []).length).toBe(3);
24
+ });
27
25
 
28
- test('renderShipPanel with installed slots + string descriptions', () => {
26
+ test("renderShipPanel with installed slots + string descriptions", () => {
29
27
  const svg = renderShipPanel({
30
- name: 'Ship T1 (Packed)',
28
+ name: "Ship T1 (Packed)",
31
29
  tier: 1,
32
- attributes: [{
33
- capability: 'Hull',
34
- attributes: [{ label: 'Mass', value: 100 }],
35
- }],
30
+ attributes: [
31
+ {
32
+ capability: "Hull",
33
+ attributes: [{ label: "Mass", value: 100 }],
34
+ },
35
+ ],
36
36
  slots: [
37
- { name: 'Engine', installed: true, description: 'generates 500 thrust for travel' },
38
- { name: 'Generator', installed: true, description: 'holds 1000 energy' },
37
+ { name: "Engine", installed: true, description: "generates 500 thrust for travel" },
38
+ { name: "Generator", installed: true, description: "holds 1000 energy" },
39
39
  ],
40
- })
41
- expect(svg).toContain('Engine:')
42
- expect(svg).toContain('generates 500 thrust')
43
- expect(svg).toContain('Generator:')
44
- expect(svg).toContain('holds 1000 energy')
45
- })
40
+ });
41
+ expect(svg).toContain("Engine:");
42
+ expect(svg).toContain("generates 500 thrust");
43
+ expect(svg).toContain("Generator:");
44
+ expect(svg).toContain("holds 1000 energy");
45
+ });
46
46
 
47
- test('renderShipPanel with TextSpan[] descriptions preserves highlights', () => {
47
+ test("renderShipPanel with TextSpan[] descriptions preserves highlights", () => {
48
48
  const svg = renderShipPanel({
49
- name: 'Ship T1 (Packed)',
49
+ name: "Ship T1 (Packed)",
50
50
  tier: 1,
51
- attributes: [{
52
- capability: 'Hull',
53
- attributes: [{ label: 'Mass', value: 100 }],
54
- }],
51
+ attributes: [
52
+ {
53
+ capability: "Hull",
54
+ attributes: [{ label: "Mass", value: 100 }],
55
+ },
56
+ ],
55
57
  slots: [
56
58
  {
57
- name: 'Engine',
59
+ name: "Engine",
58
60
  installed: true,
59
61
  description: [
60
- { text: 'generates ' },
61
- { text: '700', highlight: true },
62
- { text: ' thrust for travel' },
62
+ { text: "generates " },
63
+ { text: "700", highlight: true },
64
+ { text: " thrust for travel" },
63
65
  ],
64
66
  },
65
67
  ],
66
- })
67
- expect(svg).toContain('>700<')
68
- expect(svg).toContain('generates')
69
- })
68
+ });
69
+ expect(svg).toContain(">700<");
70
+ expect(svg).toContain("generates");
71
+ });
70
72
 
71
- test('renderShipPanel mixed slots (installed + empty)', () => {
73
+ test("renderShipPanel mixed slots (installed + empty)", () => {
72
74
  const svg = renderShipPanel({
73
- name: 'Ship T1 (Packed)',
75
+ name: "Ship T1 (Packed)",
74
76
  tier: 1,
75
- attributes: [{
76
- capability: 'Hull',
77
- attributes: [{ label: 'Mass', value: 100 }],
78
- }],
77
+ attributes: [
78
+ {
79
+ capability: "Hull",
80
+ attributes: [{ label: "Mass", value: 100 }],
81
+ },
82
+ ],
79
83
  slots: [
80
- { name: 'Engine', installed: true, description: 'generates 500 thrust' },
84
+ { name: "Engine", installed: true, description: "generates 500 thrust" },
81
85
  { installed: false },
82
86
  { installed: false },
83
87
  ],
84
- })
85
- expect(svg).toContain('Engine:')
86
- expect((svg.match(/Empty module/g) ?? []).length).toBe(2)
87
- })
88
+ });
89
+ expect(svg).toContain("Engine:");
90
+ expect((svg.match(/Empty module/g) ?? []).length).toBe(2);
91
+ });
@@ -1,32 +1,34 @@
1
- import { expect, test } from 'bun:test'
2
- import { tierColors as sdkTierColors } from '@shipload/sdk'
3
- import { tokens } from '../src/tokens/index.ts'
1
+ import { expect, test } from "bun:test";
2
+ import { tierColors as sdkTierColors } from "@shipload/sdk";
3
+ import { tokens } from "../src/tokens/index.ts";
4
4
 
5
- test('colors include all resource categories', () => {
6
- for (const cat of ['ore', 'crystal', 'gas', 'regolith', 'biomass']) {
7
- expect(tokens.colors.category).toHaveProperty(cat)
8
- expect(tokens.colors.category[cat as keyof typeof tokens.colors.category]).toMatch(/^#[0-9a-f]{6}$/i)
5
+ test("colors include all resource categories", () => {
6
+ for (const cat of ["ore", "crystal", "gas", "regolith", "biomass"]) {
7
+ expect(tokens.colors.category).toHaveProperty(cat);
8
+ expect(tokens.colors.category[cat as keyof typeof tokens.colors.category]).toMatch(
9
+ /^#[0-9a-f]{6}$/i,
10
+ );
9
11
  }
10
- })
12
+ });
11
13
 
12
- test('colors.tier is sourced from SDK tierColors', () => {
13
- expect(tokens.colors.tier).toEqual(sdkTierColors)
14
- })
14
+ test("colors.tier is sourced from SDK tierColors", () => {
15
+ expect(tokens.colors.tier).toEqual(sdkTierColors);
16
+ });
15
17
 
16
- test('colors include all tiers t1..t5 with SDK values', () => {
17
- expect(tokens.colors.tier.t1).toBe('#8b8b8b')
18
- expect(tokens.colors.tier.t2).toBe('#4ade80')
19
- expect(tokens.colors.tier.t3).toBe('#818cf8')
20
- expect(tokens.colors.tier.t4).toBe('#c084fc')
21
- expect(tokens.colors.tier.t5).toBe('#fbbf24')
22
- })
18
+ test("colors include all tiers t1..t5 with SDK values", () => {
19
+ expect(tokens.colors.tier.t1).toBe("#8b8b8b");
20
+ expect(tokens.colors.tier.t2).toBe("#4ade80");
21
+ expect(tokens.colors.tier.t3).toBe("#818cf8");
22
+ expect(tokens.colors.tier.t4).toBe("#c084fc");
23
+ expect(tokens.colors.tier.t5).toBe("#fbbf24");
24
+ });
23
25
 
24
- test('typography names three font stacks', () => {
25
- expect(tokens.typography.display).toContain('Orbitron')
26
- expect(tokens.typography.sans).toContain('Inter')
27
- expect(tokens.typography.mono).toContain('JetBrains Mono')
28
- })
26
+ test("typography names three font stacks", () => {
27
+ expect(tokens.typography.display).toContain("Orbitron");
28
+ expect(tokens.typography.sans).toContain("Inter");
29
+ expect(tokens.typography.mono).toContain("JetBrains Mono");
30
+ });
29
31
 
30
- test('spacing has a default panel width', () => {
31
- expect(tokens.spacing.panelWidth).toBe(280)
32
- })
32
+ test("spacing has a default panel width", () => {
33
+ expect(tokens.spacing.panelWidth).toBe(280);
34
+ });