@shipload/item-renderer 0.2.3 → 1.0.0-beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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,108 +1,110 @@
1
- import type { CategoryIconShape } from "@shipload/sdk";
2
- import { el } from "./svg.ts";
1
+ import type {CategoryIconShape} from '@shipload/sdk'
2
+ import {el} from './svg.ts'
3
3
 
4
4
  export interface CategoryIconPathOpts {
5
- shape: CategoryIconShape;
6
- cx: number;
7
- cy: number;
8
- size: number;
9
- color: string;
10
- strokeWidth?: number;
5
+ shape: CategoryIconShape
6
+ cx: number
7
+ cy: number
8
+ size: number
9
+ color: string
10
+ strokeWidth?: number
11
11
  }
12
12
 
13
13
  export interface CategoryIconSvgOpts {
14
- size?: number;
15
- color?: string;
16
- strokeWidth?: number;
14
+ size?: number
15
+ color?: string
16
+ strokeWidth?: number
17
17
  }
18
18
 
19
19
  function hexPoints(cx: number, cy: number, r: number): string {
20
- const pts: string[] = [];
21
- for (let i = 0; i < 6; i++) {
22
- const angle = (Math.PI / 3) * i - Math.PI / 2;
23
- pts.push(`${(cx + r * Math.cos(angle)).toFixed(2)},${(cy + r * Math.sin(angle)).toFixed(2)}`);
24
- }
25
- return pts.join(" ");
20
+ const pts: string[] = []
21
+ for (let i = 0; i < 6; i++) {
22
+ const angle = (Math.PI / 3) * i - Math.PI / 2
23
+ pts.push(
24
+ `${(cx + r * Math.cos(angle)).toFixed(2)},${(cy + r * Math.sin(angle)).toFixed(2)}`
25
+ )
26
+ }
27
+ return pts.join(' ')
26
28
  }
27
29
 
28
30
  function diamondPoints(cx: number, cy: number, r: number): string {
29
- return [
30
- `${cx.toFixed(2)},${(cy - r).toFixed(2)}`,
31
- `${(cx + r).toFixed(2)},${cy.toFixed(2)}`,
32
- `${cx.toFixed(2)},${(cy + r).toFixed(2)}`,
33
- `${(cx - r).toFixed(2)},${cy.toFixed(2)}`,
34
- ].join(" ");
31
+ return [
32
+ `${cx.toFixed(2)},${(cy - r).toFixed(2)}`,
33
+ `${(cx + r).toFixed(2)},${cy.toFixed(2)}`,
34
+ `${cx.toFixed(2)},${(cy + r).toFixed(2)}`,
35
+ `${(cx - r).toFixed(2)},${cy.toFixed(2)}`,
36
+ ].join(' ')
35
37
  }
36
38
 
37
39
  function starPoints(cx: number, cy: number, r: number): string {
38
- const inner = r * 0.45;
39
- const pts: string[] = [];
40
- for (let i = 0; i < 10; i++) {
41
- const angle = (Math.PI / 5) * i - Math.PI / 2;
42
- const radius = i % 2 === 0 ? r : inner;
43
- pts.push(
44
- `${(cx + radius * Math.cos(angle)).toFixed(2)},${(cy + radius * Math.sin(angle)).toFixed(2)}`,
45
- );
46
- }
47
- return pts.join(" ");
40
+ const inner = r * 0.45
41
+ const pts: string[] = []
42
+ for (let i = 0; i < 10; i++) {
43
+ const angle = (Math.PI / 5) * i - Math.PI / 2
44
+ const radius = i % 2 === 0 ? r : inner
45
+ pts.push(
46
+ `${(cx + radius * Math.cos(angle)).toFixed(2)},${(cy + radius * Math.sin(angle)).toFixed(2)}`
47
+ )
48
+ }
49
+ return pts.join(' ')
48
50
  }
49
51
 
