@shipload/item-renderer 1.0.0-next.5 → 1.0.0-next.50

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.
@@ -1,23 +1,31 @@
1
1
  import type {ResolvedItem} from '@shipload/sdk'
2
- import {describeModuleForItem, formatTier, renderDescription} from '@shipload/sdk'
2
+ import {describeModuleForItem, formatLocation, renderDescription} from '@shipload/sdk'
3
3
  import type {CargoItem} from '../payload/codec.ts'
4
4
  import {panel} from '../primitives/panel.ts'
5
5
  import {iconHex} from '../primitives/icon-hex.ts'
6
+ import {moduleIcon, moduleIconSlugForName} from '../primitives/module-icon.ts'
6
7
  import {text} from '../primitives/text.ts'
7
- import {divider} from '../primitives/divider.ts'
8
- import {compactRow} from '../primitives/compact-row.ts'
9
8
  import {quantityBadge} from '../primitives/quantity-badge.ts'
10
9
  import {spanParagraph} from '../primitives/span-paragraph.ts'
11
10
  import {tokens} from '../tokens/index.ts'
12
- import {shortCode, formatMass, tierBorder} from './_shared.ts'
13
-
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
17
- }
11
+ import {
12
+ shortCode,
13
+ formatMass,
14
+ tierBorder,
15
+ metaRowBlock,
16
+ titleText,
17
+ capabilityColor,
18
+ BADGE_Y,
19
+ HEADER_H,
20
+ ICON_Y,
21
+ META_BLOCK_GAP,
22
+ CAP_HEADER_H,
23
+ BODY_TAIL,
24
+ } from './_shared.ts'
18
25
 
19
26
  export interface RenderModuleOpts {
20
27
  mode?: 'values' | 'ranges'
28
+ location?: {x: number; y: number}
21
29
  }
22
30
 
23
31
  export function renderModule(
@@ -31,162 +39,72 @@ export function renderModule(
31
39
  const innerW = w - pad * 2
32
40
 
33
41
  const group = resolved.attributes?.[0]
34
- const attrs = group?.attributes ?? []
35
42
  const desc = mode === 'values' ? describeModuleForItem(resolved) : undefined
36
43
 
37
44
  const capabilityName = group?.capability ?? resolved.name.replace(/\s+T\d+$/i, '')
38
45
 
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
46
  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
- })
47
+ const metaRows = [
48
+ {label: 'Mass', value: formatMass(resolved.mass * Math.max(quantity, 1))},
49
+ ...(opts?.location ? [{label: 'Location', value: formatLocation(opts.location)}] : []),
50
+ ]
82
51
 
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
- })
52
+ const metaYStart = pad + HEADER_H
53
+ const {svg: metaSvg, height: metaH} = metaRowBlock(pad, metaYStart, innerW, metaRows)
54
+ const bodyYStart = metaYStart + metaH + META_BLOCK_GAP
91
55
 
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
- })
56
+ const iconColor = group ? capabilityColor(group.capability) : capabilityColor(capabilityName)
107
57
 
