@shipload/item-renderer 0.2.2 → 1.0.0-beta1

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