50
52
  export function categoryIconPath({
51
- shape,
52
- cx,
53
- cy,
54
- size,
55
- color,
56
- strokeWidth,
57
- }: CategoryIconPathOpts): string {
58
- const r = size / 2;
59
- const stroked = strokeWidth && strokeWidth > 0;
60
- const shapeAttrs = stroked
61
- ? {
62
- fill: "none",
63
- stroke: color,
64
- "stroke-width": strokeWidth,
65
- "stroke-linejoin": "round" as const,
66
- }
67
- : { fill: color };
68
- switch (shape) {
69
- case "hex":
70
- return el("polygon", { points: hexPoints(cx, cy, r), ...shapeAttrs });
71
- case "diamond":
72
- return el("polygon", { points: diamondPoints(cx, cy, r), ...shapeAttrs });
73
- case "star":
74
- return el("polygon", { points: starPoints(cx, cy, r), ...shapeAttrs });
75
- case "circle":
76
- return el("circle", { cx, cy, r, ...shapeAttrs });
77
- case "square":
78
- return el("rect", { x: cx - r, y: cy - r, width: size, height: size, ...shapeAttrs });
79
- }
80
- const _exhaustive: never = shape;
81
- throw new Error(`Unknown CategoryIconShape: ${String(_exhaustive)}`);
82
- }
83
-
84
- export function categoryIconSvg(shape: CategoryIconShape, opts: CategoryIconSvgOpts = {}): string {
85
- const size = opts.size ?? 16;
86
- const color = opts.color ?? "#ffffff";
87
- const cx = size / 2;
88
- const cy = size / 2;
89
- const iconSize = size * 0.85;
90
- const inner = categoryIconPath({
91
53
  shape,
92
54
  cx,
93
55
  cy,
94
- size: iconSize,
56
+ size,
95
57
  color,
96
- strokeWidth: opts.strokeWidth,
97
- });
98
- return el(
99
- "svg",
100
- {
101
- xmlns: "http://www.w3.org/2000/svg",
102
- width: size,
103
- height: size,
104
- viewBox: `0 0 ${size} ${size}`,
105
- },
106
- inner,
107
- );
58
+ strokeWidth,
59
+ }: CategoryIconPathOpts): string {
60
+ const r = size / 2
61
+ const stroked = strokeWidth && strokeWidth > 0
62
+ const shapeAttrs = stroked
63
+ ? {
64
+ fill: 'none',
65
+ stroke: color,
66
+ 'stroke-width': strokeWidth,
67
+ 'stroke-linejoin': 'round' as const,
68
+ }
69
+ : {fill: color}
70
+ switch (shape) {
71
+ case 'hex':
72
+ return el('polygon', {points: hexPoints(cx, cy, r), ...shapeAttrs})
73
+ case 'diamond':
74
+ return el('polygon', {points: diamondPoints(cx, cy, r), ...shapeAttrs})
75
+ case 'star':
76
+ return el('polygon', {points: starPoints(cx, cy, r), ...shapeAttrs})
77
+ case 'circle':
78
+ return el('circle', {cx, cy, r, ...shapeAttrs})
79
+ case 'square':
80
+ return el('rect', {x: cx - r, y: cy - r, width: size, height: size, ...shapeAttrs})
81
+ }
82
+ const _exhaustive: never = shape
83
+ throw new Error(`Unknown CategoryIconShape: ${String(_exhaustive)}`)
84
+ }
85
+
86
+ export function categoryIconSvg(shape: CategoryIconShape, opts: CategoryIconSvgOpts = {}): string {
87
+ const size = opts.size ?? 16
88
+ const color = opts.color ?? '#ffffff'
89
+ const cx = size / 2
90
+ const cy = size / 2
91
+ const iconSize = size * 0.85
92
+ const inner = categoryIconPath({
93
+ shape,
94
+ cx,
95
+ cy,
96
+ size: iconSize,
97
+ color,
98
+ strokeWidth: opts.strokeWidth,
99
+ })
100
+ return el(
101
+ 'svg',
102
+ {
103
+ xmlns: 'http://www.w3.org/2000/svg',
104
+ width: size,
105
+ height: size,
106
+ viewBox: `0 0 ${size} ${size}`,
107
+ },
108
+ inner
109
+ )
108
110
  }
@@ -1,38 +1,38 @@
1
- import { text } from "./text.ts";
2
- import { tokens } from "../tokens/index.ts";
1
+ import {text} from './text.ts'
2
+ import {tokens} from '../tokens/index.ts'
3
3
 
