@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
|
@@ -71,7 +71,7 @@ export function moduleSlot(props: ModuleSlotProps): string {
|
|
|
71
71
|
)
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
const accent = props.accentColor ?? tokens.colors.
|
|
74
|
+
const accent = props.accentColor ?? tokens.colors.accent.component
|
|
75
75
|
const label = `${props.capability ?? 'Module'}: `
|
|
76
76
|
|
|
77
77
|
const desc = props.description
|
|
@@ -89,7 +89,7 @@ export function moduleSlot(props: ModuleSlotProps): string {
|
|
|
89
89
|
value: label.trimEnd(),
|
|
90
90
|
size: tokens.typography.sizes.body,
|
|
91
91
|
weight: 600,
|
|
92
|
-
color:
|
|
92
|
+
color: accent,
|
|
93
93
|
})
|
|
94
94
|
)
|
|
95
95
|
}
|
|
@@ -99,9 +99,9 @@ export function moduleSlot(props: ModuleSlotProps): string {
|
|
|
99
99
|
const combined = label + descPlain
|
|
100
100
|
const lines = wrapText({value: combined, charsPerLine: 36})
|
|
101
101
|
|
|
102
|
-
const highlightColor = tokens.colors.text.
|
|
102
|
+
const highlightColor = tokens.colors.text.primary
|
|
103
103
|
const bodyColor = tokens.colors.text.secondary
|
|
104
|
-
const labelColor =
|
|
104
|
+
const labelColor = accent
|
|
105
105
|
const size = tokens.typography.sizes.body
|
|
106
106
|
const fontFamily = escapeXml(tokens.typography.sans)
|
|
107
107
|
const labelEnd = label.length
|
|
@@ -6,11 +6,19 @@ export interface QuantityBadgeProps {
|
|
|
6
6
|
x: number
|
|
7
7
|
y: number
|
|
8
8
|
quantity: number
|
|
9
|
+
label?: string
|
|
10
|
+
tone: string
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
export function quantityBadge({
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
export function quantityBadge({
|
|
14
|
+
x,
|
|
15
|
+
y,
|
|
16
|
+
quantity,
|
|
17
|
+
label: labelOverride,
|
|
18
|
+
tone,
|
|
19
|
+
}: QuantityBadgeProps): string {
|
|
20
|
+
if (quantity <= 0) return ''
|
|
21
|
+
const label = labelOverride ?? `×${quantity}`
|
|
14
22
|
const w = label.length * 7 + 12
|
|
15
23
|
const h = tokens.spacing.quantityBadgeHeight
|
|
16
24
|
return (
|
|
@@ -21,7 +29,9 @@ export function quantityBadge({x, y, quantity}: QuantityBadgeProps): string {
|
|
|
21
29
|
height: h,
|
|
22
30
|
rx: h / 2,
|
|
23
31
|
ry: h / 2,
|
|
24
|
-
fill: tokens.colors.
|
|
32
|
+
fill: tokens.colors.surface.panel,
|
|
33
|
+
stroke: tone,
|
|
34
|
+
'stroke-width': 1.5,
|
|
25
35
|
}) +
|
|
26
36
|
text({
|
|
27
37
|
x: x - w / 2,
|
|
@@ -30,7 +40,7 @@ export function quantityBadge({x, y, quantity}: QuantityBadgeProps): string {
|
|
|
30
40
|
size: tokens.typography.sizes.label,
|
|
31
41
|
weight: 700,
|
|
32
42
|
family: tokens.typography.mono,
|
|
33
|
-
color: tokens.colors.
|
|
43
|
+
color: tokens.colors.text.primary,
|
|
34
44
|
anchor: 'middle',
|
|
35
45
|
})
|
|
36
46
|
)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import {CATEGORY_LABELS, categoryColors, type ResourceCategory} from '@shipload/sdk'
|
|
2
|
+
import {el, escapeXml} from './svg.ts'
|
|
3
|
+
|
|
4
|
+
export const resourceIconCategories = [
|
|
5
|
+
'ore',
|
|
6
|
+
'crystal',
|
|
7
|
+
'gas',
|
|
8
|
+
'regolith',
|
|
9
|
+
'biomass',
|
|
10
|
+
] as const satisfies readonly ResourceCategory[]
|
|
11
|
+
|
|
12
|
+
export interface ResourceIconSvgOpts {
|
|
13
|
+
size?: number
|
|
14
|
+
title?: string
|
|
15
|
+
className?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ResourceIconInlineOpts {
|
|
19
|
+
x: number
|
|
20
|
+
y: number
|
|
21
|
+
size: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const OUTLINE = '#06142f'
|
|
25
|
+
const HILITE = '#f7fbff'
|
|
26
|
+
const OUTER_STROKE = 4
|
|
27
|
+
const DETAIL_STROKE = 2.5
|
|
28
|
+
|
|
29
|
+
function oreIcon(): string {
|
|
30
|
+
return [
|
|
31
|
+
el('path', {
|
|
32
|
+
d: 'M13 24 L25 11 L43 13 L55 27 L48 49 L27 56 L12 42 Z',
|
|
33
|
+
fill: categoryColors.ore,
|
|
34
|
+
stroke: OUTLINE,
|
|
35
|
+
'stroke-width': OUTER_STROKE,
|
|
36
|
+
'stroke-linejoin': 'round',
|
|
37
|
+
}),
|
|
38
|
+
el('path', {d: 'M25 11 L31 30 L13 24 Z', fill: '#e39a5e'}),
|
|
39
|
+
el('path', {d: 'M31 30 L43 13 L55 27 L41 32 Z', fill: '#b85b35'}),
|
|
40
|
+
el('path', {d: 'M31 30 L41 32 L48 49 L27 56 Z', fill: '#8f432e'}),
|
|
41
|
+
el('path', {
|
|
42
|
+
d: 'M31 30 L25 11 M31 30 L13 24 M31 30 L41 32 M41 32 L48 49 M31 30 L27 56',
|
|
43
|
+
fill: 'none',
|
|
44
|
+
stroke: OUTLINE,
|
|
45
|
+
'stroke-width': 1.5,
|
|
46
|
+
'stroke-linecap': 'round',
|
|
47
|
+
'stroke-linejoin': 'round',
|
|
48
|
+
opacity: 0.28,
|
|
49
|
+
}),
|
|
50
|
+
el('path', {
|
|
51
|
+
d: 'M21 23 L27 18 M42 20 L48 27',
|
|
52
|
+
fill: 'none',
|
|
53
|
+
stroke: '#ffd29c',
|
|
54
|
+
'stroke-width': DETAIL_STROKE,
|
|
55
|
+
'stroke-linecap': 'round',
|
|
56
|
+
opacity: 0.72,
|
|
57
|
+
}),
|
|
58
|
+
].join('')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function crystalIcon(): string {
|
|
62
|
+
return [
|
|
63
|
+
el('path', {
|
|
64
|
+
d: 'M32 6 L49 24 L39 58 L23 58 L14 25 Z',
|
|
65
|
+
fill: categoryColors.crystal,
|
|
66
|
+
stroke: OUTLINE,
|
|
67
|
+
'stroke-width': OUTER_STROKE,
|
|
68
|
+
'stroke-linejoin': 'round',
|
|
69
|
+
}),
|
|
70
|
+
el('path', {d: 'M32 6 L32 58 L14 25 Z', fill: '#1fb9e4'}),
|
|
71
|
+
el('path', {d: 'M32 6 L49 24 L32 58 Z', fill: '#8df0ff'}),
|
|
72
|
+
el('path', {d: 'M23 58 L32 32 L39 58 Z', fill: '#2f87d7', opacity: 0.72}),
|
|
73
|
+
el('path', {
|
|
74
|
+
d: 'M32 6 L32 58 M14 25 L32 32 L49 24 M23 58 L32 32 L39 58',
|
|
75
|
+
fill: 'none',
|
|
76
|
+
stroke: OUTLINE,
|
|
77
|
+
'stroke-width': 1.5,
|
|
78
|
+
'stroke-linecap': 'round',
|
|
79
|
+
'stroke-linejoin': 'round',
|
|
80
|
+
opacity: 0.25,
|
|
81
|
+
}),
|
|
82
|
+
el('path', {
|
|
83
|
+
d: 'M25 20 L31 13 M38 19 L43 25',
|
|
84
|
+
fill: 'none',
|
|
85
|
+
stroke: HILITE,
|
|
86
|
+
'stroke-width': DETAIL_STROKE,
|
|
87
|
+
'stroke-linecap': 'round',
|
|
88
|
+
opacity: 0.78,
|
|
89
|
+
}),
|
|
90
|
+
].join('')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function gasIcon(): string {
|
|
94
|
+
return [
|
|
95
|
+
el('circle', {
|
|
96
|
+
cx: 32,
|
|
97
|
+
cy: 32,
|
|
98
|
+
r: 23,
|
|
99
|
+
fill: categoryColors.gas,
|
|
100
|
+
stroke: OUTLINE,
|
|
101
|
+
'stroke-width': OUTER_STROKE,
|
|
102
|
+
}),
|
|
103
|
+
el('path', {
|
|
104
|
+
d: 'M17 33 C21 20 38 18 44 27 C51 38 37 51 25 43',
|
|
105
|
+
fill: 'none',
|
|
106
|
+
stroke: '#7a48d9',
|
|
107
|
+
'stroke-width': 7.5,
|
|
108
|
+
'stroke-linecap': 'round',
|
|
109
|
+
}),
|
|
110
|
+
el('path', {
|
|
111
|
+
d: 'M21 33 C25 25 36 25 39 31 C43 38 35 44 28 39',
|
|
112
|
+
fill: 'none',
|
|
113
|
+
stroke: '#f0d7ff',
|
|
114
|
+
'stroke-width': 5,
|
|
115
|
+
'stroke-linecap': 'round',
|
|
116
|
+
}),
|
|
117
|
+
el('circle', {
|
|
118
|
+
cx: 44,
|
|
119
|
+
cy: 18,
|
|
120
|
+
r: 5,
|
|
121
|
+
fill: '#f0d7ff',
|
|
122
|
+
stroke: OUTLINE,
|
|
123
|
+
'stroke-width': DETAIL_STROKE,
|
|
124
|
+
}),
|
|
125
|
+
el('circle', {
|
|
126
|
+
cx: 19,
|
|
127
|
+
cy: 47,
|
|
128
|
+
r: 4,
|
|
129
|
+
fill: '#f0d7ff',
|
|
130
|
+
stroke: OUTLINE,
|
|
131
|
+
'stroke-width': DETAIL_STROKE,
|
|
132
|
+
}),
|
|
133
|
+
].join('')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function regolithIcon(): string {
|
|
137
|
+
return [
|
|
138
|
+
el('path', {
|
|
139
|
+
d: 'M16 14 H45 L54 25 V46 L43 55 H17 L9 43 V24 Z',
|
|
140
|
+
fill: categoryColors.regolith,
|
|
141
|
+
stroke: OUTLINE,
|
|
142
|
+
'stroke-width': OUTER_STROKE,
|
|
143
|
+
'stroke-linejoin': 'round',
|
|
144
|
+
}),
|
|
145
|
+
el('path', {d: 'M16 14 H45 L54 25 L34 28 Z', fill: '#dec59a'}),
|
|
146
|
+
el('path', {d: 'M9 43 L34 28 L43 55 H17 Z', fill: '#9e7c55', opacity: 0.78}),
|
|
147
|
+
el('path', {
|
|
148
|
+
d: 'M16 14 L34 28 L54 25 M34 28 L43 55 M34 28 L9 43',
|
|
149
|
+
fill: 'none',
|
|
150
|
+
stroke: OUTLINE,
|
|
151
|
+
'stroke-width': 1.5,
|
|
152
|
+
'stroke-linecap': 'round',
|
|
153
|
+
'stroke-linejoin': 'round',
|
|
154
|
+
opacity: 0.25,
|
|
155
|
+
}),
|
|
156
|
+
el('circle', {cx: 25, cy: 28, r: 4, fill: '#806248', stroke: OUTLINE, 'stroke-width': 1.5}),
|
|
157
|
+
el('circle', {cx: 42, cy: 40, r: 5, fill: '#8b6a4d', stroke: OUTLINE, 'stroke-width': 1.5}),
|
|
158
|
+
el('circle', {
|
|
159
|
+
cx: 22,
|
|
160
|
+
cy: 44,
|
|
161
|
+
r: 3,
|
|
162
|
+
fill: '#e7d0a9',
|
|
163
|
+
stroke: OUTLINE,
|
|
164
|
+
'stroke-width': 1.25,
|
|
165
|
+
}),
|
|
166
|
+
el('path', {
|
|
167
|
+
d: 'M35 18 L45 23',
|
|
168
|
+
fill: 'none',
|
|
169
|
+
stroke: '#f0dbb7',
|
|
170
|
+
'stroke-width': DETAIL_STROKE,
|
|
171
|
+
'stroke-linecap': 'round',
|
|
172
|
+
opacity: 0.72,
|
|
173
|
+
}),
|
|
174
|
+
].join('')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function biomassIcon(): string {
|
|
178
|
+
return [
|
|
179
|
+
el('path', {
|
|
180
|
+
d: 'M32 7 C37 14 42 17 50 16 C50 24 54 29 57 35 C51 39 48 44 47 52 C39 50 36 55 31 59 C27 52 21 50 13 53 C14 45 10 40 7 34 C13 29 14 23 12 16 C21 18 26 14 32 7 Z',
|
|
181
|
+
fill: categoryColors.biomass,
|
|
182
|
+
stroke: OUTLINE,
|
|
183
|
+
'stroke-width': OUTER_STROKE,
|
|
184
|
+
'stroke-linejoin': 'round',
|
|
185
|
+
}),
|
|
186
|
+
el('circle', {cx: 32, cy: 32, r: 13, fill: '#73ad49', stroke: OUTLINE, 'stroke-width': 2}),
|
|
187
|
+
el('circle', {cx: 27, cy: 28, r: 5, fill: '#a8db6f', stroke: OUTLINE, 'stroke-width': 1.5}),
|
|
188
|
+
el('circle', {cx: 38, cy: 34, r: 5, fill: '#2f6f35', stroke: OUTLINE, 'stroke-width': 1.5}),
|
|
189
|
+
el('circle', {cx: 30, cy: 41, r: 4, fill: '#89c85a', stroke: OUTLINE, 'stroke-width': 1.5}),
|
|
190
|
+
el('circle', {
|
|
191
|
+
cx: 40,
|
|
192
|
+
cy: 24,
|
|
193
|
+
r: 3,
|
|
194
|
+
fill: '#c9ef8a',
|
|
195
|
+
stroke: OUTLINE,
|
|
196
|
+
'stroke-width': 1.25,
|
|
197
|
+
}),
|
|
198
|
+
].join('')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const iconBodies: Record<ResourceCategory, string> = {
|
|
202
|
+
ore: oreIcon(),
|
|
203
|
+
crystal: crystalIcon(),
|
|
204
|
+
gas: gasIcon(),
|
|
205
|
+
regolith: regolithIcon(),
|
|
206
|
+
biomass: biomassIcon(),
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function resourceIconBody(category: ResourceCategory): string {
|
|
210
|
+
return iconBodies[category]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function resourceIcon(category: ResourceCategory, opts: ResourceIconInlineOpts): string {
|
|
214
|
+
const scale = opts.size / 64
|
|
215
|
+
return el(
|
|
216
|
+
'g',
|
|
217
|
+
{transform: `translate(${opts.x} ${opts.y}) scale(${scale})`, 'data-resource': category},
|
|
218
|
+
resourceIconBody(category)
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function resourceIconSvg(
|
|
223
|
+
category: ResourceCategory,
|
|
224
|
+
opts: ResourceIconSvgOpts = {}
|
|
225
|
+
): string {
|
|
226
|
+
const size = opts.size ?? 64
|
|
227
|
+
const title = opts.title ?? `${CATEGORY_LABELS[category]} resource icon`
|
|
228
|
+
const children = `<title>${escapeXml(title)}</title>${resourceIconBody(category)}`
|
|
229
|
+
return el(
|
|
230
|
+
'svg',
|
|
231
|
+
{
|
|
232
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
|
233
|
+
width: size,
|
|
234
|
+
height: size,
|
|
235
|
+
viewBox: '0 0 64 64',
|
|
236
|
+
role: 'img',
|
|
237
|
+
class: opts.className,
|
|
238
|
+
'aria-label': title,
|
|
239
|
+
},
|
|
240
|
+
children
|
|
241
|
+
)
|
|
242
|
+
}
|
|
@@ -43,7 +43,7 @@ export function spanParagraph(props: SpanParagraphProps): {svg: string; lineCoun
|
|
|
43
43
|
const chars = props.charsPerLine ?? 36
|
|
44
44
|
const lh = props.lineHeight ?? 14
|
|
45
45
|
const bodyColor = props.bodyColor ?? tokens.colors.text.secondary
|
|
46
|
-
const highlightColor = props.highlightColor ?? tokens.colors.text.
|
|
46
|
+
const highlightColor = props.highlightColor ?? tokens.colors.text.primary
|
|
47
47
|
const size = props.fontSize ?? tokens.typography.sizes.body
|
|
48
48
|
|
|
49
49
|
const plain = props.spans.map((s) => s.text).join('')
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {el} from './svg.ts'
|
|
2
2
|
import {text} from './text.ts'
|
|
3
3
|
import {tokens} from '../tokens/index.ts'
|
|
4
|
+
import {statStars, STAR_BLOCK_WIDTH} from './stat-stars.ts'
|
|
4
5
|
|
|
5
6
|
export interface StatBarProps {
|
|
6
7
|
x: number
|
|
@@ -11,6 +12,7 @@ export interface StatBarProps {
|
|
|
11
12
|
value: number | null // 0..1023, or null for ranges mode (no value text, no fill)
|
|
12
13
|
color: string
|
|
13
14
|
inverted?: boolean
|
|
15
|
+
stars?: number // 0..3 per-stat rating; only drawn in values mode
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export function statBar({
|
|
@@ -22,6 +24,7 @@ export function statBar({
|
|
|
22
24
|
value,
|
|
23
25
|
color,
|
|
24
26
|
inverted,
|
|
27
|
+
stars,
|
|
25
28
|
}: StatBarProps): string {
|
|
26
29
|
const h = tokens.spacing.statBarHeight
|
|
27
30
|
|
|
@@ -59,16 +62,29 @@ export function statBar({
|
|
|
59
62
|
const displayFraction = inverted ? 1 - clamped / 1023 : clamped / 1023
|
|
60
63
|
const filled = Math.floor(width * displayFraction)
|
|
61
64
|
|
|
65
|
+
const VALUE_COL_W = 34 // room for up to 4 mono digits at size 14, anchored end
|
|
66
|
+
|
|
67
|
+
// value text = primary; identity color = bar + code + chrome
|
|
62
68
|
labelOut += text({
|
|
63
69
|
x: x + width,
|
|
64
70
|
y: y - 6,
|
|
65
71
|
value: String(clamped),
|
|
66
72
|
size: tokens.typography.sizes.statValue,
|
|
67
73
|
weight: 700,
|
|
68
|
-
|
|
74
|
+
family: tokens.typography.mono,
|
|
75
|
+
color: tokens.colors.text.primary,
|
|
69
76
|
anchor: 'end',
|
|
70
77
|
})
|
|
71
78
|
|
|
79
|
+
// stars sit immediately left of the reserved value column
|
|
80
|
+
if (stars !== undefined) {
|
|
81
|
+
labelOut += statStars({
|
|
82
|
+
x: x + width - VALUE_COL_W - STAR_BLOCK_WIDTH,
|
|
83
|
+
y: y - 6,
|
|
84
|
+
n: stars,
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
const bar = el('rect', {
|
|
73
89
|
x,
|
|
74
90
|
y,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {el} from './svg.ts'
|
|
2
|
+
|
|
3
|
+
// 24×24 path, identical to the webapp ItemStatRow star.
|
|
4
|
+
const STAR_PATH =
|
|
5
|
+
'M12 2.2l2.95 5.98 6.6.96-4.77 4.65 1.13 6.57L12 17.23 6.09 20.36l1.13-6.57L2.45 9.14l6.6-.96z'
|
|
6
|
+
|
|
7
|
+
export const STAR_SIZE = 9
|
|
8
|
+
export const STAR_GAP = 1
|
|
9
|
+
export const MAX_STARS = 3
|
|
10
|
+
export const STAR_BLOCK_WIDTH = MAX_STARS * STAR_SIZE + (MAX_STARS - 1) * STAR_GAP
|
|
11
|
+
|
|
12
|
+
const ON_COLOR = '#ffce5c'
|
|
13
|
+
const EMPTY_COLOR = 'rgba(255,255,255,0.16)'
|
|
14
|
+
const SCALE = STAR_SIZE / 24
|
|
15
|
+
|
|
16
|
+
export interface StatStarsProps {
|
|
17
|
+
x: number // left edge of the star block
|
|
18
|
+
y: number // vertical center the stars should sit on (text baseline)
|
|
19
|
+
n: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Draws MAX_STARS glyphs left-to-right starting at x; the first `n` are filled.
|
|
23
|
+
export function statStars({x, y, n}: StatStarsProps): string {
|
|
24
|
+
const filled = Math.max(0, Math.min(MAX_STARS, Math.floor(n)))
|
|
25
|
+
const top = y - STAR_SIZE + 1 // center the glyph on the text baseline
|
|
26
|
+
let out = ''
|
|
27
|
+
for (let i = 0; i < MAX_STARS; i++) {
|
|
28
|
+
const gx = x + i * (STAR_SIZE + STAR_GAP)
|
|
29
|
+
out += el('path', {
|
|
30
|
+
d: STAR_PATH,
|
|
31
|
+
transform: `translate(${gx} ${top}) scale(${SCALE})`,
|
|
32
|
+
fill: i < filled ? ON_COLOR : EMPTY_COLOR,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
return out
|
|
36
|
+
}
|
package/src/primitives/text.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import {el} from './svg.ts'
|
|
2
2
|
import {tokens} from '../tokens/index.ts'
|
|
3
3
|
|
|
4
|
+
export interface TextSpan {
|
|
5
|
+
value: string
|
|
6
|
+
size?: number
|
|
7
|
+
weight?: 400 | 600 | 700 | 500
|
|
8
|
+
color?: string
|
|
9
|
+
dx?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
export interface TextProps {
|
|
5
13
|
x: number
|
|
6
14
|
y: number
|
|
@@ -12,9 +20,14 @@ export interface TextProps {
|
|
|
12
20
|
anchor?: 'start' | 'middle' | 'end'
|
|
13
21
|
letterSpacing?: number
|
|
14
22
|
dominantBaseline?: 'auto' | 'middle' | 'central' | 'hanging' | 'text-top' | 'text-bottom'
|
|
23
|
+
// Optional trailing tspans that flow inline after value (no manual width math).
|
|
24
|
+
spans?: TextSpan[]
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
export function text(props: TextProps): string {
|
|
28
|
+
const body = props.spans?.length
|
|
29
|
+
? escapeValue(props.value) + props.spans.map(renderSpan).join('')
|
|
30
|
+
: escapeValue(props.value)
|
|
18
31
|
return el(
|
|
19
32
|
'text',
|
|
20
33
|
{
|
|
@@ -28,7 +41,20 @@ export function text(props: TextProps): string {
|
|
|
28
41
|
'letter-spacing': props.letterSpacing,
|
|
29
42
|
'dominant-baseline': props.dominantBaseline,
|
|
30
43
|
},
|
|
31
|
-
|
|
44
|
+
body
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderSpan(span: TextSpan): string {
|
|
49
|
+
return el(
|
|
50
|
+
'tspan',
|
|
51
|
+
{
|
|
52
|
+
dx: span.dx,
|
|
53
|
+
'font-size': span.size,
|
|
54
|
+
'font-weight': span.weight,
|
|
55
|
+
fill: span.color,
|
|
56
|
+
},
|
|
57
|
+
escapeValue(span.value)
|
|
32
58
|
)
|
|
33
59
|
}
|
|
34
60
|
|
package/src/render.ts
CHANGED
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
import {resolveItem, type ResolvedItem} from '@shipload/sdk'
|
|
2
2
|
import type {CargoItem} from './payload/codec.ts'
|
|
3
|
-
import {
|
|
3
|
+
import {decodeNftPayload} from './payload/codec.ts'
|
|
4
4
|
import {renderByType} from './templates/index.ts'
|
|
5
5
|
import {UnknownItemError} from './errors.ts'
|
|
6
6
|
|
|
7
7
|
export interface RenderOptions {
|
|
8
8
|
width?: number
|
|
9
9
|
theme?: 'dark' | 'light'
|
|
10
|
+
location?: {x: number; y: number}
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export function renderItem(item: CargoItem, resolved: ResolvedItem,
|
|
13
|
-
return renderByType(item, resolved)
|
|
13
|
+
export function renderItem(item: CargoItem, resolved: ResolvedItem, opts?: RenderOptions): string {
|
|
14
|
+
return renderByType(item, resolved, {location: opts?.location})
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export async function renderFromPayload(
|
|
17
18
|
payload: string,
|
|
18
19
|
opts?: RenderOptions
|
|
19
20
|
): Promise<{svg: string; item: ResolvedItem}> {
|
|
20
|
-
const
|
|
21
|
+
const decoded = decodeNftPayload(payload)
|
|
22
|
+
const cargo = decoded.item
|
|
21
23
|
let resolved: ResolvedItem
|
|
22
24
|
try {
|
|
23
|
-
resolved = resolveItem(
|
|
25
|
+
resolved = resolveItem(cargo.item_id, cargo.stats, cargo.modules)
|
|
24
26
|
} catch {
|
|
25
|
-
throw new UnknownItemError(Number(BigInt(
|
|
27
|
+
throw new UnknownItemError(Number(BigInt(cargo.item_id.toString())))
|
|
26
28
|
}
|
|
27
|
-
const
|
|
29
|
+
const location = decoded.location
|
|
30
|
+
? {x: Number(decoded.location.x), y: Number(decoded.location.y)}
|
|
31
|
+
: opts?.location
|
|
32
|
+
const svg = renderItem(cargo, resolved, {...opts, location})
|
|
28
33
|
return {svg, item: resolved}
|
|
29
34
|
}
|
package/src/templates/_shared.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import type {ResolvedItem} from '@shipload/sdk'
|
|
2
|
+
import {baseName, formatMassScaled} from '@shipload/sdk'
|
|
3
|
+
import {text} from '../primitives/text.ts'
|
|
4
|
+
import {divider} from '../primitives/divider.ts'
|
|
1
5
|
import {tokens} from '../tokens/index.ts'
|
|
2
6
|
|
|
3
|
-
export function formatMass(
|
|
4
|
-
return
|
|
7
|
+
export function formatMass(kg: number): string {
|
|
8
|
+
return formatMassScaled(kg)
|
|
5
9
|
}
|
|
6
10
|
|
|
7
11
|
export function tierBorder(tier: number): string {
|
|
@@ -12,3 +16,110 @@ export function shortCode(itemId: number): string {
|
|
|
12
16
|
const str = itemId.toString(10)
|
|
13
17
|
return str.slice(-2).padStart(2, '0')
|
|
14
18
|
}
|
|
19
|
+
|
|
20
|
+
export function capabilityColor(name: string): string {
|
|
21
|
+
const key = name.toLowerCase().replace(/\s+/g, '') as keyof typeof tokens.colors.capability
|
|
22
|
+
return tokens.colors.capability[key] ?? tokens.colors.accent.component
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const META_ROW_H = 22
|
|
26
|
+
export const HEADER_H = 48
|
|
27
|
+
export const ICON_Y = 4
|
|
28
|
+
export const BADGE_Y = 6
|
|
29
|
+
export const META_BLOCK_GAP = 16
|
|
30
|
+
|
|
31
|
+
export const STAT_ROW_H = 26
|
|
32
|
+
export const CAP_HEADER_H = 22
|
|
33
|
+
export const CAP_ROW_H = 18
|
|
34
|
+
export const BODY_TAIL = 8
|
|
35
|
+
|
|
36
|
+
// Gap from the meta block to the first stat row. Resources/components have no
|
|
37
|
+
// body sub-header, and statBar draws its label 6px above its y, so this is
|
|
38
|
+
// META_BLOCK_GAP plus the offset that puts the first stat label level with
|
|
39
|
+
// where module/entity body sections begin — keeping the meta→body gap uniform.
|
|
40
|
+
export const STAT_BLOCK_GAP = META_BLOCK_GAP + 22
|
|
41
|
+
|
|
42
|
+
// Uniform gap between a card's last body element and the bottom frame edge.
|
|
43
|
+
// Cards size their height to (last element bottom) + BOTTOM_PAD so trailing
|
|
44
|
+
// space is consistent across resource / component / module / entity types.
|
|
45
|
+
export const BOTTOM_PAD = 22
|
|
46
|
+
|
|
47
|
+
export function titleParts(x: number, y: number, name: string, tier: number): string {
|
|
48
|
+
return text({
|
|
49
|
+
x,
|
|
50
|
+
y,
|
|
51
|
+
value: name,
|
|
52
|
+
size: tokens.typography.sizes.title,
|
|
53
|
+
weight: 700,
|
|
54
|
+
family: tokens.typography.display,
|
|
55
|
+
spans: [
|
|
56
|
+
{
|
|
57
|
+
value: `T${tier}`,
|
|
58
|
+
dx: 6,
|
|
59
|
+
size: tokens.typography.sizes.subtitle,
|
|
60
|
+
weight: 700,
|
|
61
|
+
color: tokens.colors.text.secondary,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function titleText(x: number, y: number, resolved: ResolvedItem): string {
|
|
68
|
+
// Prominent base name; tier rendered as a smaller, muted inline suffix.
|
|
69
|
+
return titleParts(x, y, baseName(resolved), resolved.tier)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface MetaRowProps {
|
|
73
|
+
x: number
|
|
74
|
+
y: number
|
|
75
|
+
width: number
|
|
76
|
+
label: string
|
|
77
|
+
value: string
|
|
78
|
+
showDivider?: boolean
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function metaRow({x, y, width, label, value, showDivider = true}: MetaRowProps): string {
|
|
82
|
+
const labelText = text({
|
|
83
|
+
x,
|
|
84
|
+
y: y + 12,
|
|
85
|
+
value: label,
|
|
86
|
+
size: tokens.typography.sizes.body,
|
|
87
|
+
color: tokens.colors.text.secondary,
|
|
88
|
+
})
|
|
89
|
+
const valueText = text({
|
|
90
|
+
x: x + width,
|
|
91
|
+
y: y + 12,
|
|
92
|
+
value,
|
|
93
|
+
size: tokens.typography.sizes.body,
|
|
94
|
+
weight: 700,
|
|
95
|
+
anchor: 'end',
|
|
96
|
+
})
|
|
97
|
+
const sep = showDivider
|
|
98
|
+
? divider({
|
|
99
|
+
x,
|
|
100
|
+
y: y + META_ROW_H - 4,
|
|
101
|
+
width,
|
|
102
|
+
color: tokens.colors.surface.panelBorderBright,
|
|
103
|
+
})
|
|
104
|
+
: ''
|
|
105
|
+
return labelText + valueText + sep
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function metaRowBlock(
|
|
109
|
+
x: number,
|
|
110
|
+
yStart: number,
|
|
111
|
+
width: number,
|
|
112
|
+
rows: {label: string; value: string}[]
|
|
113
|
+
): {svg: string; height: number} {
|
|
114
|
+
let svg = ''
|
|
115
|
+
rows.forEach((row, i) => {
|
|
116
|
+
svg += metaRow({
|
|
117
|
+
x,
|
|
118
|
+
y: yStart + i * META_ROW_H,
|
|
119
|
+
width,
|
|
120
|
+
...row,
|
|
121
|
+
showDivider: i < rows.length - 1,
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
return {svg, height: rows.length * META_ROW_H}
|
|
125
|
+
}
|