@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.
Files changed (67) hide show
  1. package/.claude/settings.local.json +6 -0
  2. package/bun.lock +2 -2
  3. package/package.json +2 -2
  4. package/scripts/check-bundle-size.ts +21 -21
  5. package/scripts/copy-fonts.ts +19 -19
  6. package/scripts/preview.ts +13 -15
  7. package/src/assets/stardust-base64.ts +1 -1
  8. package/src/errors.ts +8 -8
  9. package/src/fonts/index.ts +25 -26
  10. package/src/fonts/load-bun.ts +9 -9
  11. package/src/index.ts +21 -21
  12. package/src/links.ts +11 -11
  13. package/src/meta.ts +16 -16
  14. package/src/payload/base64url.ts +16 -16
  15. package/src/payload/codec.ts +13 -13
  16. package/src/primitives/category-icon.ts +69 -48
  17. package/src/primitives/compact-row.ts +13 -13
  18. package/src/primitives/divider.ts +9 -9
  19. package/src/primitives/icon-hex.ts +18 -16
  20. package/src/primitives/module-slot.ts +73 -73
  21. package/src/primitives/panel.ts +10 -10
  22. package/src/primitives/quantity-badge.ts +13 -13
  23. package/src/primitives/span-paragraph.ts +48 -50
  24. package/src/primitives/stat-bar.ts +24 -24
  25. package/src/primitives/svg.ts +13 -13
  26. package/src/primitives/text.ts +25 -25
  27. package/src/primitives/wrap.ts +12 -12
  28. package/src/render.ts +15 -19
  29. package/src/templates/_shared.ts +5 -6
  30. package/src/templates/component.ts +67 -65
  31. package/src/templates/index.ts +17 -17
  32. package/src/templates/item-cell.ts +48 -45
  33. package/src/templates/module.ts +83 -81
  34. package/src/templates/packed-entity.ts +12 -14
  35. package/src/templates/resource.ts +63 -65
  36. package/src/templates/ship-panel.ts +66 -71
  37. package/src/templates/social-card.ts +27 -25
  38. package/src/tokens/colors.ts +29 -29
  39. package/src/tokens/index.ts +6 -6
  40. package/src/tokens/spacing.ts +1 -1
  41. package/src/tokens/typography.ts +1 -1
  42. package/test/__image_snapshots__/module-storage-t1.diff.png +0 -0
  43. package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.diff.png +0 -0
  44. package/test/base64url.test.ts +22 -22
  45. package/test/codec.test.ts +26 -35
  46. package/test/errors.test.ts +21 -21
  47. package/test/fixtures/cargo-items.ts +43 -43
  48. package/test/fonts.test.ts +23 -23
  49. package/test/links-meta.test.ts +37 -37
  50. package/test/pixel.test.ts +44 -41
  51. package/test/primitives-category-icon.test.ts +74 -67
  52. package/test/primitives-compact-row.test.ts +29 -29
  53. package/test/primitives-domain.test.ts +61 -50
  54. package/test/primitives-layout.test.ts +47 -47
  55. package/test/primitives-module-slot.test.ts +58 -58
  56. package/test/render.test.ts +38 -35
  57. package/test/sanity.test.ts +5 -5
  58. package/test/sdk-link.test.ts +13 -13
  59. package/test/svg.test.ts +24 -22
  60. package/test/templates-component.test.ts +32 -32
  61. package/test/templates-dispatch.test.ts +29 -29
  62. package/test/templates-item-cell.test.ts +79 -79
  63. package/test/templates-module.test.ts +52 -52
  64. package/test/templates-packed-entity.test.ts +42 -42
  65. package/test/templates-resource.test.ts +61 -61
  66. package/test/templates-ship-panel.test.ts +65 -61
  67. package/test/tokens.test.ts +28 -26
