@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.
Files changed (88) hide show
  1. package/package.json +47 -49
  2. package/src/assets/stardust-base64.ts +1 -1
  3. package/src/errors.ts +14 -14
  4. package/src/fonts/index.ts +23 -24
  5. package/src/fonts/load-bun.ts +11 -11
  6. package/src/index.ts +28 -28
  7. package/src/links.ts +11 -11
  8. package/src/meta.ts +25 -25
  9. package/src/payload/base64url.ts +21 -21
  10. package/src/payload/codec.ts +17 -17
  11. package/src/primitives/category-icon.ts +90 -67
  12. package/src/primitives/compact-row.ts +32 -32
  13. package/src/primitives/divider.ts +14 -14
  14. package/src/primitives/icon-hex.ts +36 -34
  15. package/src/primitives/module-slot.ts +131 -131
  16. package/src/primitives/panel.ts +18 -18
  17. package/src/primitives/quantity-badge.ts +32 -32
  18. package/src/primitives/span-paragraph.ts +55 -57
  19. package/src/primitives/stat-bar.ts +71 -71
  20. package/src/primitives/svg.ts +15 -15
  21. package/src/primitives/text.ts +33 -33
  22. package/src/primitives/wrap.ts +19 -19
  23. package/src/render.ts +21 -25
  24. package/src/templates/_shared.ts +5 -6
  25. package/src/templates/component.ts +123 -121
  26. package/src/templates/index.ts +23 -23
  27. package/src/templates/item-cell.ts +84 -81
  28. package/src/templates/module.ts +177 -174
  29. package/src/templates/packed-entity.ts +22 -24
  30. package/src/templates/resource.ts +134 -134
  31. package/src/templates/ship-panel.ts +120 -121
  32. package/src/templates/social-card.ts +28 -26
  33. package/src/tokens/colors.ts +38 -38
  34. package/src/tokens/index.ts +5 -5
  35. package/src/tokens/spacing.ts +8 -8
  36. package/src/tokens/typography.ts +17 -17
  37. package/.github/workflows/ci.yml +0 -14
  38. package/.gitignore +0 -6
  39. package/Makefile +0 -50
  40. package/biome.json +0 -18
  41. package/bun.lock +0 -123
  42. package/scripts/check-bundle-size.ts +0 -37
  43. package/scripts/copy-fonts.ts +0 -41
  44. package/scripts/preview.ts +0 -43
  45. package/test/__image_snapshots__/.gitkeep +0 -0
  46. package/test/__image_snapshots__/component-hull-plates.diff.png +0 -0
  47. package/test/__image_snapshots__/component-hull-plates.png +0 -0
  48. package/test/__image_snapshots__/module-engine-t1.diff.png +0 -0
  49. package/test/__image_snapshots__/module-engine-t1.png +0 -0
  50. package/test/__image_snapshots__/module-storage-t1.diff.png +0 -0
  51. package/test/__image_snapshots__/module-storage-t1.png +0 -0
  52. package/test/__image_snapshots__/packed-entity-ship-t1-only-engine.diff.png +0 -0
  53. package/test/__image_snapshots__/packed-entity-ship-t1-only-engine.png +0 -0
  54. package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.diff.png +0 -0
  55. package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.png +0 -0
  56. package/test/__image_snapshots__/resource-ore-t1.diff.png +0 -0
  57. package/test/__image_snapshots__/resource-ore-t1.png +0 -0
  58. package/test/__snapshots__/templates-component.test.ts.snap +0 -5
  59. package/test/__snapshots__/templates-item-cell.test.ts.snap +0 -9
  60. package/test/__snapshots__/templates-module.test.ts.snap +0 -17
  61. package/test/__snapshots__/templates-packed-entity.test.ts.snap +0 -5
  62. package/test/__snapshots__/templates-resource.test.ts.snap +0 -7
  63. package/test/base64url.test.ts +0 -33
  64. package/test/codec.test.ts +0 -43
  65. package/test/errors.test.ts +0 -24
  66. package/test/fixtures/cargo-items.ts +0 -122
  67. package/test/fonts.test.ts +0 -28
  68. package/test/links-meta.test.ts +0 -43
  69. package/test/pixel.test.ts +0 -66
  70. package/test/primitives-category-icon.test.ts +0 -79
  71. package/test/primitives-compact-row.test.ts +0 -44
  72. package/test/primitives-domain.test.ts +0 -72
  73. package/test/primitives-layout.test.ts +0 -56
  74. package/test/primitives-module-slot.test.ts +0 -88
  75. package/test/render.test.ts +0 -40
  76. package/test/sanity.test.ts +0 -6
  77. package/test/sdk-link.test.ts +0 -19
  78. package/test/snapshots/.gitkeep +0 -0
  79. package/test/svg.test.ts +0 -28
  80. package/test/templates-component.test.ts +0 -36
  81. package/test/templates-dispatch.test.ts +0 -35
  82. package/test/templates-item-cell.test.ts +0 -94
  83. package/test/templates-module.test.ts +0 -63
  84. package/test/templates-packed-entity.test.ts +0 -47
  85. package/test/templates-resource.test.ts +0 -71
  86. package/test/templates-ship-panel.test.ts +0 -87
  87. package/test/tokens.test.ts +0 -32
  88. package/tsconfig.json +0 -20
