@shipload/item-renderer 0.2.1 → 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 +8 -4
- 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 +6 -7
- package/src/templates/component.ts +68 -63
- package/src/templates/index.ts +17 -17
- package/src/templates/item-cell.ts +48 -41
- package/src/templates/module.ts +84 -83
- package/src/templates/packed-entity.ts +12 -14
- package/src/templates/resource.ts +63 -65
- package/src/templates/ship-panel.ts +67 -72
- 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__/component-hull-plates.diff.png +0 -0
- package/test/__image_snapshots__/module-engine-t1.diff.png +0 -0
- package/test/__image_snapshots__/module-storage-t1.diff.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-two-modules.diff.png +0 -0
- package/test/__image_snapshots__/resource-ore-t1.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 +69 -65
- package/test/tokens.test.ts +28 -26
package/src/payload/codec.ts
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import { Serializer } from
|
|
2
|
-
import { ServerContract } from
|
|
3
|
-
import { InvalidPayloadError } from
|
|
4
|
-
import { base64UrlToBytes, bytesToBase64Url } from
|
|
1
|
+
import { Serializer } from "@wharfkit/antelope";
|
|
2
|
+
import { ServerContract } from "@shipload/sdk";
|
|
3
|
+
import { InvalidPayloadError } from "../errors.ts";
|
|
4
|
+
import { base64UrlToBytes, bytesToBase64Url } from "./base64url.ts";
|
|
5
5
|
|
|
6
|
-
export type CargoItem = InstanceType<typeof ServerContract.Types.cargo_item
|
|
7
|
-
export type CargoItemLike = Parameters<typeof ServerContract.Types.cargo_item.from>[0]
|
|
6
|
+
export type CargoItem = InstanceType<typeof ServerContract.Types.cargo_item>;
|
|
7
|
+
export type CargoItemLike = Parameters<typeof ServerContract.Types.cargo_item.from>[0];
|
|
8
8
|
|
|
9
9
|
export function encodePayload(input: CargoItemLike): string {
|
|
10
|
-
const item = ServerContract.Types.cargo_item.from(input)
|
|
11
|
-
const bytes = Serializer.encode({ object: item }).array
|
|
12
|
-
return bytesToBase64Url(bytes)
|
|
10
|
+
const item = ServerContract.Types.cargo_item.from(input);
|
|
11
|
+
const bytes = Serializer.encode({ object: item }).array;
|
|
12
|
+
return bytesToBase64Url(bytes);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function decodePayload(input: string): CargoItem {
|
|
16
|
-
if (input.length === 0) throw new InvalidPayloadError(
|
|
17
|
-
const bytes = base64UrlToBytes(input)
|
|
16
|
+
if (input.length === 0) throw new InvalidPayloadError("empty payload");
|
|
17
|
+
const bytes = base64UrlToBytes(input);
|
|
18
18
|
try {
|
|
19
19
|
return Serializer.decode({
|
|
20
20
|
data: bytes,
|
|
21
21
|
type: ServerContract.Types.cargo_item,
|
|
22
|
-
}) as CargoItem
|
|
22
|
+
}) as CargoItem;
|
|
23
23
|
} catch (e) {
|
|
24
|
-
throw new InvalidPayloadError(`cargo_item decode failed: ${(e as Error).message}`)
|
|
24
|
+
throw new InvalidPayloadError(`cargo_item decode failed: ${(e as Error).message}`);
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import type { CategoryIconShape } from
|
|
2
|
-
import { el } from
|
|
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[] = []
|
|
20
|
+
const pts: string[] = [];
|
|
21
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)}`)
|
|
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
24
|
}
|
|
25
|
-
return pts.join(
|
|
25
|
+
return pts.join(" ");
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function diamondPoints(cx: number, cy: number, r: number): string {
|
|
@@ -31,57 +31,78 @@ function diamondPoints(cx: number, cy: number, r: number): string {
|
|
|
31
31
|
`${(cx + r).toFixed(2)},${cy.toFixed(2)}`,
|
|
32
32
|
`${cx.toFixed(2)},${(cy + r).toFixed(2)}`,
|
|
33
33
|
`${(cx - r).toFixed(2)},${cy.toFixed(2)}`,
|
|
34
|
-
].join(
|
|
34
|
+
].join(" ");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function starPoints(cx: number, cy: number, r: number): string {
|
|
38
|
-
const inner = r * 0.45
|
|
39
|
-
const pts: string[] = []
|
|
38
|
+
const inner = r * 0.45;
|
|
39
|
+
const pts: string[] = [];
|
|
40
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(
|
|
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
|
+
);
|
|
44
46
|
}
|
|
45
|
-
return pts.join(
|
|
47
|
+
return pts.join(" ");
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
export function categoryIconPath({
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
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;
|
|
51
60
|
const shapeAttrs = stroked
|
|
52
|
-
? {
|
|
53
|
-
|
|
61
|
+
? {
|
|
62
|
+
fill: "none",
|
|
63
|
+
stroke: color,
|
|
64
|
+
"stroke-width": strokeWidth,
|
|
65
|
+
"stroke-linejoin": "round" as const,
|
|
66
|
+
}
|
|
67
|
+
: { fill: color };
|
|
54
68
|
switch (shape) {
|
|
55
|
-
case
|
|
56
|
-
return el(
|
|
57
|
-
case
|
|
58
|
-
return el(
|
|
59
|
-
case
|
|
60
|
-
return el(
|
|
61
|
-
case
|
|
62
|
-
return el(
|
|
63
|
-
case
|
|
64
|
-
return el(
|
|
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 });
|
|
65
79
|
}
|
|
66
|
-
const _exhaustive: never = shape
|
|
67
|
-
throw new Error(`Unknown CategoryIconShape: ${String(_exhaustive)}`)
|
|
80
|
+
const _exhaustive: never = shape;
|
|
81
|
+
throw new Error(`Unknown CategoryIconShape: ${String(_exhaustive)}`);
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
export function categoryIconSvg(shape: CategoryIconShape, opts: CategoryIconSvgOpts = {}): string {
|
|
71
|
-
const size = opts.size ?? 16
|
|
72
|
-
const color = opts.color ??
|
|
73
|
-
const cx = size / 2
|
|
74
|
-
const cy = size / 2
|
|
75
|
-
const iconSize = size * 0.85
|
|
76
|
-
const inner = categoryIconPath({
|
|
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
|
+
shape,
|
|
92
|
+
cx,
|
|
93
|
+
cy,
|
|
94
|
+
size: iconSize,
|
|
95
|
+
color,
|
|
96
|
+
strokeWidth: opts.strokeWidth,
|
|
97
|
+
});
|
|
77
98
|
return el(
|
|
78
|
-
|
|
99
|
+
"svg",
|
|
79
100
|
{
|
|
80
|
-
xmlns:
|
|
101
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
81
102
|
width: size,
|
|
82
103
|
height: size,
|
|
83
104
|
viewBox: `0 0 ${size} ${size}`,
|
|
84
105
|
},
|
|
85
106
|
inner,
|
|
86
|
-
)
|
|
107
|
+
);
|
|
87
108
|
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { text } from
|
|
2
|
-
import { tokens } from
|
|
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
|
|
15
|
+
const labelColor = p.labelColor ?? tokens.colors.text.secondary;
|
|
16
|
+
const valueColor = p.valueColor ?? tokens.colors.text.primary;
|
|
17
17
|
return (
|
|
18
18
|
text({
|
|
19
19
|
x: p.x,
|
|
@@ -32,7 +32,7 @@ export function compactRow(p: CompactRowProps): string {
|
|
|
32
32
|
weight: 700,
|
|
33
33
|
family: tokens.typography.sans,
|
|
34
34
|
color: valueColor,
|
|
35
|
-
anchor:
|
|
35
|
+
anchor: "end",
|
|
36
36
|
})
|
|
37
|
-
)
|
|
37
|
+
);
|
|
38
38
|
}
|
|
@@ -1,20 +1,20 @@
|
|
|
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 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(
|
|
12
|
+
return el("line", {
|
|
13
13
|
x1: props.x,
|
|
14
14
|
x2: props.x + props.width,
|
|
15
15
|
y1: props.y,
|
|
16
16
|
y2: props.y,
|
|
17
17
|
stroke: props.color ?? tokens.colors.surface.panelBorder,
|
|
18
|
-
|
|
19
|
-
})
|
|
18
|
+
"stroke-width": 1,
|
|
19
|
+
});
|
|
20
20
|
}
|
|
@@ -1,20 +1,20 @@
|
|
|
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 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
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
|
|
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
18
|
const points = [
|
|
19
19
|
[cx - w / 2, cy],
|
|
20
20
|
[cx - w / 4, cy - h / 2],
|
|
@@ -22,9 +22,11 @@ export function iconHex({ x, y, color, code }: IconHexProps): string {
|
|
|
22
22
|
[cx + w / 2, cy],
|
|
23
23
|
[cx + w / 4, cy + h / 2],
|
|
24
24
|
[cx - w / 4, cy + h / 2],
|
|
25
|
-
]
|
|
25
|
+
]
|
|
26
|
+
.map(([px, py]) => `${px?.toFixed(1)},${py?.toFixed(1)}`)
|
|
27
|
+
.join(" ");
|
|
26
28
|
return (
|
|
27
|
-
el(
|
|
29
|
+
el("polygon", { points, fill: "none", stroke: color, "stroke-width": 1.5 }) +
|
|
28
30
|
text({
|
|
29
31
|
x: cx,
|
|
30
32
|
y: cy + 3,
|
|
@@ -33,7 +35,7 @@ export function iconHex({ x, y, color, code }: IconHexProps): string {
|
|
|
33
35
|
weight: 700,
|
|
34
36
|
family: tokens.typography.mono,
|
|
35
37
|
color,
|
|
36
|
-
anchor:
|
|
38
|
+
anchor: "middle",
|
|
37
39
|
})
|
|
38
|
-
)
|
|
40
|
+
);
|
|
39
41
|
}
|
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
import { el } from
|
|
2
|
-
import { text } from
|
|
3
|
-
import { wrapText } from
|
|
4
|
-
import { tokens } from
|
|
5
|
-
import type { TextSpan } from
|
|
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(
|
|
18
|
+
el("polygon", {
|
|
19
19
|
points: `${cx},${cy - 5} ${cx + 5},${cy} ${cx},${cy + 5} ${cx - 5},${cy}`,
|
|
20
|
-
fill:
|
|
20
|
+
fill: "none",
|
|
21
21
|
stroke: color,
|
|
22
|
-
|
|
23
|
-
})
|
|
22
|
+
"stroke-width": 1,
|
|
23
|
+
});
|
|
24
24
|
|
|
25
25
|
const FILLED_DIAMOND = (cx: number, cy: number, color: string) =>
|
|
26
|
-
el(
|
|
26
|
+
el("polygon", {
|
|
27
27
|
points: `${cx},${cy - 5} ${cx + 5},${cy} ${cx},${cy + 5} ${cx - 5},${cy}`,
|
|
28
28
|
fill: color,
|
|
29
|
-
})
|
|
29
|
+
});
|
|
30
30
|
|
|
31
31
|
function escapeXml(s: string): string {
|
|
32
32
|
return s
|
|
33
|
-
.replace(/&/g,
|
|
34
|
-
.replace(/</g,
|
|
35
|
-
.replace(/>/g,
|
|
36
|
-
.replace(/"/g,
|
|
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
|
-
const out: TextSpan[] = []
|
|
41
|
-
let cursor = 0
|
|
40
|
+
const out: TextSpan[] = [];
|
|
41
|
+
let cursor = 0;
|
|
42
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 })
|
|
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
52
|
}
|
|
53
|
-
return out
|
|
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
|
|
57
|
+
const iconX = props.x + 6;
|
|
58
|
+
const iconY = props.y + 6;
|
|
59
|
+
const textX = props.x + 20;
|
|
60
60
|
|
|
61
61
|
if (!props.installed) {
|
|
62
62
|
return (
|
|
@@ -64,21 +64,21 @@ export function moduleSlot(props: ModuleSlotProps): string {
|
|
|
64
64
|
text({
|
|
65
65
|
x: textX,
|
|
66
66
|
y: iconY + 3,
|
|
67
|
-
value:
|
|
67
|
+
value: "Empty module",
|
|
68
68
|
size: tokens.typography.sizes.body,
|
|
69
69
|
color: tokens.colors.text.muted,
|
|
70
70
|
})
|
|
71
|
-
)
|
|
71
|
+
);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
const accent = props.accentColor ?? tokens.colors.text.accent
|
|
75
|
-
const label = `${props.capability ??
|
|
74
|
+
const accent = props.accentColor ?? tokens.colors.text.accent;
|
|
75
|
+
const label = `${props.capability ?? "Module"}: `;
|
|
76
76
|
|
|
77
|
-
const desc = props.description
|
|
77
|
+
const desc = props.description;
|
|
78
78
|
const isEmpty =
|
|
79
79
|
!desc ||
|
|
80
|
-
(typeof desc ===
|
|
81
|
-
(Array.isArray(desc) && desc.length === 0)
|
|
80
|
+
(typeof desc === "string" && desc.length === 0) ||
|
|
81
|
+
(Array.isArray(desc) && desc.length === 0);
|
|
82
82
|
|
|
83
83
|
if (isEmpty) {
|
|
84
84
|
return (
|
|
@@ -91,57 +91,57 @@ export function moduleSlot(props: ModuleSlotProps): string {
|
|
|
91
91
|
weight: 600,
|
|
92
92
|
color: tokens.colors.text.primary,
|
|
93
93
|
})
|
|
94
|
-
)
|
|
94
|
+
);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
const descSpans: TextSpan[] = typeof desc ===
|
|
98
|
-
const descPlain = descSpans.map((s) => s.text).join(
|
|
99
|
-
const combined = label + descPlain
|
|
100
|
-
const lines = wrapText({ value: combined, charsPerLine: 36 })
|
|
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
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
|
|
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
108
|
|
|
109
|
-
let offset = 0
|
|
109
|
+
let offset = 0;
|
|
110
110
|
const textBlocks = lines
|
|
111
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
|
|
112
|
+
const lineStart = combined.indexOf(line, offset);
|
|
113
|
+
const lineEnd = lineStart + line.length;
|
|
114
|
+
offset = lineEnd;
|
|
115
|
+
const y = iconY + 3 + i * 14;
|
|
116
116
|
|
|
117
|
-
const tspans: string[] = []
|
|
117
|
+
const tspans: string[] = [];
|
|
118
118
|
|
|
119
119
|
if (lineStart < labelEnd) {
|
|
120
|
-
const labelSliceEnd = Math.min(lineEnd, labelEnd)
|
|
121
|
-
const labelText = combined.slice(lineStart, labelSliceEnd)
|
|
120
|
+
const labelSliceEnd = Math.min(lineEnd, labelEnd);
|
|
121
|
+
const labelText = combined.slice(lineStart, labelSliceEnd);
|
|
122
122
|
if (labelText.length > 0) {
|
|
123
123
|
tspans.push(
|
|
124
124
|
`<tspan font-weight="600" fill="${labelColor}">${escapeXml(labelText)}</tspan>`,
|
|
125
|
-
)
|
|
125
|
+
);
|
|
126
126
|
}
|
|
127
127
|
if (lineEnd > labelEnd) {
|
|
128
|
-
const descSlice = sliceSpans(descSpans, 0, lineEnd - labelEnd)
|
|
128
|
+
const descSlice = sliceSpans(descSpans, 0, lineEnd - labelEnd);
|
|
129
129
|
for (const s of descSlice) {
|
|
130
|
-
const fill = s.highlight ? highlightColor : bodyColor
|
|
131
|
-
tspans.push(`<tspan fill="${fill}">${escapeXml(s.text)}</tspan>`)
|
|
130
|
+
const fill = s.highlight ? highlightColor : bodyColor;
|
|
131
|
+
tspans.push(`<tspan fill="${fill}">${escapeXml(s.text)}</tspan>`);
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
} else {
|
|
135
|
-
const descSlice = sliceSpans(descSpans, lineStart - labelEnd, lineEnd - labelEnd)
|
|
135
|
+
const descSlice = sliceSpans(descSpans, lineStart - labelEnd, lineEnd - labelEnd);
|
|
136
136
|
for (const s of descSlice) {
|
|
137
|
-
const fill = s.highlight ? highlightColor : bodyColor
|
|
138
|
-
tspans.push(`<tspan fill="${fill}">${escapeXml(s.text)}</tspan>`)
|
|
137
|
+
const fill = s.highlight ? highlightColor : bodyColor;
|
|
138
|
+
tspans.push(`<tspan fill="${fill}">${escapeXml(s.text)}</tspan>`);
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
return `<text x="${textX}" y="${y}" font-family="${fontFamily}" font-size="${size}">${tspans.join(
|
|
142
|
+
return `<text x="${textX}" y="${y}" font-family="${fontFamily}" font-size="${size}">${tspans.join("")}</text>`;
|
|
143
143
|
})
|
|
144
|
-
.join(
|
|
144
|
+
.join("");
|
|
145
145
|
|
|
146
|
-
return FILLED_DIAMOND(iconX, iconY, accent) + textBlocks
|
|
146
|
+
return FILLED_DIAMOND(iconX, iconY, accent) + textBlocks;
|
|
147
147
|
}
|
package/src/primitives/panel.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
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 PanelProps {
|
|
5
|
-
width: number
|
|
6
|
-
height: number
|
|
7
|
-
borderColor?: string
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
borderColor?: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function panel(props: PanelProps): string {
|
|
11
|
-
const { width, height, borderColor } = props
|
|
12
|
-
const r = tokens.spacing.cornerRadius
|
|
13
|
-
return el(
|
|
11
|
+
const { width, height, borderColor } = props;
|
|
12
|
+
const r = tokens.spacing.cornerRadius;
|
|
13
|
+
return el("rect", {
|
|
14
14
|
x: 0.5,
|
|
15
15
|
y: 0.5,
|
|
16
16
|
width: width - 1,
|
|
@@ -19,6 +19,6 @@ export function panel(props: PanelProps): string {
|
|
|
19
19
|
ry: r,
|
|
20
20
|
fill: tokens.colors.surface.panel,
|
|
21
21
|
stroke: borderColor ?? tokens.colors.surface.panelBorder,
|
|
22
|
-
|
|
23
|
-
})
|
|
22
|
+
"stroke-width": 1,
|
|
23
|
+
});
|
|
24
24
|
}
|
|
@@ -1,20 +1,20 @@
|
|
|
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 QuantityBadgeProps {
|
|
6
|
-
x: number
|
|
7
|
-
y: number
|
|
8
|
-
quantity: number
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
quantity: number;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function quantityBadge({ x, y, quantity }: QuantityBadgeProps): string {
|
|
12
|
-
if (quantity <= 1) return
|
|
13
|
-
const label = `×${quantity}
|
|
14
|
-
const w = label.length * 7 + 12
|
|
15
|
-
const h = tokens.spacing.quantityBadgeHeight
|
|
12
|
+
if (quantity <= 1) return "";
|
|
13
|
+
const label = `×${quantity}`;
|
|
14
|
+
const w = label.length * 7 + 12;
|
|
15
|
+
const h = tokens.spacing.quantityBadgeHeight;
|
|
16
16
|
return (
|
|
17
|
-
el(
|
|
17
|
+
el("rect", {
|
|
18
18
|
x: x - w,
|
|
19
19
|
y,
|
|
20
20
|
width: w,
|
|
@@ -31,7 +31,7 @@ export function quantityBadge({ x, y, quantity }: QuantityBadgeProps): string {
|
|
|
31
31
|
weight: 700,
|
|
32
32
|
family: tokens.typography.mono,
|
|
33
33
|
color: tokens.colors.surface.background,
|
|
34
|
-
anchor:
|
|
34
|
+
anchor: "middle",
|
|
35
35
|
})
|
|
36
|
-
)
|
|
36
|
+
);
|
|
37
37
|
}
|