web-a2e 1.0.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 (295) hide show
  1. package/.clangd +5 -0
  2. package/.mcp.json +12 -0
  3. package/CLAUDE.md +362 -0
  4. package/CMakeLists.txt +774 -0
  5. package/LICENSE +21 -0
  6. package/README.md +392 -0
  7. package/build-wasm/generated/roms.cpp +2447 -0
  8. package/docker-compose.staging.yml +9 -0
  9. package/docs/basic-rom-disassembly.md +6663 -0
  10. package/docs/softswitch-comparison.md +273 -0
  11. package/docs/thunderclock-debug.md +89 -0
  12. package/examples/cube.bas +72 -0
  13. package/examples/hello.s +55 -0
  14. package/examples/scroll.s +140 -0
  15. package/package.json +18 -0
  16. package/public/assets/apple-logo-old.png +0 -0
  17. package/public/assets/apple-logo.png +0 -0
  18. package/public/assets/drive-closed-light-on.png +0 -0
  19. package/public/assets/drive-closed.png +0 -0
  20. package/public/assets/drive-open-light-on.png +0 -0
  21. package/public/assets/drive-open.png +0 -0
  22. package/public/audio-worklet.js +82 -0
  23. package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
  24. package/public/disks/ProDOS 2.4.3.po +0 -0
  25. package/public/disks/h32mb.2mg +0 -0
  26. package/public/disks/library.json +26 -0
  27. package/public/docs/llms/llm-assembler.txt +90 -0
  28. package/public/docs/llms/llm-basic-program.txt +256 -0
  29. package/public/docs/llms/llm-disk-drives.txt +72 -0
  30. package/public/docs/llms/llm-file-explorer.txt +50 -0
  31. package/public/docs/llms/llm-hard-drives.txt +80 -0
  32. package/public/docs/llms/llm-main.txt +51 -0
  33. package/public/docs/llms/llm-slot-configuration.txt +66 -0
  34. package/public/icons/icon-192.svg +4 -0
  35. package/public/icons/icon-512.svg +4 -0
  36. package/public/index.html +661 -0
  37. package/public/llms.txt +49 -0
  38. package/public/manifest.json +29 -0
  39. package/public/shaders/burnin.glsl +22 -0
  40. package/public/shaders/crt.glsl +706 -0
  41. package/public/shaders/edge.glsl +109 -0
  42. package/public/shaders/vertex.glsl +8 -0
  43. package/public/sw.js +186 -0
  44. package/roms/341-0027.bin +0 -0
  45. package/roms/341-0160-A-US-UK.bin +0 -0
  46. package/roms/341-0160-A.bin +0 -0
  47. package/roms/342-0273-A-US-UK.bin +0 -0
  48. package/roms/342-0349-B-C0-FF.bin +0 -0
  49. package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
  50. package/roms/Thunderclock Plus ROM.bin +0 -0
  51. package/scripts/generate_roms.sh +69 -0
  52. package/src/bindings/wasm_interface.cpp +1940 -0
  53. package/src/core/assembler/assembler.cpp +1239 -0
  54. package/src/core/assembler/assembler.hpp +115 -0
  55. package/src/core/audio/audio.cpp +160 -0
  56. package/src/core/audio/audio.hpp +81 -0
  57. package/src/core/basic/basic_detokenizer.cpp +436 -0
  58. package/src/core/basic/basic_detokenizer.hpp +41 -0
  59. package/src/core/basic/basic_tokenizer.cpp +286 -0
  60. package/src/core/basic/basic_tokenizer.hpp +26 -0
  61. package/src/core/basic/basic_tokens.hpp +295 -0
  62. package/src/core/cards/disk2_card.cpp +568 -0
  63. package/src/core/cards/disk2_card.hpp +316 -0
  64. package/src/core/cards/expansion_card.hpp +185 -0
  65. package/src/core/cards/mockingboard/ay8910.cpp +616 -0
  66. package/src/core/cards/mockingboard/ay8910.hpp +159 -0
  67. package/src/core/cards/mockingboard/via6522.cpp +530 -0
  68. package/src/core/cards/mockingboard/via6522.hpp +163 -0
  69. package/src/core/cards/mockingboard_card.cpp +312 -0
  70. package/src/core/cards/mockingboard_card.hpp +159 -0
  71. package/src/core/cards/mouse_card.cpp +654 -0
  72. package/src/core/cards/mouse_card.hpp +190 -0
  73. package/src/core/cards/smartport/block_device.cpp +202 -0
  74. package/src/core/cards/smartport/block_device.hpp +60 -0
  75. package/src/core/cards/smartport/smartport_card.cpp +603 -0
  76. package/src/core/cards/smartport/smartport_card.hpp +120 -0
  77. package/src/core/cards/thunderclock_card.cpp +237 -0
  78. package/src/core/cards/thunderclock_card.hpp +122 -0
  79. package/src/core/cpu/cpu6502.cpp +1609 -0
  80. package/src/core/cpu/cpu6502.hpp +203 -0
  81. package/src/core/debug/condition_evaluator.cpp +470 -0
  82. package/src/core/debug/condition_evaluator.hpp +87 -0
  83. package/src/core/disassembler/disassembler.cpp +552 -0
  84. package/src/core/disassembler/disassembler.hpp +171 -0
  85. package/src/core/disk-image/disk_image.hpp +267 -0
  86. package/src/core/disk-image/dsk_disk_image.cpp +827 -0
  87. package/src/core/disk-image/dsk_disk_image.hpp +204 -0
  88. package/src/core/disk-image/gcr_encoding.cpp +147 -0
  89. package/src/core/disk-image/gcr_encoding.hpp +78 -0
  90. package/src/core/disk-image/woz_disk_image.cpp +1049 -0
  91. package/src/core/disk-image/woz_disk_image.hpp +343 -0
  92. package/src/core/emulator.cpp +2126 -0
  93. package/src/core/emulator.hpp +434 -0
  94. package/src/core/filesystem/dos33.cpp +178 -0
  95. package/src/core/filesystem/dos33.hpp +66 -0
  96. package/src/core/filesystem/pascal.cpp +262 -0
  97. package/src/core/filesystem/pascal.hpp +87 -0
  98. package/src/core/filesystem/prodos.cpp +369 -0
  99. package/src/core/filesystem/prodos.hpp +119 -0
  100. package/src/core/input/keyboard.cpp +227 -0
  101. package/src/core/input/keyboard.hpp +111 -0
  102. package/src/core/mmu/mmu.cpp +1387 -0
  103. package/src/core/mmu/mmu.hpp +236 -0
  104. package/src/core/types.hpp +196 -0
  105. package/src/core/video/video.cpp +680 -0
  106. package/src/core/video/video.hpp +156 -0
  107. package/src/css/assembler-editor.css +1617 -0
  108. package/src/css/base.css +470 -0
  109. package/src/css/basic-debugger.css +791 -0
  110. package/src/css/basic-editor.css +792 -0
  111. package/src/css/controls.css +783 -0
  112. package/src/css/cpu-debugger.css +1413 -0
  113. package/src/css/debug-base.css +160 -0
  114. package/src/css/debug-windows.css +6455 -0
  115. package/src/css/disk-drives.css +406 -0
  116. package/src/css/documentation.css +392 -0
  117. package/src/css/file-explorer.css +867 -0
  118. package/src/css/hard-drive.css +180 -0
  119. package/src/css/layout.css +217 -0
  120. package/src/css/memory-windows.css +798 -0
  121. package/src/css/modals.css +510 -0
  122. package/src/css/monitor.css +425 -0
  123. package/src/css/release-notes.css +101 -0
  124. package/src/css/responsive.css +400 -0
  125. package/src/css/rule-builder.css +340 -0
  126. package/src/css/save-states.css +201 -0
  127. package/src/css/settings-windows.css +1231 -0
  128. package/src/css/window-switcher.css +150 -0
  129. package/src/js/agent/agent-manager.js +643 -0
  130. package/src/js/agent/agent-tools.js +293 -0
  131. package/src/js/agent/agent-version-tools.js +131 -0
  132. package/src/js/agent/assembler-tools.js +357 -0
  133. package/src/js/agent/basic-program-tools.js +894 -0
  134. package/src/js/agent/disk-tools.js +417 -0
  135. package/src/js/agent/file-explorer-tools.js +269 -0
  136. package/src/js/agent/index.js +13 -0
  137. package/src/js/agent/main-tools.js +222 -0
  138. package/src/js/agent/slot-tools.js +303 -0
  139. package/src/js/agent/smartport-tools.js +257 -0
  140. package/src/js/agent/window-tools.js +80 -0
  141. package/src/js/audio/audio-driver.js +417 -0
  142. package/src/js/audio/audio-worklet.js +85 -0
  143. package/src/js/audio/index.js +8 -0
  144. package/src/js/config/default-layout.js +34 -0
  145. package/src/js/config/version.js +8 -0
  146. package/src/js/data/apple2-rom-routines.js +577 -0
  147. package/src/js/debug/assembler-editor-window.js +2993 -0
  148. package/src/js/debug/basic-breakpoint-manager.js +529 -0
  149. package/src/js/debug/basic-program-parser.js +436 -0
  150. package/src/js/debug/basic-program-window.js +2594 -0
  151. package/src/js/debug/basic-variable-inspector.js +447 -0
  152. package/src/js/debug/breakpoint-manager.js +472 -0
  153. package/src/js/debug/cpu-debugger-window.js +2396 -0
  154. package/src/js/debug/index.js +22 -0
  155. package/src/js/debug/label-manager.js +238 -0
  156. package/src/js/debug/memory-browser-window.js +416 -0
  157. package/src/js/debug/memory-heat-map-window.js +481 -0
  158. package/src/js/debug/memory-map-window.js +206 -0
  159. package/src/js/debug/mockingboard-window.js +882 -0
  160. package/src/js/debug/mouse-card-window.js +355 -0
  161. package/src/js/debug/rule-builder-window.js +648 -0
  162. package/src/js/debug/soft-switch-window.js +458 -0
  163. package/src/js/debug/stack-viewer-window.js +221 -0
  164. package/src/js/debug/symbols.js +416 -0
  165. package/src/js/debug/trace-panel.js +291 -0
  166. package/src/js/debug/zero-page-watch-window.js +297 -0
  167. package/src/js/disk-manager/disk-drives-window.js +212 -0
  168. package/src/js/disk-manager/disk-operations.js +284 -0
  169. package/src/js/disk-manager/disk-persistence.js +301 -0
  170. package/src/js/disk-manager/disk-surface-renderer.js +388 -0
  171. package/src/js/disk-manager/drive-sounds.js +139 -0
  172. package/src/js/disk-manager/hard-drive-manager.js +481 -0
  173. package/src/js/disk-manager/hard-drive-persistence.js +187 -0
  174. package/src/js/disk-manager/hard-drive-window.js +57 -0
  175. package/src/js/disk-manager/index.js +890 -0
  176. package/src/js/display/display-settings-window.js +383 -0
  177. package/src/js/display/index.js +10 -0
  178. package/src/js/display/screen-window.js +342 -0
  179. package/src/js/display/webgl-renderer.js +705 -0
  180. package/src/js/file-explorer/disassembler.js +574 -0
  181. package/src/js/file-explorer/dos33.js +266 -0
  182. package/src/js/file-explorer/file-viewer.js +359 -0
  183. package/src/js/file-explorer/index.js +1261 -0
  184. package/src/js/file-explorer/prodos.js +549 -0
  185. package/src/js/file-explorer/utils.js +67 -0
  186. package/src/js/help/documentation-window.js +1096 -0
  187. package/src/js/help/index.js +10 -0
  188. package/src/js/help/release-notes-window.js +85 -0
  189. package/src/js/help/release-notes.js +612 -0
  190. package/src/js/input/gamepad-handler.js +176 -0
  191. package/src/js/input/index.js +12 -0
  192. package/src/js/input/input-handler.js +396 -0
  193. package/src/js/input/joystick-window.js +404 -0
  194. package/src/js/input/mouse-handler.js +99 -0
  195. package/src/js/input/text-selection.js +462 -0
  196. package/src/js/main.js +653 -0
  197. package/src/js/state/index.js +15 -0
  198. package/src/js/state/save-states-window.js +393 -0
  199. package/src/js/state/state-manager.js +409 -0
  200. package/src/js/state/state-persistence.js +218 -0
  201. package/src/js/ui/confirm.js +43 -0
  202. package/src/js/ui/disk-drive-positioner.js +347 -0
  203. package/src/js/ui/reminder-controller.js +129 -0
  204. package/src/js/ui/slot-configuration-window.js +560 -0
  205. package/src/js/ui/theme-manager.js +61 -0
  206. package/src/js/ui/toast.js +44 -0
  207. package/src/js/ui/ui-controller.js +897 -0
  208. package/src/js/ui/window-switcher.js +275 -0
  209. package/src/js/utils/basic-autocomplete.js +832 -0
  210. package/src/js/utils/basic-highlighting.js +473 -0
  211. package/src/js/utils/basic-tokenizer.js +153 -0
  212. package/src/js/utils/basic-tokens.js +117 -0
  213. package/src/js/utils/constants.js +28 -0
  214. package/src/js/utils/indexeddb-helper.js +225 -0
  215. package/src/js/utils/merlin-editor-support.js +905 -0
  216. package/src/js/utils/merlin-highlighting.js +551 -0
  217. package/src/js/utils/storage.js +125 -0
  218. package/src/js/utils/string-utils.js +19 -0
  219. package/src/js/utils/wasm-memory.js +54 -0
  220. package/src/js/windows/base-window.js +690 -0
  221. package/src/js/windows/index.js +9 -0
  222. package/src/js/windows/window-manager.js +375 -0
  223. package/tests/catch2/catch.hpp +17976 -0
  224. package/tests/common/basic_program_builder.cpp +119 -0
  225. package/tests/common/basic_program_builder.hpp +209 -0
  226. package/tests/common/disk_image_builder.cpp +444 -0
  227. package/tests/common/disk_image_builder.hpp +141 -0
  228. package/tests/common/test_helpers.hpp +118 -0
  229. package/tests/gcr/gcr-test.cpp +142 -0
  230. package/tests/integration/check-rom.js +70 -0
  231. package/tests/integration/compare-boot.js +239 -0
  232. package/tests/integration/crash-trace.js +102 -0
  233. package/tests/integration/disk-boot-test.js +264 -0
  234. package/tests/integration/memory-crash.js +108 -0
  235. package/tests/integration/nibble-read-test.js +249 -0
  236. package/tests/integration/phase-test.js +159 -0
  237. package/tests/integration/test_emulator.cpp +291 -0
  238. package/tests/integration/test_emulator_basic.cpp +91 -0
  239. package/tests/integration/test_emulator_debug.cpp +344 -0
  240. package/tests/integration/test_emulator_disk.cpp +153 -0
  241. package/tests/integration/test_emulator_state.cpp +163 -0
  242. package/tests/klaus/6502_functional_test.bin +0 -0
  243. package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
  244. package/tests/klaus/klaus_6502_test.cpp +184 -0
  245. package/tests/klaus/klaus_65c02_test.cpp +197 -0
  246. package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
  247. package/tests/thunderclock/thunderclock_test.cpp +550 -0
  248. package/tests/unit/test_assembler.cpp +521 -0
  249. package/tests/unit/test_audio.cpp +196 -0
  250. package/tests/unit/test_ay8910.cpp +311 -0
  251. package/tests/unit/test_basic_detokenizer.cpp +265 -0
  252. package/tests/unit/test_basic_tokenizer.cpp +382 -0
  253. package/tests/unit/test_block_device.cpp +259 -0
  254. package/tests/unit/test_condition_evaluator.cpp +219 -0
  255. package/tests/unit/test_cpu6502.cpp +1301 -0
  256. package/tests/unit/test_cpu_addressing.cpp +361 -0
  257. package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
  258. package/tests/unit/test_cpu_decimal.cpp +166 -0
  259. package/tests/unit/test_cpu_interrupts.cpp +285 -0
  260. package/tests/unit/test_disassembler.cpp +323 -0
  261. package/tests/unit/test_disk2_card.cpp +330 -0
  262. package/tests/unit/test_dos33.cpp +273 -0
  263. package/tests/unit/test_dsk_disk_image.cpp +315 -0
  264. package/tests/unit/test_expansion_card.cpp +178 -0
  265. package/tests/unit/test_gcr_encoding.cpp +232 -0
  266. package/tests/unit/test_keyboard.cpp +262 -0
  267. package/tests/unit/test_mmu.cpp +555 -0
  268. package/tests/unit/test_mmu_slots.cpp +323 -0
  269. package/tests/unit/test_mockingboard.cpp +352 -0
  270. package/tests/unit/test_mouse_card.cpp +386 -0
  271. package/tests/unit/test_pascal.cpp +248 -0
  272. package/tests/unit/test_prodos.cpp +259 -0
  273. package/tests/unit/test_smartport_card.cpp +321 -0
  274. package/tests/unit/test_thunderclock.cpp +354 -0
  275. package/tests/unit/test_via6522.cpp +323 -0
  276. package/tests/unit/test_video.cpp +319 -0
  277. package/tests/unit/test_woz_disk_image.cpp +257 -0
  278. package/vite.config.js +96 -0
  279. package/wiki/AI-Agent.md +372 -0
  280. package/wiki/Architecture-Overview.md +303 -0
  281. package/wiki/Audio-System.md +449 -0
  282. package/wiki/CPU-Emulation.md +477 -0
  283. package/wiki/Debugger.md +516 -0
  284. package/wiki/Disk-Drives.md +161 -0
  285. package/wiki/Disk-System-Internals.md +547 -0
  286. package/wiki/Display-Settings.md +88 -0
  287. package/wiki/Expansion-Slots.md +187 -0
  288. package/wiki/File-Explorer.md +259 -0
  289. package/wiki/Getting-Started.md +156 -0
  290. package/wiki/Home.md +69 -0
  291. package/wiki/Input-Devices.md +183 -0
  292. package/wiki/Keyboard-Shortcuts.md +158 -0
  293. package/wiki/Memory-System.md +364 -0
  294. package/wiki/Save-States.md +172 -0
  295. package/wiki/Video-Rendering.md +658 -0
