@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.
- package/package.json +47 -49
- package/src/assets/stardust-base64.ts +1 -1
- package/src/errors.ts +14 -14
- package/src/fonts/index.ts +23 -24
- package/src/fonts/load-bun.ts +11 -11
- package/src/index.ts +28 -28
- package/src/links.ts +11 -11
- package/src/meta.ts +25 -25
- package/src/payload/base64url.ts +21 -21
- package/src/payload/codec.ts +17 -17
- package/src/primitives/category-icon.ts +90 -67
- package/src/primitives/compact-row.ts +32 -32
- package/src/primitives/divider.ts +14 -14
- package/src/primitives/icon-hex.ts +36 -34
- package/src/primitives/module-slot.ts +131 -131
- package/src/primitives/panel.ts +18 -18
- package/src/primitives/quantity-badge.ts +32 -32
- package/src/primitives/span-paragraph.ts +55 -57
- package/src/primitives/stat-bar.ts +71 -71
- package/src/primitives/svg.ts +15 -15
- package/src/primitives/text.ts +33 -33
- package/src/primitives/wrap.ts +19 -19
- package/src/render.ts +21 -25
- package/src/templates/_shared.ts +5 -6
- package/src/templates/component.ts +123 -121
- package/src/templates/index.ts +23 -23
- package/src/templates/item-cell.ts +84 -81
- package/src/templates/module.ts +177 -174
- package/src/templates/packed-entity.ts +22 -24
- package/src/templates/resource.ts +134 -134
- package/src/templates/ship-panel.ts +120 -121
- package/src/templates/social-card.ts +28 -26
- package/src/tokens/colors.ts +38 -38
- package/src/tokens/index.ts +5 -5
- package/src/tokens/spacing.ts +8 -8
- package/src/tokens/typography.ts +17 -17
- package/.github/workflows/ci.yml +0 -14
- package/.gitignore +0 -6
- package/Makefile +0 -50
- package/biome.json +0 -18
- package/bun.lock +0 -123
- package/scripts/check-bundle-size.ts +0 -37
- package/scripts/copy-fonts.ts +0 -41
- package/scripts/preview.ts +0 -43
- package/test/__image_snapshots__/.gitkeep +0 -0
- package/test/__image_snapshots__/component-hull-plates.diff.png +0 -0
- package/test/__image_snapshots__/component-hull-plates.png +0 -0
- package/test/__image_snapshots__/module-engine-t1.diff.png +0 -0
- package/test/__image_snapshots__/module-engine-t1.png +0 -0
- package/test/__image_snapshots__/module-storage-t1.diff.png +0 -0
- package/test/__image_snapshots__/module-storage-t1.png +0 -0
- package/test/__image_snapshots__/packed-entity-ship-t1-only-engine.diff.png +0 -0
- package/test/__image_snapshots__/packed-entity-ship-t1-only-engine.png +0 -0
- package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.diff.png +0 -0
- package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.png +0 -0
- package/test/__image_snapshots__/resource-ore-t1.diff.png +0 -0
- package/test/__image_snapshots__/resource-ore-t1.png +0 -0
- package/test/__snapshots__/templates-component.test.ts.snap +0 -5
- package/test/__snapshots__/templates-item-cell.test.ts.snap +0 -9
- package/test/__snapshots__/templates-module.test.ts.snap +0 -17
- package/test/__snapshots__/templates-packed-entity.test.ts.snap +0 -5
- package/test/__snapshots__/templates-resource.test.ts.snap +0 -7
- package/test/base64url.test.ts +0 -33
- package/test/codec.test.ts +0 -43
- package/test/errors.test.ts +0 -24
- package/test/fixtures/cargo-items.ts +0 -122
- package/test/fonts.test.ts +0 -28
- package/test/links-meta.test.ts +0 -43
- package/test/pixel.test.ts +0 -66
- package/test/primitives-category-icon.test.ts +0 -79
- package/test/primitives-compact-row.test.ts +0 -44
- package/test/primitives-domain.test.ts +0 -72
- package/test/primitives-layout.test.ts +0 -56
- package/test/primitives-module-slot.test.ts +0 -88
- package/test/render.test.ts +0 -40
- package/test/sanity.test.ts +0 -6
- package/test/sdk-link.test.ts +0 -19
- package/test/snapshots/.gitkeep +0 -0
- package/test/svg.test.ts +0 -28
- package/test/templates-component.test.ts +0 -36
- package/test/templates-dispatch.test.ts +0 -35
- package/test/templates-item-cell.test.ts +0 -94
- package/test/templates-module.test.ts +0 -63
- package/test/templates-packed-entity.test.ts +0 -47
- package/test/templates-resource.test.ts +0 -71
- package/test/templates-ship-panel.test.ts +0 -87
- package/test/tokens.test.ts +0 -32
- package/tsconfig.json +0 -20
|
@@ -1,151 +1,151 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
1
|
+
import type {ResolvedItem} from '@shipload/sdk'
|
|
2
|
+
import {getStatDefinitions, categoryColors, displayName} 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 {statBar} from '../primitives/stat-bar.ts'
|
|
9
|
+
import {quantityBadge} from '../primitives/quantity-badge.ts'
|
|
10
|
+
import {tokens} from '../tokens/index.ts'
|
|
11
|
+
import {shortCode, formatMass, tierBorder} from './_shared.ts'
|
|
12
12
|
|
|
13
13
|
const CATEGORY_LABELS: Record<string, string> = {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
ore: 'Ore',
|
|
15
|
+
crystal: 'Crystal',
|
|
16
|
+
gas: 'Gas',
|
|
17
|
+
regolith: 'Regolith',
|
|
18
|
+
biomass: 'Biomass',
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function categoryColor(category?: string): string {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
if (!category) return tokens.colors.text.muted
|
|
23
|
+
const key = category as keyof typeof tokens.colors.category
|
|
24
|
+
return tokens.colors.category[key] ?? tokens.colors.text.muted
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export interface RenderResourceOpts {
|
|
28
|
-
|
|
28
|
+
mode?: 'values' | 'ranges'
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
type StatRow = {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
label: string
|
|
33
|
+
abbreviation: string
|
|
34
|
+
value: number | null
|
|
35
|
+
color: string
|
|
36
|
+
inverted?: boolean
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export function renderResource(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
item: CargoItem,
|
|
41
|
+
resolved: ResolvedItem,
|
|
42
|
+
opts?: RenderResourceOpts
|
|
43
43
|
): string {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
x: pad,
|
|
84
|
-
y: pad + 4,
|
|
85
|
-
color: categoryColor(resolved.category),
|
|
86
|
-
code: shortCode(resolved.itemId),
|
|
87
|
-
})
|
|
88
|
-
|
|
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
|
-
})
|
|
97
|
-
|
|
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({
|
|
122
|
-
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',
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
const sepY = pad + headerH + metaRowH + 6
|
|
131
|
-
const sep = divider({ x: pad, y: sepY, width: innerW })
|
|
132
|
-
|
|
133
|
-
const statsSvg = rows
|
|
134
|
-
.map((row, i) =>
|
|
135
|
-
statBar({
|
|
44
|
+
const mode = opts?.mode ?? 'values'
|
|
45
|
+
const w = tokens.spacing.panelWidth
|
|
46
|
+
const pad = tokens.spacing.panelPadding
|
|
47
|
+
const innerW = w - pad * 2
|
|
48
|
+
|
|
49
|
+
let rows: StatRow[]
|
|
50
|
+
if (mode === 'values') {
|
|
51
|
+
rows = (resolved.stats ?? []).map((s) => ({
|
|
52
|
+
label: s.label,
|
|
53
|
+
abbreviation: s.abbreviation,
|
|
54
|
+
value: s.value,
|
|
55
|
+
color: s.color,
|
|
56
|
+
inverted: s.inverted,
|
|
57
|
+
}))
|
|
58
|
+
} else {
|
|
59
|
+
const defs = resolved.category ? getStatDefinitions(resolved.category) : []
|
|
60
|
+
const color = resolved.category
|
|
61
|
+
? categoryColors[resolved.category]
|
|
62
|
+
: tokens.colors.text.muted
|
|
63
|
+
rows = defs.map((d) => ({
|
|
64
|
+
label: d.label,
|
|
65
|
+
abbreviation: d.abbreviation,
|
|
66
|
+
value: null,
|
|
67
|
+
color,
|
|
68
|
+
inverted: d.inverted,
|
|
69
|
+
}))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const headerH = 48
|
|
73
|
+
const metaRowH = 28
|
|
74
|
+
const statsH = rows.length * 26 + 8
|
|
75
|
+
const height = headerH + metaRowH + 14 + statsH + pad
|
|
76
|
+
|
|
77
|
+
const chrome = panel({width: w, height, borderColor: tierBorder(resolved.tier)})
|
|
78
|
+
|
|
79
|
+
const quantity = Number(BigInt(item.quantity.toString()))
|
|
80
|
+
const badge = quantityBadge({x: w - pad, y: pad, quantity})
|
|
81
|
+
|
|
82
|
+
const icon = iconHex({
|
|
136
83
|
x: pad,
|
|
137
|
-
y:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
84
|
+
y: pad + 4,
|
|
85
|
+
color: categoryColor(resolved.category),
|
|
86
|
+
code: shortCode(resolved.itemId),
|
|
87
|
+
})
|
|
88
|
+
|
|
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
|
+
})
|
|
97
|
+
|
|
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({
|
|
122
|
+
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',
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const sepY = pad + headerH + metaRowH + 6
|
|
131
|
+
const sep = divider({x: pad, y: sepY, width: innerW})
|
|
132
|
+
|
|
133
|
+
const statsSvg = rows
|
|
134
|
+
.map((row, i) =>
|
|
135
|
+
statBar({
|
|
136
|
+
x: pad,
|
|
137
|
+
y: sepY + 18 + i * 26,
|
|
138
|
+
width: innerW,
|
|
139
|
+
label: row.label,
|
|
140
|
+
abbreviation: row.abbreviation,
|
|
141
|
+
value: row.value,
|
|
142
|
+
color: row.color,
|
|
143
|
+
inverted: row.inverted,
|
|
144
|
+
})
|
|
145
|
+
)
|
|
146
|
+
.join('')
|
|
147
|
+
|
|
148
|
+
const inner = `${chrome}${icon}${name}${badge}${catText}${catValue}${massLabel}${massValue}${sep}${statsSvg}`
|
|
149
|
+
|
|
150
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`
|
|
151
151
|
}
|
|
@@ -1,145 +1,144 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
1
|
+
import type {TextSpan} from '@shipload/sdk'
|
|
2
|
+
import {panel} from '../primitives/panel.ts'
|
|
3
|
+
import {iconHex} from '../primitives/icon-hex.ts'
|
|
4
|
+
import {text} from '../primitives/text.ts'
|
|
5
|
+
import {divider} from '../primitives/divider.ts'
|
|
6
|
+
import {moduleSlot} from '../primitives/module-slot.ts'
|
|
7
|
+
import {quantityBadge} from '../primitives/quantity-badge.ts'
|
|
8
|
+
import {wrapText} from '../primitives/wrap.ts'
|
|
9
|
+
import {tokens} from '../tokens/index.ts'
|
|
10
10
|
|
|
11
11
|
export interface ShipPanelSlot {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
name?: string
|
|
13
|
+
installed: boolean
|
|
14
|
+
description?: string | TextSpan[]
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface ShipPanelProps {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
name: string
|
|
19
|
+
tier: number
|
|
20
|
+
quantity?: number
|
|
21
|
+
attributes: {capability: string; attributes: {label: string; value: number}[]}[]
|
|
22
|
+
slots: ShipPanelSlot[]
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function formatNumber(n: number): string {
|
|
26
|
-
|
|
26
|
+
return n.toLocaleString('en-US')
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
function tierBorder(tier: number): string {
|
|
30
|
-
|
|
31
|
-
return tokens.colors.tier[key] ?? tokens.colors.surface.panelBorder
|
|
30
|
+
return tokens.colors.tier[tier] ?? tokens.colors.surface.panelBorder
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
const MODULE_LABEL_PREFIX = (capability: string) => `${capability}: `
|
|
35
34
|
|
|
36
35
|
function rowHeightFor(slot: ShipPanelSlot): number {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
if (!slot.installed) return 24
|
|
37
|
+
const desc = slot.description
|
|
38
|
+
const plain =
|
|
39
|
+
typeof desc === 'string'
|
|
40
|
+
? desc
|
|
41
|
+
: Array.isArray(desc)
|
|
42
|
+
? desc.map((s) => s.text).join('')
|
|
43
|
+
: ''
|
|
44
|
+
if (plain.length === 0) return 24
|
|
45
|
+
const combined = MODULE_LABEL_PREFIX(slot.name ?? 'Module') + plain
|
|
46
|
+
const lineCount = Math.max(1, wrapText({value: combined, charsPerLine: 36}).length)
|
|
47
|
+
return 10 + lineCount * 14
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
export function renderShipPanel(props: ShipPanelProps): string {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
pad
|
|
73
|
-
|
|
74
|
-
const chrome = panel({ width: w, height, borderColor: tierBorder(props.tier) })
|
|
75
|
-
|
|
76
|
-
const icon = iconHex({
|
|
77
|
-
x: pad,
|
|
78
|
-
y: pad + 4,
|
|
79
|
-
color: tokens.colors.text.accent,
|
|
80
|
-
code: 'SH',
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
const name = text({
|
|
84
|
-
x: pad + 34,
|
|
85
|
-
y: pad + 22,
|
|
86
|
-
value: props.name,
|
|
87
|
-
size: tokens.typography.sizes.title,
|
|
88
|
-
weight: 700,
|
|
89
|
-
family: tokens.typography.display,
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
const badge = quantityBadge({ x: w - pad, y: pad, quantity })
|
|
93
|
-
|
|
94
|
-
const hullHeader = text({
|
|
95
|
-
x: pad,
|
|
96
|
-
y: pad + headerH,
|
|
97
|
-
value: 'HULL',
|
|
98
|
-
size: tokens.typography.sizes.subtitle,
|
|
99
|
-
weight: 700,
|
|
100
|
-
color: tokens.colors.text.secondary,
|
|
101
|
-
letterSpacing: 1,
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
let y = pad + headerH + 6
|
|
105
|
-
let hullSvg = ''
|
|
106
|
-
for (const row of hullRows) {
|
|
107
|
-
hullSvg +=
|
|
108
|
-
text({
|
|
51
|
+
const w = tokens.spacing.panelWidth
|
|
52
|
+
const pad = tokens.spacing.panelPadding
|
|
53
|
+
const innerW = w - pad * 2
|
|
54
|
+
const quantity = props.quantity ?? 0
|
|
55
|
+
|
|
56
|
+
const hullGroup = props.attributes?.find((g) => g.capability.toLowerCase() === 'hull')
|
|
57
|
+
const hullRows = hullGroup?.attributes ?? []
|
|
58
|
+
|
|
59
|
+
const headerH = 48
|
|
60
|
+
const hullHeaderH = 20
|
|
61
|
+
const hullRowH = 22
|
|
62
|
+
const sectionGap = 12
|
|
63
|
+
const rowHeights = props.slots.map(rowHeightFor)
|
|
64
|
+
const modulesHeight = rowHeights.reduce((a, b) => a + b, 0)
|
|
65
|
+
const height =
|
|
66
|
+
headerH + hullHeaderH + hullRows.length * hullRowH + sectionGap + modulesHeight + pad
|
|
67
|
+
|
|
68
|
+
const chrome = panel({width: w, height, borderColor: tierBorder(props.tier)})
|
|
69
|
+
|
|
70
|
+
const icon = iconHex({
|
|
109
71
|
x: pad,
|
|
110
|
-
y:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
x:
|
|
117
|
-
y:
|
|
118
|
-
value:
|
|
119
|
-
size: tokens.typography.sizes.
|
|
72
|
+
y: pad + 4,
|
|
73
|
+
color: tokens.colors.text.accent,
|
|
74
|
+
code: 'SH',
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const name = text({
|
|
78
|
+
x: pad + 34,
|
|
79
|
+
y: pad + 22,
|
|
80
|
+
value: props.name,
|
|
81
|
+
size: tokens.typography.sizes.title,
|
|
120
82
|
weight: 700,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
y
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
width: innerW,
|
|
135
|
-
installed: slot.installed,
|
|
136
|
-
capability: slot.name,
|
|
137
|
-
description: slot.description,
|
|
138
|
-
accentColor: tokens.colors.brand.teal,
|
|
83
|
+
family: tokens.typography.display,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const badge = quantityBadge({x: w - pad, y: pad, quantity})
|
|
87
|
+
|
|
88
|
+
const hullHeader = text({
|
|
89
|
+
x: pad,
|
|
90
|
+
y: pad + headerH,
|
|
91
|
+
value: 'HULL',
|
|
92
|
+
size: tokens.typography.sizes.subtitle,
|
|
93
|
+
weight: 700,
|
|
94
|
+
color: tokens.colors.text.secondary,
|
|
95
|
+
letterSpacing: 1,
|
|
139
96
|
})
|
|
140
|
-
y += rowHeights[i]!
|
|
141
|
-
}
|
|
142
97
|
|
|
143
|
-
|
|
144
|
-
|
|
98
|
+
let y = pad + headerH + 6
|
|
99
|
+
let hullSvg = ''
|
|
100
|
+
for (const row of hullRows) {
|
|
101
|
+
hullSvg +=
|
|
102
|
+
text({
|
|
103
|
+
x: pad,
|
|
104
|
+
y: y + 12,
|
|
105
|
+
value: row.label,
|
|
106
|
+
size: tokens.typography.sizes.body,
|
|
107
|
+
color: tokens.colors.text.secondary,
|
|
108
|
+
}) +
|
|
109
|
+
text({
|
|
110
|
+
x: w - pad,
|
|
111
|
+
y: y + 12,
|
|
112
|
+
value: formatNumber(row.value),
|
|
113
|
+
size: tokens.typography.sizes.body,
|
|
114
|
+
weight: 700,
|
|
115
|
+
anchor: 'end',
|
|
116
|
+
}) +
|
|
117
|
+
divider({
|
|
118
|
+
x: pad,
|
|
119
|
+
y: y + hullRowH - 4,
|
|
120
|
+
width: innerW,
|
|
121
|
+
color: tokens.colors.surface.panelBorderBright,
|
|
122
|
+
})
|
|
123
|
+
y += hullRowH
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
y += sectionGap
|
|
127
|
+
let modulesSvg = ''
|
|
128
|
+
for (let i = 0; i < props.slots.length; i++) {
|
|
129
|
+
const slot = props.slots[i]!
|
|
130
|
+
modulesSvg += moduleSlot({
|
|
131
|
+
x: pad,
|
|
132
|
+
y,
|
|
133
|
+
width: innerW,
|
|
134
|
+
installed: slot.installed,
|
|
135
|
+
capability: slot.name,
|
|
136
|
+
description: slot.description,
|
|
137
|
+
accentColor: tokens.colors.brand.teal,
|
|
138
|
+
})
|
|
139
|
+
y += rowHeights[i]!
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const inner = `${chrome}${icon}${name}${badge}${hullHeader}${hullSvg}${modulesSvg}`
|
|
143
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`
|
|
145
144
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import type {ResolvedItem} from '@shipload/sdk'
|
|
2
|
+
import type {CargoItem} from '../payload/codec.ts'
|
|
3
|
+
import {renderByType} from './index.ts'
|
|
4
|
+
import {STARDUST_BASE64} from '../assets/stardust-base64.ts'
|
|
5
|
+
import {svgDimensions} from '../meta.ts'
|
|
6
6
|
|
|
7
7
|
export const SOCIAL_CARD_WIDTH = 1200
|
|
8
8
|
export const SOCIAL_CARD_HEIGHT = 630
|
|
@@ -14,29 +14,31 @@ const ITEM_MAX_HEIGHT_RATIO = 0.82
|
|
|
14
14
|
const DOMAIN_LABEL = 'shiploadgame.com'
|
|
15
15
|
|
|
16
16
|
export function socialCardSvg(item: CargoItem, resolved: ResolvedItem): string {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const itemSvg = renderByType(item, resolved)
|
|
18
|
+
const {width: itemW, height: itemH} = svgDimensions(itemSvg)
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
const scale = Math.min(
|
|
21
|
+
(SOCIAL_CARD_WIDTH * ITEM_MAX_WIDTH_RATIO) / itemW,
|
|
22
|
+
(SOCIAL_CARD_HEIGHT * ITEM_MAX_HEIGHT_RATIO) / itemH
|
|
23
|
+
)
|
|
24
|
+
const scaledW = itemW * scale
|
|
25
|
+
const scaledH = itemH * scale
|
|
26
|
+
const tx = (SOCIAL_CARD_WIDTH - scaledW) / 2
|
|
27
|
+
const ty = (SOCIAL_CARD_HEIGHT - scaledH) / 2
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
const itemInner = itemSvg.replace(/^<svg[^>]*>/, '').replace(/<\/svg>\s*$/, '')
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
return (
|
|
32
|
+
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${SOCIAL_CARD_WIDTH}" height="${SOCIAL_CARD_HEIGHT}" viewBox="0 0 ${SOCIAL_CARD_WIDTH} ${SOCIAL_CARD_HEIGHT}">` +
|
|
33
|
+
`<defs>` +
|
|
34
|
+
`<pattern id="sc-stars" width="${STARDUST_TILE}" height="${STARDUST_TILE}" patternUnits="userSpaceOnUse">` +
|
|
34
35
|
`<image xlink:href="data:image/png;base64,${STARDUST_BASE64}" width="${STARDUST_TILE}" height="${STARDUST_TILE}"/>` +
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
`</pattern>` +
|
|
37
|
+
`</defs>` +
|
|
38
|
+
`<rect width="${SOCIAL_CARD_WIDTH}" height="${SOCIAL_CARD_HEIGHT}" fill="${SPACE_DEEP}"/>` +
|
|
39
|
+
`<rect width="${SOCIAL_CARD_WIDTH}" height="${SOCIAL_CARD_HEIGHT}" fill="url(#sc-stars)" opacity="0.75"/>` +
|
|
40
|
+
`<g transform="translate(${tx.toFixed(2)} ${ty.toFixed(2)}) scale(${scale.toFixed(4)})">${itemInner}</g>` +
|
|
41
|
+
`<text x="${SOCIAL_CARD_WIDTH - 40}" y="${SOCIAL_CARD_HEIGHT - 36}" text-anchor="end" fill="#e6e8ec" opacity="0.55" font-size="22" font-family="Inter, sans-serif" letter-spacing="0.04em">${DOMAIN_LABEL}</text>` +
|
|
42
|
+
`</svg>`
|
|
43
|
+
)
|
|
42
44
|
}
|