@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
|
@@ -1,72 +1,70 @@
|
|
|
1
|
-
import { wrapText } from
|
|
2
|
-
import { escapeXml as escapeAttr } from
|
|
3
|
-
import { tokens } from
|
|
4
|
-
import type { TextSpan } from
|
|
1
|
+
import { wrapText } from "./wrap.ts";
|
|
2
|
+
import { escapeXml as escapeAttr } from "./svg.ts";
|
|
3
|
+
import { tokens } from "../tokens/index.ts";
|
|
4
|
+
import type { TextSpan } from "@shipload/sdk";
|
|
5
5
|
|
|
6
6
|
export interface SpanParagraphProps {
|
|
7
|
-
x: number
|
|
8
|
-
y: number
|
|
9
|
-
spans: TextSpan[]
|
|
10
|
-
charsPerLine?: number
|
|
11
|
-
lineHeight?: number
|
|
12
|
-
bodyColor?: string
|
|
13
|
-
highlightColor?: string
|
|
14
|
-
fontSize?: number
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
spans: TextSpan[];
|
|
10
|
+
charsPerLine?: number;
|
|
11
|
+
lineHeight?: number;
|
|
12
|
+
bodyColor?: string;
|
|
13
|
+
highlightColor?: string;
|
|
14
|
+
fontSize?: number;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function escapeXml(s: string): string {
|
|
18
18
|
return s
|
|
19
|
-
.replace(/&/g,
|
|
20
|
-
.replace(/</g,
|
|
21
|
-
.replace(/>/g,
|
|
22
|
-
.replace(/"/g,
|
|
19
|
+
.replace(/&/g, "&")
|
|
20
|
+
.replace(/</g, "<")
|
|
21
|
+
.replace(/>/g, ">")
|
|
22
|
+
.replace(/"/g, """);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function sliceSpans(spans: TextSpan[], start: number, end: number): TextSpan[] {
|
|
26
|
-
const out: TextSpan[] = []
|
|
27
|
-
let cursor = 0
|
|
26
|
+
const out: TextSpan[] = [];
|
|
27
|
+
let cursor = 0;
|
|
28
28
|
for (const span of spans) {
|
|
29
|
-
const spanStart = cursor
|
|
30
|
-
const spanEnd = cursor + span.text.length
|
|
31
|
-
cursor = spanEnd
|
|
32
|
-
if (spanEnd <= start || spanStart >= end) continue
|
|
33
|
-
const sliceStart = Math.max(0, start - spanStart)
|
|
34
|
-
const sliceEnd = span.text.length - Math.max(0, spanEnd - end)
|
|
35
|
-
const txt = span.text.slice(sliceStart, sliceEnd)
|
|
36
|
-
if (txt.length === 0) continue
|
|
37
|
-
out.push(span.highlight ? { text: txt, highlight: true } : { text: txt })
|
|
29
|
+
const spanStart = cursor;
|
|
30
|
+
const spanEnd = cursor + span.text.length;
|
|
31
|
+
cursor = spanEnd;
|
|
32
|
+
if (spanEnd <= start || spanStart >= end) continue;
|
|
33
|
+
const sliceStart = Math.max(0, start - spanStart);
|
|
34
|
+
const sliceEnd = span.text.length - Math.max(0, spanEnd - end);
|
|
35
|
+
const txt = span.text.slice(sliceStart, sliceEnd);
|
|
36
|
+
if (txt.length === 0) continue;
|
|
37
|
+
out.push(span.highlight ? { text: txt, highlight: true } : { text: txt });
|
|
38
38
|
}
|
|
39
|
-
return out
|
|
39
|
+
return out;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
export function spanParagraph(
|
|
43
|
-
props
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const highlightColor = props.highlightColor ?? tokens.colors.text.accent
|
|
49
|
-
const size = props.fontSize ?? tokens.typography.sizes.body
|
|
42
|
+
export function spanParagraph(props: SpanParagraphProps): { svg: string; lineCount: number } {
|
|
43
|
+
const chars = props.charsPerLine ?? 36;
|
|
44
|
+
const lh = props.lineHeight ?? 14;
|
|
45
|
+
const bodyColor = props.bodyColor ?? tokens.colors.text.secondary;
|
|
46
|
+
const highlightColor = props.highlightColor ?? tokens.colors.text.accent;
|
|
47
|
+
const size = props.fontSize ?? tokens.typography.sizes.body;
|
|
50
48
|
|
|
51
|
-
const plain = props.spans.map((s) => s.text).join(
|
|
52
|
-
const lines = wrapText({ value: plain, charsPerLine: chars })
|
|
49
|
+
const plain = props.spans.map((s) => s.text).join("");
|
|
50
|
+
const lines = wrapText({ value: plain, charsPerLine: chars });
|
|
53
51
|
|
|
54
|
-
let charOffset = 0
|
|
52
|
+
let charOffset = 0;
|
|
55
53
|
const out = lines
|
|
56
54
|
.map((line, i) => {
|
|
57
|
-
const lineStart = charOffset
|
|
58
|
-
const lineEnd = lineStart + line.length
|
|
59
|
-
charOffset = lineEnd + 1
|
|
60
|
-
const lineSpans = sliceSpans(props.spans, lineStart, lineEnd)
|
|
61
|
-
const y = props.y + i * lh
|
|
55
|
+
const lineStart = charOffset;
|
|
56
|
+
const lineEnd = lineStart + line.length;
|
|
57
|
+
charOffset = lineEnd + 1;
|
|
58
|
+
const lineSpans = sliceSpans(props.spans, lineStart, lineEnd);
|
|
59
|
+
const y = props.y + i * lh;
|
|
62
60
|
const tspans = lineSpans
|
|
63
61
|
.map((s) => {
|
|
64
|
-
const fill = s.highlight ? highlightColor : bodyColor
|
|
65
|
-
return `<tspan fill="${fill}">${escapeXml(s.text)}</tspan
|
|
62
|
+
const fill = s.highlight ? highlightColor : bodyColor;
|
|
63
|
+
return `<tspan fill="${fill}">${escapeXml(s.text)}</tspan>`;
|
|
66
64
|
})
|
|
67
|
-
.join(
|
|
68
|
-
return `<text x="${props.x}" y="${y}" font-family="${escapeAttr(tokens.typography.sans)}" font-size="${size}">${tspans}</text
|
|
65
|
+
.join("");
|
|
66
|
+
return `<text x="${props.x}" y="${y}" font-family="${escapeAttr(tokens.typography.sans)}" font-size="${size}">${tspans}</text>`;
|
|
69
67
|
})
|
|
70
|
-
.join(
|
|
71
|
-
return { svg: out, lineCount: lines.length }
|
|
68
|
+
.join("");
|
|
69
|
+
return { svg: out, lineCount: lines.length };
|
|
72
70
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { el } from
|
|
2
|
-
import { text } from
|
|
3
|
-
import { tokens } from
|
|
1
|
+
import { el } from "./svg.ts";
|
|
2
|
+
import { text } from "./text.ts";
|
|
3
|
+
import { tokens } from "../tokens/index.ts";
|
|
4
4
|
|
|
5
5
|
export interface StatBarProps {
|
|
6
|
-
x: number
|
|
7
|
-
y: number
|
|
8
|
-
width: number
|
|
9
|
-
label: string
|
|
10
|
-
abbreviation: string
|
|
11
|
-
value: number | null // 0..1023, or null for ranges mode (no value text, no fill)
|
|
12
|
-
color: string
|
|
13
|
-
inverted?: boolean
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
width: number;
|
|
9
|
+
label: string;
|
|
10
|
+
abbreviation: string;
|
|
11
|
+
value: number | null; // 0..1023, or null for ranges mode (no value text, no fill)
|
|
12
|
+
color: string;
|
|
13
|
+
inverted?: boolean;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function statBar({
|
|
@@ -23,7 +23,7 @@ export function statBar({
|
|
|
23
23
|
color,
|
|
24
24
|
inverted,
|
|
25
25
|
}: StatBarProps): string {
|
|
26
|
-
const h = tokens.spacing.statBarHeight
|
|
26
|
+
const h = tokens.spacing.statBarHeight;
|
|
27
27
|
|
|
28
28
|
let labelOut =
|
|
29
29
|
text({
|
|
@@ -42,9 +42,9 @@ export function statBar({
|
|
|
42
42
|
size: tokens.typography.sizes.stat,
|
|
43
43
|
weight: 400,
|
|
44
44
|
color: tokens.colors.text.primary,
|
|
45
|
-
})
|
|
45
|
+
});
|
|
46
46
|
|
|
47
|
-
const track = el(
|
|
47
|
+
const track = el("rect", {
|
|
48
48
|
x,
|
|
49
49
|
y,
|
|
50
50
|
width,
|
|
@@ -52,12 +52,12 @@ export function statBar({
|
|
|
52
52
|
rx: h / 2,
|
|
53
53
|
ry: h / 2,
|
|
54
54
|
fill: tokens.colors.surface.panelBorder,
|
|
55
|
-
})
|
|
55
|
+
});
|
|
56
56
|
|
|
57
57
|
if (value !== null) {
|
|
58
|
-
const clamped = Math.max(0, Math.min(1023, value))
|
|
59
|
-
const displayFraction = inverted ? 1 - clamped / 1023 : clamped / 1023
|
|
60
|
-
const filled = Math.floor(width * displayFraction)
|
|
58
|
+
const clamped = Math.max(0, Math.min(1023, value));
|
|
59
|
+
const displayFraction = inverted ? 1 - clamped / 1023 : clamped / 1023;
|
|
60
|
+
const filled = Math.floor(width * displayFraction);
|
|
61
61
|
|
|
62
62
|
labelOut += text({
|
|
63
63
|
x: x + width,
|
|
@@ -66,10 +66,10 @@ export function statBar({
|
|
|
66
66
|
size: tokens.typography.sizes.statValue,
|
|
67
67
|
weight: 700,
|
|
68
68
|
color,
|
|
69
|
-
anchor:
|
|
70
|
-
})
|
|
69
|
+
anchor: "end",
|
|
70
|
+
});
|
|
71
71
|
|
|
72
|
-
const bar = el(
|
|
72
|
+
const bar = el("rect", {
|
|
73
73
|
x,
|
|
74
74
|
y,
|
|
75
75
|
width: filled,
|
|
@@ -77,9 +77,9 @@ export function statBar({
|
|
|
77
77
|
rx: h / 2,
|
|
78
78
|
ry: h / 2,
|
|
79
79
|
fill: color,
|
|
80
|
-
})
|
|
81
|
-
return labelOut + track + bar
|
|
80
|
+
});
|
|
81
|
+
return labelOut + track + bar;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
return labelOut + track
|
|
84
|
+
return labelOut + track;
|
|
85
85
|
}
|
package/src/primitives/svg.ts
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
export function escapeXml(input: string): string {
|
|
2
2
|
return input
|
|
3
|
-
.replaceAll(
|
|
4
|
-
.replaceAll(
|
|
5
|
-
.replaceAll(
|
|
6
|
-
.replaceAll('"',
|
|
7
|
-
.replaceAll("'",
|
|
3
|
+
.replaceAll("&", "&")
|
|
4
|
+
.replaceAll("<", "<")
|
|
5
|
+
.replaceAll(">", ">")
|
|
6
|
+
.replaceAll('"', """)
|
|
7
|
+
.replaceAll("'", "'");
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export type AttrValue = string | number | null | undefined
|
|
10
|
+
export type AttrValue = string | number | null | undefined;
|
|
11
11
|
|
|
12
12
|
export function attr(attrs: Record<string, AttrValue>): string {
|
|
13
|
-
let out =
|
|
13
|
+
let out = "";
|
|
14
14
|
for (const [k, v] of Object.entries(attrs)) {
|
|
15
|
-
if (v === undefined || v === null) continue
|
|
16
|
-
const value = typeof v ===
|
|
17
|
-
out += ` ${k}="${value}"
|
|
15
|
+
if (v === undefined || v === null) continue;
|
|
16
|
+
const value = typeof v === "number" ? String(v) : escapeXml(v);
|
|
17
|
+
out += ` ${k}="${value}"`;
|
|
18
18
|
}
|
|
19
|
-
return out
|
|
19
|
+
return out;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function el(tag: string, attrs: Record<string, AttrValue>, children?: string): string {
|
|
23
|
-
if (children === undefined) return `<${tag}${attr(attrs)}
|
|
24
|
-
return `<${tag}${attr(attrs)}>${children}</${tag}
|
|
23
|
+
if (children === undefined) return `<${tag}${attr(attrs)}/>`;
|
|
24
|
+
return `<${tag}${attr(attrs)}>${children}</${tag}>`;
|
|
25
25
|
}
|
package/src/primitives/text.ts
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import { el } from
|
|
2
|
-
import { tokens } from
|
|
1
|
+
import { el } from "./svg.ts";
|
|
2
|
+
import { tokens } from "../tokens/index.ts";
|
|
3
3
|
|
|
4
4
|
export interface TextProps {
|
|
5
|
-
x: number
|
|
6
|
-
y: number
|
|
7
|
-
value: string
|
|
8
|
-
size?: number
|
|
9
|
-
weight?: 400 | 600 | 700 | 500
|
|
10
|
-
family?: string
|
|
11
|
-
color?: string
|
|
12
|
-
anchor?:
|
|
13
|
-
letterSpacing?: number
|
|
14
|
-
dominantBaseline?:
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
value: string;
|
|
8
|
+
size?: number;
|
|
9
|
+
weight?: 400 | 600 | 700 | 500;
|
|
10
|
+
family?: string;
|
|
11
|
+
color?: string;
|
|
12
|
+
anchor?: "start" | "middle" | "end";
|
|
13
|
+
letterSpacing?: number;
|
|
14
|
+
dominantBaseline?: "auto" | "middle" | "central" | "hanging" | "text-top" | "text-bottom";
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function text(props: TextProps): string {
|
|
18
18
|
return el(
|
|
19
|
-
|
|
19
|
+
"text",
|
|
20
20
|
{
|
|
21
21
|
x: props.x,
|
|
22
22
|
y: props.y,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
"font-family": props.family ?? tokens.typography.sans,
|
|
24
|
+
"font-size": props.size ?? tokens.typography.sizes.body,
|
|
25
|
+
"font-weight": props.weight ?? 400,
|
|
26
26
|
fill: props.color ?? tokens.colors.text.primary,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
"text-anchor": props.anchor,
|
|
28
|
+
"letter-spacing": props.letterSpacing,
|
|
29
|
+
"dominant-baseline": props.dominantBaseline,
|
|
30
30
|
},
|
|
31
31
|
escapeValue(props.value),
|
|
32
|
-
)
|
|
32
|
+
);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function escapeValue(v: string): string {
|
|
36
36
|
return v
|
|
37
|
-
.replaceAll(
|
|
38
|
-
.replaceAll(
|
|
39
|
-
.replaceAll(
|
|
40
|
-
.replaceAll('"',
|
|
41
|
-
.replaceAll("'",
|
|
37
|
+
.replaceAll("&", "&")
|
|
38
|
+
.replaceAll("<", "<")
|
|
39
|
+
.replaceAll(">", ">")
|
|
40
|
+
.replaceAll('"', """)
|
|
41
|
+
.replaceAll("'", "'");
|
|
42
42
|
}
|
package/src/primitives/wrap.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
export interface WrapProps {
|
|
2
|
-
value: string
|
|
3
|
-
charsPerLine: number
|
|
2
|
+
value: string;
|
|
3
|
+
charsPerLine: number;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
export function wrapText({ value, charsPerLine }: WrapProps): string[] {
|
|
7
|
-
const words = value.split(/\s+/).filter((w) => w.length > 0)
|
|
8
|
-
const lines: string[] = []
|
|
9
|
-
let current =
|
|
7
|
+
const words = value.split(/\s+/).filter((w) => w.length > 0);
|
|
8
|
+
const lines: string[] = [];
|
|
9
|
+
let current = "";
|
|
10
10
|
for (const word of words) {
|
|
11
11
|
if (current.length === 0) {
|
|
12
|
-
current = word
|
|
13
|
-
continue
|
|
12
|
+
current = word;
|
|
13
|
+
continue;
|
|
14
14
|
}
|
|
15
15
|
if (current.length + 1 + word.length <= charsPerLine) {
|
|
16
|
-
current += ` ${word}
|
|
16
|
+
current += ` ${word}`;
|
|
17
17
|
} else {
|
|
18
|
-
lines.push(current)
|
|
19
|
-
current = word
|
|
18
|
+
lines.push(current);
|
|
19
|
+
current = word;
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
-
if (current.length > 0) lines.push(current)
|
|
23
|
-
return lines
|
|
22
|
+
if (current.length > 0) lines.push(current);
|
|
23
|
+
return lines;
|
|
24
24
|
}
|
package/src/render.ts
CHANGED
|
@@ -1,33 +1,29 @@
|
|
|
1
|
-
import { resolveItem, type ResolvedItem } from
|
|
2
|
-
import type { CargoItem } from
|
|
3
|
-
import { decodePayload } from
|
|
4
|
-
import { renderByType } from
|
|
5
|
-
import { UnknownItemError } from
|
|
1
|
+
import { resolveItem, type ResolvedItem } from "@shipload/sdk";
|
|
2
|
+
import type { CargoItem } from "./payload/codec.ts";
|
|
3
|
+
import { decodePayload } from "./payload/codec.ts";
|
|
4
|
+
import { renderByType } from "./templates/index.ts";
|
|
5
|
+
import { UnknownItemError } from "./errors.ts";
|
|
6
6
|
|
|
7
7
|
export interface RenderOptions {
|
|
8
|
-
width?: number
|
|
9
|
-
theme?:
|
|
8
|
+
width?: number;
|
|
9
|
+
theme?: "dark" | "light";
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function renderItem(
|
|
13
|
-
item
|
|
14
|
-
resolved: ResolvedItem,
|
|
15
|
-
_opts?: RenderOptions,
|
|
16
|
-
): string {
|
|
17
|
-
return renderByType(item, resolved)
|
|
12
|
+
export function renderItem(item: CargoItem, resolved: ResolvedItem, _opts?: RenderOptions): string {
|
|
13
|
+
return renderByType(item, resolved);
|
|
18
14
|
}
|
|
19
15
|
|
|
20
16
|
export async function renderFromPayload(
|
|
21
17
|
payload: string,
|
|
22
18
|
opts?: RenderOptions,
|
|
23
19
|
): Promise<{ svg: string; item: ResolvedItem }> {
|
|
24
|
-
const cargoItem = decodePayload(payload)
|
|
25
|
-
let resolved: ResolvedItem
|
|
20
|
+
const cargoItem = decodePayload(payload);
|
|
21
|
+
let resolved: ResolvedItem;
|
|
26
22
|
try {
|
|
27
|
-
resolved = resolveItem(cargoItem.item_id, cargoItem.stats, cargoItem.modules)
|
|
23
|
+
resolved = resolveItem(cargoItem.item_id, cargoItem.stats, cargoItem.modules);
|
|
28
24
|
} catch {
|
|
29
|
-
throw new UnknownItemError(Number(BigInt(cargoItem.item_id.toString())))
|
|
25
|
+
throw new UnknownItemError(Number(BigInt(cargoItem.item_id.toString())));
|
|
30
26
|
}
|
|
31
|
-
const svg = renderItem(cargoItem, resolved, opts)
|
|
32
|
-
return { svg, item: resolved }
|
|
27
|
+
const svg = renderItem(cargoItem, resolved, opts);
|
|
28
|
+
return { svg, item: resolved };
|
|
33
29
|
}
|
package/src/templates/_shared.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { tokens } from
|
|
1
|
+
import { tokens } from "../tokens/index.ts";
|
|
2
2
|
|
|
3
3
|
export function formatMass(n: number): string {
|
|
4
|
-
return n.toLocaleString(
|
|
4
|
+
return n.toLocaleString("en-US");
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export function tierBorder(tier: number): string {
|
|
8
|
-
|
|
9
|
-
return tokens.colors.tier[key] ?? tokens.colors.surface.panelBorder
|
|
8
|
+
return tokens.colors.tier[tier] ?? tokens.colors.surface.panelBorder;
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
export function shortCode(itemId: number): string {
|
|
13
|
-
const str = itemId.toString(10)
|
|
14
|
-
return str.slice(-2).padStart(2,
|
|
12
|
+
const str = itemId.toString(10);
|
|
13
|
+
return str.slice(-2).padStart(2, "0");
|
|
15
14
|
}
|
|
@@ -1,82 +1,84 @@
|
|
|
1
|
-
import type { ResolvedItem, ResourceCategory } from
|
|
2
|
-
import { formatTier, getRecipe, getStatDefinitions, categoryColors } from
|
|
3
|
-
import type { CargoItem } from
|
|
4
|
-
import { panel } from
|
|
5
|
-
import { iconHex } from
|
|
6
|
-
import { text } from
|
|
7
|
-
import { divider } from
|
|
8
|
-
import { statBar } from
|
|
9
|
-
import { quantityBadge } from
|
|
10
|
-
import { tokens } from
|
|
11
|
-
import { shortCode, formatMass, tierBorder } from
|
|
1
|
+
import type { ResolvedItem, ResourceCategory } from "@shipload/sdk";
|
|
2
|
+
import { formatTier, getRecipe, getStatDefinitions, categoryColors } 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
|
export interface RenderComponentOpts {
|
|
14
|
-
mode?:
|
|
14
|
+
mode?: "values" | "ranges";
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
type StatRow = {
|
|
18
|
-
label: string
|
|
19
|
-
abbreviation: string
|
|
20
|
-
value: number | null
|
|
21
|
-
color: string
|
|
22
|
-
inverted?: boolean
|
|
23
|
-
}
|
|
18
|
+
label: string;
|
|
19
|
+
abbreviation: string;
|
|
20
|
+
value: number | null;
|
|
21
|
+
color: string;
|
|
22
|
+
inverted?: boolean;
|
|
23
|
+
};
|
|
24
24
|
|
|
25
25
|
export function renderComponent(
|
|
26
26
|
item: CargoItem,
|
|
27
27
|
resolved: ResolvedItem,
|
|
28
28
|
opts?: RenderComponentOpts,
|
|
29
29
|
): string {
|
|
30
|
-
const mode = opts?.mode ??
|
|
31
|
-
const w = tokens.spacing.panelWidth
|
|
32
|
-
const pad = tokens.spacing.panelPadding
|
|
33
|
-
const innerW = w - pad * 2
|
|
30
|
+
const mode = opts?.mode ?? "values";
|
|
31
|
+
const w = tokens.spacing.panelWidth;
|
|
32
|
+
const pad = tokens.spacing.panelPadding;
|
|
33
|
+
const innerW = w - pad * 2;
|
|
34
34
|
|
|
35
|
-
let rows: StatRow[]
|
|
36
|
-
if (mode ===
|
|
37
|
-
rows = (resolved.stats ?? []).map(s => ({
|
|
35
|
+
let rows: StatRow[];
|
|
36
|
+
if (mode === "values") {
|
|
37
|
+
rows = (resolved.stats ?? []).map((s) => ({
|
|
38
38
|
label: s.label,
|
|
39
39
|
abbreviation: s.abbreviation,
|
|
40
40
|
value: s.value,
|
|
41
41
|
color: s.color,
|
|
42
42
|
inverted: s.inverted,
|
|
43
|
-
}))
|
|
43
|
+
}));
|
|
44
44
|
} else {
|
|
45
|
-
const recipe = getRecipe(resolved.itemId)
|
|
46
|
-
rows = (recipe?.statSlots ?? []).flatMap(slot => {
|
|
47
|
-
const src = slot.sources[0]
|
|
48
|
-
if (!src) return []
|
|
49
|
-
const input = recipe!.inputs[src.inputIndex]
|
|
50
|
-
if (!input || !(
|
|
51
|
-
const category = input.category as ResourceCategory
|
|
52
|
-
const def = getStatDefinitions(category)[src.statIndex]
|
|
53
|
-
if (!def) return []
|
|
54
|
-
return [
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
45
|
+
const recipe = getRecipe(resolved.itemId);
|
|
46
|
+
rows = (recipe?.statSlots ?? []).flatMap((slot) => {
|
|
47
|
+
const src = slot.sources[0];
|
|
48
|
+
if (!src) return [];
|
|
49
|
+
const input = recipe!.inputs[src.inputIndex];
|
|
50
|
+
if (!input || !("category" in input)) return [];
|
|
51
|
+
const category = input.category as ResourceCategory;
|
|
52
|
+
const def = getStatDefinitions(category)[src.statIndex];
|
|
53
|
+
if (!def) return [];
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
label: def.label,
|
|
57
|
+
abbreviation: def.abbreviation,
|
|
58
|
+
value: null,
|
|
59
|
+
color: categoryColors[category],
|
|
60
|
+
inverted: def.inverted,
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
});
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
const headerH = 48
|
|
65
|
-
const metaRowH = 28
|
|
66
|
-
const statsH = rows.length * 26 + 8
|
|
67
|
-
const height = headerH + metaRowH + 14 + statsH + pad
|
|
66
|
+
const headerH = 48;
|
|
67
|
+
const metaRowH = 28;
|
|
68
|
+
const statsH = rows.length * 26 + 8;
|
|
69
|
+
const height = headerH + metaRowH + 14 + statsH + pad;
|
|
68
70
|
|
|
69
|
-
const chrome = panel({ width: w, height, borderColor: tierBorder(resolved.tier) })
|
|
71
|
+
const chrome = panel({ width: w, height, borderColor: tierBorder(resolved.tier) });
|
|
70
72
|
|
|
71
|
-
const quantity = Number(BigInt(item.quantity.toString()))
|
|
72
|
-
const badge = quantityBadge({ x: w - pad, y: pad, quantity })
|
|
73
|
+
const quantity = Number(BigInt(item.quantity.toString()));
|
|
74
|
+
const badge = quantityBadge({ x: w - pad, y: pad, quantity });
|
|
73
75
|
|
|
74
76
|
const icon = iconHex({
|
|
75
77
|
x: pad,
|
|
76
78
|
y: pad + 4,
|
|
77
79
|
color: tokens.colors.accent.component,
|
|
78
80
|
code: shortCode(resolved.itemId),
|
|
79
|
-
})
|
|
81
|
+
});
|
|
80
82
|
|
|
81
83
|
const name = text({
|
|
82
84
|
x: pad + 34,
|
|
@@ -85,41 +87,41 @@ export function renderComponent(
|
|
|
85
87
|
size: tokens.typography.sizes.title,
|
|
86
88
|
weight: 700,
|
|
87
89
|
family: tokens.typography.display,
|
|
88
|
-
})
|
|
90
|
+
});
|
|
89
91
|
|
|
90
92
|
const subtitleText = text({
|
|
91
93
|
x: pad,
|
|
92
94
|
y: pad + headerH + 4,
|
|
93
|
-
value:
|
|
95
|
+
value: "Type",
|
|
94
96
|
size: tokens.typography.sizes.body,
|
|
95
97
|
color: tokens.colors.text.secondary,
|
|
96
|
-
})
|
|
98
|
+
});
|
|
97
99
|
const subtitleValue = text({
|
|
98
100
|
x: w - pad,
|
|
99
101
|
y: pad + headerH + 4,
|
|
100
102
|
value: `COMPONENT · ${formatTier(resolved.tier)}`,
|
|
101
103
|
size: tokens.typography.sizes.body,
|
|
102
104
|
weight: 600,
|
|
103
|
-
anchor:
|
|
104
|
-
})
|
|
105
|
+
anchor: "end",
|
|
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 sepY = pad + headerH + metaRowH + 6
|
|
122
|
-
const sep = divider({ x: pad, y: sepY, width: innerW })
|
|
123
|
+
const sepY = pad + headerH + metaRowH + 6;
|
|
124
|
+
const sep = divider({ x: pad, y: sepY, width: innerW });
|
|
123
125
|
|
|
124
126
|
const statsSvg = rows
|
|
125
127
|
.map((row, i) =>
|
|
@@ -134,9 +136,9 @@ export function renderComponent(
|
|
|
134
136
|
inverted: row.inverted,
|
|
135
137
|
}),
|
|
136
138
|
)
|
|
137
|
-
.join(
|
|
139
|
+
.join("");
|
|
138
140
|
|
|
139
|
-
const inner = `${chrome}${icon}${name}${badge}${subtitleText}${subtitleValue}${massLabel}${massValue}${sep}${statsSvg}
|
|
141
|
+
const inner = `${chrome}${icon}${name}${badge}${subtitleText}${subtitleValue}${massLabel}${massValue}${sep}${statsSvg}`;
|
|
140
142
|
|
|
141
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg
|
|
143
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`;
|
|
142
144
|
}
|