4
4
  export interface CompactRowProps {
5
- x: number;
6
- y: number;
7
- width: number;
8
- label: string;
9
- value: string;
10
- labelColor?: string;
11
- valueColor?: string;
5
+ x: number
6
+ y: number
7
+ width: number
8
+ label: string
9
+ value: string
10
+ labelColor?: string
11
+ valueColor?: string
12
12
  }
13
13
 
14
14
  export function compactRow(p: CompactRowProps): string {
15
- const labelColor = p.labelColor ?? tokens.colors.text.secondary;
16
- const valueColor = p.valueColor ?? tokens.colors.text.primary;
17
- return (
18
- text({
19
- x: p.x,
20
- y: p.y,
21
- value: p.label,
22
- size: 11,
23
- weight: 500,
24
- family: tokens.typography.sans,
25
- color: labelColor,
26
- }) +
27
- text({
28
- x: p.x + p.width,
29
- y: p.y,
30
- value: p.value,
31
- size: 11,
32
- weight: 700,
33
- family: tokens.typography.sans,
34
- color: valueColor,
35
- anchor: "end",
36
- })
37
- );
15
+ const labelColor = p.labelColor ?? tokens.colors.text.secondary
16
+ const valueColor = p.valueColor ?? tokens.colors.text.primary
17
+ return (
18
+ text({
19
+ x: p.x,
20
+ y: p.y,
21
+ value: p.label,
22
+ size: 11,
23
+ weight: 500,
24
+ family: tokens.typography.sans,
25
+ color: labelColor,
26
+ }) +
27
+ text({
28
+ x: p.x + p.width,
29
+ y: p.y,
30
+ value: p.value,
31
+ size: 11,
32
+ weight: 700,
33
+ family: tokens.typography.sans,
34
+ color: valueColor,
35
+ anchor: 'end',
36
+ })
37
+ )
38
38
  }
@@ -1,20 +1,20 @@
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 DividerProps {
5
- x: number;
6
- y: number;
7
- width: number;
8
- color?: string;
5
+ x: number
6
+ y: number
7
+ width: number
8
+ color?: string
9
9
  }
10
10
 
11
11
  export function divider(props: DividerProps): string {
12
- return el("line", {
13
- x1: props.x,
14
- x2: props.x + props.width,
15
- y1: props.y,
16
- y2: props.y,
17
- stroke: props.color ?? tokens.colors.surface.panelBorder,
18
- "stroke-width": 1,
19
- });
12
+ return el('line', {
13
+ x1: props.x,
14
+ x2: props.x + props.width,
15
+ y1: props.y,
16
+ y2: props.y,
17
+ stroke: props.color ?? tokens.colors.surface.panelBorder,
18
+ 'stroke-width': 1,
19
+ })
20
20
  }
@@ -1,41 +1,41 @@
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 IconHexProps {
6
- x: number;
7
- y: number;
8
- color: string;
9
- code: string;
6
+ x: number
7
+ y: number
8
+ color: string
9
+ code: string
10
10
  }
11
11
 
