@shipload/item-renderer 0.2.3 → 1.0.0-next.0

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 (89) hide show
  1. package/package.json +47 -49
  2. package/src/assets/stardust-base64.ts +1 -1
  3. package/src/errors.ts +14 -14
  4. package/src/fonts/index.ts +24 -24
  5. package/src/fonts/load-bun.ts +12 -12
  6. package/src/index.ts +31 -31
  7. package/src/links.ts +13 -13
  8. package/src/meta.ts +26 -26
  9. package/src/payload/base64url.ts +21 -21
  10. package/src/payload/codec.ts +19 -19
  11. package/src/primitives/category-icon.ts +88 -86
  12. package/src/primitives/compact-row.ts +32 -32
  13. package/src/primitives/divider.ts +14 -14
  14. package/src/primitives/icon-hex.ts +36 -36
  15. package/src/primitives/module-slot.ts +131 -131
  16. package/src/primitives/panel.ts +18 -18
  17. package/src/primitives/quantity-badge.ts +32 -32
  18. package/src/primitives/span-paragraph.ts +56 -56
  19. package/src/primitives/stat-bar.ts +72 -72
  20. package/src/primitives/svg.ts +16 -16
  21. package/src/primitives/text.ts +33 -33
  22. package/src/primitives/wrap.ts +19 -19
  23. package/src/render.ts +20 -20
  24. package/src/templates/_shared.ts +5 -5
  25. package/src/templates/component.ts +124 -124
  26. package/src/templates/index.ts +23 -23
  27. package/src/templates/item-cell.ts +84 -84
  28. package/src/templates/module.ts +182 -181
  29. package/src/templates/packed-entity.ts +22 -22
  30. package/src/templates/resource.ts +136 -134
  31. package/src/templates/ship-panel.ts +122 -118
  32. package/src/templates/social-card.ts +36 -36
  33. package/src/tokens/colors.ts +42 -42
  34. package/src/tokens/index.ts +6 -6
  35. package/src/tokens/spacing.ts +9 -9
  36. package/src/tokens/typography.ts +18 -18
  37. package/.claude/settings.local.json +0 -6
  38. package/.github/workflows/ci.yml +0 -14
  39. package/.gitignore +0 -6
  40. package/Makefile +0 -50
  41. package/biome.json +0 -18
  42. package/bun.lock +0 -123
  43. package/scripts/check-bundle-size.ts +0 -37
  44. package/scripts/copy-fonts.ts +0 -41
  45. package/scripts/preview.ts +0 -41
  46. package/test/__image_snapshots__/.gitkeep +0 -0
  47. package/test/__image_snapshots__/component-hull-plates.diff.png +0 -0
  48. package/test/__image_snapshots__/component-hull-plates.png +0 -0
  49. package/test/__image_snapshots__/module-engine-t1.diff.png +0 -0
  50. package/test/__image_snapshots__/module-engine-t1.png +0 -0
  51. package/test/__image_snapshots__/module-storage-t1.diff.png +0 -0
  52. package/test/__image_snapshots__/module-storage-t1.png +0 -0
  53. package/test/__image_snapshots__/packed-entity-ship-t1-only-engine.diff.png +0 -0
  54. package/test/__image_snapshots__/packed-entity-ship-t1-only-engine.png +0 -0
  55. package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.diff.png +0 -0
  56. package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.png +0 -0
  57. package/test/__image_snapshots__/resource-ore-t1.diff.png +0 -0
  58. package/test/__image_snapshots__/resource-ore-t1.png +0 -0
  59. package/test/__snapshots__/templates-component.test.ts.snap +0 -5
  60. package/test/__snapshots__/templates-item-cell.test.ts.snap +0 -9
  61. package/test/__snapshots__/templates-module.test.ts.snap +0 -17
  62. package/test/__snapshots__/templates-packed-entity.test.ts.snap +0 -5
  63. package/test/__snapshots__/templates-resource.test.ts.snap +0 -7
  64. package/test/base64url.test.ts +0 -33
  65. package/test/codec.test.ts +0 -34
  66. package/test/errors.test.ts +0 -24
  67. package/test/fixtures/cargo-items.ts +0 -122
  68. package/test/fonts.test.ts +0 -28
  69. package/test/links-meta.test.ts +0 -43
  70. package/test/pixel.test.ts +0 -69
  71. package/test/primitives-category-icon.test.ts +0 -86
  72. package/test/primitives-compact-row.test.ts +0 -44
  73. package/test/primitives-domain.test.ts +0 -83
  74. package/test/primitives-layout.test.ts +0 -56
  75. package/test/primitives-module-slot.test.ts +0 -88
  76. package/test/render.test.ts +0 -43
  77. package/test/sanity.test.ts +0 -6
  78. package/test/sdk-link.test.ts +0 -19
  79. package/test/snapshots/.gitkeep +0 -0
  80. package/test/svg.test.ts +0 -30
  81. package/test/templates-component.test.ts +0 -36
  82. package/test/templates-dispatch.test.ts +0 -35
  83. package/test/templates-item-cell.test.ts +0 -94
  84. package/test/templates-module.test.ts +0 -63
  85. package/test/templates-packed-entity.test.ts +0 -47
  86. package/test/templates-resource.test.ts +0 -71
  87. package/test/templates-ship-panel.test.ts +0 -91
  88. package/test/tokens.test.ts +0 -34
  89. package/tsconfig.json +0 -20
