@shipload/item-renderer 0.2.3 → 1.0.0-next.0

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 (89) 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 +24 -24
  5. package/src/fonts/load-bun.ts +12 -12
  6. package/src/index.ts +31 -31
  7. package/src/links.ts +13 -13
  8. package/src/meta.ts +26 -26
  9. package/src/payload/base64url.ts +21 -21
  10. package/src/payload/codec.ts +19 -19
  11. package/src/primitives/category-icon.ts +88 -86
  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 -36
  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 +56 -56
  19. package/src/primitives/stat-bar.ts +72 -72
  20. package/src/primitives/svg.ts +16 -16
  21. package/src/primitives/text.ts +33 -33
  22. package/src/primitives/wrap.ts +19 -19
  23. package/src/render.ts +20 -20
  24. package/src/templates/_shared.ts +5 -5
  25. package/src/templates/component.ts +124 -124
  26. package/src/templates/index.ts +23 -23
  27. package/src/templates/item-cell.ts +84 -84
  28. package/src/templates/module.ts +182 -181
  29. package/src/templates/packed-entity.ts +22 -22
  30. package/src/templates/resource.ts +136 -134
  31. package/src/templates/ship-panel.ts +122 -118
  32. package/src/templates/social-card.ts +36 -36
  33. package/src/tokens/colors.ts +42 -42
  34. package/src/tokens/index.ts +6 -6
  35. package/src/tokens/spacing.ts +9 -9
  36. package/src/tokens/typography.ts +18 -18
  37. package/.claude/settings.local.json +0 -6
  38. package/.github/workflows/ci.yml +0 -14
  39. package/.gitignore +0 -6
  40. package/Makefile +0 -50
  41. package/biome.json +0 -18
  42. package/bun.lock +0 -123
  43. package/scripts/check-bundle-size.ts +0 -37
  44. package/scripts/copy-fonts.ts +0 -41
  45. package/scripts/preview.ts +0 -41
  46. package/test/__image_snapshots__/.gitkeep +0 -0
  47. package/test/__image_snapshots__/component-hull-plates.diff.png +0 -0
  48. package/test/__image_snapshots__/component-hull-plates.png +0 -0
  49. package/test/__image_snapshots__/module-engine-t1.diff.png +0 -0
  50. package/test/__image_snapshots__/module-engine-t1.png +0 -0
  51. package/test/__image_snapshots__/module-storage-t1.diff.png +0 -0
  52. package/test/__image_snapshots__/module-storage-t1.png +0 -0
  53. package/test/__image_snapshots__/packed-entity-ship-t1-only-engine.diff.png +0 -0
  54. package/test/__image_snapshots__/packed-entity-ship-t1-only-engine.png +0 -0
  55. package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.diff.png +0 -0
  56. package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.png +0 -0
  57. package/test/__image_snapshots__/resource-ore-t1.diff.png +0 -0
  58. package/test/__image_snapshots__/resource-ore-t1.png +0 -0
  59. package/test/__snapshots__/templates-component.test.ts.snap +0 -5
  60. package/test/__snapshots__/templates-item-cell.test.ts.snap +0 -9
  61. package/test/__snapshots__/templates-module.test.ts.snap +0 -17
  62. package/test/__snapshots__/templates-packed-entity.test.ts.snap +0 -5
  63. package/test/__snapshots__/templates-resource.test.ts.snap +0 -7
  64. package/test/base64url.test.ts +0 -33
  65. package/test/codec.test.ts +0 -34
  66. package/test/errors.test.ts +0 -24
  67. package/test/fixtures/cargo-items.ts +0 -122
  68. package/test/fonts.test.ts +0 -28
  69. package/test/links-meta.test.ts +0 -43
  70. package/test/pixel.test.ts +0 -69
  71. package/test/primitives-category-icon.test.ts +0 -86
  72. package/test/primitives-compact-row.test.ts +0 -44
  73. package/test/primitives-domain.test.ts +0 -83
  74. package/test/primitives-layout.test.ts +0 -56
  75. package/test/primitives-module-slot.test.ts +0 -88
  76. package/test/render.test.ts +0 -43
  77. package/test/sanity.test.ts +0 -6
  78. package/test/sdk-link.test.ts +0 -19
  79. package/test/snapshots/.gitkeep +0 -0
  80. package/test/svg.test.ts +0 -30
  81. package/test/templates-component.test.ts +0 -36
  82. package/test/templates-dispatch.test.ts +0 -35
  83. package/test/templates-item-cell.test.ts +0 -94
  84. package/test/templates-module.test.ts +0 -63
  85. package/test/templates-packed-entity.test.ts +0 -47
  86. package/test/templates-resource.test.ts +0 -71
  87. package/test/templates-ship-panel.test.ts +0 -91
  88. package/test/tokens.test.ts +0 -34
  89. package/tsconfig.json +0 -20