12
- export function iconHex({ x, y, color, code }: IconHexProps): string {
13
- const size = tokens.spacing.iconHexSize;
14
- const h = size;
15
- const w = size * 1.1547; // flat-top hex aspect
16
- const cx = x + w / 2;
17
- const cy = y + h / 2;
18
- const points = [
19
- [cx - w / 2, cy],
20
- [cx - w / 4, cy - h / 2],
21
- [cx + w / 4, cy - h / 2],
22
- [cx + w / 2, cy],
23
- [cx + w / 4, cy + h / 2],
24
- [cx - w / 4, cy + h / 2],
25
- ]
26
- .map(([px, py]) => `${px?.toFixed(1)},${py?.toFixed(1)}`)
27
- .join(" ");
28
- return (
29
- el("polygon", { points, fill: "none", stroke: color, "stroke-width": 1.5 }) +
30
- text({
31
- x: cx,
32
- y: cy + 3,
33
- value: code,
34
- size: 9,
35
- weight: 700,
36
- family: tokens.typography.mono,
37
- color,
38
- anchor: "middle",
39
- })
40
- );
12
+ export function iconHex({x, y, color, code}: IconHexProps): string {
13
+ const size = tokens.spacing.iconHexSize
14
+ const h = size
15
+ const w = size * 1.1547 // flat-top hex aspect
16
+ const cx = x + w / 2
17
+ const cy = y + h / 2
18
+ const points = [
19
+ [cx - w / 2, cy],
20
+ [cx - w / 4, cy - h / 2],
21
+ [cx + w / 4, cy - h / 2],
22
+ [cx + w / 2, cy],
23
+ [cx + w / 4, cy + h / 2],
24
+ [cx - w / 4, cy + h / 2],
25
+ ]
26
+ .map(([px, py]) => `${px?.toFixed(1)},${py?.toFixed(1)}`)
27
+ .join(' ')
28
+ return (
29
+ el('polygon', {points, fill: 'none', stroke: color, 'stroke-width': 1.5}) +
30
+ text({
31
+ x: cx,
32
+ y: cy + 3,
33
+ value: code,
34
+ size: 9,
35
+ weight: 700,
36
+ family: tokens.typography.mono,
37
+ color,
38
+ anchor: 'middle',
39
+ })
40
+ )
41
41
  }
@@ -1,147 +1,147 @@
1
- import { el } from "./svg.ts";
2
- import { text } from "./text.ts";
3
- import { wrapText } from "./wrap.ts";
4
- import { tokens } from "../tokens/index.ts";
5
- import type { TextSpan } from "@shipload/sdk";
1
+ import {el} from './svg.ts'
2
+ import {text} from './text.ts'
3
+ import {wrapText} from './wrap.ts'
4
+ import {tokens} from '../tokens/index.ts'
5
+ import type {TextSpan} from '@shipload/sdk'
6
6
 
7
7
  export interface ModuleSlotProps {
8
- x: number;
9
- y: number;
10
- width: number;
11
- installed: boolean;
12
- capability?: string;
13
- description?: string | TextSpan[];
14
- accentColor?: string;
8
+ x: number
9
+ y: number
10
+ width: number
11
+ installed: boolean
12
+ capability?: string
13
+ description?: string | TextSpan[]
14
+ accentColor?: string
15
15
  }
16
16
 
17
17
  const EMPTY_DIAMOND = (cx: number, cy: number, color: string) =>
18
- el("polygon", {
19
- points: `${cx},${cy - 5} ${cx + 5},${cy} ${cx},${cy + 5} ${cx - 5},${cy}`,
20
- fill: "none",
21
- stroke: color,
22
- "stroke-width": 1,
23
- });
18
+ el('polygon', {
19
+ points: `${cx},${cy - 5} ${cx + 5},${cy} ${cx},${cy + 5} ${cx - 5},${cy}`,
20
+ fill: 'none',
21
+ stroke: color,
22
+ 'stroke-width': 1,
23
+ })
24
24
 
25
25
  const FILLED_DIAMOND = (cx: number, cy: number, color: string) =>
26
- el("polygon", {
27
- points: `${cx},${cy - 5} ${cx + 5},${cy} ${cx},${cy + 5} ${cx - 5},${cy}`,
28
- fill: color,
29
- });
26
+ el('polygon', {
27
+ points: `${cx},${cy - 5} ${cx + 5},${cy} ${cx},${cy + 5} ${cx - 5},${cy}`,
28
+ fill: color,
29
+ })
30
30
 
31
31
  function escapeXml(s: string): string {
32
- return s
33
- .replace(/&/g, "&amp;")
34
- .replace(/</g, "&lt;")
35
- .replace(/>/g, "&gt;")
36
- .replace(/"/g, "&quot;");
32
+ return s
33
+ .replace(/&/g, '&amp;')
34
+ .replace(/</g, '&lt;')
35
+ .replace(/>/g, '&gt;')
36
+ .replace(/"/g, '&quot;')
37
37
  }
38
38
 
39
39
  function sliceSpans(spans: TextSpan[], start: number, end: number): TextSpan[] {
40
- const out: TextSpan[] = [];
41
- let cursor = 0;
42
- for (const span of spans) {
43
- const spanStart = cursor;
44
- const spanEnd = cursor + span.text.length;
45
- cursor = spanEnd;
46
- if (spanEnd <= start || spanStart >= end) continue;
47
- const sliceStart = Math.max(0, start - spanStart);
48
- const sliceEnd = span.text.length - Math.max(0, spanEnd - end);
49
- const txt = span.text.slice(sliceStart, sliceEnd);
50
- if (txt.length === 0) continue;
51
- out.push(span.highlight ? { text: txt, highlight: true } : { text: txt });
52
- }
53
- return out;
40
+ const out: TextSpan[] = []
41
+ let cursor = 0
42
+ for (const span of spans) {
43
+ const spanStart = cursor
44
+ const spanEnd = cursor + span.text.length
45
+ cursor = spanEnd
46
+ if (spanEnd <= start || spanStart >= end) continue
47
+ const sliceStart = Math.max(0, start - spanStart)
48
+ const sliceEnd = span.text.length - Math.max(0, spanEnd - end)
49
+ const txt = span.text.slice(sliceStart, sliceEnd)
50
+ if (txt.length === 0) continue
51
+ out.push(span.highlight ? {text: txt, highlight: true} : {text: txt})
52
+ }
53
+ return out
54
54
  }
55
55
 
56
56
  export function moduleSlot(props: ModuleSlotProps): string {
57
- const iconX = props.x + 6;
58
- const iconY = props.y + 6;
59
- const textX = props.x + 20;
60
-
61
- if (!props.installed) {
62
- return (
63
- EMPTY_DIAMOND(iconX, iconY, tokens.colors.surface.panelBorderBright) +
64
- text({
65
- x: textX,
66
- y: iconY + 3,
67
- value: "Empty module",
68
- size: tokens.typography.sizes.body,
69
- color: tokens.colors.text.muted,
70
- })
71
- );
72
- }
73
-
74
- const accent = props.accentColor ?? tokens.colors.text.accent;
75
- const label = `${props.capability ?? "Module"}: `;
76
-
77
- const desc = props.description;
78
- const isEmpty =
79
- !desc ||
80
- (typeof desc === "string" && desc.length === 0) ||
81
- (Array.isArray(desc) && desc.length === 0);
82
-
83
- if (isEmpty) {
84
- return (
85
- FILLED_DIAMOND(iconX, iconY, accent) +
86
- text({
87
- x: textX,
88
- y: iconY + 3,
89
- value: label.trimEnd(),
90
- size: tokens.typography.sizes.body,
91
- weight: 600,
92
- color: tokens.colors.text.primary,
93
- })
94
- );
95
- }
96
-
97
- const descSpans: TextSpan[] = typeof desc === "string" ? [{ text: desc }] : desc;
98
- const descPlain = descSpans.map((s) => s.text).join("");
99
- const combined = label + descPlain;
100
- const lines = wrapText({ value: combined, charsPerLine: 36 });
101
-
102
- const highlightColor = tokens.colors.text.accent;
103
- const bodyColor = tokens.colors.text.secondary;
104
- const labelColor = tokens.colors.text.primary;
105
- const size = tokens.typography.sizes.body;
106
- const fontFamily = escapeXml(tokens.typography.sans);
107
- const labelEnd = label.length;
108
-
109
- let offset = 0;
110
- const textBlocks = lines
111
- .map((line, i) => {
112
- const lineStart = combined.indexOf(line, offset);
113
- const lineEnd = lineStart + line.length;
114
- offset = lineEnd;
115
- const y = iconY + 3 + i * 14;
116
-
117
- const tspans: string[] = [];
118
-
119
- if (lineStart < labelEnd) {
120
- const labelSliceEnd = Math.min(lineEnd, labelEnd);
121
- const labelText = combined.slice(lineStart, labelSliceEnd);
122
- if (labelText.length > 0) {
123
- tspans.push(
124
- `<tspan font-weight="600" fill="${labelColor}">${escapeXml(labelText)}</tspan>`,
125
- );
126
- }
127
- if (lineEnd > labelEnd) {
128
- const descSlice = sliceSpans(descSpans, 0, lineEnd - labelEnd);
129
- for (const s of descSlice) {
130
- const fill = s.highlight ? highlightColor : bodyColor;
131
- tspans.push(`<tspan fill="${fill}">${escapeXml(s.text)}</tspan>`);
132
- }
133
- }
134
- } else {
135
- const descSlice = sliceSpans(descSpans, lineStart - labelEnd, lineEnd - labelEnd);
136
- for (const s of descSlice) {
137
- const fill = s.highlight ? highlightColor : bodyColor;
138
- tspans.push(`<tspan fill="${fill}">${escapeXml(s.text)}</tspan>`);
139
- }
140
- }
141
-
142
- return `<text x="${textX}" y="${y}" font-family="${fontFamily}" font-size="${size}">${tspans.join("")}</text>`;
143
- })
144
- .join("");
145
-
146
- return FILLED_DIAMOND(iconX, iconY, accent) + textBlocks;
57
+ const iconX = props.x + 6
58
+ const iconY = props.y + 6
59
+ const textX = props.x + 20
60
+
61
+ if (!props.installed) {
62
+ return (
63
+ EMPTY_DIAMOND(iconX, iconY, tokens.colors.surface.panelBorderBright) +
64
+ text({
65
+ x: textX,
66
+ y: iconY + 3,
67
+ value: 'Empty module',
68
+ size: tokens.typography.sizes.body,
69
+ color: tokens.colors.text.muted,
70
+ })
71
+ )
72
+ }
73
+
74
+ const accent = props.accentColor ?? tokens.colors.text.accent
75
+ const label = `${props.capability ?? 'Module'}: `
76
+
77
+ const desc = props.description
78
+ const isEmpty =
79
+ !desc ||
80
+ (typeof desc === 'string' && desc.length === 0) ||
81
+ (Array.isArray(desc) && desc.length === 0)
82
+
83
+ if (isEmpty) {
84
+ return (
85
+ FILLED_DIAMOND(iconX, iconY, accent) +
86
+ text({
87
+ x: textX,
88
+ y: iconY + 3,
89
+ value: label.trimEnd(),
90
+ size: tokens.typography.sizes.body,
91
+ weight: 600,
92
+ color: tokens.colors.text.primary,
93
+ })
94
+ )
95
+ }
96
+
97
+ const descSpans: TextSpan[] = typeof desc === 'string' ? [{text: desc}] : desc
98
+ const descPlain = descSpans.map((s) => s.text).join('')
99
+ const combined = label + descPlain
100
+ const lines = wrapText({value: combined, charsPerLine: 36})
101
+
102
+ const highlightColor = tokens.colors.text.accent
103
+ const bodyColor = tokens.colors.text.secondary
104
+ const labelColor = tokens.colors.text.primary
105
+ const size = tokens.typography.sizes.body
106
+ const fontFamily = escapeXml(tokens.typography.sans)
107
+ const labelEnd = label.length
108
+
109
+ let offset = 0
110
+ const textBlocks = lines
111
+ .map((line, i) => {
112
+ const lineStart = combined.indexOf(line, offset)
113
+ const lineEnd = lineStart + line.length
114
+ offset = lineEnd
115
+ const y = iconY + 3 + i * 14
116
+
117
+ const tspans: string[] = []
118
+
119
+ if (lineStart < labelEnd) {
120
+ const labelSliceEnd = Math.min(lineEnd, labelEnd)
121
+ const labelText = combined.slice(lineStart, labelSliceEnd)
122
+ if (labelText.length > 0) {
123
+ tspans.push(
124
+ `<tspan font-weight="600" fill="${labelColor}">${escapeXml(labelText)}</tspan>`
125
+ )
126
+ }
127
+ if (lineEnd > labelEnd) {
128
+ const descSlice = sliceSpans(descSpans, 0, lineEnd - labelEnd)
129
+ for (const s of descSlice) {
130
+ const fill = s.highlight ? highlightColor : bodyColor
131
+ tspans.push(`<tspan fill="${fill}">${escapeXml(s.text)}</tspan>`)
132
+ }
133
+ }
134
+ } else {
135
+ const descSlice = sliceSpans(descSpans, lineStart - labelEnd, lineEnd - labelEnd)
136
+ for (const s of descSlice) {
137
+ const fill = s.highlight ? highlightColor : bodyColor
138
+ tspans.push(`<tspan fill="${fill}">${escapeXml(s.text)}</tspan>`)
139
+ }
140
+ }
141
+
142
+ return `<text x="${textX}" y="${y}" font-family="${fontFamily}" font-size="${size}">${tspans.join('')}</text>`
143
+ })
144
+ .join('')
145
+
146
+ return FILLED_DIAMOND(iconX, iconY, accent) + textBlocks
147
147
  }