108
- const massLabel = text({
58
+ const capLabel = (group?.capability ?? capabilityName).toUpperCase()
59
+ const capHeader = text({
109
60
  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',
61
+ y: bodyYStart + 16,
62
+ value: capLabel,
63
+ size: tokens.typography.sizes.subtitle,
64
+ weight: 700,
65
+ family: tokens.typography.sans,
66
+ color: iconColor,
67
+ letterSpacing: 1,
122
68
  })
123
69
 
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
- })
70
+ let bodyHeight: number
71
+ let capSection: string
72
+ if (mode === 'values' && desc && group) {
151
73
  const spans = renderDescription(desc)
152
- const {svg: paraSvg} = spanParagraph({
74
+ const {svg: paraSvg, lineCount} = spanParagraph({
153
75
  x: pad,
154
- y: sepY + 36,
76
+ y: bodyYStart + 36,
155
77
  spans,
156
78
  charsPerLine: 36,
157
79
  lineHeight: 14,
80
+ highlightColor: tokens.colors.text.primary,
158
81
  })
82
+ bodyHeight = CAP_HEADER_H + lineCount * 14 + BODY_TAIL
159
83
  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
84
+ } else {
85
+ bodyHeight = CAP_HEADER_H + BODY_TAIL
86
+ capSection = capHeader
187
87
  }
188
88
 
189
- const inner = `${chrome}${icon}${name}${badge}${subtitleLabel}${subtitleValue}${massLabel}${massValue}${sep}${capSection}`
89
+ const height = bodyYStart + bodyHeight + pad
90
+
91
+ const chrome = panel({width: w, height, borderColor: tierBorder(resolved.tier)})
92
+
93
+ const badge = quantityBadge({x: w - pad, y: pad + BADGE_Y, quantity, tone: iconColor})
94
+
95
+ const moduleSlug = moduleIconSlugForName(resolved.name)
96
+ const icon = moduleSlug
97
+ ? moduleIcon(moduleSlug, {x: pad, y: pad + ICON_Y - 2, size: 28})
98
+ : iconHex({
99
+ x: pad,
100
+ y: pad + ICON_Y,
101
+ color: iconColor,
102
+ code: shortCode(resolved.itemId),
103
+ })
104
+
105
+ const name = titleText(pad + 34, pad + 22, resolved)
106
+
107
+ const inner = `${chrome}${icon}${name}${badge}${metaSvg}${capSection}`
190
108
 
191
109
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`
192
110
  }
@@ -1,27 +1,47 @@
1
1
  import type {ResolvedItem, ResolvedModuleSlot} from '@shipload/sdk'
2
- import {describeModuleForSlot, renderDescription} from '@shipload/sdk'
2
+ import {baseName, describeModuleForSlot, renderDescription} from '@shipload/sdk'
3
3
  import type {CargoItem} from '../payload/codec.ts'
4
4
  import {renderShipPanel, type ShipPanelSlot} from './ship-panel.ts'
5
5
 
6
+ function capabilityFromName(name: string): string {
7
+ return name.replace(/\s+T\d+\s*$/i, '').trim()
8
+ }
9
+
6
10
  function slotToPanelSlot(slot: ResolvedModuleSlot): ShipPanelSlot {
7
11
  if (!slot.installed || !slot.attributes || !slot.name) {
8
12
  return {installed: false}
9
13
  }
14
+ const capability = capabilityFromName(slot.name)
10
15
  const desc = describeModuleForSlot(slot)
11
16
  if (desc) {
12
- return {name: slot.name, installed: true, description: renderDescription(desc)}
17
+ return {
18
+ name: slot.name,
19
+ installed: true,
20
+ capability,
21
+ description: renderDescription(desc),
22
+ }
13
23
  }
14
24
  const shorthand = slot.attributes.map((a) => `${a.value} ${a.label.toLowerCase()}`).join(' · ')
15
- return {name: slot.name, installed: true, description: shorthand}
25
+ return {name: slot.name, installed: true, capability, description: shorthand}
26
+ }
27
+
28
+ export interface RenderPackedEntityOpts {
29
+ mode?: 'values' | 'ranges'
30
+ location?: {x: number; y: number}
16
31
  }
17
32
 
18
- export function renderPackedEntity(item: CargoItem, resolved: ResolvedItem): string {
33
+ export function renderPackedEntity(
34
+ item: CargoItem,
35
+ resolved: ResolvedItem,
36
+ opts?: RenderPackedEntityOpts
37
+ ): string {
19
38
  const quantity = Number(BigInt(item.quantity.toString()))
20
39
  const slots = (resolved.moduleSlots ?? []).map(slotToPanelSlot)
21
40
  return renderShipPanel({
22
- name: `${resolved.name} (Packed)`,
41
+ name: baseName(resolved),
23
42
  tier: resolved.tier,
24
43
  quantity,
44
+ location: opts?.location,
25
45
  attributes: resolved.attributes ?? [],
26
46
  slots,
27
47
  })
@@ -1,22 +1,23 @@
1
1
  import type {ResolvedItem} from '@shipload/sdk'
2
- import {getStatDefinitions, categoryColors, displayName} from '@shipload/sdk'
2
+ import {getStatDefinitions, categoryColors, formatLocation, starsForStat} from '@shipload/sdk'
3
3
  import type {CargoItem} from '../payload/codec.ts'
4
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'
5
+ import {resourceIcon} from '../primitives/resource-icon.ts'
8
6
  import {statBar} from '../primitives/stat-bar.ts'
9
7
  import {quantityBadge} from '../primitives/quantity-badge.ts'
10
8
  import {tokens} from '../tokens/index.ts'
11
- import {shortCode, formatMass, tierBorder} from './_shared.ts'
12
-
13
- const CATEGORY_LABELS: Record<string, string> = {
14
- ore: 'Ore',
15
- crystal: 'Crystal',
16
- gas: 'Gas',
17
- regolith: 'Regolith',
18
- biomass: 'Biomass',
19
- }
9
+ import {
10
+ formatMass,
11
+ tierBorder,
12
+ metaRowBlock,
13
+ titleText,
14
+ BADGE_Y,
15
+ HEADER_H,
16
+ ICON_Y,
17
+ STAT_BLOCK_GAP,
18
+ STAT_ROW_H,
19
+ BOTTOM_PAD,
20
+ } from './_shared.ts'
20
21
 
21
22
  function categoryColor(category?: string): string {
22
23
  if (!category) return tokens.colors.text.muted
@@ -26,6 +27,7 @@ function categoryColor(category?: string): string {
26
27
 
27
28
  export interface RenderResourceOpts {
28
29
  mode?: 'values' | 'ranges'
30
+ location?: {x: number; y: number}
29
31
  }
30
32
 
31
33
  type StatRow = {
@@ -69,83 +71,53 @@ export function renderResource(
69
71
  }))
70
72
  }
71
73
 
72
- const headerH = 48
73
- const metaRowH = 28
74
- const statsH = rows.length * 26 + 8
75
- const height = headerH + metaRowH + 14 + statsH + pad
74
+ const metaRows = [
75
+ ...(opts?.location ? [{label: 'Location', value: formatLocation(opts.location)}] : []),
76
+ ]
76
77
 
77
- const chrome = panel({width: w, height, borderColor: tierBorder(resolved.tier)})
78
+ const metaYStart = pad + HEADER_H
79
+ const {svg: metaSvg, height: metaH} = metaRowBlock(pad, metaYStart, innerW, metaRows)
80
+ const statsYStart = metaYStart + metaH + STAT_BLOCK_GAP
81
+ const statsBottom =
82
+ statsYStart + Math.max(0, rows.length - 1) * STAT_ROW_H + tokens.spacing.statBarHeight
83
+ const height = statsBottom + BOTTOM_PAD
78
84
 
79
- const quantity = Number(BigInt(item.quantity.toString()))
80
- const badge = quantityBadge({x: w - pad, y: pad, quantity})
81
-
82
- const icon = iconHex({
83
- x: pad,
84
- y: pad + 4,
85
- color: categoryColor(resolved.category),
86
- code: shortCode(resolved.itemId),
87
- })
85
+ const chrome = panel({width: w, height, borderColor: tierBorder(resolved.tier)})
88
86
 
89
- const name = text({
90
- x: pad + 34,
91
- y: pad + 22,
92
- value: displayName(resolved),
93
- size: tokens.typography.sizes.title,
94
- weight: 700,
95
- family: tokens.typography.display,
96
- })
87
+ const identity = categoryColor(resolved.category)
97
88
 
98
- const catLabel = resolved.category ? (CATEGORY_LABELS[resolved.category] ?? 'Item') : 'Item'
99
- const catText = text({
100
- x: pad,
101
- y: pad + headerH + 4,
102
- value: 'Category',
103
- size: tokens.typography.sizes.body,
104
- color: tokens.colors.text.secondary,
105
- })
106
- const catValue = text({
107
- x: w - pad,
108
- y: pad + headerH + 4,
109
- value: catLabel,
110
- size: tokens.typography.sizes.body,
111
- weight: 600,
112
- anchor: 'end',
113
- })
114
- const massLabel = text({
115
- x: pad,
116
- y: pad + headerH + metaRowH - 8,
117
- value: 'Mass',
118
- size: tokens.typography.sizes.body,
119
- color: tokens.colors.text.secondary,
120
- })
121
- const massValue = text({
89
+ const quantity = Number(BigInt(item.quantity.toString()))
90
+ const badge = quantityBadge({
122
91
  x: w - pad,
123
- y: pad + headerH + metaRowH - 8,
124
- value: formatMass(resolved.mass),
125
- size: tokens.typography.sizes.body,
126
- weight: 600,
127
- anchor: 'end',
92
+ y: pad + BADGE_Y,
93
+ quantity,
94
+ label: formatMass(quantity * resolved.mass),
95
+ tone: identity,
128
96
  })
129
97
 
130
- const sepY = pad + headerH + metaRowH + 6
131
- const sep = divider({x: pad, y: sepY, width: innerW})
98
+ const icon = resolved.category
99
+ ? resourceIcon(resolved.category, {x: pad, y: pad + ICON_Y - 2, size: 28})
100
+ : ''
101
+
102
+ const name = titleText(pad + 34, pad + 22, resolved)
132
103
 
133
104
  const statsSvg = rows
134
105
  .map((row, i) =>
135
106
  statBar({
136
107
  x: pad,
137
- y: sepY + 18 + i * 26,
108
+ y: statsYStart + i * STAT_ROW_H,
138
109
  width: innerW,
139
110
  label: row.label,
140
111
  abbreviation: row.abbreviation,
141
112
  value: row.value,
142
113
  color: row.color,
143
114
  inverted: row.inverted,
115
+ stars: row.value !== null ? starsForStat(row.value) : undefined,
144
116
  })
145
117
  )
146
118
  .join('')
147
119
 
148
- const inner = `${chrome}${icon}${name}${badge}${catText}${catValue}${massLabel}${massValue}${sep}${statsSvg}`
120
+ const inner = `${chrome}${icon}${name}${badge}${metaSvg}${statsSvg}`
149
121
 
150
122
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`
151
123
  }