@@ -1,72 +1,70 @@
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'
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, '&lt;')
21
- .replace(/>/g, '&gt;')
22
- .replace(/"/g, '&quot;')
19
+ .replace(/&/g, "&amp;")
20
+ .replace(/</g, "&lt;")
21
+ .replace(/>/g, "&gt;")
22
+ .replace(/"/g, "&quot;");
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: SpanParagraphProps,
44
- ): { svg: string; lineCount: number } {
45
- const chars = props.charsPerLine ?? 36
46
- const lh = props.lineHeight ?? 14
47
- const bodyColor = props.bodyColor ?? tokens.colors.text.secondary
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 './svg.ts'
2
- import { text } from './text.ts'
3
- import { tokens } from '../tokens/index.ts'
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('rect', {
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: 'end',
70
- })
69
+ anchor: "end",
70
+ });
71
71
 
72
- const bar = el('rect', {
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
  }
@@ -1,25 +1,25 @@
1
1
  export function escapeXml(input: string): string {
2
2
  return input
3
- .replaceAll('&', '&amp;')
4
- .replaceAll('<', '&lt;')
5
- .replaceAll('>', '&gt;')
6
- .replaceAll('"', '&quot;')
7
- .replaceAll("'", '&apos;')
3
+ .replaceAll("&", "&amp;")
4
+ .replaceAll("<", "&lt;")
5
+ .replaceAll(">", "&gt;")
6
+ .replaceAll('"', "&quot;")
7
+ .replaceAll("'", "&apos;");
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 === 'number' ? String(v) : escapeXml(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
  }
@@ -1,42 +1,42 @@
1
- import { el } from './svg.ts'
2
- import { tokens } from '../tokens/index.ts'
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?: 'start' | 'middle' | 'end'
13
- letterSpacing?: number
14
- dominantBaseline?: 'auto' | 'middle' | 'central' | 'hanging' | 'text-top' | 'text-bottom'
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
- 'text',
19
+ "text",
20
20
  {
21
21
  x: props.x,
22
22
  y: props.y,
23
- 'font-family': props.family ?? tokens.typography.sans,
24
- 'font-size': props.size ?? tokens.typography.sizes.body,
25
- 'font-weight': props.weight ?? 400,
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
- 'text-anchor': props.anchor,
28
- 'letter-spacing': props.letterSpacing,
29
- 'dominant-baseline': props.dominantBaseline,
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('&', '&amp;')
38
- .replaceAll('<', '&lt;')
39
- .replaceAll('>', '&gt;')
40
- .replaceAll('"', '&quot;')
41
- .replaceAll("'", '&apos;')
37
+ .replaceAll("&", "&amp;")
38
+ .replaceAll("<", "&lt;")
39
+ .replaceAll(">", "&gt;")
40
+ .replaceAll('"', "&quot;")
41
+ .replaceAll("'", "&apos;");
42
42
  }
@@ -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 '@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'
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?: 'dark' | 'light'
8
+ width?: number;
9
+ theme?: "dark" | "light";
10
10
  }
11
11
 
12
- export function renderItem(
13
- item: CargoItem,
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
  }
@@ -1,15 +1,14 @@
1
- import { tokens } from '../tokens/index.ts'
1
+ import { tokens } from "../tokens/index.ts";
2
2
 
3
3
  export function formatMass(n: number): string {
4
- return n.toLocaleString('en-US')
4
+ return n.toLocaleString("en-US");
5
5
  }
6
6
 
7
7
  export function tierBorder(tier: number): string {
8
- const key = `t${tier}` as keyof typeof tokens.colors.tier
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, '0')
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 '@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'
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?: 'values' | 'ranges'
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 ?? 'values'
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 === 'values') {
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 || !('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
- label: def.label,
56
- abbreviation: def.abbreviation,
57
- value: null,
58
- color: categoryColors[category],
59
- inverted: def.inverted,
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: 'Type',
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: 'end',
104
- })
105
+ anchor: "end",
106
+ });
105
107
  const massLabel = text({
106
108
  x: pad,
107
109
  y: pad + headerH + metaRowH - 8,
108
- value: 'Mass',
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: 'end',
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
  }