@shipload/item-renderer 0.2.2 → 0.2.3
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/.claude/settings.local.json +6 -0
- package/bun.lock +2 -2
- package/package.json +2 -2
- package/scripts/check-bundle-size.ts +21 -21
- package/scripts/copy-fonts.ts +19 -19
- package/scripts/preview.ts +13 -15
- package/src/assets/stardust-base64.ts +1 -1
- package/src/errors.ts +8 -8
- package/src/fonts/index.ts +25 -26
- package/src/fonts/load-bun.ts +9 -9
- package/src/index.ts +21 -21
- package/src/links.ts +11 -11
- package/src/meta.ts +16 -16
- package/src/payload/base64url.ts +16 -16
- package/src/payload/codec.ts +13 -13
- package/src/primitives/category-icon.ts +69 -48
- package/src/primitives/compact-row.ts +13 -13
- package/src/primitives/divider.ts +9 -9
- package/src/primitives/icon-hex.ts +18 -16
- package/src/primitives/module-slot.ts +73 -73
- package/src/primitives/panel.ts +10 -10
- package/src/primitives/quantity-badge.ts +13 -13
- package/src/primitives/span-paragraph.ts +48 -50
- package/src/primitives/stat-bar.ts +24 -24
- package/src/primitives/svg.ts +13 -13
- package/src/primitives/text.ts +25 -25
- package/src/primitives/wrap.ts +12 -12
- package/src/render.ts +15 -19
- package/src/templates/_shared.ts +5 -6
- package/src/templates/component.ts +67 -65
- package/src/templates/index.ts +17 -17
- package/src/templates/item-cell.ts +48 -45
- package/src/templates/module.ts +83 -81
- package/src/templates/packed-entity.ts +12 -14
- package/src/templates/resource.ts +63 -65
- package/src/templates/ship-panel.ts +66 -71
- package/src/templates/social-card.ts +27 -25
- package/src/tokens/colors.ts +29 -29
- package/src/tokens/index.ts +6 -6
- package/src/tokens/spacing.ts +1 -1
- package/src/tokens/typography.ts +1 -1
- package/test/__image_snapshots__/module-storage-t1.diff.png +0 -0
- package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.diff.png +0 -0
- package/test/base64url.test.ts +22 -22
- package/test/codec.test.ts +26 -35
- package/test/errors.test.ts +21 -21
- package/test/fixtures/cargo-items.ts +43 -43
- package/test/fonts.test.ts +23 -23
- package/test/links-meta.test.ts +37 -37
- package/test/pixel.test.ts +44 -41
- package/test/primitives-category-icon.test.ts +74 -67
- package/test/primitives-compact-row.test.ts +29 -29
- package/test/primitives-domain.test.ts +61 -50
- package/test/primitives-layout.test.ts +47 -47
- package/test/primitives-module-slot.test.ts +58 -58
- package/test/render.test.ts +38 -35
- package/test/sanity.test.ts +5 -5
- package/test/sdk-link.test.ts +13 -13
- package/test/svg.test.ts +24 -22
- package/test/templates-component.test.ts +32 -32
- package/test/templates-dispatch.test.ts +29 -29
- package/test/templates-item-cell.test.ts +79 -79
- package/test/templates-module.test.ts +52 -52
- package/test/templates-packed-entity.test.ts +42 -42
- package/test/templates-resource.test.ts +61 -61
- package/test/templates-ship-panel.test.ts +65 -61
- package/test/tokens.test.ts +28 -26
package/src/templates/index.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type { ResolvedItem } from
|
|
2
|
-
import type { CargoItem } from
|
|
3
|
-
import { RenderError } from
|
|
4
|
-
import { renderResource } from
|
|
5
|
-
import { renderPackedEntity } from
|
|
6
|
-
import { renderComponent } from
|
|
7
|
-
import { renderModule } from
|
|
1
|
+
import type { ResolvedItem } from "@shipload/sdk";
|
|
2
|
+
import type { CargoItem } from "../payload/codec.ts";
|
|
3
|
+
import { RenderError } from "../errors.ts";
|
|
4
|
+
import { renderResource } from "./resource.ts";
|
|
5
|
+
import { renderPackedEntity } from "./packed-entity.ts";
|
|
6
|
+
import { renderComponent } from "./component.ts";
|
|
7
|
+
import { renderModule } from "./module.ts";
|
|
8
8
|
|
|
9
9
|
export interface RenderByTypeOpts {
|
|
10
|
-
mode?:
|
|
10
|
+
mode?: "values" | "ranges";
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function renderByType(
|
|
@@ -16,15 +16,15 @@ export function renderByType(
|
|
|
16
16
|
opts?: RenderByTypeOpts,
|
|
17
17
|
): string {
|
|
18
18
|
switch (resolved.itemType) {
|
|
19
|
-
case
|
|
20
|
-
return renderResource(item, resolved, opts)
|
|
21
|
-
case
|
|
22
|
-
return renderPackedEntity(item, resolved)
|
|
23
|
-
case
|
|
24
|
-
return renderComponent(item, resolved, opts)
|
|
25
|
-
case
|
|
26
|
-
return renderModule(item, resolved, opts)
|
|
19
|
+
case "resource":
|
|
20
|
+
return renderResource(item, resolved, opts);
|
|
21
|
+
case "entity":
|
|
22
|
+
return renderPackedEntity(item, resolved);
|
|
23
|
+
case "component":
|
|
24
|
+
return renderComponent(item, resolved, opts);
|
|
25
|
+
case "module":
|
|
26
|
+
return renderModule(item, resolved, opts);
|
|
27
27
|
default:
|
|
28
|
-
throw new RenderError(`unknown itemType '${String(resolved.itemType)}'`)
|
|
28
|
+
throw new RenderError(`unknown itemType '${String(resolved.itemType)}'`);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -1,32 +1,28 @@
|
|
|
1
|
-
import type { ResolvedItem
|
|
2
|
-
import { tierColors, categoryColors, categoryIconShapes } from
|
|
3
|
-
import { el } from
|
|
4
|
-
import { text } from
|
|
5
|
-
import { categoryIconPath } from
|
|
6
|
-
import { tokens } from
|
|
7
|
-
|
|
8
|
-
function tierKey(tier: number): ResourceTier {
|
|
9
|
-
return `t${tier}` as ResourceTier
|
|
10
|
-
}
|
|
1
|
+
import type { ResolvedItem } from "@shipload/sdk";
|
|
2
|
+
import { tierColors, categoryColors, categoryIconShapes } from "@shipload/sdk";
|
|
3
|
+
import { el } from "../primitives/svg.ts";
|
|
4
|
+
import { text } from "../primitives/text.ts";
|
|
5
|
+
import { categoryIconPath } from "../primitives/category-icon.ts";
|
|
6
|
+
import { tokens } from "../tokens/index.ts";
|
|
11
7
|
|
|
12
8
|
export interface ItemCellProps {
|
|
13
|
-
resolved: ResolvedItem
|
|
14
|
-
quantity?: number
|
|
15
|
-
size?: number
|
|
9
|
+
resolved: ResolvedItem;
|
|
10
|
+
quantity?: number;
|
|
11
|
+
size?: number;
|
|
16
12
|
}
|
|
17
13
|
|
|
18
14
|
export interface ItemCellGroupProps extends ItemCellProps {
|
|
19
|
-
x: number
|
|
20
|
-
y: number
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
21
17
|
}
|
|
22
18
|
|
|
23
19
|
function cellInner(props: ItemCellProps): string {
|
|
24
|
-
const size = props.size ?? 48
|
|
25
|
-
const height = Math.round(size * 1.25)
|
|
26
|
-
const r = Math.max(4, Math.round(size * 0.12))
|
|
27
|
-
const cx = size / 2
|
|
20
|
+
const size = props.size ?? 48;
|
|
21
|
+
const height = Math.round(size * 1.25);
|
|
22
|
+
const r = Math.max(4, Math.round(size * 0.12));
|
|
23
|
+
const cx = size / 2;
|
|
28
24
|
|
|
29
|
-
const border = el(
|
|
25
|
+
const border = el("rect", {
|
|
30
26
|
x: 0.5,
|
|
31
27
|
y: 0.5,
|
|
32
28
|
width: size - 1,
|
|
@@ -34,28 +30,35 @@ function cellInner(props: ItemCellProps): string {
|
|
|
34
30
|
rx: r,
|
|
35
31
|
ry: r,
|
|
36
32
|
fill: tokens.colors.surface.panel,
|
|
37
|
-
stroke: tierColors[
|
|
38
|
-
|
|
39
|
-
})
|
|
33
|
+
stroke: tierColors[props.resolved.tier] ?? tokens.colors.surface.panelBorder,
|
|
34
|
+
"stroke-width": 1.5,
|
|
35
|
+
});
|
|
40
36
|
|
|
41
|
-
let content =
|
|
37
|
+
let content = "";
|
|
42
38
|
if (props.resolved.abbreviation) {
|
|
43
|
-
const iconCy = size * 0.45
|
|
39
|
+
const iconCy = size * 0.45;
|
|
44
40
|
content = text({
|
|
45
41
|
x: cx,
|
|
46
42
|
y: iconCy,
|
|
47
43
|
value: props.resolved.abbreviation,
|
|
48
44
|
size: Math.round(size * 0.28),
|
|
49
45
|
weight: 700,
|
|
50
|
-
anchor:
|
|
46
|
+
anchor: "middle",
|
|
51
47
|
color: tokens.colors.text.primary,
|
|
52
48
|
family: tokens.typography.display,
|
|
53
|
-
})
|
|
49
|
+
});
|
|
54
50
|
} else if (props.resolved.category) {
|
|
55
|
-
const shape = categoryIconShapes[props.resolved.category]
|
|
56
|
-
const color = categoryColors[props.resolved.category]
|
|
57
|
-
const iconCy = size * 0.4
|
|
58
|
-
content = categoryIconPath({
|
|
51
|
+
const shape = categoryIconShapes[props.resolved.category];
|
|
52
|
+
const color = categoryColors[props.resolved.category];
|
|
53
|
+
const iconCy = size * 0.4;
|
|
54
|
+
content = categoryIconPath({
|
|
55
|
+
shape,
|
|
56
|
+
cx,
|
|
57
|
+
cy: iconCy,
|
|
58
|
+
size: size * 0.32,
|
|
59
|
+
color,
|
|
60
|
+
strokeWidth: 1.5,
|
|
61
|
+
});
|
|
59
62
|
} else if (props.resolved.icon) {
|
|
60
63
|
content = text({
|
|
61
64
|
x: cx,
|
|
@@ -63,38 +66,38 @@ function cellInner(props: ItemCellProps): string {
|
|
|
63
66
|
value: props.resolved.icon,
|
|
64
67
|
size: Math.round(size * 0.44),
|
|
65
68
|
weight: 400,
|
|
66
|
-
anchor:
|
|
67
|
-
dominantBaseline:
|
|
69
|
+
anchor: "middle",
|
|
70
|
+
dominantBaseline: "central",
|
|
68
71
|
color: tokens.colors.text.primary,
|
|
69
|
-
})
|
|
72
|
+
});
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
const qty = props.quantity ?? 0
|
|
73
|
-
let quantityText =
|
|
75
|
+
const qty = props.quantity ?? 0;
|
|
76
|
+
let quantityText = "";
|
|
74
77
|
if (qty > 1) {
|
|
75
|
-
const qtyFontSize = Math.max(9, Math.round(size * 0.22))
|
|
76
|
-
const qtyPad = Math.max(3, Math.round(size * 0.12))
|
|
78
|
+
const qtyFontSize = Math.max(9, Math.round(size * 0.22));
|
|
79
|
+
const qtyPad = Math.max(3, Math.round(size * 0.12));
|
|
77
80
|
quantityText = text({
|
|
78
81
|
x: size - qtyPad,
|
|
79
82
|
y: height - qtyPad,
|
|
80
83
|
value: String(qty),
|
|
81
84
|
size: qtyFontSize,
|
|
82
85
|
weight: 700,
|
|
83
|
-
anchor:
|
|
86
|
+
anchor: "end",
|
|
84
87
|
color: tokens.colors.text.primary,
|
|
85
88
|
family: tokens.typography.display,
|
|
86
|
-
})
|
|
89
|
+
});
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
return border + content + quantityText
|
|
92
|
+
return border + content + quantityText;
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
export function itemCellGroup(props: ItemCellGroupProps): string {
|
|
93
|
-
return `<g transform="translate(${props.x}, ${props.y})">${cellInner(props)}</g
|
|
96
|
+
return `<g transform="translate(${props.x}, ${props.y})">${cellInner(props)}</g>`;
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
export function renderItemCell(props: ItemCellProps): string {
|
|
97
|
-
const size = props.size ?? 48
|
|
98
|
-
const height = Math.round(size * 1.25)
|
|
99
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${height}" viewBox="0 0 ${size} ${height}">${cellInner(props)}</svg
|
|
100
|
+
const size = props.size ?? 48;
|
|
101
|
+
const height = Math.round(size * 1.25);
|
|
102
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${height}" viewBox="0 0 ${size} ${height}">${cellInner(props)}</svg>`;
|
|
100
103
|
}
|
package/src/templates/module.ts
CHANGED
|
@@ -1,81 +1,83 @@
|
|
|
1
|
-
import type { ResolvedItem } from
|
|
2
|
-
import { describeModuleForItem, formatTier, renderDescription } from
|
|
3
|
-
import type { CargoItem } from
|
|
4
|
-
import { panel } from
|
|
5
|
-
import { iconHex } from
|
|
6
|
-
import { text } from
|
|
7
|
-
import { divider } from
|
|
8
|
-
import { compactRow } from
|
|
9
|
-
import { quantityBadge } from
|
|
10
|
-
import { spanParagraph } from
|
|
11
|
-
import { tokens } from
|
|
12
|
-
import { shortCode, formatMass, tierBorder } from
|
|
1
|
+
import type { ResolvedItem } from "@shipload/sdk";
|
|
2
|
+
import { describeModuleForItem, formatTier, renderDescription } 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 { compactRow } from "../primitives/compact-row.ts";
|
|
9
|
+
import { quantityBadge } from "../primitives/quantity-badge.ts";
|
|
10
|
+
import { spanParagraph } from "../primitives/span-paragraph.ts";
|
|
11
|
+
import { tokens } from "../tokens/index.ts";
|
|
12
|
+
import { shortCode, formatMass, tierBorder } from "./_shared.ts";
|
|
13
13
|
|
|
14
14
|
function capabilityColor(name: string): string {
|
|
15
|
-
const key = name.toLowerCase().replace(/\s+/g,
|
|
16
|
-
return tokens.colors.capability[key] ?? tokens.colors.accent.component
|
|
15
|
+
const key = name.toLowerCase().replace(/\s+/g, "") as keyof typeof tokens.colors.capability;
|
|
16
|
+
return tokens.colors.capability[key] ?? tokens.colors.accent.component;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface RenderModuleOpts {
|
|
20
|
-
mode?:
|
|
20
|
+
mode?: "values" | "ranges";
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export function renderModule(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
23
|
+
export function renderModule(
|
|
24
|
+
item: CargoItem,
|
|
25
|
+
resolved: ResolvedItem,
|
|
26
|
+
opts?: RenderModuleOpts,
|
|
27
|
+
): string {
|
|
28
|
+
const mode = opts?.mode ?? "values";
|
|
29
|
+
const w = tokens.spacing.panelWidth;
|
|
30
|
+
const pad = tokens.spacing.panelPadding;
|
|
31
|
+
const innerW = w - pad * 2;
|
|
32
|
+
|
|
33
|
+
const group = resolved.attributes?.[0];
|
|
34
|
+
const attrs = group?.attributes ?? [];
|
|
35
|
+
const desc = mode === "values" ? describeModuleForItem(resolved) : undefined;
|
|
36
|
+
|
|
37
|
+
const capabilityName = group?.capability ?? resolved.name.replace(/\s+T\d+$/i, "");
|
|
38
|
+
|
|
39
|
+
const headerH = 48;
|
|
40
|
+
const metaRowH = 28;
|
|
41
|
+
const sepY = pad + headerH + metaRowH + 6;
|
|
42
|
+
|
|
43
|
+
let bodyHeight = 0;
|
|
44
|
+
if (mode === "ranges") {
|
|
45
|
+
bodyHeight = 20 + 8;
|
|
44
46
|
} else if (desc && group) {
|
|
45
47
|
const plain = renderDescription(desc)
|
|
46
48
|
.map((s) => s.text)
|
|
47
|
-
.join(
|
|
49
|
+
.join("");
|
|
48
50
|
const lines = plain.split(/\s+/).reduce(
|
|
49
51
|
(acc, word) => {
|
|
50
|
-
const last = acc[acc.length - 1] ??
|
|
51
|
-
if (last.length === 0) return [...acc.slice(0, -1), word]
|
|
52
|
-
if (last.length + 1 + word.length <= 36) return [...acc.slice(0, -1), `${last} ${word}`]
|
|
53
|
-
return [...acc, word]
|
|
52
|
+
const last = acc[acc.length - 1] ?? "";
|
|
53
|
+
if (last.length === 0) return [...acc.slice(0, -1), word];
|
|
54
|
+
if (last.length + 1 + word.length <= 36) return [...acc.slice(0, -1), `${last} ${word}`];
|
|
55
|
+
return [...acc, word];
|
|
54
56
|
},
|
|
55
|
-
[
|
|
56
|
-
)
|
|
57
|
-
const lineCount = lines.filter((l) => l.length > 0).length
|
|
58
|
-
bodyHeight = 20 + lineCount * 14 + 8
|
|
57
|
+
[""],
|
|
58
|
+
);
|
|
59
|
+
const lineCount = lines.filter((l) => l.length > 0).length;
|
|
60
|
+
bodyHeight = 20 + lineCount * 14 + 8;
|
|
59
61
|
} else if (group && attrs.length > 0) {
|
|
60
|
-
const capHeaderH = 22
|
|
61
|
-
const attrsH = attrs.length * 18
|
|
62
|
-
bodyHeight = capHeaderH + attrsH + 8
|
|
62
|
+
const capHeaderH = 22;
|
|
63
|
+
const attrsH = attrs.length * 18;
|
|
64
|
+
bodyHeight = capHeaderH + attrsH + 8;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
const height = headerH + metaRowH + 14 + bodyHeight + pad
|
|
67
|
+
const height = headerH + metaRowH + 14 + bodyHeight + pad;
|
|
66
68
|
|
|
67
|
-
const chrome = panel({ width: w, height, borderColor: tierBorder(resolved.tier) })
|
|
69
|
+
const chrome = panel({ width: w, height, borderColor: tierBorder(resolved.tier) });
|
|
68
70
|
|
|
69
|
-
const quantity = Number(BigInt(item.quantity.toString()))
|
|
70
|
-
const badge = quantityBadge({ x: w - pad, y: pad, quantity })
|
|
71
|
+
const quantity = Number(BigInt(item.quantity.toString()));
|
|
72
|
+
const badge = quantityBadge({ x: w - pad, y: pad, quantity });
|
|
71
73
|
|
|
72
|
-
const iconColor = group ? capabilityColor(group.capability) : capabilityColor(capabilityName)
|
|
74
|
+
const iconColor = group ? capabilityColor(group.capability) : capabilityColor(capabilityName);
|
|
73
75
|
const icon = iconHex({
|
|
74
76
|
x: pad,
|
|
75
77
|
y: pad + 4,
|
|
76
78
|
color: iconColor,
|
|
77
79
|
code: shortCode(resolved.itemId),
|
|
78
|
-
})
|
|
80
|
+
});
|
|
79
81
|
|
|
80
82
|
const name = text({
|
|
81
83
|
x: pad + 34,
|
|
@@ -84,45 +86,45 @@ export function renderModule(item: CargoItem, resolved: ResolvedItem, opts?: Ren
|
|
|
84
86
|
size: tokens.typography.sizes.title,
|
|
85
87
|
weight: 700,
|
|
86
88
|
family: tokens.typography.display,
|
|
87
|
-
})
|
|
89
|
+
});
|
|
88
90
|
|
|
89
91
|
const subtitleLabel = text({
|
|
90
92
|
x: pad,
|
|
91
93
|
y: pad + headerH + 4,
|
|
92
|
-
value:
|
|
94
|
+
value: "Type",
|
|
93
95
|
size: tokens.typography.sizes.body,
|
|
94
96
|
color: tokens.colors.text.secondary,
|
|
95
|
-
})
|
|
97
|
+
});
|
|
96
98
|
const subtitleValue = text({
|
|
97
99
|
x: w - pad,
|
|
98
100
|
y: pad + headerH + 4,
|
|
99
101
|
value: `MODULE · ${formatTier(resolved.tier)}`,
|
|
100
102
|
size: tokens.typography.sizes.body,
|
|
101
103
|
weight: 600,
|
|
102
|
-
anchor:
|
|
103
|
-
})
|
|
104
|
+
anchor: "end",
|
|
105
|
+
});
|
|
104
106
|
|
|
105
107
|
const massLabel = text({
|
|
106
108
|
x: pad,
|
|
107
109
|
y: pad + headerH + metaRowH - 8,
|
|
108
|
-
value:
|
|
110
|
+
value: "Mass",
|
|
109
111
|
size: tokens.typography.sizes.body,
|
|
110
112
|
color: tokens.colors.text.secondary,
|
|
111
|
-
})
|
|
113
|
+
});
|
|
112
114
|
const massValue = text({
|
|
113
115
|
x: w - pad,
|
|
114
116
|
y: pad + headerH + metaRowH - 8,
|
|
115
117
|
value: formatMass(resolved.mass),
|
|
116
118
|
size: tokens.typography.sizes.body,
|
|
117
119
|
weight: 600,
|
|
118
|
-
anchor:
|
|
119
|
-
})
|
|
120
|
+
anchor: "end",
|
|
121
|
+
});
|
|
120
122
|
|
|
121
|
-
const sep = divider({ x: pad, y: sepY, width: innerW })
|
|
123
|
+
const sep = divider({ x: pad, y: sepY, width: innerW });
|
|
122
124
|
|
|
123
|
-
let capSection =
|
|
124
|
-
if (mode ===
|
|
125
|
-
const accentColor = capabilityColor(capabilityName)
|
|
125
|
+
let capSection = "";
|
|
126
|
+
if (mode === "ranges") {
|
|
127
|
+
const accentColor = capabilityColor(capabilityName);
|
|
126
128
|
capSection = text({
|
|
127
129
|
x: pad,
|
|
128
130
|
y: sepY + 16,
|
|
@@ -132,9 +134,9 @@ export function renderModule(item: CargoItem, resolved: ResolvedItem, opts?: Ren
|
|
|
132
134
|
family: tokens.typography.sans,
|
|
133
135
|
color: accentColor,
|
|
134
136
|
letterSpacing: 1,
|
|
135
|
-
})
|
|
137
|
+
});
|
|
136
138
|
} else if (desc && group) {
|
|
137
|
-
const accentColor = capabilityColor(group.capability)
|
|
139
|
+
const accentColor = capabilityColor(group.capability);
|
|
138
140
|
const capHeader = text({
|
|
139
141
|
x: pad,
|
|
140
142
|
y: sepY + 16,
|
|
@@ -144,18 +146,18 @@ export function renderModule(item: CargoItem, resolved: ResolvedItem, opts?: Ren
|
|
|
144
146
|
family: tokens.typography.sans,
|
|
145
147
|
color: accentColor,
|
|
146
148
|
letterSpacing: 1,
|
|
147
|
-
})
|
|
148
|
-
const spans = renderDescription(desc)
|
|
149
|
+
});
|
|
150
|
+
const spans = renderDescription(desc);
|
|
149
151
|
const { svg: paraSvg } = spanParagraph({
|
|
150
152
|
x: pad,
|
|
151
153
|
y: sepY + 36,
|
|
152
154
|
spans,
|
|
153
155
|
charsPerLine: 36,
|
|
154
156
|
lineHeight: 14,
|
|
155
|
-
})
|
|
156
|
-
capSection = capHeader + paraSvg
|
|
157
|
+
});
|
|
158
|
+
capSection = capHeader + paraSvg;
|
|
157
159
|
} else if (group && attrs.length > 0) {
|
|
158
|
-
const capY = sepY + 22
|
|
160
|
+
const capY = sepY + 22;
|
|
159
161
|
const capHeader = text({
|
|
160
162
|
x: pad,
|
|
161
163
|
y: capY,
|
|
@@ -165,25 +167,25 @@ export function renderModule(item: CargoItem, resolved: ResolvedItem, opts?: Ren
|
|
|
165
167
|
family: tokens.typography.sans,
|
|
166
168
|
color: capabilityColor(group.capability),
|
|
167
169
|
letterSpacing: 0.8,
|
|
168
|
-
})
|
|
170
|
+
});
|
|
169
171
|
|
|
170
172
|
const attrRows = attrs
|
|
171
173
|
.map((attr, i) => {
|
|
172
|
-
const displayValue = String(attr.value)
|
|
174
|
+
const displayValue = String(attr.value);
|
|
173
175
|
return compactRow({
|
|
174
176
|
x: pad,
|
|
175
177
|
y: capY + 14 + i * 18,
|
|
176
178
|
width: innerW,
|
|
177
179
|
label: attr.label,
|
|
178
180
|
value: displayValue,
|
|
179
|
-
})
|
|
181
|
+
});
|
|
180
182
|
})
|
|
181
|
-
.join(
|
|
183
|
+
.join("");
|
|
182
184
|
|
|
183
|
-
capSection = capHeader + attrRows
|
|
185
|
+
capSection = capHeader + attrRows;
|
|
184
186
|
}
|
|
185
187
|
|
|
186
|
-
const inner = `${chrome}${icon}${name}${badge}${subtitleLabel}${subtitleValue}${massLabel}${massValue}${sep}${capSection}
|
|
188
|
+
const inner = `${chrome}${icon}${name}${badge}${subtitleLabel}${subtitleValue}${massLabel}${massValue}${sep}${capSection}`;
|
|
187
189
|
|
|
188
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg
|
|
190
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`;
|
|
189
191
|
}
|
|
@@ -1,30 +1,28 @@
|
|
|
1
|
-
import type { ResolvedItem, ResolvedModuleSlot } from
|
|
2
|
-
import { describeModuleForSlot, renderDescription } from
|
|
3
|
-
import type { CargoItem } from
|
|
4
|
-
import { renderShipPanel, type ShipPanelSlot } from
|
|
1
|
+
import type { ResolvedItem, ResolvedModuleSlot } from "@shipload/sdk";
|
|
2
|
+
import { describeModuleForSlot, renderDescription } from "@shipload/sdk";
|
|
3
|
+
import type { CargoItem } from "../payload/codec.ts";
|
|
4
|
+
import { renderShipPanel, type ShipPanelSlot } from "./ship-panel.ts";
|
|
5
5
|
|
|
6
6
|
function slotToPanelSlot(slot: ResolvedModuleSlot): ShipPanelSlot {
|
|
7
7
|
if (!slot.installed || !slot.attributes || !slot.name) {
|
|
8
|
-
return { installed: false }
|
|
8
|
+
return { installed: false };
|
|
9
9
|
}
|
|
10
|
-
const desc = describeModuleForSlot(slot)
|
|
10
|
+
const desc = describeModuleForSlot(slot);
|
|
11
11
|
if (desc) {
|
|
12
|
-
return { name: slot.name, installed: true, description: renderDescription(desc) }
|
|
12
|
+
return { name: slot.name, installed: true, description: renderDescription(desc) };
|
|
13
13
|
}
|
|
14
|
-
const shorthand = slot.attributes
|
|
15
|
-
|
|
16
|
-
.join(' · ')
|
|
17
|
-
return { name: slot.name, installed: true, description: shorthand }
|
|
14
|
+
const shorthand = slot.attributes.map((a) => `${a.value} ${a.label.toLowerCase()}`).join(" · ");
|
|
15
|
+
return { name: slot.name, installed: true, description: shorthand };
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
export function renderPackedEntity(item: CargoItem, resolved: ResolvedItem): string {
|
|
21
|
-
const quantity = Number(BigInt(item.quantity.toString()))
|
|
22
|
-
const slots = (resolved.moduleSlots ?? []).map(slotToPanelSlot)
|
|
19
|
+
const quantity = Number(BigInt(item.quantity.toString()));
|
|
20
|
+
const slots = (resolved.moduleSlots ?? []).map(slotToPanelSlot);
|
|
23
21
|
return renderShipPanel({
|
|
24
22
|
name: `${resolved.name} (Packed)`,
|
|
25
23
|
tier: resolved.tier,
|
|
26
24
|
quantity,
|
|
27
25
|
attributes: resolved.attributes ?? [],
|
|
28
26
|
slots,
|
|
29
|
-
})
|
|
27
|
+
});
|
|
30
28
|
}
|