@shipload/item-renderer 1.0.0-next.4 → 1.0.0-next.41
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/package.json +2 -2
- package/src/index.ts +50 -8
- package/src/links.ts +11 -8
- package/src/meta.ts +1 -1
- package/src/payload/codec.ts +24 -2
- package/src/primitives/component-icon.ts +229 -0
- package/src/primitives/entity-icon.ts +167 -0
- package/src/primitives/module-slot.ts +4 -4
- package/src/primitives/quantity-badge.ts +15 -5
- package/src/primitives/resource-icon.ts +242 -0
- package/src/primitives/span-paragraph.ts +1 -1
- package/src/primitives/stat-bar.ts +17 -1
- package/src/primitives/stat-stars.ts +36 -0
- package/src/primitives/text.ts +27 -1
- package/src/render.ts +12 -7
- package/src/templates/_shared.ts +113 -2
- package/src/templates/component.ts +41 -61
- package/src/templates/index.ts +2 -1
- package/src/templates/item-cell.ts +111 -38
- package/src/templates/module.ts +59 -145
- package/src/templates/packed-entity.ts +25 -5
- package/src/templates/resource.ts +40 -68
- package/src/templates/ship-panel.ts +99 -85
- package/src/tokens/colors.ts +4 -10
- package/test/fixtures/cargo-items.ts +10 -3
- package/src/primitives/category-icon.ts +0 -110
- package/src/primitives/compact-row.ts +0 -38
|
@@ -1,17 +1,28 @@
|
|
|
1
|
-
import type {ResolvedItem
|
|
2
|
-
import {
|
|
1
|
+
import type {ResolvedItem} from '@shipload/sdk'
|
|
2
|
+
import {getRecipe, getStatDefinitions, resolveItemCategory, formatLocation} 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 {text} from '../primitives/text.ts'
|
|
7
|
-
import {divider} from '../primitives/divider.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 {
|
|
9
|
+
import {
|
|
10
|
+
shortCode,
|
|
11
|
+
formatMass,
|
|
12
|
+
tierBorder,
|
|
13
|
+
metaRowBlock,
|
|
14
|
+
titleText,
|
|
15
|
+
BADGE_Y,
|
|
16
|
+
HEADER_H,
|
|
17
|
+
ICON_Y,
|
|
18
|
+
STAT_BLOCK_GAP,
|
|
19
|
+
STAT_ROW_H,
|
|
20
|
+
BOTTOM_PAD,
|
|
21
|
+
} from './_shared.ts'
|
|
12
22
|
|
|
13
23
|
export interface RenderComponentOpts {
|
|
14
24
|
mode?: 'values' | 'ranges'
|
|
25
|
+
location?: {x: number; y: number}
|
|
15
26
|
}
|
|
16
27
|
|
|
17
28
|
type StatRow = {
|
|
@@ -32,13 +43,15 @@ export function renderComponent(
|
|
|
32
43
|
const pad = tokens.spacing.panelPadding
|
|
33
44
|
const innerW = w - pad * 2
|
|
34
45
|
|
|
46
|
+
const identity = tokens.colors.accent.component
|
|
47
|
+
|
|
35
48
|
let rows: StatRow[]
|
|
36
49
|
if (mode === 'values') {
|
|
37
50
|
rows = (resolved.stats ?? []).map((s) => ({
|
|
38
51
|
label: s.label,
|
|
39
52
|
abbreviation: s.abbreviation,
|
|
40
53
|
value: s.value,
|
|
41
|
-
color:
|
|
54
|
+
color: identity,
|
|
42
55
|
inverted: s.inverted,
|
|
43
56
|
}))
|
|
44
57
|
} else {
|
|
@@ -47,8 +60,9 @@ export function renderComponent(
|
|
|
47
60
|
const src = slot.sources[0]
|
|
48
61
|
if (!src) return []
|
|
49
62
|
const input = recipe!.inputs[src.inputIndex]
|
|
50
|
-
if (!input
|
|
51
|
-
const category = input.
|
|
63
|
+
if (!input) return []
|
|
64
|
+
const category = resolveItemCategory(input.itemId)
|
|
65
|
+
if (!category) return []
|
|
52
66
|
const def = getStatDefinitions(category)[src.statIndex]
|
|
53
67
|
if (!def) return []
|
|
54
68
|
return [
|
|
@@ -56,78 +70,44 @@ export function renderComponent(
|
|
|
56
70
|
label: def.label,
|
|
57
71
|
abbreviation: def.abbreviation,
|
|
58
72
|
value: null,
|
|
59
|
-
color:
|
|
73
|
+
color: identity,
|
|
60
74
|
inverted: def.inverted,
|
|
61
75
|
},
|
|
62
76
|
]
|
|
63
77
|
})
|
|
64
78
|
}
|
|
65
79
|
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
80
|
+
const quantity = Number(BigInt(item.quantity.toString()))
|
|
81
|
+
const metaRows = [
|
|
82
|
+
{label: 'Mass', value: formatMass(resolved.mass * Math.max(quantity, 1))},
|
|
83
|
+
...(opts?.location ? [{label: 'Location', value: formatLocation(opts.location)}] : []),
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
const metaYStart = pad + HEADER_H
|
|
87
|
+
const {svg: metaSvg, height: metaH} = metaRowBlock(pad, metaYStart, innerW, metaRows)
|
|
88
|
+
const statsYStart = metaYStart + metaH + STAT_BLOCK_GAP
|
|
89
|
+
const statsBottom =
|
|
90
|
+
statsYStart + Math.max(0, rows.length - 1) * STAT_ROW_H + tokens.spacing.statBarHeight
|
|
91
|
+
const height = statsBottom + BOTTOM_PAD
|
|
70
92
|
|
|
71
93
|
const chrome = panel({width: w, height, borderColor: tierBorder(resolved.tier)})
|
|
72
94
|
|
|
73
|
-
const
|
|
74
|
-
const badge = quantityBadge({x: w - pad, y: pad, quantity})
|
|
95
|
+
const badge = quantityBadge({x: w - pad, y: pad + BADGE_Y, quantity, tone: identity})
|
|
75
96
|
|
|
76
97
|
const icon = iconHex({
|
|
77
98
|
x: pad,
|
|
78
|
-
y: pad +
|
|
79
|
-
color:
|
|
99
|
+
y: pad + ICON_Y,
|
|
100
|
+
color: identity,
|
|
80
101
|
code: shortCode(resolved.itemId),
|
|
81
102
|
})
|
|
82
103
|
|
|
83
|
-
const name =
|
|
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 subtitleText = 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: `COMPONENT · ${formatTier(resolved.tier)}`,
|
|
103
|
-
size: tokens.typography.sizes.body,
|
|
104
|
-
weight: 600,
|
|
105
|
-
anchor: 'end',
|
|
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 sepY = pad + headerH + metaRowH + 6
|
|
124
|
-
const sep = divider({x: pad, y: sepY, width: innerW})
|
|
104
|
+
const name = titleText(pad + 34, pad + 22, resolved)
|
|
125
105
|
|
|
126
106
|
const statsSvg = rows
|
|
127
107
|
.map((row, i) =>
|
|
128
108
|
statBar({
|
|
129
109
|
x: pad,
|
|
130
|
-
y:
|
|
110
|
+
y: statsYStart + i * STAT_ROW_H,
|
|
131
111
|
width: innerW,
|
|
132
112
|
label: row.label,
|
|
133
113
|
abbreviation: row.abbreviation,
|
|
@@ -138,7 +118,7 @@ export function renderComponent(
|
|
|
138
118
|
)
|
|
139
119
|
.join('')
|
|
140
120
|
|
|
141
|
-
const inner = `${chrome}${icon}${name}${badge}${
|
|
121
|
+
const inner = `${chrome}${icon}${name}${badge}${metaSvg}${statsSvg}`
|
|
142
122
|
|
|
143
123
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`
|
|
144
124
|
}
|
package/src/templates/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {renderModule} from './module.ts'
|
|
|
8
8
|
|
|
9
9
|
export interface RenderByTypeOpts {
|
|
10
10
|
mode?: 'values' | 'ranges'
|
|
11
|
+
location?: {x: number; y: number}
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function renderByType(
|
|
@@ -19,7 +20,7 @@ export function renderByType(
|
|
|
19
20
|
case 'resource':
|
|
20
21
|
return renderResource(item, resolved, opts)
|
|
21
22
|
case 'entity':
|
|
22
|
-
return renderPackedEntity(item, resolved)
|
|
23
|
+
return renderPackedEntity(item, resolved, opts)
|
|
23
24
|
case 'component':
|
|
24
25
|
return renderComponent(item, resolved, opts)
|
|
25
26
|
case 'module':
|
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
import type {ResolvedItem} from '@shipload/sdk'
|
|
2
|
-
import {tierColors
|
|
2
|
+
import {tierColors} from '@shipload/sdk'
|
|
3
3
|
import {el} from '../primitives/svg.ts'
|
|
4
4
|
import {text} from '../primitives/text.ts'
|
|
5
|
-
import {
|
|
5
|
+
import {componentIcon, componentIconSlugForName} from '../primitives/component-icon.ts'
|
|
6
|
+
import {entityIcon, entityIconSlugForName} from '../primitives/entity-icon.ts'
|
|
7
|
+
import {resourceIcon} from '../primitives/resource-icon.ts'
|
|
6
8
|
import {tokens} from '../tokens/index.ts'
|
|
7
9
|
|
|
8
10
|
export interface ItemCellProps {
|
|
9
11
|
resolved: ResolvedItem
|
|
10
12
|
quantity?: number
|
|
11
13
|
size?: number
|
|
14
|
+
quantityColor?: string
|
|
15
|
+
quantityPrefix?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function abbreviateQuantity(n: number): string {
|
|
19
|
+
const abs = Math.abs(n)
|
|
20
|
+
if (abs < 1000) return String(n)
|
|
21
|
+
const units = [
|
|
22
|
+
{v: 1e9, s: 'b'},
|
|
23
|
+
{v: 1e6, s: 'm'},
|
|
24
|
+
{v: 1e3, s: 'k'},
|
|
25
|
+
]
|
|
26
|
+
for (const u of units) {
|
|
27
|
+
if (abs >= u.v) {
|
|
28
|
+
return (n / u.v).toFixed(1).replace(/\.0$/, '') + u.s
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return String(n)
|
|
12
32
|
}
|
|
13
33
|
|
|
14
34
|
export interface ItemCellGroupProps extends ItemCellProps {
|
|
@@ -34,37 +54,70 @@ function cellInner(props: ItemCellProps): string {
|
|
|
34
54
|
'stroke-width': 1.5,
|
|
35
55
|
})
|
|
36
56
|
|
|
57
|
+
const qty = props.quantity ?? 0
|
|
58
|
+
const showQuantity = props.quantityPrefix ? qty >= 1 : qty > 1
|
|
59
|
+
|
|
37
60
|
let content = ''
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
61
|
+
const componentSlug =
|
|
62
|
+
props.resolved.itemType === 'component'
|
|
63
|
+
? componentIconSlugForName(props.resolved.name)
|
|
64
|
+
: null
|
|
65
|
+
const entitySlug =
|
|
66
|
+
props.resolved.itemType === 'entity' ? entityIconSlugForName(props.resolved.name) : null
|
|
67
|
+
|
|
68
|
+
if (componentSlug) {
|
|
69
|
+
const iconSize = Math.round(size * (showQuantity ? 0.66 : 0.84))
|
|
70
|
+
const iconY = showQuantity ? Math.round(size * 0.12) : Math.round(height / 2 - iconSize / 2)
|
|
71
|
+
content = componentIcon(componentSlug, {
|
|
72
|
+
x: (size - iconSize) / 2,
|
|
73
|
+
y: iconY,
|
|
74
|
+
size: iconSize,
|
|
49
75
|
})
|
|
76
|
+
} else if (entitySlug) {
|
|
77
|
+
const iconSize = Math.round(size * (showQuantity ? 0.66 : 0.84))
|
|
78
|
+
const iconY = showQuantity ? Math.round(size * 0.12) : Math.round(height / 2 - iconSize / 2)
|
|
79
|
+
content = entityIcon(entitySlug, {
|
|
80
|
+
x: (size - iconSize) / 2,
|
|
81
|
+
y: iconY,
|
|
82
|
+
size: iconSize,
|
|
83
|
+
})
|
|
84
|
+
} else if (props.resolved.abbreviation) {
|
|
85
|
+
content = showQuantity
|
|
86
|
+
? text({
|
|
87
|
+
x: cx,
|
|
88
|
+
y: size * 0.45,
|
|
89
|
+
value: props.resolved.abbreviation,
|
|
90
|
+
size: Math.round(size * 0.28),
|
|
91
|
+
weight: 700,
|
|
92
|
+
anchor: 'middle',
|
|
93
|
+
color: tokens.colors.text.primary,
|
|
94
|
+
family: tokens.typography.display,
|
|
95
|
+
})
|
|
96
|
+
: text({
|
|
97
|
+
x: cx,
|
|
98
|
+
y: height / 2,
|
|
99
|
+
value: props.resolved.abbreviation,
|
|
100
|
+
size: Math.round(size * 0.36),
|
|
101
|
+
weight: 700,
|
|
102
|
+
anchor: 'middle',
|
|
103
|
+
dominantBaseline: 'central',
|
|
104
|
+
color: tokens.colors.text.primary,
|
|
105
|
+
family: tokens.typography.display,
|
|
106
|
+
})
|
|
50
107
|
} else if (props.resolved.category) {
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
cy: iconCy,
|
|
58
|
-
size: size * 0.32,
|
|
59
|
-
color,
|
|
60
|
-
strokeWidth: 1.5,
|
|
108
|
+
const iconSize = Math.round(size * (showQuantity ? 0.66 : 0.84))
|
|
109
|
+
const iconY = showQuantity ? Math.round(size * 0.12) : Math.round(height / 2 - iconSize / 2)
|
|
110
|
+
content = resourceIcon(props.resolved.category, {
|
|
111
|
+
x: (size - iconSize) / 2,
|
|
112
|
+
y: iconY,
|
|
113
|
+
size: iconSize,
|
|
61
114
|
})
|
|
62
115
|
} else if (props.resolved.icon) {
|
|
63
116
|
content = text({
|
|
64
117
|
x: cx,
|
|
65
|
-
y: size * 0.4,
|
|
118
|
+
y: showQuantity ? size * 0.4 : height / 2,
|
|
66
119
|
value: props.resolved.icon,
|
|
67
|
-
size: Math.round(size * 0.44),
|
|
120
|
+
size: Math.round(size * (showQuantity ? 0.44 : 0.56)),
|
|
68
121
|
weight: 400,
|
|
69
122
|
anchor: 'middle',
|
|
70
123
|
dominantBaseline: 'central',
|
|
@@ -72,21 +125,41 @@ function cellInner(props: ItemCellProps): string {
|
|
|
72
125
|
})
|
|
73
126
|
}
|
|
74
127
|
|
|
75
|
-
const qty = props.quantity ?? 0
|
|
76
128
|
let quantityText = ''
|
|
77
|
-
if (
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
129
|
+
if (showQuantity) {
|
|
130
|
+
const label = (props.quantityPrefix ?? '') + abbreviateQuantity(qty)
|
|
131
|
+
const fontSize = Math.max(8, Math.round(size * 0.2))
|
|
132
|
+
const charW = fontSize * 0.6
|
|
133
|
+
const padX = Math.max(2, Math.round(fontSize * 0.34))
|
|
134
|
+
const padY = Math.max(1, Math.round(fontSize * 0.18))
|
|
135
|
+
const margin = Math.max(2, Math.round(size * 0.06))
|
|
136
|
+
const plateW = Math.round(label.length * charW) + padX * 2
|
|
137
|
+
const plateH = fontSize + padY * 2
|
|
138
|
+
const plateRight = size - margin
|
|
139
|
+
const plateBottom = height - margin
|
|
140
|
+
const plate = el('rect', {
|
|
141
|
+
x: plateRight - plateW,
|
|
142
|
+
y: plateBottom - plateH,
|
|
143
|
+
width: plateW,
|
|
144
|
+
height: plateH,
|
|
145
|
+
rx: Math.round(plateH / 2),
|
|
146
|
+
ry: Math.round(plateH / 2),
|
|
147
|
+
fill: '#050c24',
|
|
148
|
+
'fill-opacity': 0.82,
|
|
89
149
|
})
|
|
150
|
+
quantityText =
|
|
151
|
+
plate +
|
|
152
|
+
text({
|
|
153
|
+
x: plateRight - padX,
|
|
154
|
+
y: plateBottom - plateH / 2,
|
|
155
|
+
value: label,
|
|
156
|
+
size: fontSize,
|
|
157
|
+
weight: 700,
|
|
158
|
+
anchor: 'end',
|
|
159
|
+
dominantBaseline: 'central',
|
|
160
|
+
color: props.quantityColor ?? tokens.colors.text.primary,
|
|
161
|
+
family: tokens.typography.mono,
|
|
162
|
+
})
|
|
90
163
|
}
|
|
91
164
|
|
|
92
165
|
return border + content + quantityText
|
package/src/templates/module.ts
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
import type {ResolvedItem} from '@shipload/sdk'
|
|
2
|
-
import {describeModuleForItem,
|
|
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
6
|
import {text} from '../primitives/text.ts'
|
|
7
|
-
import {divider} from '../primitives/divider.ts'
|
|
8
|
-
import {compactRow} from '../primitives/compact-row.ts'
|
|
9
7
|
import {quantityBadge} from '../primitives/quantity-badge.ts'
|
|
10
8
|
import {spanParagraph} from '../primitives/span-paragraph.ts'
|
|
11
9
|
import {tokens} from '../tokens/index.ts'
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
import {
|
|
11
|
+
shortCode,
|
|
12
|
+
formatMass,
|
|
13
|
+
tierBorder,
|
|
14
|
+
metaRowBlock,
|
|
15
|
+
titleText,
|
|
16
|
+
capabilityColor,
|
|
17
|
+
BADGE_Y,
|
|
18
|
+
HEADER_H,
|
|
19
|
+
ICON_Y,
|
|
20
|
+
META_BLOCK_GAP,
|
|
21
|
+
CAP_HEADER_H,
|
|
22
|
+
BODY_TAIL,
|
|
23
|
+
} from './_shared.ts'
|
|
18
24
|
|
|
19
25
|
export interface RenderModuleOpts {
|
|
20
26
|
mode?: 'values' | 'ranges'
|
|
27
|
+
location?: {x: number; y: number}
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
export function renderModule(
|
|
@@ -31,162 +38,69 @@ export function renderModule(
|
|
|
31
38
|
const innerW = w - pad * 2
|
|
32
39
|
|
|
33
40
|
const group = resolved.attributes?.[0]
|
|
34
|
-
const attrs = group?.attributes ?? []
|
|
35
41
|
const desc = mode === 'values' ? describeModuleForItem(resolved) : undefined
|
|
36
42
|
|
|
37
43
|
const capabilityName = group?.capability ?? resolved.name.replace(/\s+T\d+$/i, '')
|
|
38
44
|
|
|
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
45
|
const quantity = Number(BigInt(item.quantity.toString()))
|
|
73
|
-
const
|
|
46
|
+
const metaRows = [
|
|
47
|
+
{label: 'Mass', value: formatMass(resolved.mass * Math.max(quantity, 1))},
|
|
48
|
+
...(opts?.location ? [{label: 'Location', value: formatLocation(opts.location)}] : []),
|
|
49
|
+
]
|
|
74
50
|
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
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
|
-
})
|
|
51
|
+
const metaYStart = pad + HEADER_H
|
|
52
|
+
const {svg: metaSvg, height: metaH} = metaRowBlock(pad, metaYStart, innerW, metaRows)
|
|
53
|
+
const bodyYStart = metaYStart + metaH + META_BLOCK_GAP
|
|
91
54
|
|
|
92
|
-
const
|
|
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
|
-
})
|
|
55
|
+
const iconColor = group ? capabilityColor(group.capability) : capabilityColor(capabilityName)
|
|
107
56
|
|
|
108
|
-
const
|
|
57
|
+
const capLabel = (group?.capability ?? capabilityName).toUpperCase()
|
|
58
|
+
const capHeader = text({
|
|
109
59
|
x: pad,
|
|
110
|
-
y:
|
|
111
|
-
value:
|
|
112
|
-
size: tokens.typography.sizes.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
y: pad + headerH + metaRowH - 8,
|
|
118
|
-
value: formatMass(resolved.mass),
|
|
119
|
-
size: tokens.typography.sizes.body,
|
|
120
|
-
weight: 600,
|
|
121
|
-
anchor: 'end',
|
|
60
|
+
y: bodyYStart + 16,
|
|
61
|
+
value: capLabel,
|
|
62
|
+
size: tokens.typography.sizes.subtitle,
|
|
63
|
+
weight: 700,
|
|
64
|
+
family: tokens.typography.sans,
|
|
65
|
+
color: iconColor,
|
|
66
|
+
letterSpacing: 1,
|
|
122
67
|
})
|
|
123
68
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
})
|
|
69
|
+
let bodyHeight: number
|
|
70
|
+
let capSection: string
|
|
71
|
+
if (mode === 'values' && desc && group) {
|
|
151
72
|
const spans = renderDescription(desc)
|
|
152
|
-
const {svg: paraSvg} = spanParagraph({
|
|
73
|
+
const {svg: paraSvg, lineCount} = spanParagraph({
|
|
153
74
|
x: pad,
|
|
154
|
-
y:
|
|
75
|
+
y: bodyYStart + 36,
|
|
155
76
|
spans,
|
|
156
77
|
charsPerLine: 36,
|
|
157
78
|
lineHeight: 14,
|
|
79
|
+
highlightColor: tokens.colors.text.primary,
|
|
158
80
|
})
|
|
81
|
+
bodyHeight = CAP_HEADER_H + lineCount * 14 + BODY_TAIL
|
|
159
82
|
capSection = capHeader + paraSvg
|
|
160
|
-
} else
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
83
|
+
} else {
|
|
84
|
+
bodyHeight = CAP_HEADER_H + BODY_TAIL
|
|
85
|
+
capSection = capHeader
|
|
187
86
|
}
|
|
188
87
|
|
|
189
|
-
const
|
|
88
|
+
const height = bodyYStart + bodyHeight + pad
|
|
89
|
+
|
|
90
|
+
const chrome = panel({width: w, height, borderColor: tierBorder(resolved.tier)})
|
|
91
|
+
|
|
92
|
+
const badge = quantityBadge({x: w - pad, y: pad + BADGE_Y, quantity, tone: iconColor})
|
|
93
|
+
|
|
94
|
+
const icon = iconHex({
|
|
95
|
+
x: pad,
|
|
96
|
+
y: pad + ICON_Y,
|
|
97
|
+
color: iconColor,
|
|
98
|
+
code: shortCode(resolved.itemId),
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const name = titleText(pad + 34, pad + 22, resolved)
|
|
102
|
+
|
|
103
|
+
const inner = `${chrome}${icon}${name}${badge}${metaSvg}${capSection}`
|
|
190
104
|
|
|
191
105
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`
|
|
192
106
|
}
|
|
@@ -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 {
|
|
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(
|
|
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:
|
|
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
|
})
|