@@ -1,191 +1,192 @@
1
- import type { ResolvedItem } from "@shipload/sdk";
2
- import { describeModuleForItem, formatTier, renderDescription } from "@shipload/sdk";
3
- import type { CargoItem } from "../payload/codec.ts";
4
- import { panel } from "../primitives/panel.ts";
5
- import { iconHex } from "../primitives/icon-hex.ts";
6
- import { text } from "../primitives/text.ts";
7
- import { divider } from "../primitives/divider.ts";
8
- import { compactRow } from "../primitives/compact-row.ts";
9
- import { quantityBadge } from "../primitives/quantity-badge.ts";
10
- import { spanParagraph } from "../primitives/span-paragraph.ts";
11
- import { tokens } from "../tokens/index.ts";
12
- import { shortCode, formatMass, tierBorder } from "./_shared.ts";
1
+ import type {ResolvedItem} from '@shipload/sdk'
2
+ import {describeModuleForItem, formatTier, renderDescription} from '@shipload/sdk'
3
+ import type {CargoItem} from '../payload/codec.ts'
4
+ import {panel} from '../primitives/panel.ts'
5
+ import {iconHex} from '../primitives/icon-hex.ts'
6
+ import {text} from '../primitives/text.ts'
7
+ import {divider} from '../primitives/divider.ts'
8
+ import {compactRow} from '../primitives/compact-row.ts'
9
+ import {quantityBadge} from '../primitives/quantity-badge.ts'
10
+ import {spanParagraph} from '../primitives/span-paragraph.ts'
11
+ import {tokens} from '../tokens/index.ts'
12
+ import {shortCode, formatMass, tierBorder} from './_shared.ts'
13
13
 
14
14
  function capabilityColor(name: string): string {
15
- const key = name.toLowerCase().replace(/\s+/g, "") as keyof typeof tokens.colors.capability;
16
- return tokens.colors.capability[key] ?? tokens.colors.accent.component;
15
+ const key = name.toLowerCase().replace(/\s+/g, '') as keyof typeof tokens.colors.capability
16
+ return tokens.colors.capability[key] ?? tokens.colors.accent.component
17
17
  }
18
18
 
19
19
  export interface RenderModuleOpts {
20
- mode?: "values" | "ranges";
20
+ mode?: 'values' | 'ranges'
21
21
  }
22
22
 