@@ -1,149 +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",
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
- 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;
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
- 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 ? categoryColors[resolved.category] : tokens.colors.text.muted;
61
- rows = defs.map((d) => ({
62
- label: d.label,
63
- abbreviation: d.abbreviation,
64
- value: null,
65
- color,
66
- inverted: d.inverted,
67
- }));
68
- }
69
-
70
- const headerH = 48;
71
- const metaRowH = 28;
72
- const statsH = rows.length * 26 + 8;
73
- const height = headerH + metaRowH + 14 + statsH + pad;
74
-
75
- const chrome = panel({ width: w, height, borderColor: tierBorder(resolved.tier) });
76
-
77
- const quantity = Number(BigInt(item.quantity.toString()));
78
- const badge = quantityBadge({ x: w - pad, y: pad, quantity });
79
-
80
- const icon = iconHex({
81
- x: pad,
82
- y: pad + 4,
83
- color: categoryColor(resolved.category),
84
- code: shortCode(resolved.itemId),
85
- });
86
-
87
- const name = text({
88
- x: pad + 34,
89
- y: pad + 22,
90
- value: displayName(resolved),
91
- size: tokens.typography.sizes.title,
92
- weight: 700,
93
- family: tokens.typography.display,
94
- });
95
-
96
- const catLabel = resolved.category ? (CATEGORY_LABELS[resolved.category] ?? "Item") : "Item";
97
- const catText = text({
98
- x: pad,
99
- y: pad + headerH + 4,
100
- value: "Category",
101
- size: tokens.typography.sizes.body,
102
- color: tokens.colors.text.secondary,
103
- });
104
- const catValue = text({
105
- x: w - pad,
106
- y: pad + headerH + 4,
107
- value: catLabel,
108
- size: tokens.typography.sizes.body,
109
- weight: 600,
110
- anchor: "end",
111
- });
112
- const massLabel = text({
113
- x: pad,
114
- y: pad + headerH + metaRowH - 8,
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: sepY + 18 + i * 26,
136
- width: innerW,
137
- label: row.label,
138
- abbreviation: row.abbreviation,
139
- value: row.value,
140
- color: row.color,
141
- inverted: row.inverted,
142
- }),
143
- )
144
- .join("");
145
-
146
- const inner = `${chrome}${icon}${name}${badge}${catText}${catValue}${massLabel}${massValue}${sep}${statsSvg}`;
147
-
148
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`;
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 { 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
- return tokens.colors.tier[tier] ?? tokens.colors.surface.panelBorder;
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
- if (!slot.installed) return 24;
37
- const desc = slot.description;
38
- const plain =
39
- typeof desc === "string" ? desc : Array.isArray(desc) ? desc.map((s) => s.text).join("") : "";
40
- if (plain.length === 0) return 24;
41
- const combined = MODULE_LABEL_PREFIX(slot.name ?? "Module") + plain;
42
- const lineCount = Math.max(1, wrapText({ value: combined, charsPerLine: 36 }).length);
43
- 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
44
48
  }
45
49
 
46
50
  export function renderShipPanel(props: ShipPanelProps): string {
47
- const w = tokens.spacing.panelWidth;
48
- const pad = tokens.spacing.panelPadding;
49
- const innerW = w - pad * 2;
50
- const quantity = props.quantity ?? 0;
51
-
52
- const hullGroup = props.attributes?.find((g) => g.capability.toLowerCase() === "hull");
53
- const hullRows = hullGroup?.attributes ?? [];
54
-
55
- const headerH = 48;
56
- const hullHeaderH = 20;
57
- const hullRowH = 22;
58
- const sectionGap = 12;
59
- const rowHeights = props.slots.map(rowHeightFor);
60
- const modulesHeight = rowHeights.reduce((a, b) => a + b, 0);
61
- const height =
62
- headerH + hullHeaderH + hullRows.length * hullRowH + sectionGap + modulesHeight + pad;
63
-
64
- const chrome = panel({ width: w, height, borderColor: tierBorder(props.tier) });
65
-
66
- const icon = iconHex({
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: y + 12,
101
- value: row.label,
102
- size: tokens.typography.sizes.body,
103
- color: tokens.colors.text.secondary,
104
- }) +
105
- text({
106
- x: w - pad,
107
- y: y + 12,
108
- value: formatNumber(row.value),
109
- 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,
110
82
  weight: 700,
111
- anchor: "end",
112
- }) +
113
- divider({
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: y + hullRowH - 4,
116
- width: innerW,
117
- color: tokens.colors.surface.panelBorderBright,
118
- });
119
- y += hullRowH;
120
- }
121
-
122
- y += sectionGap;
123
- let modulesSvg = "";
124
- for (let i = 0; i < props.slots.length; i++) {
125
- const slot = props.slots[i]!;
126
- modulesSvg += moduleSlot({
127
- x: pad,
128
- y,
129
- width: innerW,
130
- installed: slot.installed,
131
- capability: slot.name,
132
- description: slot.description,
133
- accentColor: tokens.colors.brand.teal,
134
- });
135
- y += rowHeights[i]!;
136
- }
137
-
138
- const inner = `${chrome}${icon}${name}${badge}${hullHeader}${hullSvg}${modulesSvg}`;
139
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`;
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 { 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
- 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 = "#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";
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
- 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 (
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
- );
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
  }