@shipload/item-renderer 0.2.2 → 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.
- package/package.json +47 -49
- package/src/assets/stardust-base64.ts +1 -1
- package/src/errors.ts +14 -14
- package/src/fonts/index.ts +23 -24
- package/src/fonts/load-bun.ts +11 -11
- package/src/index.ts +28 -28
- package/src/links.ts +11 -11
- package/src/meta.ts +25 -25
- package/src/payload/base64url.ts +21 -21
- package/src/payload/codec.ts +17 -17
- package/src/primitives/category-icon.ts +90 -67
- package/src/primitives/compact-row.ts +32 -32
- package/src/primitives/divider.ts +14 -14
- package/src/primitives/icon-hex.ts +36 -34
- package/src/primitives/module-slot.ts +131 -131
- package/src/primitives/panel.ts +18 -18
- package/src/primitives/quantity-badge.ts +32 -32
- package/src/primitives/span-paragraph.ts +55 -57
- package/src/primitives/stat-bar.ts +71 -71
- package/src/primitives/svg.ts +15 -15
- package/src/primitives/text.ts +33 -33
- package/src/primitives/wrap.ts +19 -19
- package/src/render.ts +21 -25
- package/src/templates/_shared.ts +5 -6
- package/src/templates/component.ts +123 -121
- package/src/templates/index.ts +23 -23
- package/src/templates/item-cell.ts +84 -81
- package/src/templates/module.ts +177 -174
- package/src/templates/packed-entity.ts +22 -24
- package/src/templates/resource.ts +134 -134
- package/src/templates/ship-panel.ts +120 -121
- package/src/templates/social-card.ts +28 -26
- package/src/tokens/colors.ts +38 -38
- package/src/tokens/index.ts +5 -5
- package/src/tokens/spacing.ts +8 -8
- package/src/tokens/typography.ts +17 -17
- package/.github/workflows/ci.yml +0 -14
- package/.gitignore +0 -6
- package/Makefile +0 -50
- package/biome.json +0 -18
- package/bun.lock +0 -123
- package/scripts/check-bundle-size.ts +0 -37
- package/scripts/copy-fonts.ts +0 -41
- package/scripts/preview.ts +0 -43
- package/test/__image_snapshots__/.gitkeep +0 -0
- package/test/__image_snapshots__/component-hull-plates.diff.png +0 -0
- package/test/__image_snapshots__/component-hull-plates.png +0 -0
- package/test/__image_snapshots__/module-engine-t1.diff.png +0 -0
- package/test/__image_snapshots__/module-engine-t1.png +0 -0
- package/test/__image_snapshots__/module-storage-t1.diff.png +0 -0
- package/test/__image_snapshots__/module-storage-t1.png +0 -0
- package/test/__image_snapshots__/packed-entity-ship-t1-only-engine.diff.png +0 -0
- package/test/__image_snapshots__/packed-entity-ship-t1-only-engine.png +0 -0
- package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.diff.png +0 -0
- package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.png +0 -0
- package/test/__image_snapshots__/resource-ore-t1.diff.png +0 -0
- package/test/__image_snapshots__/resource-ore-t1.png +0 -0
- package/test/__snapshots__/templates-component.test.ts.snap +0 -5
- package/test/__snapshots__/templates-item-cell.test.ts.snap +0 -9
- package/test/__snapshots__/templates-module.test.ts.snap +0 -17
- package/test/__snapshots__/templates-packed-entity.test.ts.snap +0 -5
- package/test/__snapshots__/templates-resource.test.ts.snap +0 -7
- package/test/base64url.test.ts +0 -33
- package/test/codec.test.ts +0 -43
- package/test/errors.test.ts +0 -24
- package/test/fixtures/cargo-items.ts +0 -122
- package/test/fonts.test.ts +0 -28
- package/test/links-meta.test.ts +0 -43
- package/test/pixel.test.ts +0 -66
- package/test/primitives-category-icon.test.ts +0 -79
- package/test/primitives-compact-row.test.ts +0 -44
- package/test/primitives-domain.test.ts +0 -72
- package/test/primitives-layout.test.ts +0 -56
- package/test/primitives-module-slot.test.ts +0 -88
- package/test/render.test.ts +0 -40
- package/test/sanity.test.ts +0 -6
- package/test/sdk-link.test.ts +0 -19
- package/test/snapshots/.gitkeep +0 -0
- package/test/svg.test.ts +0 -28
- package/test/templates-component.test.ts +0 -36
- package/test/templates-dispatch.test.ts +0 -35
- package/test/templates-item-cell.test.ts +0 -94
- package/test/templates-module.test.ts +0 -63
- package/test/templates-packed-entity.test.ts +0 -47
- package/test/templates-resource.test.ts +0 -71
- package/test/templates-ship-panel.test.ts +0 -87
- package/test/tokens.test.ts +0 -32
- package/tsconfig.json +0 -20
|
@@ -1,87 +1,110 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
1
|
+
import type {CategoryIconShape} from '@shipload/sdk'
|
|
2
|
+
import {el} from './svg.ts'
|
|
3
3
|
|
|
4
4
|
export interface CategoryIconPathOpts {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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(' ')
|
|
46
50
|
}
|
|
47
51
|
|
|
48
|
-
export function categoryIconPath({
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
52
|
+
export function categoryIconPath({
|
|
53
|
+
shape,
|
|
54
|
+
cx,
|
|
55
|
+
cy,
|
|
56
|
+
size,
|
|
57
|
+
color,
|
|
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)}`)
|
|
68
84
|
}
|
|
69
85
|
|
|
70
86
|
export function categoryIconSvg(shape: CategoryIconShape, opts: CategoryIconSvgOpts = {}): string {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
)
|
|
87
110
|
}
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {text} from './text.ts'
|
|
2
|
+
import {tokens} from '../tokens/index.ts'
|
|
3
3
|
|
|
4
4
|
export interface CompactRowProps {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
1
|
+
import {el} from './svg.ts'
|
|
2
|
+
import {tokens} from '../tokens/index.ts'
|
|
3
3
|
|
|
4
4
|
export interface DividerProps {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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,39 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
x: number
|
|
7
|
+
y: number
|
|
8
|
+
color: string
|
|
9
|
+
code: string
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function iconHex({
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
)
|
|
39
41
|
}
|
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
return s
|
|
33
|
+
.replace(/&/g, '&')
|
|
34
|
+
.replace(/</g, '<')
|
|
35
|
+
.replace(/>/g, '>')
|
|
36
|
+
.replace(/"/g, '"')
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function sliceSpans(spans: TextSpan[], start: number, end: number): TextSpan[] {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
}
|