23
23
  export function renderModule(
24
- item: CargoItem,
25
- resolved: ResolvedItem,
26
- opts?: RenderModuleOpts,
24
+ item: CargoItem,
25
+ resolved: ResolvedItem,
26
+ opts?: RenderModuleOpts
27
27
  ): string {
28
- const mode = opts?.mode ?? "values";
29
- const w = tokens.spacing.panelWidth;
30
- const pad = tokens.spacing.panelPadding;
31
- const innerW = w - pad * 2;
32
-
33
- const group = resolved.attributes?.[0];
34
- const attrs = group?.attributes ?? [];
35
- const desc = mode === "values" ? describeModuleForItem(resolved) : undefined;
36
-
37
- const capabilityName = group?.capability ?? resolved.name.replace(/\s+T\d+$/i, "");
38
-
39
- const headerH = 48;
40
- const metaRowH = 28;
41
- const sepY = pad + headerH + metaRowH + 6;
42
-
43
- let bodyHeight = 0;
44
- if (mode === "ranges") {
45
- bodyHeight = 20 + 8;
46
- } else if (desc && group) {
47
- const plain = renderDescription(desc)
48
- .map((s) => s.text)
49
- .join("");
50
- const lines = plain.split(/\s+/).reduce(
51
- (acc, word) => {
52
- const last = acc[acc.length - 1] ?? "";
53
- if (last.length === 0) return [...acc.slice(0, -1), word];
54
- if (last.length + 1 + word.length <= 36) return [...acc.slice(0, -1), `${last} ${word}`];
55
- return [...acc, word];
56
- },
57
- [""],
58
- );
59
- const lineCount = lines.filter((l) => l.length > 0).length;
60
- bodyHeight = 20 + lineCount * 14 + 8;
61
- } else if (group && attrs.length > 0) {
62
- const capHeaderH = 22;
63
- const attrsH = attrs.length * 18;
64
- bodyHeight = capHeaderH + attrsH + 8;
65
- }
66
-
67
- const height = headerH + metaRowH + 14 + bodyHeight + pad;
68
-
69
- const chrome = panel({ width: w, height, borderColor: tierBorder(resolved.tier) });
70
-
71
- const quantity = Number(BigInt(item.quantity.toString()));
72
- const badge = quantityBadge({ x: w - pad, y: pad, quantity });
73
-
74
- const iconColor = group ? capabilityColor(group.capability) : capabilityColor(capabilityName);
75
- const icon = iconHex({
76
- x: pad,
77
- y: pad + 4,
78
- color: iconColor,
79
- code: shortCode(resolved.itemId),
80
- });
81
-
82
- const name = text({
83
- x: pad + 34,
84
- y: pad + 22,
85
- value: resolved.name,
86
- size: tokens.typography.sizes.title,
87
- weight: 700,
88
- family: tokens.typography.display,
89
- });
90
-
91
- const subtitleLabel = text({
92
- x: pad,
93
- y: pad + headerH + 4,
94
- value: "Type",
95
- size: tokens.typography.sizes.body,
96
- color: tokens.colors.text.secondary,
97
- });
98
- const subtitleValue = text({
99
- x: w - pad,
100
- y: pad + headerH + 4,
101
- value: `MODULE · ${formatTier(resolved.tier)}`,
102
- size: tokens.typography.sizes.body,
103
- weight: 600,
104
- anchor: "end",
105
- });
106
-
107
- const massLabel = text({
108
- x: pad,
109
- y: pad + headerH + metaRowH - 8,
110
- value: "Mass",
111
- size: tokens.typography.sizes.body,
112
- color: tokens.colors.text.secondary,
113
- });
114
- const massValue = text({
115
- x: w - pad,
116
- y: pad + headerH + metaRowH - 8,
117
- value: formatMass(resolved.mass),
118
- size: tokens.typography.sizes.body,
119
- weight: 600,
120
- anchor: "end",
121
- });
122
-
123
- const sep = divider({ x: pad, y: sepY, width: innerW });
124
-
125
- let capSection = "";
126
- if (mode === "ranges") {
127
- const accentColor = capabilityColor(capabilityName);
128
- capSection = text({
129
- x: pad,
130
- y: sepY + 16,
131
- value: capabilityName.toUpperCase(),
132
- size: tokens.typography.sizes.subtitle,
133
- weight: 700,
134
- family: tokens.typography.sans,
135
- color: accentColor,
136
- letterSpacing: 1,
137
- });
138
- } else if (desc && group) {
139
- const accentColor = capabilityColor(group.capability);
140
- const capHeader = text({
141
- x: pad,
142
- y: sepY + 16,
143
- value: group.capability.toUpperCase(),
144
- size: tokens.typography.sizes.subtitle,
145
- weight: 700,
146
- family: tokens.typography.sans,
147
- color: accentColor,
148
- letterSpacing: 1,
149
- });
150
- const spans = renderDescription(desc);
151
- const { svg: paraSvg } = spanParagraph({
152
- x: pad,
153
- y: sepY + 36,
154
- spans,
155
- charsPerLine: 36,
156
- lineHeight: 14,
157
- });
158
- capSection = capHeader + paraSvg;
159
- } else if (group && attrs.length > 0) {
160
- const capY = sepY + 22;
161
- const capHeader = text({
162
- x: pad,
163
- y: capY,
164
- value: group.capability.toUpperCase(),
165
- size: 10,
166
- weight: 700,
167
- family: tokens.typography.sans,
168
- color: capabilityColor(group.capability),
169
- letterSpacing: 0.8,
170
- });
171
-
172
- const attrRows = attrs
173
- .map((attr, i) => {
174
- const displayValue = String(attr.value);
175
- return compactRow({
176
- x: pad,
177
- y: capY + 14 + i * 18,
178
- width: innerW,
179
- label: attr.label,
180
- value: displayValue,
181
- });
182
- })
183
- .join("");
184
-
185
- capSection = capHeader + attrRows;
186
- }
187
-
188
- const inner = `${chrome}${icon}${name}${badge}${subtitleLabel}${subtitleValue}${massLabel}${massValue}${sep}${capSection}`;
189
-
190
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`;
28
+ const mode = opts?.mode ?? 'values'
29
+ const w = tokens.spacing.panelWidth
30
+ const pad = tokens.spacing.panelPadding
31
+ const innerW = w - pad * 2
32
+
33
+ const group = resolved.attributes?.[0]
34
+ const attrs = group?.attributes ?? []
35
+ const desc = mode === 'values' ? describeModuleForItem(resolved) : undefined
36
+
37
+ const capabilityName = group?.capability ?? resolved.name.replace(/\s+T\d+$/i, '')
38
+
39
+ const headerH = 48
40
+ const metaRowH = 28
41
+ const sepY = pad + headerH + metaRowH + 6
42
+
43
+ let bodyHeight = 0
44
+ if (mode === 'ranges') {
45
+ bodyHeight = 20 + 8
46
+ } else if (desc && group) {
47
+ const plain = renderDescription(desc)
48
+ .map((s) => s.text)
49
+ .join('')
50
+ const lines = plain.split(/\s+/).reduce(
51
+ (acc, word) => {
52
+ const last = acc[acc.length - 1] ?? ''
53
+ if (last.length === 0) return [...acc.slice(0, -1), word]
54
+ if (last.length + 1 + word.length <= 36)
55
+ return [...acc.slice(0, -1), `${last} ${word}`]
56
+ return [...acc, word]
57
+ },
58
+ ['']
59
+ )
60
+ const lineCount = lines.filter((l) => l.length > 0).length
61
+ bodyHeight = 20 + lineCount * 14 + 8
62
+ } else if (group && attrs.length > 0) {
63
+ const capHeaderH = 22
64
+ const attrsH = attrs.length * 18
65
+ bodyHeight = capHeaderH + attrsH + 8
66
+ }
67
+
68
+ const height = headerH + metaRowH + 14 + bodyHeight + pad
69
+
70
+ const chrome = panel({width: w, height, borderColor: tierBorder(resolved.tier)})
71
+
72
+ const quantity = Number(BigInt(item.quantity.toString()))
73
+ const badge = quantityBadge({x: w - pad, y: pad, quantity})
74
+
75
+ const iconColor = group ? capabilityColor(group.capability) : capabilityColor(capabilityName)
76
+ const icon = iconHex({
77
+ x: pad,
78
+ y: pad + 4,
79
+ color: iconColor,
80
+ code: shortCode(resolved.itemId),
81
+ })
82
+
83
+ const name = text({
84
+ x: pad + 34,
85
+ y: pad + 22,
86
+ value: resolved.name,
87
+ size: tokens.typography.sizes.title,
88
+ weight: 700,
89
+ family: tokens.typography.display,
90
+ })
91
+
92
+ const subtitleLabel = text({
93
+ x: pad,
94
+ y: pad + headerH + 4,
95
+ value: 'Type',
96
+ size: tokens.typography.sizes.body,
97
+ color: tokens.colors.text.secondary,
98
+ })
99
+ const subtitleValue = text({
100
+ x: w - pad,
101
+ y: pad + headerH + 4,
102
+ value: `MODULE · ${formatTier(resolved.tier)}`,
103
+ size: tokens.typography.sizes.body,
104
+ weight: 600,
105
+ anchor: 'end',
106
+ })
107
+
108
+ const massLabel = text({
109
+ x: pad,
110
+ y: pad + headerH + metaRowH - 8,
111
+ value: 'Mass',
112
+ size: tokens.typography.sizes.body,
113
+ color: tokens.colors.text.secondary,
114
+ })
115
+ const massValue = text({
116
+ x: w - pad,
117
+ y: pad + headerH + metaRowH - 8,
118
+ value: formatMass(resolved.mass),
119
+ size: tokens.typography.sizes.body,
120
+ weight: 600,
121
+ anchor: 'end',
122
+ })
123
+
124
+ const sep = divider({x: pad, y: sepY, width: innerW})
125
+
126
+ let capSection = ''
127
+ if (mode === 'ranges') {
128
+ const accentColor = capabilityColor(capabilityName)
129
+ capSection = text({
130
+ x: pad,
131
+ y: sepY + 16,
132
+ value: capabilityName.toUpperCase(),
133
+ size: tokens.typography.sizes.subtitle,
134
+ weight: 700,
135
+ family: tokens.typography.sans,
136
+ color: accentColor,
137
+ letterSpacing: 1,
138
+ })
139
+ } else if (desc && group) {
140
+ const accentColor = capabilityColor(group.capability)
141
+ const capHeader = text({
142
+ x: pad,
143
+ y: sepY + 16,
144
+ value: group.capability.toUpperCase(),
145
+ size: tokens.typography.sizes.subtitle,
146
+ weight: 700,
147
+ family: tokens.typography.sans,
148
+ color: accentColor,
149
+ letterSpacing: 1,
150
+ })
151
+ const spans = renderDescription(desc)
152
+ const {svg: paraSvg} = spanParagraph({
153
+ x: pad,
154
+ y: sepY + 36,
155
+ spans,
156
+ charsPerLine: 36,
157
+ lineHeight: 14,
158
+ })
159
+ capSection = capHeader + paraSvg
160
+ } else if (group && attrs.length > 0) {
161
+ const capY = sepY + 22
162
+ const capHeader = text({
163
+ x: pad,
164
+ y: capY,
165
+ value: group.capability.toUpperCase(),
166
+ size: 10,
167
+ weight: 700,
168
+ family: tokens.typography.sans,
169
+ color: capabilityColor(group.capability),
170
+ letterSpacing: 0.8,
171
+ })
172
+
173
+ const attrRows = attrs
174
+ .map((attr, i) => {
175
+ const displayValue = String(attr.value)
176
+ return compactRow({
177
+ x: pad,
178
+ y: capY + 14 + i * 18,
179
+ width: innerW,
180
+ label: attr.label,
181
+ value: displayValue,
182
+ })
183
+ })
184
+ .join('')
185
+
186
+ capSection = capHeader + attrRows
187
+ }
188
+
189
+ const inner = `${chrome}${icon}${name}${badge}${subtitleLabel}${subtitleValue}${massLabel}${massValue}${sep}${capSection}`
190
+
191
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`
191
192
  }