@@ -0,0 +1,560 @@
1
+ /*
2
+ * slot-configuration-window.js - Expansion slot configuration window
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ import { BaseWindow } from "../windows/base-window.js";
9
+ import { showToast } from "./toast.js";
10
+
11
+ /**
12
+ * SlotConfigurationWindow - Configure Apple IIe expansion slots
13
+ * Visual drag-and-drop card tray and motherboard slot layout
14
+ */
15
+ export class SlotConfigurationWindow extends BaseWindow {
16
+ constructor(wasmModule, onResetCallback) {
17
+ const maxHeight = 625;
18
+ super({
19
+ id: "slot-configuration",
20
+ title: "Expansion Slots",
21
+ minWidth: 450,
22
+ minHeight: maxHeight,
23
+ defaultWidth: 450,
24
+ defaultHeight: maxHeight,
25
+ resizeDirections: [],
26
+ });
27
+
28
+ this.wasmModule = wasmModule;
29
+ this.onResetCallback = onResetCallback;
30
+
31
+ // Available card types with accent colors
32
+ this.cards = [
33
+ { id: "disk2", name: "Disk II", color: "green" },
34
+ { id: "mockingboard", name: "Mockingboard", color: "purple" },
35
+ { id: "thunderclock", name: "Thunderclock", color: "orange" },
36
+ { id: "mouse", name: "Mouse Card", color: "blue" },
37
+ { id: "smartport", name: "SmartPort", color: "red" },
38
+ ];
39
+
40
+ // Card icon SVGs (simple representations)
41
+ this.cardIcons = {
42
+ disk2: `<svg viewBox="0 0 24 24" width="20" height="20"><rect x="3" y="4" width="18" height="16" rx="2" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="12" cy="12" r="4" fill="none" stroke="currentColor" stroke-width="1.2"/><circle cx="12" cy="12" r="1" fill="currentColor"/></svg>`,
43
+ mockingboard: `<svg viewBox="0 0 24 24" width="20" height="20"><rect x="3" y="6" width="18" height="12" rx="1" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="8" cy="12" r="2" fill="none" stroke="currentColor" stroke-width="1"/><circle cx="16" cy="12" r="2" fill="none" stroke="currentColor" stroke-width="1"/></svg>`,
44
+ thunderclock: `<svg viewBox="0 0 24 24" width="20" height="20"><circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="1.5"/><line x1="12" y1="12" x2="12" y2="7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="12" y1="12" x2="16" y2="12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
45
+ mouse: `<svg viewBox="0 0 24 24" width="20" height="20"><rect x="6" y="3" width="12" height="18" rx="6" fill="none" stroke="currentColor" stroke-width="1.5"/><line x1="12" y1="3" x2="12" y2="10" stroke="currentColor" stroke-width="1"/></svg>`,
46
+ smartport: `<svg viewBox="0 0 24 24" width="20" height="20"><rect x="4" y="3" width="16" height="18" rx="2" fill="none" stroke="currentColor" stroke-width="1.5"/><rect x="7" y="6" width="10" height="3" rx="0.5" fill="none" stroke="currentColor" stroke-width="0.8"/><rect x="7" y="11" width="10" height="3" rx="0.5" fill="none" stroke="currentColor" stroke-width="0.8"/><circle cx="12" cy="18" r="1" fill="currentColor"/></svg>`,
47
+ };
48
+
49
+ // Slot metadata
50
+ this.slots = [
51
+ { slot: 1, label: "Slot 1", available: [], note: "Printer / Serial" },
52
+ {
53
+ slot: 2,
54
+ label: "Slot 2",
55
+ available: ["smartport"],
56
+ note: "Serial / Modem",
57
+ },
58
+ {
59
+ slot: 3,
60
+ label: "Slot 3",
61
+ available: [],
62
+ note: "80-Column (Built-in)",
63
+ fixed: true,
64
+ },
65
+ {
66
+ slot: 4,
67
+ label: "Slot 4",
68
+ available: ["mockingboard", "mouse", "smartport"],
69
+ note: "Sound / Mouse",
70
+ },
71
+ {
72
+ slot: 5,
73
+ label: "Slot 5",
74
+ available: ["thunderclock", "smartport"],
75
+ note: "Clock / Drive",
76
+ },
77
+ { slot: 6, label: "Slot 6", available: ["disk2"], note: "Disk drives" },
78
+ {
79
+ slot: 7,
80
+ label: "Slot 7",
81
+ available: ["thunderclock", "smartport"],
82
+ note: "RAM / Clock",
83
+ },
84
+ ];
85
+
86
+ // Current slot assignments (working state for drag-and-drop)
87
+ this.slotAssignments = {};
88
+
89
+ // Track pending changes vs the actual WASM state
90
+ this.pendingChanges = {};
91
+ this.hasChanges = false;
92
+
93
+ // Drag state
94
+ this.dragState = null;
95
+ this.ghostElement = null;
96
+
97
+ // Bind drag handlers
98
+ this.handleDragMove = this.handleDragMove.bind(this);
99
+ this.handleDragEnd = this.handleDragEnd.bind(this);
100
+ }
101
+
102
+ renderContent() {
103
+ return `
104
+ <div class="slot-config-content">
105
+ <div class="slot-section">
106
+ <div class="slot-section-title">Available Cards</div>
107
+ <div class="card-tray" id="card-tray"></div>
108
+ </div>
109
+ <div class="slot-section">
110
+ <div class="slot-section-title">Motherboard Slots</div>
111
+ <div class="motherboard-slots" id="motherboard-slots"></div>
112
+ </div>
113
+ <div class="slot-footer">
114
+ <button id="slot-apply-btn" class="slot-apply-btn" disabled>Apply &amp; Reset</button>
115
+ </div>
116
+ </div>`;
117
+ }
118
+
119
+ setupContentEventListeners() {
120
+ // Apply button
121
+ const applyBtn = this.contentElement.querySelector("#slot-apply-btn");
122
+ if (applyBtn) {
123
+ applyBtn.addEventListener("click", () => this.applyChanges());
124
+ }
125
+ }
126
+
127
+ create() {
128
+ super.create();
129
+ this.applyInitialSettings();
130
+ this.initSlotAssignments();
131
+ this.setupContentEventListeners();
132
+ this.updateView();
133
+ }
134
+
135
+ /**
136
+ * Initialize working slot assignments from current WASM state or saved settings
137
+ */
138
+ initSlotAssignments() {
139
+ this.slotAssignments = {};
140
+ for (const slotInfo of this.slots) {
141
+ if (slotInfo.fixed) continue;
142
+ const card = this.getCurrentSlotCard(slotInfo.slot);
143
+ if (card && card !== "empty") {
144
+ this.slotAssignments[slotInfo.slot] = card;
145
+ }
146
+ }
147
+ // Store the original state for change detection
148
+ this.originalAssignments = { ...this.slotAssignments };
149
+ }
150
+
151
+ /**
152
+ * Get the list of cards not currently installed in any slot
153
+ */
154
+ getAvailableCards() {
155
+ const installed = new Set(Object.values(this.slotAssignments));
156
+ return this.cards.filter((c) => !installed.has(c.id));
157
+ }
158
+
159
+ /**
160
+ * Re-render the card tray and motherboard slots
161
+ */
162
+ updateView() {
163
+ this.renderCardTray();
164
+ this.renderMotherboardSlots();
165
+ this.detectChanges();
166
+ this.updateUI();
167
+ }
168
+
169
+ renderCardTray() {
170
+ const tray = this.contentElement.querySelector("#card-tray");
171
+ if (!tray) return;
172
+
173
+ const available = this.getAvailableCards();
174
+ if (available.length === 0) {
175
+ tray.innerHTML = '<div class="card-tray-empty">All cards installed</div>';
176
+ return;
177
+ }
178
+
179
+ tray.innerHTML = available
180
+ .map(
181
+ (card) => `
182
+ <div class="card-tile card-color-${card.color}" data-card-id="${card.id}" data-source="tray">
183
+ <div class="card-tile-icon">${this.cardIcons[card.id]}</div>
184
+ <div class="card-tile-name">${card.name}</div>
185
+ </div>`,
186
+ )
187
+ .join("");
188
+
189
+ // Attach drag listeners to tray cards
190
+ tray.querySelectorAll(".card-tile").forEach((tile) => {
191
+ tile.addEventListener("mousedown", (e) => {
192
+ e.preventDefault();
193
+ this.startCardDrag(tile.dataset.cardId, "tray", null, e);
194
+ });
195
+ });
196
+ }
197
+
198
+ renderMotherboardSlots() {
199
+ const container = this.contentElement.querySelector("#motherboard-slots");
200
+ if (!container) return;
201
+
202
+ container.innerHTML = this.slots
203
+ .map((slotInfo) => {
204
+ const cardId = this.slotAssignments[slotInfo.slot];
205
+ const card = cardId ? this.cards.find((c) => c.id === cardId) : null;
206
+ const isFixed = slotInfo.fixed;
207
+
208
+ if (isFixed) {
209
+ return `
210
+ <div class="mb-slot-row mb-slot-fixed">
211
+ <div class="mb-slot-badge">S${slotInfo.slot}</div>
212
+ <div class="mb-slot-connector">
213
+ <div class="mb-connector-teeth"></div>
214
+ <div class="mb-slot-card-fixed">
215
+ <span class="mb-lock-icon">&#128274;</span>
216
+ <span>80-Column</span>
217
+ </div>
218
+ </div>
219
+ <div class="mb-slot-note">${slotInfo.note}</div>
220
+ </div>`;
221
+ }
222
+
223
+ if (card) {
224
+ return `
225
+ <div class="mb-slot-row" data-slot="${slotInfo.slot}">
226
+ <div class="mb-slot-badge">S${slotInfo.slot}</div>
227
+ <div class="mb-slot-connector">
228
+ <div class="mb-connector-teeth"></div>
229
+ <div class="mb-slot-card card-color-${card.color}" data-card-id="${card.id}" data-source="slot" data-slot="${slotInfo.slot}">
230
+ <div class="card-tile-icon">${this.cardIcons[card.id]}</div>
231
+ <div class="card-tile-name">${card.name}</div>
232
+ </div>
233
+ </div>
234
+ <div class="mb-slot-note">${slotInfo.note}</div>
235
+ </div>`;
236
+ }
237
+
238
+ return `
239
+ <div class="mb-slot-row mb-slot-empty" data-slot="${slotInfo.slot}">
240
+ <div class="mb-slot-badge">S${slotInfo.slot}</div>
241
+ <div class="mb-slot-connector">
242
+ <div class="mb-connector-teeth"></div>
243
+ <div class="mb-slot-dropzone">Empty</div>
244
+ </div>
245
+ <div class="mb-slot-note">${slotInfo.note}</div>
246
+ </div>`;
247
+ })
248
+ .join("");
249
+
250
+ // Attach drag listeners to installed cards
251
+ container.querySelectorAll(".mb-slot-card").forEach((tile) => {
252
+ tile.addEventListener("mousedown", (e) => {
253
+ e.preventDefault();
254
+ const slot = parseInt(tile.dataset.slot, 10);
255
+ this.startCardDrag(tile.dataset.cardId, "slot", slot, e);
256
+ });
257
+ });
258
+ }
259
+
260
+ // ---- Drag & Drop ----
261
+
262
+ startCardDrag(cardId, sourceType, sourceSlot, e) {
263
+ const card = this.cards.find((c) => c.id === cardId);
264
+ if (!card) return;
265
+
266
+ this.dragState = { cardId, sourceType, sourceSlot };
267
+
268
+ // Create ghost element
269
+ this.ghostElement = document.createElement("div");
270
+ this.ghostElement.className = `card-tile card-color-${card.color} card-ghost`;
271
+ this.ghostElement.innerHTML = `
272
+ <div class="card-tile-icon">${this.cardIcons[card.id]}</div>
273
+ <div class="card-tile-name">${card.name}</div>`;
274
+ document.body.appendChild(this.ghostElement);
275
+ this.positionGhost(e);
276
+
277
+ // Dim the source element
278
+ const sourceEl =
279
+ sourceType === "tray"
280
+ ? this.contentElement.querySelector(
281
+ `.card-tray .card-tile[data-card-id="${cardId}"]`,
282
+ )
283
+ : this.contentElement.querySelector(
284
+ `.mb-slot-card[data-card-id="${cardId}"][data-slot="${sourceSlot}"]`,
285
+ );
286
+ if (sourceEl) sourceEl.classList.add("card-dragging");
287
+
288
+ // Highlight compatible slots
289
+ this.highlightSlots(cardId);
290
+
291
+ document.addEventListener("mousemove", this.handleDragMove);
292
+ document.addEventListener("mouseup", this.handleDragEnd);
293
+ }
294
+
295
+ handleDragMove(e) {
296
+ if (!this.dragState) return;
297
+ e.preventDefault();
298
+ this.positionGhost(e);
299
+ }
300
+
301
+ positionGhost(e) {
302
+ if (!this.ghostElement) return;
303
+ this.ghostElement.style.left = e.clientX - 40 + "px";
304
+ this.ghostElement.style.top = e.clientY - 20 + "px";
305
+ }
306
+
307
+ handleDragEnd(e) {
308
+ e.preventDefault();
309
+ document.removeEventListener("mousemove", this.handleDragMove);
310
+ document.removeEventListener("mouseup", this.handleDragEnd);
311
+
312
+ if (!this.dragState) return;
313
+
314
+ const { cardId, sourceType, sourceSlot } = this.dragState;
315
+ const target = this.getDropTarget(e);
316
+
317
+ if (target) {
318
+ if (target.type === "tray") {
319
+ // Dragged to tray — remove from slot
320
+ if (sourceType === "slot" && sourceSlot != null) {
321
+ delete this.slotAssignments[sourceSlot];
322
+ }
323
+ } else if (target.type === "slot") {
324
+ const targetSlot = target.slot;
325
+ if (this.isCompatible(cardId, targetSlot)) {
326
+ // Remove card from source slot if it came from a slot
327
+ if (sourceType === "slot" && sourceSlot != null) {
328
+ delete this.slotAssignments[sourceSlot];
329
+ }
330
+ // If target slot already has a card, return it to tray
331
+ if (this.slotAssignments[targetSlot]) {
332
+ delete this.slotAssignments[targetSlot];
333
+ }
334
+ this.slotAssignments[targetSlot] = cardId;
335
+ } else {
336
+ // Incompatible — flash red
337
+ this.flashIncompatible(targetSlot);
338
+ }
339
+ }
340
+ }
341
+
342
+ this.cleanupDrag();
343
+ this.updateView();
344
+ }
345
+
346
+ getDropTarget(e) {
347
+ // Remove ghost temporarily so elementFromPoint sees through it
348
+ if (this.ghostElement) this.ghostElement.style.pointerEvents = "none";
349
+ const el = document.elementFromPoint(e.clientX, e.clientY);
350
+ if (this.ghostElement) this.ghostElement.style.pointerEvents = "";
351
+
352
+ if (!el) return null;
353
+
354
+ // Check if dropped on card tray
355
+ const tray = el.closest("#card-tray") || el.closest(".card-tray");
356
+ if (tray) return { type: "tray" };
357
+
358
+ // Check if dropped on a slot row
359
+ const slotRow = el.closest(".mb-slot-row[data-slot]");
360
+ if (slotRow) {
361
+ return { type: "slot", slot: parseInt(slotRow.dataset.slot, 10) };
362
+ }
363
+
364
+ return null;
365
+ }
366
+
367
+ highlightSlots(cardId) {
368
+ const rows = this.contentElement.querySelectorAll(
369
+ ".mb-slot-row[data-slot]",
370
+ );
371
+ rows.forEach((row) => {
372
+ const slot = parseInt(row.dataset.slot, 10);
373
+ if (this.isCompatible(cardId, slot)) {
374
+ row.classList.add("mb-slot-compat");
375
+ } else {
376
+ row.classList.add("mb-slot-incompat");
377
+ }
378
+ });
379
+ }
380
+
381
+ flashIncompatible(slot) {
382
+ const row = this.contentElement.querySelector(
383
+ `.mb-slot-row[data-slot="${slot}"]`,
384
+ );
385
+ if (!row) return;
386
+ row.classList.add("mb-slot-reject");
387
+ setTimeout(() => row.classList.remove("mb-slot-reject"), 400);
388
+ }
389
+
390
+ cleanupDrag() {
391
+ // Remove ghost
392
+ if (this.ghostElement) {
393
+ this.ghostElement.remove();
394
+ this.ghostElement = null;
395
+ }
396
+ // Remove drag styling
397
+ this.contentElement
398
+ .querySelectorAll(".card-dragging")
399
+ .forEach((el) => el.classList.remove("card-dragging"));
400
+ this.contentElement
401
+ .querySelectorAll(".mb-slot-compat, .mb-slot-incompat")
402
+ .forEach((el) => {
403
+ el.classList.remove("mb-slot-compat", "mb-slot-incompat");
404
+ });
405
+ this.dragState = null;
406
+ }
407
+
408
+ isCompatible(cardId, slot) {
409
+ const slotInfo = this.slots.find((s) => s.slot === slot);
410
+ if (!slotInfo || slotInfo.fixed) return false;
411
+ return slotInfo.available.includes(cardId);
412
+ }
413
+
414
+ // ---- Change detection ----
415
+
416
+ detectChanges() {
417
+ this.pendingChanges = {};
418
+ this.hasChanges = false;
419
+
420
+ for (const slotInfo of this.slots) {
421
+ if (slotInfo.fixed) continue;
422
+ const slotNum = slotInfo.slot;
423
+ const current = this.slotAssignments[slotNum] || "empty";
424
+ const original = this.originalAssignments[slotNum] || "empty";
425
+ if (current !== original) {
426
+ this.pendingChanges[slotNum] = current;
427
+ this.hasChanges = true;
428
+ }
429
+ }
430
+ }
431
+
432
+ // ---- Existing logic (preserved) ----
433
+
434
+ getCurrentSlotCard(slot) {
435
+ if (this.pendingChanges[slot]) {
436
+ return this.pendingChanges[slot];
437
+ }
438
+
439
+ if (this.wasmModule && this.wasmModule._getSlotCard) {
440
+ const ptr = this.wasmModule._getSlotCard(slot);
441
+ if (ptr) {
442
+ return this.wasmModule.UTF8ToString(ptr);
443
+ }
444
+ }
445
+
446
+ const defaults = {
447
+ 4: "mockingboard",
448
+ 5: "smartport",
449
+ 6: "disk2",
450
+ 7: "thunderclock",
451
+ };
452
+ return defaults[slot] || "empty";
453
+ }
454
+
455
+ updateUI() {
456
+ const applyBtn = this.contentElement.querySelector("#slot-apply-btn");
457
+ if (!applyBtn) return;
458
+
459
+ if (this.hasChanges) {
460
+ applyBtn.disabled = false;
461
+ applyBtn.textContent = "Apply & Reset";
462
+ applyBtn.classList.add("has-changes");
463
+ } else {
464
+ applyBtn.disabled = true;
465
+ applyBtn.textContent = "No Changes";
466
+ applyBtn.classList.remove("has-changes");
467
+ }
468
+ }
469
+
470
+ applyChanges() {
471
+ this.saveSettings();
472
+
473
+ if (this.wasmModule && this.wasmModule._setSlotCard) {
474
+ // Apply ALL slot assignments, not just changes, to ensure empty slots are set too
475
+ for (const slotInfo of this.slots) {
476
+ if (slotInfo.fixed) continue;
477
+ const slotNum = slotInfo.slot;
478
+ const cardId = this.slotAssignments[slotNum] || "empty";
479
+ const cardIdPtr = this.wasmModule._malloc(cardId.length + 1);
480
+ this.wasmModule.stringToUTF8(cardId, cardIdPtr, cardId.length + 1);
481
+ this.wasmModule._setSlotCard(slotNum, cardIdPtr);
482
+ this.wasmModule._free(cardIdPtr);
483
+ }
484
+ }
485
+
486
+ // Update original state to reflect new baseline
487
+ this.originalAssignments = { ...this.slotAssignments };
488
+ this.pendingChanges = {};
489
+ this.hasChanges = false;
490
+ this.updateUI();
491
+
492
+ if (this.onResetCallback) {
493
+ this.onResetCallback();
494
+ } else if (this.wasmModule && this.wasmModule._reset) {
495
+ this.wasmModule._reset();
496
+ }
497
+
498
+ showToast("Expansion slot configuration updated", "info");
499
+ }
500
+
501
+ applyInitialSettings() {
502
+ const saved = this.loadSettingsFromStorage();
503
+ const config = saved || { 5: "smartport", 7: "thunderclock" };
504
+ if (this.wasmModule && this.wasmModule._setSlotCard) {
505
+ for (const [slot, cardId] of Object.entries(config)) {
506
+ const slotNum = parseInt(slot, 10);
507
+ const cardIdPtr = this.wasmModule._malloc(cardId.length + 1);
508
+ this.wasmModule.stringToUTF8(cardId, cardIdPtr, cardId.length + 1);
509
+ this.wasmModule._setSlotCard(slotNum, cardIdPtr);
510
+ this.wasmModule._free(cardIdPtr);
511
+ }
512
+ }
513
+ }
514
+
515
+ saveSettings() {
516
+ try {
517
+ const config = {};
518
+ for (const slotInfo of this.slots) {
519
+ if (slotInfo.fixed) continue;
520
+ const cardId = this.slotAssignments[slotInfo.slot] || "empty";
521
+ config[slotInfo.slot] = cardId;
522
+ }
523
+ localStorage.setItem("a2e-slot-config", JSON.stringify(config));
524
+ } catch (e) {
525
+ console.warn("Could not save slot configuration:", e);
526
+ }
527
+ }
528
+
529
+ loadSettings() {
530
+ try {
531
+ const saved = this.loadSettingsFromStorage();
532
+ if (saved) {
533
+ // Populate slot assignments from saved config
534
+ for (const [slot, cardId] of Object.entries(saved)) {
535
+ if (cardId && cardId !== "empty") {
536
+ this.slotAssignments[parseInt(slot, 10)] = cardId;
537
+ }
538
+ }
539
+ }
540
+ } catch (e) {
541
+ console.warn("Could not load slot configuration:", e);
542
+ }
543
+ }
544
+
545
+ loadSettingsFromStorage() {
546
+ try {
547
+ const saved = localStorage.getItem("a2e-slot-config");
548
+ if (saved) {
549
+ return JSON.parse(saved);
550
+ }
551
+ } catch (e) {
552
+ console.warn("Could not parse slot configuration:", e);
553
+ }
554
+ return null;
555
+ }
556
+
557
+ update() {
558
+ // No dynamic updates needed
559
+ }
560
+ }
@@ -0,0 +1,61 @@
1
+ /*
2
+ * theme-manager.js - Light/Dark theme management
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ import { STORAGE_KEYS } from "../utils/constants.js";
9
+
10
+ const THEME_META_COLORS = {
11
+ dark: "#0a0a0b",
12
+ light: "#ffffff",
13
+ };
14
+
15
+ export class ThemeManager {
16
+ constructor() {
17
+ this._preference = localStorage.getItem(STORAGE_KEYS.THEME) || "system";
18
+ this._mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
19
+ this._onSystemChange = () => {
20
+ if (this._preference === "system") {
21
+ this.applyTheme();
22
+ }
23
+ };
24
+ this._mediaQuery.addEventListener("change", this._onSystemChange);
25
+ this.applyTheme();
26
+ }
27
+
28
+ /** @returns {"dark"|"light"|"system"} */
29
+ getPreference() {
30
+ return this._preference;
31
+ }
32
+
33
+ /** @param {"dark"|"light"|"system"} value */
34
+ setPreference(value) {
35
+ this._preference = value;
36
+ localStorage.setItem(STORAGE_KEYS.THEME, value);
37
+ this.applyTheme();
38
+ }
39
+
40
+ /** @returns {"dark"|"light"} */
41
+ getEffectiveTheme() {
42
+ if (this._preference === "system") {
43
+ return this._mediaQuery.matches ? "dark" : "light";
44
+ }
45
+ return this._preference;
46
+ }
47
+
48
+ applyTheme() {
49
+ const effective = this.getEffectiveTheme();
50
+ document.documentElement.dataset.theme = effective;
51
+
52
+ const meta = document.querySelector('meta[name="theme-color"]');
53
+ if (meta) {
54
+ meta.content = THEME_META_COLORS[effective];
55
+ }
56
+ }
57
+
58
+ destroy() {
59
+ this._mediaQuery.removeEventListener("change", this._onSystemChange);
60
+ }
61
+ }
@@ -0,0 +1,44 @@
1
+ /*
2
+ * toast.js - Lightweight toast notification system
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ let container = null;
9
+
10
+ function ensureContainer() {
11
+ if (!container) {
12
+ container = document.createElement("div");
13
+ container.className = "toast-container";
14
+ document.body.appendChild(container);
15
+ }
16
+ return container;
17
+ }
18
+
19
+ /**
20
+ * Show a toast notification.
21
+ * @param {string} message - The message to display
22
+ * @param {'info'|'error'|'warning'} [type='info'] - Toast type for styling
23
+ * @param {number} [duration=4000] - Duration in ms before auto-dismiss
24
+ */
25
+ export function showToast(message, type = "info", duration = 6000) {
26
+ const parent = ensureContainer();
27
+
28
+ const toast = document.createElement("div");
29
+ toast.className = `toast toast-${type}`;
30
+ toast.textContent = message;
31
+
32
+ parent.appendChild(toast);
33
+
34
+ // Trigger entrance animation
35
+ requestAnimationFrame(() => toast.classList.add("toast-visible"));
36
+
37
+ const dismiss = () => {
38
+ toast.classList.remove("toast-visible");
39
+ toast.addEventListener("transitionend", () => toast.remove());
40
+ };
41
+
42
+ toast.addEventListener("click", dismiss);
43
+ setTimeout(dismiss, duration);
44
+ }