@@ -1,151 +1,151 @@
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'
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
- ore: 'Ore',
15
- crystal: 'Crystal',
16
- gas: 'Gas',
17
- regolith: 'Regolith',
18
- biomass: 'Biomass',
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
- 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
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
- mode?: 'values' | 'ranges'
28
+ mode?: 'values' | 'ranges'
29
29
  }
30
30
 
31
31
  type StatRow = {
32
- label: string
33
- abbreviation: string
34
- value: number | null
35
- color: string
36
- inverted?: boolean
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
- item: CargoItem,
41
- resolved: ResolvedItem,
42
- opts?: RenderResourceOpts,
40
+ item: CargoItem,
41
+ resolved: ResolvedItem,
42
+ opts?: RenderResourceOpts
43
43
  ): string {
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({
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: 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>`
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 { 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'
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
- name?: string
13
- installed: boolean
14
- description?: string | TextSpan[]
12
+ name?: string
13
+ installed: boolean
14
+ description?: string | TextSpan[]
15
15
  }
16
16
 
17
17
  export interface ShipPanelProps {
18
- name: string
19
- tier: number
20
- quantity?: number
21
- attributes: { capability: string; attributes: { label: string; value: number }[] }[]
22
- slots: ShipPanelSlot[]
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
- return n.toLocaleString('en-US')
26
+ return n.toLocaleString('en-US')
27
27
  }
28
28
 
29
29
  function tierBorder(tier: number): string {
30
- const key = `t${tier}` as keyof typeof tokens.colors.tier
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
- if (!slot.installed) return 24
38
- const desc = slot.description
39
- const plain =
40
- typeof desc === 'string'
41
- ? desc
42
- : Array.isArray(desc)
43
- ? desc.map((s) => s.text).join('')
44
- : ''
45
- if (plain.length === 0) return 24
46
- const combined = MODULE_LABEL_PREFIX(slot.name ?? 'Module') + plain
47
- const lineCount = Math.max(1, wrapText({ value: combined, charsPerLine: 36 }).length)
48
- return 10 + lineCount * 14
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
- const w = tokens.spacing.panelWidth
53
- const pad = tokens.spacing.panelPadding
54
- const innerW = w - pad * 2
55
- const quantity = props.quantity ?? 0
56
-
57
- const hullGroup = props.attributes?.find((g) => g.capability.toLowerCase() === 'hull')
58
- const hullRows = hullGroup?.attributes ?? []
59
-
60
- const headerH = 48
61
- const hullHeaderH = 20
62
- const hullRowH = 22
63
- const sectionGap = 12
64
- const rowHeights = props.slots.map(rowHeightFor)
65
- const modulesHeight = rowHeights.reduce((a, b) => a + b, 0)
66
- const height =
67
- headerH +
68
- hullHeaderH +
69
- hullRows.length * hullRowH +
70
- sectionGap +
71
- modulesHeight +
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: y + 12,
111
- value: row.label,
112
- size: tokens.typography.sizes.body,
113
- color: tokens.colors.text.secondary,
114
- }) +
115
- text({
116
- x: w - pad,
117
- y: y + 12,
118
- value: formatNumber(row.value),
119
- size: tokens.typography.sizes.body,
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
- anchor: 'end',
122
- }) +
123
- divider({ x: pad, y: y + hullRowH - 4, width: innerW, color: tokens.colors.surface.panelBorderBright })
124
- y += hullRowH
125
- }
126
-
127
- y += sectionGap
128
- let modulesSvg = ''
129
- for (let i = 0; i < props.slots.length; i++) {
130
- const slot = props.slots[i]!
131
- modulesSvg += moduleSlot({
132
- x: pad,
133
- y,
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
- const inner = `${chrome}${icon}${name}${badge}${hullHeader}${hullSvg}${modulesSvg}`
144
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`
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 { 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'
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
- const itemSvg = renderByType(item, resolved)
18
- const { width: itemW, height: itemH } = svgDimensions(itemSvg)
17
+ const itemSvg = renderByType(item, resolved)
18
+ const {width: itemW, height: itemH} = svgDimensions(itemSvg)
19
19
 
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
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
- const itemInner = itemSvg.replace(/^<svg[^>]*>/, '').replace(/<\/svg>\s*$/, '')
29
+ const itemInner = itemSvg.replace(/^<svg[^>]*>/, '').replace(/<\/svg>\s*$/, '')
30
30
 
31
- return `<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}">` +
32
- `<defs>` +
33
- `<pattern id="sc-stars" width="${STARDUST_TILE}" height="${STARDUST_TILE}" patternUnits="userSpaceOnUse">` +
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
- `</pattern>` +
36
- `</defs>` +
37
- `<rect width="${SOCIAL_CARD_WIDTH}" height="${SOCIAL_CARD_HEIGHT}" fill="${SPACE_DEEP}"/>` +
38
- `<rect width="${SOCIAL_CARD_WIDTH}" height="${SOCIAL_CARD_HEIGHT}" fill="url(#sc-stars)" opacity="0.75"/>` +
39
- `<g transform="translate(${tx.toFixed(2)} ${ty.toFixed(2)}) scale(${scale.toFixed(4)})">${itemInner}</g>` +
40
- `<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>` +
41
- `</svg>`
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
  }