pixelweaver 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 (392) hide show
  1. package/.env.development +1 -0
  2. package/.github/workflows/ci.yml +22 -0
  3. package/.github/workflows/publish.yml +18 -0
  4. package/.prettierignore +5 -0
  5. package/.prettierrc +16 -0
  6. package/.python-version +1 -0
  7. package/.rlsbl/bases/.github/workflows/ci.yml +21 -0
  8. package/.rlsbl/bases/.github/workflows/publish.yml +18 -0
  9. package/.rlsbl/bases/.rlsbl/changes/unreleased.jsonl +0 -0
  10. package/.rlsbl/bases/.rlsbl/hooks/post-release.sh +8 -0
  11. package/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +5 -0
  12. package/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +8 -0
  13. package/.rlsbl/bases/.rlsbl/lint/go.toml +17 -0
  14. package/.rlsbl/bases/.rlsbl/lint/npm.toml +19 -0
  15. package/.rlsbl/bases/.rlsbl/lint/python.toml +25 -0
  16. package/.rlsbl/bases/CHANGELOG.md +5 -0
  17. package/.rlsbl/bases/LICENSE +21 -0
  18. package/.rlsbl/changes/.validated +1 -0
  19. package/.rlsbl/changes/0.1.0.jsonl +85 -0
  20. package/.rlsbl/changes/0.1.0.md +13 -0
  21. package/.rlsbl/changes/unreleased.jsonl +0 -0
  22. package/.rlsbl/config.json +6 -0
  23. package/.rlsbl/hashes.json +14 -0
  24. package/.rlsbl/hooks/post-release.sh +8 -0
  25. package/.rlsbl/hooks/pre-checks.sh +5 -0
  26. package/.rlsbl/hooks/pre-release.sh +8 -0
  27. package/.rlsbl/lint/go.toml +17 -0
  28. package/.rlsbl/lint/npm.toml +19 -0
  29. package/.rlsbl/lint/python.toml +25 -0
  30. package/.rlsbl/releases/unreleased.toml +0 -0
  31. package/.rlsbl/releases/v0.1.0.toml +3 -0
  32. package/.rlsbl/version +1 -0
  33. package/.selfdoc/hashes/hashes.json +146 -0
  34. package/.strictcli/schema.json +227 -0
  35. package/CHANGELOG.md +17 -0
  36. package/CLAUDE.md +100 -0
  37. package/LICENSE +21 -0
  38. package/README.md +116 -0
  39. package/assets/icon.png +0 -0
  40. package/docs/_README.md +117 -0
  41. package/docs/cli-config.md +35 -0
  42. package/docs/cli-dev.md +21 -0
  43. package/docs/cli-diagnose.md +12 -0
  44. package/docs/cli-index.md +30 -0
  45. package/docs/cli-list.md +18 -0
  46. package/docs/cli-mcp.md +18 -0
  47. package/docs/cli-new.md +26 -0
  48. package/docs/cli-open.md +21 -0
  49. package/docs/cli-serve.md +21 -0
  50. package/docs/cli-stop.md +12 -0
  51. package/docs/gen-index.md +36 -0
  52. package/docs/index.md +13 -0
  53. package/docs/server-src-pixelweaver-__main__.md +12 -0
  54. package/docs/server-src-pixelweaver-autosave.md +12 -0
  55. package/docs/server-src-pixelweaver-bridge.md +12 -0
  56. package/docs/server-src-pixelweaver-cli.md +12 -0
  57. package/docs/server-src-pixelweaver-config.md +12 -0
  58. package/docs/server-src-pixelweaver-connections.md +12 -0
  59. package/docs/server-src-pixelweaver-main.md +12 -0
  60. package/docs/server-src-pixelweaver-mcp_bridge.md +12 -0
  61. package/docs/server-src-pixelweaver-mcp_drawing_tools.md +12 -0
  62. package/docs/server-src-pixelweaver-mcp_export_tools.md +12 -0
  63. package/docs/server-src-pixelweaver-mcp_frame_tools.md +12 -0
  64. package/docs/server-src-pixelweaver-mcp_history_tools.md +12 -0
  65. package/docs/server-src-pixelweaver-mcp_layer_tools.md +12 -0
  66. package/docs/server-src-pixelweaver-mcp_lock.md +12 -0
  67. package/docs/server-src-pixelweaver-mcp_project_tools.md +12 -0
  68. package/docs/server-src-pixelweaver-mcp_read_tools.md +12 -0
  69. package/docs/server-src-pixelweaver-mcp_registry.md +12 -0
  70. package/docs/server-src-pixelweaver-mcp_resources.md +12 -0
  71. package/docs/server-src-pixelweaver-mcp_server.md +12 -0
  72. package/docs/server-src-pixelweaver-protocol.md +12 -0
  73. package/docs/server-src-pixelweaver-state.md +12 -0
  74. package/docs/server-src-pixelweaver-storage.md +12 -0
  75. package/docs/server-src-pixelweaver-websocket.md +12 -0
  76. package/docs/server-src-pixelweaver.md +12 -0
  77. package/e2e/app-launch.test.ts +35 -0
  78. package/e2e/menus.test.ts +26 -0
  79. package/e2e/tools.test.ts +27 -0
  80. package/e2e/undo-redo.test.ts +11 -0
  81. package/eslint.config.js +62 -0
  82. package/index.html +13 -0
  83. package/package.json +48 -0
  84. package/playwright.config.ts +19 -0
  85. package/plugins/builtin/.gitkeep +0 -0
  86. package/plugins/builtin/advanced-fill-tool.ts +146 -0
  87. package/plugins/builtin/circle-tool.ts +186 -0
  88. package/plugins/builtin/diamond-tool.ts +182 -0
  89. package/plugins/builtin/dither-tool.ts +186 -0
  90. package/plugins/builtin/drawing-primitives-plugin.ts +362 -0
  91. package/plugins/builtin/drawing-utils.test.ts +495 -0
  92. package/plugins/builtin/drawing-utils.ts +431 -0
  93. package/plugins/builtin/effects/blur.ts +97 -0
  94. package/plugins/builtin/effects/color-effects.ts +278 -0
  95. package/plugins/builtin/effects/flip.ts +83 -0
  96. package/plugins/builtin/effects/glow.ts +118 -0
  97. package/plugins/builtin/effects/outline.ts +110 -0
  98. package/plugins/builtin/effects/rotate.ts +357 -0
  99. package/plugins/builtin/effects/scale.ts +258 -0
  100. package/plugins/builtin/effects/shadow.ts +111 -0
  101. package/plugins/builtin/effects/sharpen.ts +102 -0
  102. package/plugins/builtin/effects.test.ts +715 -0
  103. package/plugins/builtin/eraser-tool.ts +23 -0
  104. package/plugins/builtin/eyedropper-tool.ts +83 -0
  105. package/plugins/builtin/fill-tool.ts +93 -0
  106. package/plugins/builtin/gradient-tool.ts +204 -0
  107. package/plugins/builtin/gradient.test.ts +142 -0
  108. package/plugins/builtin/importers/aseprite-importer-plugin.ts +174 -0
  109. package/plugins/builtin/importers/aseprite-parser.ts +497 -0
  110. package/plugins/builtin/importers/piskel-importer-plugin.ts +222 -0
  111. package/plugins/builtin/importers/sky-spec-plugin.ts +409 -0
  112. package/plugins/builtin/line-tool.ts +267 -0
  113. package/plugins/builtin/make-stroke-tool.ts +271 -0
  114. package/plugins/builtin/noise-dither.test.ts +151 -0
  115. package/plugins/builtin/noise-tool.ts +131 -0
  116. package/plugins/builtin/pattern-stamp-tool.ts +162 -0
  117. package/plugins/builtin/pencil-tool.ts +25 -0
  118. package/plugins/builtin/rect-tool.ts +179 -0
  119. package/plugins/builtin/selection-tool.ts +388 -0
  120. package/plugins/builtin/selection.test.ts +195 -0
  121. package/plugins/builtin/tool-plugins.test.ts +529 -0
  122. package/public/favicon.svg +7 -0
  123. package/public/sw.js +91 -0
  124. package/pyproject.toml +49 -0
  125. package/scripts/eslint-wave-a-list.sh +24 -0
  126. package/scripts/eslint-wave-a-status.sh +25 -0
  127. package/scripts/fix-index-signature-access.py +127 -0
  128. package/scripts/fix-unchecked-index.py +128 -0
  129. package/scripts/fix-unchecked-vars.py +127 -0
  130. package/scripts/fix-wave-a-bangs.py +36 -0
  131. package/scripts/fix-wave-a-templates.py +108 -0
  132. package/scripts/fix-wave-b-void-expr.py +167 -0
  133. package/scripts/generate-command-params.py +540 -0
  134. package/scripts/migrate-single-frame-to-multi.py +171 -0
  135. package/scripts/smoke-test.sh +77 -0
  136. package/selfdoc.json +10 -0
  137. package/server/README.md +0 -0
  138. package/server/src/pixelweaver/__init__.py +1 -0
  139. package/server/src/pixelweaver/__main__.py +4 -0
  140. package/server/src/pixelweaver/autosave.py +114 -0
  141. package/server/src/pixelweaver/bridge.py +127 -0
  142. package/server/src/pixelweaver/cli.py +199 -0
  143. package/server/src/pixelweaver/config.py +24 -0
  144. package/server/src/pixelweaver/connections.py +54 -0
  145. package/server/src/pixelweaver/main.py +271 -0
  146. package/server/src/pixelweaver/mcp_bridge.py +189 -0
  147. package/server/src/pixelweaver/mcp_drawing_tools.py +178 -0
  148. package/server/src/pixelweaver/mcp_export_tools.py +291 -0
  149. package/server/src/pixelweaver/mcp_frame_tools.py +423 -0
  150. package/server/src/pixelweaver/mcp_history_tools.py +106 -0
  151. package/server/src/pixelweaver/mcp_layer_tools.py +64 -0
  152. package/server/src/pixelweaver/mcp_lock.py +37 -0
  153. package/server/src/pixelweaver/mcp_project_tools.py +302 -0
  154. package/server/src/pixelweaver/mcp_read_tools.py +163 -0
  155. package/server/src/pixelweaver/mcp_registry.py +247 -0
  156. package/server/src/pixelweaver/mcp_resources.py +312 -0
  157. package/server/src/pixelweaver/mcp_server.py +234 -0
  158. package/server/src/pixelweaver/protocol.py +219 -0
  159. package/server/src/pixelweaver/state.py +267 -0
  160. package/server/src/pixelweaver/storage.py +293 -0
  161. package/server/src/pixelweaver/websocket.py +282 -0
  162. package/server/tests/__init__.py +0 -0
  163. package/server/tests/conftest.py +17 -0
  164. package/server/tests/test_api.py +96 -0
  165. package/server/tests/test_bridge.py +161 -0
  166. package/server/tests/test_health.py +9 -0
  167. package/server/tests/test_integration.py +86 -0
  168. package/server/tests/test_mcp_bridge.py +293 -0
  169. package/server/tests/test_mcp_lock.py +34 -0
  170. package/server/tests/test_mcp_registry.py +679 -0
  171. package/server/tests/test_mcp_resources.py +648 -0
  172. package/server/tests/test_protocol.py +125 -0
  173. package/server/tests/test_state.py +87 -0
  174. package/server/tests/test_storage.py +306 -0
  175. package/server/tests/test_websocket.py +275 -0
  176. package/src/App.svelte +107 -0
  177. package/src/app.css +215 -0
  178. package/src/lib/animation/AnimationPreviewPanel.svelte +667 -0
  179. package/src/lib/animation/animation-commands.test.ts +228 -0
  180. package/src/lib/animation/animation-commands.ts +540 -0
  181. package/src/lib/animation/animation-preview-panel-plugin.ts +25 -0
  182. package/src/lib/animation/animation-preview.svelte.ts +151 -0
  183. package/src/lib/animation/clipboard.ts +134 -0
  184. package/src/lib/animation/frame-model.svelte.ts +437 -0
  185. package/src/lib/animation/frame-model.test.ts +314 -0
  186. package/src/lib/animation/frame-selection.svelte.ts +77 -0
  187. package/src/lib/animation/frame-tags.svelte.ts +238 -0
  188. package/src/lib/animation/frame-tags.test.ts +136 -0
  189. package/src/lib/animation/import.test.ts +141 -0
  190. package/src/lib/animation/import.ts +112 -0
  191. package/src/lib/animation/spritesheet-export.test.ts +239 -0
  192. package/src/lib/animation/spritesheet-export.ts +314 -0
  193. package/src/lib/canvas/CanvasViewport.svelte +216 -0
  194. package/src/lib/canvas/canvas-init-plugin.ts +178 -0
  195. package/src/lib/canvas/canvas-renderer.ts +408 -0
  196. package/src/lib/canvas/canvas-state.svelte.ts +232 -0
  197. package/src/lib/canvas/canvas-state.test.ts +139 -0
  198. package/src/lib/canvas/input-handler.ts +221 -0
  199. package/src/lib/canvas/input-plugin.ts +150 -0
  200. package/src/lib/canvas/onion-skin.ts +94 -0
  201. package/src/lib/canvas/pixel-buffer.test.ts +249 -0
  202. package/src/lib/canvas/pixel-buffer.ts +151 -0
  203. package/src/lib/canvas/render-state.svelte.ts +18 -0
  204. package/src/lib/canvas/shape-preview-state.svelte.ts +36 -0
  205. package/src/lib/canvas/tile-mode.test.ts +53 -0
  206. package/src/lib/canvas/tile-mode.ts +92 -0
  207. package/src/lib/canvas/viewport-utils.ts +24 -0
  208. package/src/lib/canvas/zoom-utils.ts +31 -0
  209. package/src/lib/color/.gitkeep +0 -0
  210. package/src/lib/color/color-commands.ts +87 -0
  211. package/src/lib/color/color-state.svelte.ts +98 -0
  212. package/src/lib/color/color-state.test.ts +91 -0
  213. package/src/lib/color/color-utils.test.ts +220 -0
  214. package/src/lib/color/color-utils.ts +243 -0
  215. package/src/lib/color/palette-state.svelte.ts +127 -0
  216. package/src/lib/color/palette-state.test.ts +154 -0
  217. package/src/lib/color/palette.ts +79 -0
  218. package/src/lib/core/bootstrap.ts +66 -0
  219. package/src/lib/core/command-params.generated.ts +1549 -0
  220. package/src/lib/core/command-params.ts +20 -0
  221. package/src/lib/core/command-runner.ts +79 -0
  222. package/src/lib/core/commands.ts +134 -0
  223. package/src/lib/core/dispatcher.test.ts +548 -0
  224. package/src/lib/core/dispatcher.ts +361 -0
  225. package/src/lib/core/index.test.ts +7 -0
  226. package/src/lib/core/notification-state.svelte.ts +119 -0
  227. package/src/lib/core/plugin-api.ts +210 -0
  228. package/src/lib/core/plugin-discovery.test.ts +53 -0
  229. package/src/lib/core/plugin-discovery.ts +65 -0
  230. package/src/lib/core/plugin-loader.test.ts +159 -0
  231. package/src/lib/core/plugin-loader.ts +240 -0
  232. package/src/lib/core/plugin-types.ts +286 -0
  233. package/src/lib/core/registries.svelte.ts +74 -0
  234. package/src/lib/core/tool-options-state.svelte.ts +61 -0
  235. package/src/lib/dock/DockLayout.svelte +375 -0
  236. package/src/lib/dock/TabAddMenu.svelte +90 -0
  237. package/src/lib/dock/dock-persistence.ts +46 -0
  238. package/src/lib/dock/dock-plugin.ts +49 -0
  239. package/src/lib/dock/dock-presets.ts +156 -0
  240. package/src/lib/dock/dock-state.svelte.ts +77 -0
  241. package/src/lib/dock/dock-types.ts +2 -0
  242. package/src/lib/dock/dockview-theme.css +226 -0
  243. package/src/lib/dock/dockview-theme.ts +17 -0
  244. package/src/lib/dock/header-action-renderer.ts +77 -0
  245. package/src/lib/edit/clipboard-state.ts +34 -0
  246. package/src/lib/export/download.ts +201 -0
  247. package/src/lib/export/png-metadata.ts +181 -0
  248. package/src/lib/history/ActionLogPanel.svelte +418 -0
  249. package/src/lib/history/action-log-panel-plugin.ts +24 -0
  250. package/src/lib/history/action-log.svelte.ts +172 -0
  251. package/src/lib/history/action-log.test.ts +168 -0
  252. package/src/lib/history/history-commands.ts +403 -0
  253. package/src/lib/history/macros.svelte.ts +320 -0
  254. package/src/lib/history/macros.test.ts +224 -0
  255. package/src/lib/history/replay-engine.test.ts +241 -0
  256. package/src/lib/history/replay-engine.ts +149 -0
  257. package/src/lib/history/spec-format.test.ts +250 -0
  258. package/src/lib/history/spec-format.ts +210 -0
  259. package/src/lib/iso/SeamCheckerPanel.svelte +251 -0
  260. package/src/lib/iso/iso-math.test.ts +77 -0
  261. package/src/lib/iso/iso-math.ts +77 -0
  262. package/src/lib/iso/seam-checker-panel-plugin.ts +25 -0
  263. package/src/lib/iso/seam-checker.test.ts +126 -0
  264. package/src/lib/iso/seam-checker.ts +93 -0
  265. package/src/lib/iso/tessellation.ts +67 -0
  266. package/src/lib/layers/compositor.test.ts +193 -0
  267. package/src/lib/layers/compositor.ts +175 -0
  268. package/src/lib/layers/layer-commands.test.ts +263 -0
  269. package/src/lib/layers/layer-commands.ts +429 -0
  270. package/src/lib/layers/layer-tree.svelte.ts +516 -0
  271. package/src/lib/layers/layer-tree.test.ts +383 -0
  272. package/src/lib/layers/layer-types.ts +56 -0
  273. package/src/lib/leveleditor/LevelEditorViewport.svelte +1808 -0
  274. package/src/lib/leveleditor/MapPropertiesPanel.svelte +266 -0
  275. package/src/lib/leveleditor/TilePickerPanel.svelte +324 -0
  276. package/src/lib/leveleditor/depth-sort.test.ts +70 -0
  277. package/src/lib/leveleditor/depth-sort.ts +39 -0
  278. package/src/lib/leveleditor/level-editor-commands.ts +353 -0
  279. package/src/lib/leveleditor/level-editor-viewport-plugin.ts +25 -0
  280. package/src/lib/leveleditor/map-properties-panel-plugin.ts +25 -0
  281. package/src/lib/leveleditor/map-state.svelte.ts +372 -0
  282. package/src/lib/leveleditor/map-state.test.ts +243 -0
  283. package/src/lib/leveleditor/tile-picker-panel-plugin.ts +25 -0
  284. package/src/lib/leveleditor/tile-source-registry.svelte.ts +91 -0
  285. package/src/lib/leveleditor/tiled-export.test.ts +144 -0
  286. package/src/lib/leveleditor/tiled-export.ts +374 -0
  287. package/src/lib/pywebview.d.ts +15 -0
  288. package/src/lib/recovery/.gitkeep +0 -0
  289. package/src/lib/recovery/idb-store.ts +118 -0
  290. package/src/lib/recovery/recovery-manager.test.ts +321 -0
  291. package/src/lib/recovery/recovery-manager.ts +115 -0
  292. package/src/lib/recovery/recovery-plugin.ts +55 -0
  293. package/src/lib/recovery/recovery-state.svelte.ts +21 -0
  294. package/src/lib/recovery/sw-plugin.ts +18 -0
  295. package/src/lib/recovery/sw-register.ts +17 -0
  296. package/src/lib/save/directory-format.ts +42 -0
  297. package/src/lib/save/project-snapshot.ts +139 -0
  298. package/src/lib/save/recent-projects.ts +56 -0
  299. package/src/lib/save/save-state.svelte.ts +35 -0
  300. package/src/lib/save/storage.ts +167 -0
  301. package/src/lib/save/zip-format.ts +45 -0
  302. package/src/lib/shortcuts/.gitkeep +0 -0
  303. package/src/lib/shortcuts/ShortcutEditorPanel.svelte +690 -0
  304. package/src/lib/shortcuts/default-bindings.ts +61 -0
  305. package/src/lib/shortcuts/shortcut-editor-panel-plugin.ts +25 -0
  306. package/src/lib/shortcuts/shortcut-init.ts +380 -0
  307. package/src/lib/shortcuts/shortcut-manager.test.ts +466 -0
  308. package/src/lib/shortcuts/shortcut-manager.ts +281 -0
  309. package/src/lib/shortcuts/shortcut-state.svelte.ts +78 -0
  310. package/src/lib/shortcuts/shortcuts-plugin.ts +17 -0
  311. package/src/lib/sync/patch-applicator.ts +300 -0
  312. package/src/lib/sync/patch-types.ts +65 -0
  313. package/src/lib/sync/project-manager.ts +108 -0
  314. package/src/lib/sync/sync-init.ts +152 -0
  315. package/src/lib/sync/sync-plugin.ts +19 -0
  316. package/src/lib/sync/sync-state.svelte.ts +56 -0
  317. package/src/lib/sync/ws-client.test.ts +604 -0
  318. package/src/lib/sync/ws-client.ts +574 -0
  319. package/src/lib/tools/.gitkeep +0 -0
  320. package/src/lib/ui/.gitkeep +0 -0
  321. package/src/lib/ui/AboutDialog.svelte +113 -0
  322. package/src/lib/ui/ColorPicker.svelte +761 -0
  323. package/src/lib/ui/ContextMenu.svelte +216 -0
  324. package/src/lib/ui/ExportDialog.svelte +747 -0
  325. package/src/lib/ui/FrameStrip.svelte +854 -0
  326. package/src/lib/ui/LayerPanel.svelte +810 -0
  327. package/src/lib/ui/MenuBar.svelte +590 -0
  328. package/src/lib/ui/NewProjectDialog.svelte +803 -0
  329. package/src/lib/ui/PluginManagerPanel.svelte +475 -0
  330. package/src/lib/ui/PromptDialog.svelte +252 -0
  331. package/src/lib/ui/RecoveryDialog.svelte +295 -0
  332. package/src/lib/ui/ResizeDialog.svelte +416 -0
  333. package/src/lib/ui/StatusBar.svelte +145 -0
  334. package/src/lib/ui/ToolbarPanel.svelte +488 -0
  335. package/src/lib/ui/animation-menu-commands.ts +194 -0
  336. package/src/lib/ui/command-palette/CommandPalette.svelte +232 -0
  337. package/src/lib/ui/command-palette/command-palette-plugin.ts +30 -0
  338. package/src/lib/ui/command-palette/command-palette-state.svelte.ts +190 -0
  339. package/src/lib/ui/command-palette/command-palette.test.ts +129 -0
  340. package/src/lib/ui/dialog-state.svelte.ts +70 -0
  341. package/src/lib/ui/edit-commands.ts +271 -0
  342. package/src/lib/ui/file-commands.ts +275 -0
  343. package/src/lib/ui/file-open.ts +99 -0
  344. package/src/lib/ui/help-commands.ts +93 -0
  345. package/src/lib/ui/image-commands.ts +181 -0
  346. package/src/lib/ui/layer-menu-commands.ts +420 -0
  347. package/src/lib/ui/menu-builder.ts +224 -0
  348. package/src/lib/ui/notifications/NotificationBanner.svelte +137 -0
  349. package/src/lib/ui/notifications/notification-plugin.ts +29 -0
  350. package/src/lib/ui/notifications/notification-state.svelte.ts +9 -0
  351. package/src/lib/ui/plugin-manager-panel-plugin.ts +26 -0
  352. package/src/lib/ui/plugin-state.svelte.ts +62 -0
  353. package/src/lib/ui/select-commands.ts +75 -0
  354. package/src/lib/ui/theme-plugin.ts +18 -0
  355. package/src/lib/ui/theme.svelte.ts +45 -0
  356. package/src/lib/ui/theme.test.ts +51 -0
  357. package/src/lib/ui/toolbar-config.ts +90 -0
  358. package/src/lib/ui/toolbar-plugin.ts +39 -0
  359. package/src/lib/ui/view-commands.ts +629 -0
  360. package/src/lib/variants/BisectionExportDialog.svelte +500 -0
  361. package/src/lib/variants/VariantPanel.svelte +822 -0
  362. package/src/lib/variants/bisection-export.test.ts +113 -0
  363. package/src/lib/variants/bisection-export.ts +148 -0
  364. package/src/lib/variants/palette-extraction.test.ts +111 -0
  365. package/src/lib/variants/palette-extraction.ts +84 -0
  366. package/src/lib/variants/palette-interpolation.test.ts +113 -0
  367. package/src/lib/variants/palette-interpolation.ts +87 -0
  368. package/src/lib/variants/palette-swap.test.ts +101 -0
  369. package/src/lib/variants/palette-swap.ts +114 -0
  370. package/src/lib/variants/variant-commands.ts +594 -0
  371. package/src/lib/variants/variant-panel-plugin.ts +27 -0
  372. package/src/lib/variants/variant-randomizer.ts +101 -0
  373. package/src/lib/variants/variant-state.svelte.ts +166 -0
  374. package/src/lib/variants/variant-state.test.ts +138 -0
  375. package/src/main.ts +14 -0
  376. package/src/vite-env.d.ts +3 -0
  377. package/svelte.config.js +2 -0
  378. package/todo/.done/audit-design-decisions.md +812 -0
  379. package/todo/.done/audit-implementation-plan.md +1235 -0
  380. package/todo/.done/happy-path-polish.md +177 -0
  381. package/todo/.done/pixelweaver-full-build.md +937 -0
  382. package/todo/.done/server-multi-frame-design.md +405 -0
  383. package/todo/.done/typed-dispatcher-design.md +435 -0
  384. package/todo/.done/unified-toolbar-and-action-system.md +323 -0
  385. package/todo/.obsolete/comprehensive-audit-obsolete-items.md +33 -0
  386. package/todo/.obsolete/tauri-desktop-bundle.md +424 -0
  387. package/todo/comprehensive-audit.md +1085 -0
  388. package/tsconfig.app.json +26 -0
  389. package/tsconfig.json +7 -0
  390. package/tsconfig.node.json +20 -0
  391. package/uv.lock +1167 -0
  392. package/vite.config.ts +32 -0
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bash
2
+ # Counts Wave A target rule violations (no-non-null-assertion + restrict-template-expressions)
3
+ # Usage: ./scripts/eslint-wave-a-status.sh [path...]
4
+ # Without args, checks src/ plugins/
5
+ set -euo pipefail
6
+ PATHS=("$@")
7
+ if [ ${#PATHS[@]} -eq 0 ]; then
8
+ PATHS=(src/ plugins/)
9
+ fi
10
+ npx eslint "${PATHS[@]}" -f json 2>/dev/null | node -e '
11
+ let input = "";
12
+ process.stdin.on("data", d => input += d);
13
+ process.stdin.on("end", () => {
14
+ const data = JSON.parse(input);
15
+ let non = 0, tpl = 0, other = 0, total = 0;
16
+ for (const f of data) for (const m of f.messages) {
17
+ if (m.severity !== 2) continue;
18
+ total++;
19
+ if (m.ruleId === "@typescript-eslint/no-non-null-assertion") non++;
20
+ else if (m.ruleId === "@typescript-eslint/restrict-template-expressions") tpl++;
21
+ else other++;
22
+ }
23
+ console.log(`total=${total} non-null=${non} template=${tpl} other=${other}`);
24
+ });
25
+ '
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Apply targeted fixes for noPropertyAccessFromIndexSignature (TS4111) errors.
4
+
5
+ TS4111 format:
6
+ path(line,col): error TS4111: Property 'foo' comes from an index
7
+ signature, so it must be accessed with ['foo'].
8
+
9
+ At the reported column in the source line, we expect either:
10
+ - `.foo` -> replace with `["foo"]`
11
+ - `?.foo` -> replace with `?.["foo"]` (optional chaining form)
12
+
13
+ The column from tsc points to the start of the property name `foo`, so we
14
+ look back to see if the preceding character is `.`. If so we rewrite it.
15
+ We process columns right-to-left on the same line so earlier edits don't
16
+ shift later positions.
17
+
18
+ Usage: scripts/fix-index-signature-access.py <tsc-errors.log> [--apply]
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import re
23
+ import sys
24
+ from pathlib import Path
25
+ from collections import defaultdict
26
+
27
+ ERROR_LINE_RE = re.compile(
28
+ r"^(?P<path>[^(]+)\((?P<line>\d+),(?P<col>\d+)\): error TS4111: "
29
+ r"Property '(?P<prop>[^']+)' comes from an index signature"
30
+ )
31
+
32
+
33
+ def parse_errors(log_path: Path) -> dict[Path, list[tuple[int, int, str]]]:
34
+ errs: dict[Path, list[tuple[int, int, str]]] = defaultdict(list)
35
+ for raw in log_path.read_text().splitlines():
36
+ m = ERROR_LINE_RE.match(raw)
37
+ if not m:
38
+ continue
39
+ path = Path(m.group("path"))
40
+ errs[path].append((int(m.group("line")), int(m.group("col")), m.group("prop")))
41
+ return errs
42
+
43
+
44
+ def fix_line(line: str, col: int, prop: str) -> str | None:
45
+ """
46
+ col is 1-based and points to the first character of `prop`.
47
+ We look at line[col-2] which should be `.`.
48
+ If the char before that is also `?` (optional chain), handle `?.foo`.
49
+ Replace with `["prop"]` or `?.["prop"]` (keep the ? chain).
50
+ """
51
+ # Convert to 0-based
52
+ start = col - 1
53
+ end = start + len(prop)
54
+ if end > len(line):
55
+ return None
56
+ if line[start:end] != prop:
57
+ # The column may be slightly off; try a nearby match
58
+ return None
59
+ if start == 0 or line[start - 1] != ".":
60
+ return None
61
+ # Check for optional chaining ?.
62
+ if start >= 2 and line[start - 2] == "?":
63
+ # `?.foo` -> `?.["foo"]`
64
+ prefix = line[: start - 2]
65
+ suffix = line[end:]
66
+ return prefix + '?.["' + prop + '"]' + suffix
67
+ else:
68
+ # `.foo` -> `["foo"]`
69
+ prefix = line[: start - 1]
70
+ suffix = line[end:]
71
+ return prefix + '["' + prop + '"]' + suffix
72
+
73
+
74
+ def main() -> int:
75
+ if len(sys.argv) < 2:
76
+ print(__doc__)
77
+ return 1
78
+ log = Path(sys.argv[1])
79
+ apply = "--apply" in sys.argv[2:]
80
+ errs = parse_errors(log)
81
+ total_fixed = 0
82
+ total_files = 0
83
+ total_skipped = 0
84
+ for path, positions in errs.items():
85
+ if not path.exists():
86
+ continue
87
+ lines = path.read_text().splitlines(keepends=True)
88
+ # Sort positions: by line ascending, then col DESCENDING so that
89
+ # replacements on the same line don't shift earlier columns.
90
+ positions.sort(key=lambda p: (p[0], -p[1]))
91
+ file_fixed = 0
92
+ file_skipped = 0
93
+ for lineno, col, prop in positions:
94
+ idx = lineno - 1
95
+ if idx >= len(lines):
96
+ continue
97
+ original = lines[idx]
98
+ eol = ""
99
+ if original.endswith("\r\n"):
100
+ eol = "\r\n"
101
+ content = original[:-2]
102
+ elif original.endswith("\n"):
103
+ eol = "\n"
104
+ content = original[:-1]
105
+ else:
106
+ content = original
107
+ new = fix_line(content, col, prop)
108
+ if new is not None and new != content:
109
+ lines[idx] = new + eol
110
+ file_fixed += 1
111
+ else:
112
+ file_skipped += 1
113
+ if file_fixed:
114
+ total_fixed += file_fixed
115
+ total_files += 1
116
+ if apply:
117
+ path.write_text("".join(lines))
118
+ print(f"{'FIX' if apply else 'DRY'} {path}: {file_fixed} line(s), {file_skipped} skipped")
119
+ elif file_skipped:
120
+ print(f"SKIP {path}: {file_skipped} skipped")
121
+ total_skipped += file_skipped
122
+ print(f"Total: {total_fixed} fix(es) across {total_files} file(s), {total_skipped} skipped")
123
+ return 0
124
+
125
+
126
+ if __name__ == "__main__":
127
+ sys.exit(main())
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Apply targeted fixes for noUncheckedIndexedAccess errors in test files.
4
+
5
+ For each line reported by tsc, if the pattern is a simple `arr[N].foo`
6
+ or `arr[key].foo`, it rewrites to `arr[N]!.foo`. It's meant to be run
7
+ iteratively; rerun tsc after each pass.
8
+
9
+ Usage: scripts/fix-unchecked-index.py <tsc-errors.log> [--apply]
10
+
11
+ Without --apply, it only prints what it would do. Operates only on .test.ts
12
+ files by default.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ import sys
18
+ from pathlib import Path
19
+ from collections import defaultdict
20
+
21
+ ERROR_LINE_RE = re.compile(
22
+ r"^(?P<path>[^(]+)\((?P<line>\d+),(?P<col>\d+)\): error TS(?P<code>\d+): (?P<msg>.*)$"
23
+ )
24
+
25
+ # Match identifier/call chain followed by [index] followed by .prop.
26
+ # Captures: group 3 is the `.` after `[...]`. Handles identifiers, member
27
+ # accesses (`a.b.c`), and calls (`a.b()`), including multiple chained calls.
28
+ BRACKET_DOT_RE = re.compile(
29
+ r"([A-Za-z_$][\w$]*(?:\??\.[A-Za-z_$][\w$]*|\([^()]*\))*)\[([^\]\[]+)\](\.)"
30
+ )
31
+
32
+
33
+ def parse_errors(log_path: Path, only_tests: bool) -> dict[Path, list[tuple[int, int]]]:
34
+ errs: dict[Path, list[tuple[int, int]]] = defaultdict(list)
35
+ for raw in log_path.read_text().splitlines():
36
+ m = ERROR_LINE_RE.match(raw)
37
+ if not m:
38
+ continue
39
+ code = m.group("code")
40
+ if code not in ("2532", "18048"):
41
+ continue
42
+ path = Path(m.group("path"))
43
+ if only_tests and not path.name.endswith(".test.ts"):
44
+ continue
45
+ errs[path].append((int(m.group("line")), int(m.group("col"))))
46
+ return errs
47
+
48
+
49
+ def fix_line(line: str, col: int) -> str | None:
50
+ """
51
+ Look for `identifier[idx].` where the `.` (or at least the `]`) is near `col`.
52
+ Add `!` before the `.` so it becomes `identifier[idx]!.`.
53
+ Prefer the match whose `]` is closest to (but before or at) col-1.
54
+ Returns new line or None if no change.
55
+ """
56
+ matches = list(BRACKET_DOT_RE.finditer(line))
57
+ if not matches:
58
+ return None
59
+ # Column from tsc is 1-based and usually points to start of the whole expr
60
+ # or the `.` position; in practice it's the start of the object. Pick the
61
+ # first match at-or-after col-1 if any, else the last one on the line.
62
+ chosen = None
63
+ target = col - 1
64
+ for m in matches:
65
+ if m.start() >= target:
66
+ chosen = m
67
+ break
68
+ if chosen is None:
69
+ chosen = matches[0]
70
+ # Insert ! before the dot. `chosen.end(3)` is index after the dot; the dot
71
+ # is group(3). Position of dot: chosen.start(3).
72
+ dot_pos = chosen.start(3)
73
+ # Ensure there is no ! right before the dot already
74
+ if dot_pos > 0 and line[dot_pos - 1] == "!":
75
+ return None
76
+ return line[:dot_pos] + "!" + line[dot_pos:]
77
+
78
+
79
+ def main() -> int:
80
+ if len(sys.argv) < 2:
81
+ print(__doc__)
82
+ return 1
83
+ log = Path(sys.argv[1])
84
+ apply = "--apply" in sys.argv[2:]
85
+ only_tests = "--all" not in sys.argv[2:]
86
+ errs = parse_errors(log, only_tests=only_tests)
87
+ total_fixed = 0
88
+ total_files = 0
89
+ for path, positions in errs.items():
90
+ if not path.exists():
91
+ continue
92
+ lines = path.read_text().splitlines(keepends=True)
93
+ # Sort positions descending so we fix later cols first on same line.
94
+ # Actually sort by (line, -col) so we process multiple cols on a line
95
+ # from right to left, preserving indices.
96
+ positions.sort(key=lambda p: (p[0], -p[1]))
97
+ file_fixed = 0
98
+ for lineno, col in positions:
99
+ idx = lineno - 1
100
+ if idx >= len(lines):
101
+ continue
102
+ original = lines[idx]
103
+ # Strip trailing newline for regex, reattach after
104
+ eol = ""
105
+ if original.endswith("\r\n"):
106
+ eol = "\r\n"
107
+ content = original[:-2]
108
+ elif original.endswith("\n"):
109
+ eol = "\n"
110
+ content = original[:-1]
111
+ else:
112
+ content = original
113
+ new = fix_line(content, col)
114
+ if new is not None and new != content:
115
+ lines[idx] = new + eol
116
+ file_fixed += 1
117
+ if file_fixed:
118
+ total_fixed += file_fixed
119
+ total_files += 1
120
+ if apply:
121
+ path.write_text("".join(lines))
122
+ print(f"{'FIX' if apply else 'DRY'} {path}: {file_fixed} line(s)")
123
+ print(f"Total: {total_fixed} fix(es) across {total_files} file(s)")
124
+ return 0
125
+
126
+
127
+ if __name__ == "__main__":
128
+ sys.exit(main())
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Fix TS18048/TS2532 by chasing "X is possibly undefined" errors back to a
4
+ variable declaration of the form `const X = <expr>[key];` and appending `!`.
5
+
6
+ Usage: scripts/fix-unchecked-vars.py <tsc-errors.log> [--apply] [--all]
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ import sys
12
+ from pathlib import Path
13
+ from collections import defaultdict
14
+
15
+ ERROR_LINE_RE = re.compile(
16
+ r"^(?P<path>[^(]+)\((?P<line>\d+),(?P<col>\d+)\): error TS(?P<code>\d+): (?P<msg>.*)$"
17
+ )
18
+ POSSIBLY_UNDEF_RE = re.compile(r"'([A-Za-z_$][\w$]*)' is possibly 'undefined'")
19
+
20
+ # const NAME = <expr>[<key>]; or const NAME = <expr>.get(...);
21
+ # Append ! before the trailing ; (or end of statement) if not already present
22
+ DECL_INDEX_RE = re.compile(
23
+ r"^(?P<pre>\s*(?:const|let|var)\s+(?P<name>[A-Za-z_$][\w$]*)\s*=\s*[^;]*?\])(?P<post>\s*;?\s*)$"
24
+ )
25
+ DECL_GET_RE = re.compile(
26
+ r"^(?P<pre>\s*(?:const|let|var)\s+(?P<name>[A-Za-z_$][\w$]*)\s*=\s*[^;]*\.get\([^;]*\))(?P<post>\s*;?\s*)$"
27
+ )
28
+
29
+
30
+ def parse_errors(log_path: Path, only_tests: bool) -> dict[Path, list[tuple[int, str]]]:
31
+ errs: dict[Path, set[tuple[int, str]]] = defaultdict(set)
32
+ for raw in log_path.read_text().splitlines():
33
+ m = ERROR_LINE_RE.match(raw)
34
+ if not m:
35
+ continue
36
+ code = m.group("code")
37
+ if code not in ("2532", "18048"):
38
+ continue
39
+ msg = m.group("msg")
40
+ mm = POSSIBLY_UNDEF_RE.match(msg)
41
+ if not mm:
42
+ continue
43
+ name = mm.group(1)
44
+ path = Path(m.group("path"))
45
+ if only_tests and not path.name.endswith(".test.ts"):
46
+ continue
47
+ errs[path].add((int(m.group("line")), name))
48
+ return {p: sorted(s) for p, s in errs.items()}
49
+
50
+
51
+ def find_decl(lines: list[str], use_line_idx: int, name: str) -> int | None:
52
+ """Search backwards for `const|let|var NAME =` declaration."""
53
+ pattern = re.compile(rf"^\s*(?:const|let|var)\s+{re.escape(name)}\s*=")
54
+ for i in range(use_line_idx, -1, -1):
55
+ if pattern.match(lines[i]):
56
+ return i
57
+ return None
58
+
59
+
60
+ def try_fix_decl_line(content: str) -> str | None:
61
+ # index form
62
+ m = DECL_INDEX_RE.match(content)
63
+ if m:
64
+ pre = m.group("pre")
65
+ post = m.group("post")
66
+ if pre.endswith("!"):
67
+ return None
68
+ return pre + "!" + post
69
+ m = DECL_GET_RE.match(content)
70
+ if m:
71
+ pre = m.group("pre")
72
+ post = m.group("post")
73
+ if pre.endswith("!"):
74
+ return None
75
+ return pre + "!" + post
76
+ return None
77
+
78
+
79
+ def main() -> int:
80
+ if len(sys.argv) < 2:
81
+ print(__doc__)
82
+ return 1
83
+ log = Path(sys.argv[1])
84
+ apply = "--apply" in sys.argv[2:]
85
+ only_tests = "--all" not in sys.argv[2:]
86
+ errs = parse_errors(log, only_tests=only_tests)
87
+ total_fixed = 0
88
+ total_files = 0
89
+ for path, positions in errs.items():
90
+ if not path.exists():
91
+ continue
92
+ lines = path.read_text().splitlines(keepends=True)
93
+ fixed_decls: set[int] = set()
94
+ file_fixed = 0
95
+ for lineno, name in positions:
96
+ idx = lineno - 1
97
+ if idx >= len(lines):
98
+ continue
99
+ decl_idx = find_decl(lines, idx, name)
100
+ if decl_idx is None or decl_idx in fixed_decls:
101
+ continue
102
+ # Strip EOL
103
+ original = lines[decl_idx]
104
+ eol = ""
105
+ if original.endswith("\r\n"):
106
+ eol, content = "\r\n", original[:-2]
107
+ elif original.endswith("\n"):
108
+ eol, content = "\n", original[:-1]
109
+ else:
110
+ content = original
111
+ new = try_fix_decl_line(content)
112
+ if new and new != content:
113
+ lines[decl_idx] = new + eol
114
+ fixed_decls.add(decl_idx)
115
+ file_fixed += 1
116
+ if file_fixed:
117
+ total_fixed += file_fixed
118
+ total_files += 1
119
+ if apply:
120
+ path.write_text("".join(lines))
121
+ print(f"{'FIX' if apply else 'DRY'} {path}: {file_fixed} decl(s)")
122
+ print(f"Total: {total_fixed} fix(es) across {total_files} file(s)")
123
+ return 0
124
+
125
+
126
+ if __name__ == "__main__":
127
+ sys.exit(main())
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Mechanically replace `x!.prop` with `x?.prop` in expect() contexts in test files.
4
+ Safe when the result is immediately chained with .toBe/.toEqual/etc, because
5
+ `expect(undefined).toBe(expected)` still fails the assertion cleanly.
6
+
7
+ Usage: fix-wave-a-bangs.py <file>...
8
+ """
9
+ import re
10
+ import sys
11
+
12
+ # Pattern: something like "arr[0]!" or "foo.bar!" or "foo()!" followed by .prop
13
+ # We only rewrite when the `!.` is followed by an identifier (method/prop access).
14
+ # This avoids touching `x!` standalone statements.
15
+
16
+ BANG_DOT = re.compile(r'!\.')
17
+
18
+ def fix_file(path: str) -> int:
19
+ with open(path, 'r') as f:
20
+ content = f.read()
21
+ # Only rewrite lines that start with whitespace + "expect(" to be safe for tests.
22
+ # Actually also handle const x = foo[0]!.bar patterns. Let's be more aggressive
23
+ # and replace ALL `!.` occurrences, then let tsc verify.
24
+ new_content, n = BANG_DOT.subn('?.', content)
25
+ if n > 0:
26
+ with open(path, 'w') as f:
27
+ f.write(new_content)
28
+ return n
29
+
30
+ if __name__ == '__main__':
31
+ total = 0
32
+ for p in sys.argv[1:]:
33
+ n = fix_file(p)
34
+ print(f'{p}: {n} replacements')
35
+ total += n
36
+ print(f'TOTAL: {total}')
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Wrap template-literal interpolations in String(...) ONLY at locations flagged
4
+ by eslint's restrict-template-expressions rule. Reads JSON ESLint output from
5
+ stdin or generates it itself.
6
+
7
+ Usage:
8
+ ./scripts/fix-wave-a-templates.py <path> [<path>...]
9
+
10
+ The script:
11
+ 1. Runs `npx eslint <paths> -f json` to discover restrict-template-expressions
12
+ errors with their line:column locations.
13
+ 2. For each file, reads its content and at each flagged column finds the
14
+ enclosing ${...} expression and wraps the inner expression in String(...).
15
+ 3. Writes the file back if any changes were made.
16
+
17
+ Skips expressions that are already inside String(), are string literals, or
18
+ contain `instanceof Error` (likely error-handling already correct).
19
+ """
20
+ import json
21
+ import subprocess
22
+ import sys
23
+ from collections import defaultdict
24
+
25
+
26
+ def run_eslint(paths: list[str]) -> dict:
27
+ res = subprocess.run(
28
+ ['npx', 'eslint', *paths, '-f', 'json'],
29
+ capture_output=True, text=True
30
+ )
31
+ return json.loads(res.stdout)
32
+
33
+
34
+ def collect_errors(eslint_data) -> dict:
35
+ by_file = defaultdict(list)
36
+ for f in eslint_data:
37
+ for m in f['messages']:
38
+ if m.get('severity') != 2: continue
39
+ if m.get('ruleId') != '@typescript-eslint/restrict-template-expressions': continue
40
+ by_file[f['filePath']].append((m['line'], m['column']))
41
+ return by_file
42
+
43
+
44
+ def fix_file(path: str, locations: list[tuple[int, int]]) -> int:
45
+ with open(path, 'r') as f:
46
+ lines = f.readlines()
47
+ # Process from bottom to top so column offsets stay valid for earlier edits.
48
+ locations = sorted(set(locations), key=lambda lc: (-lc[0], -lc[1]))
49
+ fixed = 0
50
+ for line_no, col in locations:
51
+ idx = line_no - 1
52
+ if idx >= len(lines): continue
53
+ line = lines[idx]
54
+ # The reported column is 1-based and points at the start of the
55
+ # offending expression *inside* the ${...}. Walk left to find the '$'
56
+ # that opened it, then find the matching '}'.
57
+ c = col - 1 # 0-based
58
+ # find '${' just before c, looking back at most 200 chars
59
+ start = -1
60
+ for k in range(c - 1, max(0, c - 200) - 1, -1):
61
+ if line[k] == '{' and k > 0 and line[k-1] == '$':
62
+ start = k - 1 # index of '$'
63
+ break
64
+ if start == -1:
65
+ continue
66
+ # find matching '}' from c onward
67
+ depth = 1
68
+ j = start + 2
69
+ while j < len(line) and depth > 0:
70
+ if line[j] == '{': depth += 1
71
+ elif line[j] == '}':
72
+ depth -= 1
73
+ if depth == 0: break
74
+ j += 1
75
+ if depth != 0:
76
+ continue
77
+ expr = line[start+2:j]
78
+ stripped = expr.strip()
79
+ if stripped.startswith('String(') and stripped.endswith(')'):
80
+ continue
81
+ if stripped.startswith(("'", '"', '`')):
82
+ continue
83
+ if 'instanceof Error' in stripped:
84
+ continue
85
+ # Wrap
86
+ new_segment = '${String(' + stripped + ')}'
87
+ new_line = line[:start] + new_segment + line[j+1:]
88
+ lines[idx] = new_line
89
+ fixed += 1
90
+ if fixed > 0:
91
+ with open(path, 'w') as f:
92
+ f.writelines(lines)
93
+ return fixed
94
+
95
+
96
+ if __name__ == '__main__':
97
+ if len(sys.argv) < 2:
98
+ print("Usage: fix-wave-a-templates.py <path> [<path>...]", file=sys.stderr)
99
+ sys.exit(2)
100
+ paths = sys.argv[1:]
101
+ data = run_eslint(paths)
102
+ by_file = collect_errors(data)
103
+ total = 0
104
+ for fpath, locs in by_file.items():
105
+ n = fix_file(fpath, locs)
106
+ print(f'{fpath}: {n} wrappings')
107
+ total += n
108
+ print(f'TOTAL: {total}')