@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.
Files changed (86) hide show
  1. package/.github/workflows/ci.yml +14 -0
  2. package/.gitignore +6 -0
  3. package/Makefile +50 -0
  4. package/biome.json +18 -0
  5. package/bun.lock +123 -0
  6. package/package.json +51 -0
  7. package/scripts/check-bundle-size.ts +37 -0
  8. package/scripts/copy-fonts.ts +41 -0
  9. package/scripts/preview.ts +43 -0
  10. package/src/errors.ts +22 -0
  11. package/src/fonts/index.ts +36 -0
  12. package/src/fonts/inter-400.woff2 +0 -0
  13. package/src/fonts/inter-600.woff2 +0 -0
  14. package/src/fonts/jetbrains-500.woff2 +0 -0
  15. package/src/fonts/load-bun.ts +16 -0
  16. package/src/fonts/orbitron-700.woff2 +0 -0
  17. package/src/index.ts +46 -0
  18. package/src/links.ts +19 -0
  19. package/src/meta.ts +42 -0
  20. package/src/payload/base64url.ts +27 -0
  21. package/src/payload/codec.ts +26 -0
  22. package/src/primitives/category-icon.ts +87 -0
  23. package/src/primitives/compact-row.ts +38 -0
  24. package/src/primitives/divider.ts +20 -0
  25. package/src/primitives/icon-hex.ts +39 -0
  26. package/src/primitives/module-slot.ts +147 -0
  27. package/src/primitives/panel.ts +24 -0
  28. package/src/primitives/quantity-badge.ts +37 -0
  29. package/src/primitives/span-paragraph.ts +72 -0
  30. package/src/primitives/stat-bar.ts +85 -0
  31. package/src/primitives/svg.ts +25 -0
  32. package/src/primitives/text.ts +42 -0
  33. package/src/primitives/wrap.ts +24 -0
  34. package/src/render.ts +33 -0
  35. package/src/templates/_shared.ts +15 -0
  36. package/src/templates/component.ts +139 -0
  37. package/src/templates/index.ts +30 -0
  38. package/src/templates/item-cell.ts +96 -0
  39. package/src/templates/module.ts +190 -0
  40. package/src/templates/packed-entity.ts +30 -0
  41. package/src/templates/resource.ts +151 -0
  42. package/src/templates/ship-panel.ts +145 -0
  43. package/src/tokens/colors.ts +45 -0
  44. package/src/tokens/index.ts +7 -0
  45. package/src/tokens/spacing.ts +10 -0
  46. package/src/tokens/typography.ts +19 -0
  47. package/test/__image_snapshots__/.gitkeep +0 -0
  48. package/test/__image_snapshots__/component-hull-plates.png +0 -0
  49. package/test/__image_snapshots__/module-engine-t1.png +0 -0
  50. package/test/__image_snapshots__/module-storage-t1.png +0 -0
  51. package/test/__image_snapshots__/packed-entity-ship-t1-only-engine.png +0 -0
  52. package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.diff.png +0 -0
  53. package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.png +0 -0
  54. package/test/__image_snapshots__/resource-iron.diff.png +0 -0
  55. package/test/__image_snapshots__/resource-iron.png +0 -0
  56. package/test/__snapshots__/templates-component.test.ts.snap +5 -0
  57. package/test/__snapshots__/templates-item-cell.test.ts.snap +9 -0
  58. package/test/__snapshots__/templates-module.test.ts.snap +17 -0
  59. package/test/__snapshots__/templates-packed-entity.test.ts.snap +5 -0
  60. package/test/__snapshots__/templates-resource.test.ts.snap +7 -0
  61. package/test/base64url.test.ts +33 -0
  62. package/test/codec.test.ts +43 -0
  63. package/test/errors.test.ts +24 -0
  64. package/test/fixtures/cargo-items.ts +122 -0
  65. package/test/fonts.test.ts +28 -0
  66. package/test/links-meta.test.ts +34 -0
  67. package/test/pixel.test.ts +66 -0
  68. package/test/primitives-category-icon.test.ts +79 -0
  69. package/test/primitives-compact-row.test.ts +44 -0
  70. package/test/primitives-domain.test.ts +72 -0
  71. package/test/primitives-layout.test.ts +56 -0
  72. package/test/primitives-module-slot.test.ts +88 -0
  73. package/test/render.test.ts +40 -0
  74. package/test/sanity.test.ts +6 -0
  75. package/test/sdk-link.test.ts +19 -0
  76. package/test/snapshots/.gitkeep +0 -0
  77. package/test/svg.test.ts +28 -0
  78. package/test/templates-component.test.ts +36 -0
  79. package/test/templates-dispatch.test.ts +35 -0
  80. package/test/templates-item-cell.test.ts +94 -0
  81. package/test/templates-module.test.ts +63 -0
  82. package/test/templates-packed-entity.test.ts +47 -0
  83. package/test/templates-resource.test.ts +71 -0
  84. package/test/templates-ship-panel.test.ts +87 -0
  85. package/test/tokens.test.ts +32 -0
  86. 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
@@ -0,0 +1,6 @@
1
+ node_modules/
2
+ bun.lockb
3
+ dist/
4
+ .DS_Store
5
+ test/__image_snapshots__/*.diff.png
6
+ coverage/
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
+ }