@shipload/item-renderer 0.1.0
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/.github/workflows/ci.yml +14 -0
- package/.gitignore +6 -0
- package/Makefile +50 -0
- package/biome.json +18 -0
- package/bun.lock +123 -0
- package/package.json +51 -0
- package/scripts/check-bundle-size.ts +37 -0
- package/scripts/copy-fonts.ts +41 -0
- package/scripts/preview.ts +43 -0
- package/src/errors.ts +22 -0
- package/src/fonts/index.ts +36 -0
- package/src/fonts/inter-400.woff2 +0 -0
- package/src/fonts/inter-600.woff2 +0 -0
- package/src/fonts/jetbrains-500.woff2 +0 -0
- package/src/fonts/load-bun.ts +16 -0
- package/src/fonts/orbitron-700.woff2 +0 -0
- package/src/index.ts +46 -0
- package/src/links.ts +19 -0
- package/src/meta.ts +42 -0
- package/src/payload/base64url.ts +27 -0
- package/src/payload/codec.ts +26 -0
- package/src/primitives/category-icon.ts +87 -0
- package/src/primitives/compact-row.ts +38 -0
- package/src/primitives/divider.ts +20 -0
- package/src/primitives/icon-hex.ts +39 -0
- package/src/primitives/module-slot.ts +147 -0
- package/src/primitives/panel.ts +24 -0
- package/src/primitives/quantity-badge.ts +37 -0
- package/src/primitives/span-paragraph.ts +72 -0
- package/src/primitives/stat-bar.ts +85 -0
- package/src/primitives/svg.ts +25 -0
- package/src/primitives/text.ts +42 -0
- package/src/primitives/wrap.ts +24 -0
- package/src/render.ts +33 -0
- package/src/templates/_shared.ts +15 -0
- package/src/templates/component.ts +139 -0
- package/src/templates/index.ts +30 -0
- package/src/templates/item-cell.ts +96 -0
- package/src/templates/module.ts +190 -0
- package/src/templates/packed-entity.ts +30 -0
- package/src/templates/resource.ts +151 -0
- package/src/templates/ship-panel.ts +145 -0
- package/src/tokens/colors.ts +45 -0
- package/src/tokens/index.ts +7 -0
- package/src/tokens/spacing.ts +10 -0
- package/src/tokens/typography.ts +19 -0
- package/test/__image_snapshots__/.gitkeep +0 -0
- package/test/__image_snapshots__/component-hull-plates.png +0 -0
- package/test/__image_snapshots__/module-engine-t1.png +0 -0
- package/test/__image_snapshots__/module-storage-t1.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-iron.diff.png +0 -0
- package/test/__image_snapshots__/resource-iron.png +0 -0
- package/test/__snapshots__/templates-component.test.ts.snap +5 -0
- package/test/__snapshots__/templates-item-cell.test.ts.snap +9 -0
- package/test/__snapshots__/templates-module.test.ts.snap +17 -0
- package/test/__snapshots__/templates-packed-entity.test.ts.snap +5 -0
- package/test/__snapshots__/templates-resource.test.ts.snap +7 -0
- package/test/base64url.test.ts +33 -0
- package/test/codec.test.ts +43 -0
- package/test/errors.test.ts +24 -0
- package/test/fixtures/cargo-items.ts +122 -0
- package/test/fonts.test.ts +28 -0
- package/test/links-meta.test.ts +34 -0
- package/test/pixel.test.ts +66 -0
- package/test/primitives-category-icon.test.ts +79 -0
- package/test/primitives-compact-row.test.ts +44 -0
- package/test/primitives-domain.test.ts +72 -0
- package/test/primitives-layout.test.ts +56 -0
- package/test/primitives-module-slot.test.ts +88 -0
- package/test/render.test.ts +40 -0
- package/test/sanity.test.ts +6 -0
- package/test/sdk-link.test.ts +19 -0
- package/test/snapshots/.gitkeep +0 -0
- package/test/svg.test.ts +28 -0
- package/test/templates-component.test.ts +36 -0
- package/test/templates-dispatch.test.ts +35 -0
- package/test/templates-item-cell.test.ts +94 -0
- package/test/templates-module.test.ts +63 -0
- package/test/templates-packed-entity.test.ts +47 -0
- package/test/templates-resource.test.ts +71 -0
- package/test/templates-ship-panel.test.ts +87 -0
- package/test/tokens.test.ts +32 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on:
|
|
3
|
+
push: { branches: [main] }
|
|
4
|
+
pull_request:
|
|
5
|
+
jobs:
|
|
6
|
+
test:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
steps:
|
|
9
|
+
- uses: actions/checkout@v4
|
|
10
|
+
- uses: oven-sh/setup-bun@v1
|
|
11
|
+
with: { bun-version: latest }
|
|
12
|
+
- run: bun install
|
|
13
|
+
- run: bun test
|
|
14
|
+
- run: bun run scripts/check-bundle-size.ts
|
package/.gitignore
ADDED
package/Makefile
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
SHELL := /usr/bin/env bash
|
|
2
|
+
BIN := ./node_modules/.bin
|
|
3
|
+
|
|
4
|
+
.PHONY: dev
|
|
5
|
+
dev: node_modules
|
|
6
|
+
bun run scripts/preview.ts
|
|
7
|
+
|
|
8
|
+
.PHONY: test
|
|
9
|
+
test: node_modules
|
|
10
|
+
bun test
|
|
11
|
+
|
|
12
|
+
.PHONY: test-pixel
|
|
13
|
+
test-pixel: node_modules
|
|
14
|
+
bun test test/pixel.test.ts
|
|
15
|
+
|
|
16
|
+
.PHONY: test-update
|
|
17
|
+
test-update: node_modules
|
|
18
|
+
UPDATE_IMAGE_SNAPSHOTS=1 bun test --update-snapshots
|
|
19
|
+
|
|
20
|
+
.PHONY: typecheck
|
|
21
|
+
typecheck: node_modules
|
|
22
|
+
$(BIN)/tsc --noEmit
|
|
23
|
+
|
|
24
|
+
.PHONY: lint
|
|
25
|
+
lint: node_modules
|
|
26
|
+
$(BIN)/biome check src test scripts
|
|
27
|
+
|
|
28
|
+
.PHONY: format
|
|
29
|
+
format: node_modules
|
|
30
|
+
$(BIN)/biome format --write src test scripts
|
|
31
|
+
|
|
32
|
+
.PHONY: bundle-check
|
|
33
|
+
bundle-check: node_modules
|
|
34
|
+
bun run scripts/check-bundle-size.ts
|
|
35
|
+
|
|
36
|
+
.PHONY: fonts
|
|
37
|
+
fonts: node_modules
|
|
38
|
+
bun run scripts/copy-fonts.ts
|
|
39
|
+
|
|
40
|
+
.PHONY: check
|
|
41
|
+
check: typecheck lint test bundle-check
|
|
42
|
+
|
|
43
|
+
.PHONY: clean
|
|
44
|
+
clean:
|
|
45
|
+
rm -rf node_modules bun.lock
|
|
46
|
+
|
|
47
|
+
node_modules: package.json
|
|
48
|
+
bun install
|
|
49
|
+
bun link @shipload/sdk
|
|
50
|
+
@touch $@
|
package/biome.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/1.8.0/schema.json",
|
|
3
|
+
"organizeImports": { "enabled": true },
|
|
4
|
+
"linter": {
|
|
5
|
+
"enabled": true,
|
|
6
|
+
"rules": {
|
|
7
|
+
"recommended": true,
|
|
8
|
+
"suspicious": { "noExplicitAny": "error" },
|
|
9
|
+
"style": { "useImportType": "error" }
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"formatter": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"indentStyle": "space",
|
|
15
|
+
"indentWidth": 2,
|
|
16
|
+
"lineWidth": 100
|
|
17
|
+
}
|
|
18
|
+
}
|
package/bun.lock
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "@shipload/item-renderer",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@shipload/sdk": "^2.0.0-rc13",
|
|
9
|
+
"@wharfkit/antelope": "^1.0.0",
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@biomejs/biome": "^1.8.0",
|
|
13
|
+
"@fontsource/inter": "^5.2.8",
|
|
14
|
+
"@fontsource/jetbrains-mono": "^5.2.8",
|
|
15
|
+
"@fontsource/orbitron": "^5.2.8",
|
|
16
|
+
"@resvg/resvg-js": "^2.6.0",
|
|
17
|
+
"@types/bun": "latest",
|
|
18
|
+
"pixelmatch": "^6.0.0",
|
|
19
|
+
"pngjs": "^7.0.0",
|
|
20
|
+
"typescript": "^5.4.0",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
"packages": {
|
|
25
|
+
"@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="],
|
|
26
|
+
|
|
27
|
+
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="],
|
|
28
|
+
|
|
29
|
+
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="],
|
|
30
|
+
|
|
31
|
+
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="],
|
|
32
|
+
|
|
33
|
+
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="],
|
|
34
|
+
|
|
35
|
+
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="],
|
|
36
|
+
|
|
37
|
+
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="],
|
|
38
|
+
|
|
39
|
+
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="],
|
|
40
|
+
|
|
41
|
+
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="],
|
|
42
|
+
|
|
43
|
+
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
|
44
|
+
|
|
45
|
+
"@fontsource/jetbrains-mono": ["@fontsource/jetbrains-mono@5.2.8", "", {}, "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ=="],
|
|
46
|
+
|
|
47
|
+
"@fontsource/orbitron": ["@fontsource/orbitron@5.2.8", "", {}, "sha512-ruzrDl5vnqNykk5DZWY0Ezj4aeFZSbCnwJTc/98ojNJHSsHhlhT2r7rwQrA5sptmF8JtB8TQTAvlfRvcV28RPw=="],
|
|
48
|
+
|
|
49
|
+
"@resvg/resvg-js": ["@resvg/resvg-js@2.6.2", "", { "optionalDependencies": { "@resvg/resvg-js-android-arm-eabi": "2.6.2", "@resvg/resvg-js-android-arm64": "2.6.2", "@resvg/resvg-js-darwin-arm64": "2.6.2", "@resvg/resvg-js-darwin-x64": "2.6.2", "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", "@resvg/resvg-js-linux-arm64-musl": "2.6.2", "@resvg/resvg-js-linux-x64-gnu": "2.6.2", "@resvg/resvg-js-linux-x64-musl": "2.6.2", "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", "@resvg/resvg-js-win32-x64-msvc": "2.6.2" } }, "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q=="],
|
|
50
|
+
|
|
51
|
+
"@resvg/resvg-js-android-arm-eabi": ["@resvg/resvg-js-android-arm-eabi@2.6.2", "", { "os": "android", "cpu": "arm" }, "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA=="],
|
|
52
|
+
|
|
53
|
+
"@resvg/resvg-js-android-arm64": ["@resvg/resvg-js-android-arm64@2.6.2", "", { "os": "android", "cpu": "arm64" }, "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ=="],
|
|
54
|
+
|
|
55
|
+
"@resvg/resvg-js-darwin-arm64": ["@resvg/resvg-js-darwin-arm64@2.6.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A=="],
|
|
56
|
+
|
|
57
|
+
"@resvg/resvg-js-darwin-x64": ["@resvg/resvg-js-darwin-x64@2.6.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw=="],
|
|
58
|
+
|
|
59
|
+
"@resvg/resvg-js-linux-arm-gnueabihf": ["@resvg/resvg-js-linux-arm-gnueabihf@2.6.2", "", { "os": "linux", "cpu": "arm" }, "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw=="],
|
|
60
|
+
|
|
61
|
+
"@resvg/resvg-js-linux-arm64-gnu": ["@resvg/resvg-js-linux-arm64-gnu@2.6.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg=="],
|
|
62
|
+
|
|
63
|
+
"@resvg/resvg-js-linux-arm64-musl": ["@resvg/resvg-js-linux-arm64-musl@2.6.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg=="],
|
|
64
|
+
|
|
65
|
+
"@resvg/resvg-js-linux-x64-gnu": ["@resvg/resvg-js-linux-x64-gnu@2.6.2", "", { "os": "linux", "cpu": "x64" }, "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw=="],
|
|
66
|
+
|
|
67
|
+
"@resvg/resvg-js-linux-x64-musl": ["@resvg/resvg-js-linux-x64-musl@2.6.2", "", { "os": "linux", "cpu": "x64" }, "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ=="],
|
|
68
|
+
|
|
69
|
+
"@resvg/resvg-js-win32-arm64-msvc": ["@resvg/resvg-js-win32-arm64-msvc@2.6.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ=="],
|
|
70
|
+
|
|
71
|
+
"@resvg/resvg-js-win32-ia32-msvc": ["@resvg/resvg-js-win32-ia32-msvc@2.6.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w=="],
|
|
72
|
+
|
|
73
|
+
"@resvg/resvg-js-win32-x64-msvc": ["@resvg/resvg-js-win32-x64-msvc@2.6.2", "", { "os": "win32", "cpu": "x64" }, "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ=="],
|
|
74
|
+
|
|
75
|
+
"@shipload/sdk": ["@shipload/sdk@2.0.0-rc9", "", { "dependencies": { "@wharfkit/antelope": "^1.2.0", "@wharfkit/contract": "^1.2.1", "@wharfkit/session": "^1.3.1", "tslib": "^2.1.0" } }, "sha512-7Knz/QiJqtD5DfA/gC7eqG/e809RSBAqkU7CzKI0NWmmWGWkZbnfPxbqem2ZFQsj9K1BxOfcynAeA3jeBQfuFA=="],
|
|
76
|
+
|
|
77
|
+
"@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
|
|
78
|
+
|
|
79
|
+
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
|
|
80
|
+
|
|
81
|
+
"@wharfkit/abicache": ["@wharfkit/abicache@1.2.2", "", { "dependencies": { "@wharfkit/antelope": "^1.0.2", "@wharfkit/signing-request": "^3.1.0", "pako": "^2.0.4", "tslib": "^2.1.0" } }, "sha512-yOsYz2qQpQy7Nb8XZj62pZqp8YnmWDqFlrenYksBb9jl+1aWIpFhWd+14VEez4tUAezRH4UWW+w1SX5vhmUY9A=="],
|
|
82
|
+
|
|
83
|
+
"@wharfkit/antelope": ["@wharfkit/antelope@1.2.0", "", { "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "elliptic": "^6.5.4", "hash.js": "^1.0.0", "pako": "^2.1.0", "tslib": "^2.0.3" } }, "sha512-9q0nvM8yUtjKTQlukKZODAhUN2S2/cfSlIYdh2mPnaOCSH8KOLJ2gYCPuQVvH1FE9AKu312lM5TzhkRccf1VjQ=="],
|
|
84
|
+
|
|
85
|
+
"@wharfkit/common": ["@wharfkit/common@1.5.0", "", { "dependencies": { "tslib": "^2.1.0" }, "peerDependencies": { "@wharfkit/antelope": "^1.0.0" } }, "sha512-eqXkOy+vshcEzK8kED+EsoTPJjlBKHYglgV9CBnZQgIlGrWIRXWH4YaXH3W7EbI/nCRJCaNqxm5fC+pgpFcp8g=="],
|
|
86
|
+
|
|
87
|
+
"@wharfkit/contract": ["@wharfkit/contract@1.2.1", "", { "dependencies": { "@wharfkit/abicache": "^1.2.0", "@wharfkit/antelope": "^1.0.4", "@wharfkit/signing-request": "^3.1.0", "tslib": "^2.1.0" } }, "sha512-3UhCtDYCyapfM2nRTrslcbvko864d4MOpxRAz7TR/ZUbRAgZsxhYLFLEv1v23/SU+vsFzAHNBmvzkLEG0OLaHQ=="],
|
|
88
|
+
|
|
89
|
+
"@wharfkit/session": ["@wharfkit/session@1.6.1", "", { "dependencies": { "@wharfkit/abicache": "^1.2.1", "@wharfkit/antelope": "^1.0.11", "@wharfkit/common": "^1.2.0", "@wharfkit/signing-request": "^3.1.0", "pako": "^2.0.4", "tslib": "^2.1.0" } }, "sha512-k6ntDGOe8bvD/Ps0erTPTFMdYVFrw5cRvPcEwxytlmRRcNV/M8xWcpCYWdmGDxa8QYqynf/hAkbVh1PSwRGl5A=="],
|
|
90
|
+
|
|
91
|
+
"@wharfkit/signing-request": ["@wharfkit/signing-request@3.4.0", "", { "dependencies": { "@wharfkit/antelope": "^1.1.1", "tslib": "^2.0.3" } }, "sha512-WstXfmR9i5pKaYXDUwNFNCgBIvN6u5IRGWSfj5O3XzthbtJUmRoJNtjGMaNnUqZ1MMx5YY4/JpY3b2e6LbpXLw=="],
|
|
92
|
+
|
|
93
|
+
"bn.js": ["bn.js@4.12.3", "", {}, "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g=="],
|
|
94
|
+
|
|
95
|
+
"brorand": ["brorand@1.1.0", "", {}, "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="],
|
|
96
|
+
|
|
97
|
+
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
|
98
|
+
|
|
99
|
+
"elliptic": ["elliptic@6.6.1", "", { "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.1", "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g=="],
|
|
100
|
+
|
|
101
|
+
"hash.js": ["hash.js@1.1.7", "", { "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" } }, "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA=="],
|
|
102
|
+
|
|
103
|
+
"hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg=="],
|
|
104
|
+
|
|
105
|
+
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
|
106
|
+
|
|
107
|
+
"minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="],
|
|
108
|
+
|
|
109
|
+
"minimalistic-crypto-utils": ["minimalistic-crypto-utils@1.0.1", "", {}, "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="],
|
|
110
|
+
|
|
111
|
+
"pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="],
|
|
112
|
+
|
|
113
|
+
"pixelmatch": ["pixelmatch@6.0.0", "", { "dependencies": { "pngjs": "^7.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-FYpL4XiIWakTnIqLqvt3uN4L9B3TsuHIvhLILzTiJZMJUsGvmKNeL4H3b6I99LRyerK9W4IuOXw+N28AtRgK2g=="],
|
|
114
|
+
|
|
115
|
+
"pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
|
|
116
|
+
|
|
117
|
+
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
|
118
|
+
|
|
119
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
120
|
+
|
|
121
|
+
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
|
|
122
|
+
}
|
|
123
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shipload/item-renderer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Deterministic SVG rendering for Shipload items",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"default": "./src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"./fonts": {
|
|
14
|
+
"types": "./src/fonts/index.ts",
|
|
15
|
+
"default": "./src/fonts/index.ts"
|
|
16
|
+
},
|
|
17
|
+
"./fonts/load-bun": {
|
|
18
|
+
"types": "./src/fonts/load-bun.ts",
|
|
19
|
+
"default": "./src/fonts/load-bun.ts"
|
|
20
|
+
},
|
|
21
|
+
"./fonts/*.woff2": "./src/fonts/*.woff2",
|
|
22
|
+
"./test/fixtures": {
|
|
23
|
+
"types": "./test/fixtures/cargo-items.ts",
|
|
24
|
+
"default": "./test/fixtures/cargo-items.ts"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": ["src", "!src/**/*.test.ts"],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "bun test",
|
|
30
|
+
"test:pixel": "bun test test/pixel.test.ts",
|
|
31
|
+
"dev:preview": "bun run scripts/preview.ts",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"lint": "biome check src test scripts",
|
|
34
|
+
"fonts:copy": "bun run scripts/copy-fonts.ts"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@shipload/sdk": "^2.0.0-rc13",
|
|
38
|
+
"@wharfkit/antelope": "^1.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@biomejs/biome": "^1.8.0",
|
|
42
|
+
"@fontsource/inter": "^5.2.8",
|
|
43
|
+
"@fontsource/jetbrains-mono": "^5.2.8",
|
|
44
|
+
"@fontsource/orbitron": "^5.2.8",
|
|
45
|
+
"@resvg/resvg-js": "^2.6.0",
|
|
46
|
+
"@types/bun": "latest",
|
|
47
|
+
"pixelmatch": "^6.0.0",
|
|
48
|
+
"pngjs": "^7.0.0",
|
|
49
|
+
"typescript": "^5.4.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { gzipSync } from 'node:zlib'
|
|
2
|
+
|
|
3
|
+
const CORE_LIMIT_BYTES = 50 * 1024
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
const result = await Bun.build({
|
|
7
|
+
entrypoints: ['./src/index.ts'],
|
|
8
|
+
target: 'browser',
|
|
9
|
+
format: 'esm',
|
|
10
|
+
minify: true,
|
|
11
|
+
external: ['@shipload/sdk', '@wharfkit/antelope'],
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
if (!result.success) {
|
|
15
|
+
for (const msg of result.logs) console.error(msg)
|
|
16
|
+
process.exit(1)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let totalBytes = 0
|
|
20
|
+
let totalGzipped = 0
|
|
21
|
+
for (const out of result.outputs) {
|
|
22
|
+
const text = await out.text()
|
|
23
|
+
const bytes = new TextEncoder().encode(text).length
|
|
24
|
+
const gz = gzipSync(new TextEncoder().encode(text)).length
|
|
25
|
+
console.log(`${out.path}: ${bytes} bytes (${gz} gzipped)`)
|
|
26
|
+
totalBytes += bytes
|
|
27
|
+
totalGzipped += gz
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log(`core bundle total: ${totalBytes} bytes, ${totalGzipped} gzipped`)
|
|
31
|
+
if (totalGzipped > CORE_LIMIT_BYTES) {
|
|
32
|
+
console.error(`FAIL: core bundle is ${totalGzipped} gzipped, limit is ${CORE_LIMIT_BYTES}`)
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
main()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { copyFile, mkdir } from 'node:fs/promises'
|
|
2
|
+
import { dirname, resolve } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
6
|
+
const OUT_DIR = resolve(__dirname, '../src/fonts')
|
|
7
|
+
const NM = resolve(__dirname, '../node_modules')
|
|
8
|
+
|
|
9
|
+
const FACES = [
|
|
10
|
+
{
|
|
11
|
+
src: `${NM}/@fontsource/orbitron/files/orbitron-latin-700-normal.woff2`,
|
|
12
|
+
out: 'orbitron-700.woff2',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
src: `${NM}/@fontsource/inter/files/inter-latin-400-normal.woff2`,
|
|
16
|
+
out: 'inter-400.woff2',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
src: `${NM}/@fontsource/inter/files/inter-latin-600-normal.woff2`,
|
|
20
|
+
out: 'inter-600.woff2',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
src: `${NM}/@fontsource/jetbrains-mono/files/jetbrains-mono-latin-500-normal.woff2`,
|
|
24
|
+
out: 'jetbrains-500.woff2',
|
|
25
|
+
},
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
async function main() {
|
|
29
|
+
await mkdir(OUT_DIR, { recursive: true })
|
|
30
|
+
for (const face of FACES) {
|
|
31
|
+
const dest = resolve(OUT_DIR, face.out)
|
|
32
|
+
await copyFile(face.src, dest)
|
|
33
|
+
const size = (await Bun.file(dest).arrayBuffer()).byteLength
|
|
34
|
+
console.log(`wrote ${face.out} (${size} bytes)`)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
main().catch((e) => {
|
|
39
|
+
console.error(e)
|
|
40
|
+
process.exit(1)
|
|
41
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { resolveItem } from '@shipload/sdk'
|
|
2
|
+
import { renderItem } from '../src/render.ts'
|
|
3
|
+
import { FIXTURES } from '../test/fixtures/cargo-items.ts'
|
|
4
|
+
|
|
5
|
+
function page(): string {
|
|
6
|
+
const sections: string[] = []
|
|
7
|
+
for (const [name, item] of Object.entries(FIXTURES)) {
|
|
8
|
+
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
9
|
+
const svg = renderItem(item, resolved)
|
|
10
|
+
sections.push(
|
|
11
|
+
`<section><h3>${name}</h3><div class="wrap">${svg}</div></section>`,
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
return `
|
|
15
|
+
<!doctype html>
|
|
16
|
+
<html>
|
|
17
|
+
<head>
|
|
18
|
+
<meta charset="utf-8">
|
|
19
|
+
<title>item-renderer preview</title>
|
|
20
|
+
<style>
|
|
21
|
+
body { background: #0a0a0c; color: #e6e8ec; font-family: Inter, sans-serif; padding: 24px; }
|
|
22
|
+
main { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 24px; }
|
|
23
|
+
section { background: #11141a; padding: 16px; border-radius: 10px; }
|
|
24
|
+
h3 { margin: 0 0 8px; color: #8f98a8; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; }
|
|
25
|
+
.wrap svg { display: block; }
|
|
26
|
+
</style>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<h1>Fixtures</h1>
|
|
30
|
+
<main>${sections.join('')}</main>
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
|
33
|
+
`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const port = Number(process.env.PORT ?? 5173)
|
|
37
|
+
|
|
38
|
+
Bun.serve({
|
|
39
|
+
port,
|
|
40
|
+
fetch: () => new Response(page(), { headers: { 'content-type': 'text/html' } }),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
console.log(`preview running at http://localhost:${port}`)
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class InvalidPayloadError extends Error {
|
|
2
|
+
override readonly name = 'InvalidPayloadError'
|
|
3
|
+
constructor(message: string) {
|
|
4
|
+
super(message)
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class UnknownItemError extends Error {
|
|
9
|
+
override readonly name = 'UnknownItemError'
|
|
10
|
+
readonly itemId: number
|
|
11
|
+
constructor(itemId: number) {
|
|
12
|
+
super(`unknown item id: ${itemId}`)
|
|
13
|
+
this.itemId = itemId
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class RenderError extends Error {
|
|
18
|
+
override readonly name = 'RenderError'
|
|
19
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
20
|
+
super(message, options)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type FontKey = 'orbitron-700' | 'inter-400' | 'inter-600' | 'jetbrains-500'
|
|
2
|
+
|
|
3
|
+
export interface FontMeta {
|
|
4
|
+
family: string
|
|
5
|
+
weight: number
|
|
6
|
+
fileName: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const FONT_MANIFEST: Record<FontKey, FontMeta> = {
|
|
10
|
+
'orbitron-700': { family: 'Orbitron', weight: 700, fileName: 'orbitron-700.woff2' },
|
|
11
|
+
'inter-400': { family: 'Inter', weight: 400, fileName: 'inter-400.woff2' },
|
|
12
|
+
'inter-600': { family: 'Inter', weight: 600, fileName: 'inter-600.woff2' },
|
|
13
|
+
'jetbrains-500': { family: 'JetBrains Mono', weight: 500, fileName: 'jetbrains-500.woff2' },
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function bytesToBase64(bytes: Uint8Array): string {
|
|
17
|
+
let binary = ''
|
|
18
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]!)
|
|
19
|
+
return btoa(binary)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function embedFontsInSvg(
|
|
23
|
+
svg: string,
|
|
24
|
+
fontData: Record<FontKey, Uint8Array>,
|
|
25
|
+
): string {
|
|
26
|
+
const faceBlocks = (Object.keys(fontData) as FontKey[]).map((key) => {
|
|
27
|
+
const meta = FONT_MANIFEST[key]
|
|
28
|
+
const b64 = bytesToBase64(fontData[key])
|
|
29
|
+
return (
|
|
30
|
+
`@font-face { font-family: "${meta.family}"; font-weight: ${meta.weight}; ` +
|
|
31
|
+
`font-style: normal; src: url(data:font/woff2;base64,${b64}) format("woff2"); }`
|
|
32
|
+
)
|
|
33
|
+
}).join('\n')
|
|
34
|
+
const style = `<defs><style type="text/css"><![CDATA[\n${faceBlocks}\n]]></style></defs>`
|
|
35
|
+
return svg.replace('>', `>${style}`)
|
|
36
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { FONT_MANIFEST, type FontKey } from './index.ts'
|
|
5
|
+
|
|
6
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
|
|
8
|
+
export async function loadFontData(): Promise<Record<FontKey, Uint8Array>> {
|
|
9
|
+
const entries = await Promise.all(
|
|
10
|
+
(Object.keys(FONT_MANIFEST) as FontKey[]).map(async (key) => {
|
|
11
|
+
const buf = await readFile(join(HERE, FONT_MANIFEST[key].fileName))
|
|
12
|
+
return [key, new Uint8Array(buf)] as const
|
|
13
|
+
}),
|
|
14
|
+
)
|
|
15
|
+
return Object.fromEntries(entries) as Record<FontKey, Uint8Array>
|
|
16
|
+
}
|
|
Binary file
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Version
|
|
2
|
+
export const VERSION = '0.1.0'
|
|
3
|
+
|
|
4
|
+
// Errors
|
|
5
|
+
export { InvalidPayloadError, UnknownItemError, RenderError } from './errors.ts'
|
|
6
|
+
|
|
7
|
+
// Payload
|
|
8
|
+
export { encodePayload, decodePayload } from './payload/codec.ts'
|
|
9
|
+
export type { CargoItem, CargoItemLike } from './payload/codec.ts'
|
|
10
|
+
|
|
11
|
+
// Rendering
|
|
12
|
+
export { renderItem, renderFromPayload, type RenderOptions } from './render.ts'
|
|
13
|
+
export { renderByType, type RenderByTypeOpts } from './templates/index.ts'
|
|
14
|
+
|
|
15
|
+
// Links + meta
|
|
16
|
+
export { linkToItemPage, linkToItemImage } from './links.ts'
|
|
17
|
+
export { itemPageMeta } from './meta.ts'
|
|
18
|
+
export type { ItemPageMeta, ItemPageMetaOptions } from './meta.ts'
|
|
19
|
+
|
|
20
|
+
// Tokens (consumed by testmap tailwind.config)
|
|
21
|
+
export { tokens } from './tokens/index.ts'
|
|
22
|
+
export type { Tokens, CategoryColorKey, TierColorKey } from './tokens/colors.ts'
|
|
23
|
+
|
|
24
|
+
// Category icon primitive
|
|
25
|
+
export { categoryIconSvg, categoryIconPath } from './primitives/category-icon.ts'
|
|
26
|
+
export type { CategoryIconPathOpts, CategoryIconSvgOpts } from './primitives/category-icon.ts'
|
|
27
|
+
|
|
28
|
+
// Item cell templates
|
|
29
|
+
export { renderItemCell, itemCellGroup } from './templates/item-cell.ts'
|
|
30
|
+
export type { ItemCellProps, ItemCellGroupProps } from './templates/item-cell.ts'
|
|
31
|
+
|
|
32
|
+
// Ship panel template
|
|
33
|
+
export { renderShipPanel } from './templates/ship-panel.ts'
|
|
34
|
+
export type { ShipPanelProps, ShipPanelSlot } from './templates/ship-panel.ts'
|
|
35
|
+
|
|
36
|
+
// Re-exports from sdkv2 so consumers only need one import boundary
|
|
37
|
+
export {
|
|
38
|
+
resolveItem,
|
|
39
|
+
ServerContract,
|
|
40
|
+
type ResolvedItem,
|
|
41
|
+
type ResolvedItemStat,
|
|
42
|
+
type ResolvedItemType,
|
|
43
|
+
type ResolvedModuleSlot,
|
|
44
|
+
type ResolvedAttributeGroup,
|
|
45
|
+
} from '@shipload/sdk'
|
|
46
|
+
export type { CategoryIconShape } from '@shipload/sdk'
|
package/src/links.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CargoItem } from './payload/codec.ts'
|
|
2
|
+
import { encodePayload } from './payload/codec.ts'
|
|
3
|
+
|
|
4
|
+
const DEFAULT_WEBSITE_BASE = 'https://shiploadgame.com'
|
|
5
|
+
const DEFAULT_IMAGE_BASE = 'https://img.shiploadgame.com'
|
|
6
|
+
|
|
7
|
+
export function linkToItemPage(item: CargoItem, baseUrl = DEFAULT_WEBSITE_BASE): string {
|
|
8
|
+
const payload = encodePayload(item)
|
|
9
|
+
return `${baseUrl}/guide/item/${payload}`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function linkToItemImage(
|
|
13
|
+
item: CargoItem,
|
|
14
|
+
ext: 'png' | 'svg',
|
|
15
|
+
baseUrl = DEFAULT_IMAGE_BASE,
|
|
16
|
+
): string {
|
|
17
|
+
const payload = encodePayload(item)
|
|
18
|
+
return `${baseUrl}/item/${payload}.${ext}`
|
|
19
|
+
}
|
package/src/meta.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ResolvedItem } from '@shipload/sdk'
|
|
2
|
+
import type { CargoItem } from './payload/codec.ts'
|
|
3
|
+
import { linkToItemImage } from './links.ts'
|
|
4
|
+
|
|
5
|
+
function tierLabel(tier: string): string {
|
|
6
|
+
return tier.toUpperCase()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function categoryLabel(resolved: ResolvedItem): string {
|
|
10
|
+
if (!resolved.category) return ''
|
|
11
|
+
return resolved.category[0]!.toUpperCase() + resolved.category.slice(1)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function describeItem(resolved: ResolvedItem): string {
|
|
15
|
+
const parts: string[] = []
|
|
16
|
+
if (resolved.category) parts.push(categoryLabel(resolved))
|
|
17
|
+
parts.push(tierLabel(resolved.tier))
|
|
18
|
+
parts.push(`${resolved.mass.toLocaleString('en-US')} kg`)
|
|
19
|
+
return parts.join(' · ')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ItemPageMeta {
|
|
23
|
+
title: string
|
|
24
|
+
description: string
|
|
25
|
+
ogImage: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ItemPageMetaOptions {
|
|
29
|
+
imageBaseUrl?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function itemPageMeta(
|
|
33
|
+
item: CargoItem,
|
|
34
|
+
resolved: ResolvedItem,
|
|
35
|
+
opts?: ItemPageMetaOptions,
|
|
36
|
+
): ItemPageMeta {
|
|
37
|
+
return {
|
|
38
|
+
title: `${resolved.name} · Shipload Guide`,
|
|
39
|
+
description: describeItem(resolved),
|
|
40
|
+
ogImage: linkToItemImage(item, 'png', opts?.imageBaseUrl),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { InvalidPayloadError } from '../errors.ts'
|
|
2
|
+
|
|
3
|
+
export function bytesToBase64Url(bytes: Uint8Array): string {
|
|
4
|
+
let binary = ''
|
|
5
|
+
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]!)
|
|
6
|
+
const b64 = btoa(binary)
|
|
7
|
+
return b64.replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function base64UrlToBytes(input: string): Uint8Array {
|
|
11
|
+
if (!/^[A-Za-z0-9_-]*$/.test(input)) {
|
|
12
|
+
throw new InvalidPayloadError('payload contains non-base64url characters')
|
|
13
|
+
}
|
|
14
|
+
const padded = input.replaceAll('-', '+').replaceAll('_', '/').padEnd(
|
|
15
|
+
Math.ceil(input.length / 4) * 4,
|
|
16
|
+
'='
|
|
17
|
+
)
|
|
18
|
+
let binary: string
|
|
19
|
+
try {
|
|
20
|
+
binary = atob(padded)
|
|
21
|
+
} catch (e) {
|
|
22
|
+
throw new InvalidPayloadError(`base64 decode failed: ${(e as Error).message}`)
|
|
23
|
+
}
|
|
24
|
+
const bytes = new Uint8Array(binary.length)
|
|
25
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
|
|
26
|
+
return bytes
|
|
27
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
|
|
6
|
+
export type CargoItem = InstanceType<typeof ServerContract.Types.cargo_item>
|
|
7
|
+
export type CargoItemLike = Parameters<typeof ServerContract.Types.cargo_item.from>[0]
|
|
8
|
+
|
|
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)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function decodePayload(input: string): CargoItem {
|
|
16
|
+
if (input.length === 0) throw new InvalidPayloadError('empty payload')
|
|
17
|
+
const bytes = base64UrlToBytes(input)
|
|
18
|
+
try {
|
|
19
|
+
return Serializer.decode({
|
|
20
|
+
data: bytes,
|
|
21
|
+
type: ServerContract.Types.cargo_item,
|
|
22
|
+
}) as CargoItem
|
|
23
|
+
} catch (e) {
|
|
24
|
+
throw new InvalidPayloadError(`cargo_item decode failed: ${(e as Error).message}`)
|
|
25
|
+
}
|
|
26
|
+
}
|