@@ -1,28 +1,28 @@
1
- import type { ResolvedItem, ResolvedModuleSlot } from "@shipload/sdk";
2
- import { describeModuleForSlot, renderDescription } from "@shipload/sdk";
3
- import type { CargoItem } from "../payload/codec.ts";
4
- import { renderShipPanel, type ShipPanelSlot } from "./ship-panel.ts";
1
+ import type {ResolvedItem, ResolvedModuleSlot} from '@shipload/sdk'
2
+ import {describeModuleForSlot, renderDescription} from '@shipload/sdk'
3
+ import type {CargoItem} from '../payload/codec.ts'
4
+ import {renderShipPanel, type ShipPanelSlot} from './ship-panel.ts'
5
5
 
6
6
  function slotToPanelSlot(slot: ResolvedModuleSlot): ShipPanelSlot {
7
- if (!slot.installed || !slot.attributes || !slot.name) {
8
- return { installed: false };
9
- }
10
- const desc = describeModuleForSlot(slot);
11
- if (desc) {
12
- return { name: slot.name, installed: true, description: renderDescription(desc) };
13
- }
14
- const shorthand = slot.attributes.map((a) => `${a.value} ${a.label.toLowerCase()}`).join(" · ");
15
- return { name: slot.name, installed: true, description: shorthand };
7
+ if (!slot.installed || !slot.attributes || !slot.name) {
8
+ return {installed: false}
9
+ }
10
+ const desc = describeModuleForSlot(slot)
11
+ if (desc) {
12
+ return {name: slot.name, installed: true, description: renderDescription(desc)}
13
+ }
14
+ const shorthand = slot.attributes.map((a) => `${a.value} ${a.label.toLowerCase()}`).join(' · ')
15
+ return {name: slot.name, installed: true, description: shorthand}
16
16
  }
17
17
 
18
18
  export function renderPackedEntity(item: CargoItem, resolved: ResolvedItem): string {
19
- const quantity = Number(BigInt(item.quantity.toString()));
20
- const slots = (resolved.moduleSlots ?? []).map(slotToPanelSlot);
21
- return renderShipPanel({
22
- name: `${resolved.name} (Packed)`,
23
- tier: resolved.tier,
24
- quantity,
25
- attributes: resolved.attributes ?? [],
26
- slots,
27
- });
19
+ const quantity = Number(BigInt(item.quantity.toString()))
20
+ const slots = (resolved.moduleSlots ?? []).map(slotToPanelSlot)
21
+ return renderShipPanel({
22
+ name: `${resolved.name} (Packed)`,
23
+ tier: resolved.tier,
24
+ quantity,
25
+ attributes: resolved.attributes ?? [],
26
+ slots,
27
+ })
28
28
  }