@shipload/item-renderer 0.2.3 → 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 +24 -24
- package/src/fonts/load-bun.ts +12 -12
- package/src/index.ts +31 -31
- package/src/links.ts +13 -13
- package/src/meta.ts +26 -26
- package/src/payload/base64url.ts +21 -21
- package/src/payload/codec.ts +19 -19
- package/src/primitives/category-icon.ts +88 -86
- package/src/primitives/compact-row.ts +32 -32
- package/src/primitives/divider.ts +14 -14
- package/src/primitives/icon-hex.ts +36 -36
- 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 +56 -56
- package/src/primitives/stat-bar.ts +72 -72
- package/src/primitives/svg.ts +16 -16
- package/src/primitives/text.ts +33 -33
- package/src/primitives/wrap.ts +19 -19
- package/src/render.ts +20 -20
- package/src/templates/_shared.ts +5 -5
- package/src/templates/component.ts +124 -124
- package/src/templates/index.ts +23 -23
- package/src/templates/item-cell.ts +84 -84
- package/src/templates/module.ts +182 -181
- package/src/templates/packed-entity.ts +22 -22
- package/src/templates/resource.ts +136 -134
- package/src/templates/ship-panel.ts +122 -118
- package/src/templates/social-card.ts +36 -36
- package/src/tokens/colors.ts +42 -42
- package/src/tokens/index.ts +6 -6
- package/src/tokens/spacing.ts +9 -9
- package/src/tokens/typography.ts +18 -18
- package/.claude/settings.local.json +0 -6
- 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 -41
- 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 -34
- 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 -69
- package/test/primitives-category-icon.test.ts +0 -86
- package/test/primitives-compact-row.test.ts +0 -44
- package/test/primitives-domain.test.ts +0 -83
- package/test/primitives-layout.test.ts +0 -56
- package/test/primitives-module-slot.test.ts +0 -88
- package/test/render.test.ts +0 -43
- 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 -30
- 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 -91
- package/test/tokens.test.ts +0 -34
- package/tsconfig.json +0 -20
|
@@ -1,149 +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
|
-
|
|
19
|
-
}
|
|
14
|
+
ore: 'Ore',
|
|
15
|
+
crystal: 'Crystal',
|
|
16
|
+
gas: 'Gas',
|
|
17
|
+
regolith: 'Regolith',
|
|
18
|
+
biomass: 'Biomass',
|
|
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
|
-
|
|
37
|
-
}
|
|
32
|
+
label: string
|
|
33
|
+
abbreviation: string
|
|
34
|
+
value: number | null
|
|
35
|
+
color: string
|
|
36
|
+
inverted?: boolean
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
value: "Mass",
|
|
116
|
-
size: tokens.typography.sizes.body,
|
|
117
|
-
color: tokens.colors.text.secondary,
|
|
118
|
-
});
|
|
119
|
-
const massValue = text({
|
|
120
|
-
x: w - pad,
|
|
121
|
-
y: pad + headerH + metaRowH - 8,
|
|
122
|
-
value: formatMass(resolved.mass),
|
|
123
|
-
size: tokens.typography.sizes.body,
|
|
124
|
-
weight: 600,
|
|
125
|
-
anchor: "end",
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const sepY = pad + headerH + metaRowH + 6;
|
|
129
|
-
const sep = divider({ x: pad, y: sepY, width: innerW });
|
|
130
|
-
|
|
131
|
-
const statsSvg = rows
|
|
132
|
-
.map((row, i) =>
|
|
133
|
-
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({
|
|
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({
|
|
134
115
|
x: pad,
|
|
135
|
-
y:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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>`
|
|
149
151
|
}
|
|
@@ -1,140 +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
|
-
|
|
30
|
+
return tokens.colors.tier[tier] ?? tokens.colors.surface.panelBorder
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
const MODULE_LABEL_PREFIX = (capability: string) => `${capability}:
|
|
33
|
+
const MODULE_LABEL_PREFIX = (capability: string) => `${capability}: `
|
|
34
34
|
|
|
35
35
|
function rowHeightFor(slot: ShipPanelSlot): number {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
export function renderShipPanel(props: ShipPanelProps): string {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
x: pad,
|
|
68
|
-
y: pad + 4,
|
|
69
|
-
color: tokens.colors.text.accent,
|
|
70
|
-
code: "SH",
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const name = text({
|
|
74
|
-
x: pad + 34,
|
|
75
|
-
y: pad + 22,
|
|
76
|
-
value: props.name,
|
|
77
|
-
size: tokens.typography.sizes.title,
|
|
78
|
-
weight: 700,
|
|
79
|
-
family: tokens.typography.display,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const badge = quantityBadge({ x: w - pad, y: pad, quantity });
|
|
83
|
-
|
|
84
|
-
const hullHeader = text({
|
|
85
|
-
x: pad,
|
|
86
|
-
y: pad + headerH,
|
|
87
|
-
value: "HULL",
|
|
88
|
-
size: tokens.typography.sizes.subtitle,
|
|
89
|
-
weight: 700,
|
|
90
|
-
color: tokens.colors.text.secondary,
|
|
91
|
-
letterSpacing: 1,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
let y = pad + headerH + 6;
|
|
95
|
-
let hullSvg = "";
|
|
96
|
-
for (const row of hullRows) {
|
|
97
|
-
hullSvg +=
|
|
98
|
-
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({
|
|
99
71
|
x: pad,
|
|
100
|
-
y:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
x:
|
|
107
|
-
y:
|
|
108
|
-
value:
|
|
109
|
-
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,
|
|
110
82
|
weight: 700,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
83
|
+
family: tokens.typography.display,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const badge = quantityBadge({x: w - pad, y: pad, quantity})
|
|
87
|
+
|
|
88
|
+
const hullHeader = text({
|
|
114
89
|
x: pad,
|
|
115
|
-
y:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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,
|
|
96
|
+
})
|
|
97
|
+
|
|
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>`
|
|
140
144
|
}
|
|
@@ -1,44 +1,44 @@
|
|
|
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
|
-
export const SOCIAL_CARD_WIDTH = 1200
|
|
8
|
-
export const SOCIAL_CARD_HEIGHT = 630
|
|
7
|
+
export const SOCIAL_CARD_WIDTH = 1200
|
|
8
|
+
export const SOCIAL_CARD_HEIGHT = 630
|
|
9
9
|
|
|
10
|
-
const SPACE_DEEP =
|
|
11
|
-
const STARDUST_TILE = 512
|
|
12
|
-
const ITEM_MAX_WIDTH_RATIO = 0.35
|
|
13
|
-
const ITEM_MAX_HEIGHT_RATIO = 0.82
|
|
14
|
-
const DOMAIN_LABEL =
|
|
10
|
+
const SPACE_DEEP = '#050c24'
|
|
11
|
+
const STARDUST_TILE = 512
|
|
12
|
+
const ITEM_MAX_WIDTH_RATIO = 0.35
|
|
13
|
+
const ITEM_MAX_HEIGHT_RATIO = 0.82
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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">` +
|
|
35
|
+
`<image xlink:href="data:image/png;base64,${STARDUST_BASE64}" width="${STARDUST_TILE}" height="${STARDUST_TILE}"/>` +
|
|
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
|
+
)
|
|
44
44
|
}
|