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.
- package/.clangd +5 -0
- package/.mcp.json +12 -0
- package/CLAUDE.md +362 -0
- package/CMakeLists.txt +774 -0
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/build-wasm/generated/roms.cpp +2447 -0
- package/docker-compose.staging.yml +9 -0
- package/docs/basic-rom-disassembly.md +6663 -0
- package/docs/softswitch-comparison.md +273 -0
- package/docs/thunderclock-debug.md +89 -0
- package/examples/cube.bas +72 -0
- package/examples/hello.s +55 -0
- package/examples/scroll.s +140 -0
- package/package.json +18 -0
- package/public/assets/apple-logo-old.png +0 -0
- package/public/assets/apple-logo.png +0 -0
- package/public/assets/drive-closed-light-on.png +0 -0
- package/public/assets/drive-closed.png +0 -0
- package/public/assets/drive-open-light-on.png +0 -0
- package/public/assets/drive-open.png +0 -0
- package/public/audio-worklet.js +82 -0
- package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
- package/public/disks/ProDOS 2.4.3.po +0 -0
- package/public/disks/h32mb.2mg +0 -0
- package/public/disks/library.json +26 -0
- package/public/docs/llms/llm-assembler.txt +90 -0
- package/public/docs/llms/llm-basic-program.txt +256 -0
- package/public/docs/llms/llm-disk-drives.txt +72 -0
- package/public/docs/llms/llm-file-explorer.txt +50 -0
- package/public/docs/llms/llm-hard-drives.txt +80 -0
- package/public/docs/llms/llm-main.txt +51 -0
- package/public/docs/llms/llm-slot-configuration.txt +66 -0
- package/public/icons/icon-192.svg +4 -0
- package/public/icons/icon-512.svg +4 -0
- package/public/index.html +661 -0
- package/public/llms.txt +49 -0
- package/public/manifest.json +29 -0
- package/public/shaders/burnin.glsl +22 -0
- package/public/shaders/crt.glsl +706 -0
- package/public/shaders/edge.glsl +109 -0
- package/public/shaders/vertex.glsl +8 -0
- package/public/sw.js +186 -0
- package/roms/341-0027.bin +0 -0
- package/roms/341-0160-A-US-UK.bin +0 -0
- package/roms/341-0160-A.bin +0 -0
- package/roms/342-0273-A-US-UK.bin +0 -0
- package/roms/342-0349-B-C0-FF.bin +0 -0
- package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
- package/roms/Thunderclock Plus ROM.bin +0 -0
- package/scripts/generate_roms.sh +69 -0
- package/src/bindings/wasm_interface.cpp +1940 -0
- package/src/core/assembler/assembler.cpp +1239 -0
- package/src/core/assembler/assembler.hpp +115 -0
- package/src/core/audio/audio.cpp +160 -0
- package/src/core/audio/audio.hpp +81 -0
- package/src/core/basic/basic_detokenizer.cpp +436 -0
- package/src/core/basic/basic_detokenizer.hpp +41 -0
- package/src/core/basic/basic_tokenizer.cpp +286 -0
- package/src/core/basic/basic_tokenizer.hpp +26 -0
- package/src/core/basic/basic_tokens.hpp +295 -0
- package/src/core/cards/disk2_card.cpp +568 -0
- package/src/core/cards/disk2_card.hpp +316 -0
- package/src/core/cards/expansion_card.hpp +185 -0
- package/src/core/cards/mockingboard/ay8910.cpp +616 -0
- package/src/core/cards/mockingboard/ay8910.hpp +159 -0
- package/src/core/cards/mockingboard/via6522.cpp +530 -0
- package/src/core/cards/mockingboard/via6522.hpp +163 -0
- package/src/core/cards/mockingboard_card.cpp +312 -0
- package/src/core/cards/mockingboard_card.hpp +159 -0
- package/src/core/cards/mouse_card.cpp +654 -0
- package/src/core/cards/mouse_card.hpp +190 -0
- package/src/core/cards/smartport/block_device.cpp +202 -0
- package/src/core/cards/smartport/block_device.hpp +60 -0
- package/src/core/cards/smartport/smartport_card.cpp +603 -0
- package/src/core/cards/smartport/smartport_card.hpp +120 -0
- package/src/core/cards/thunderclock_card.cpp +237 -0
- package/src/core/cards/thunderclock_card.hpp +122 -0
- package/src/core/cpu/cpu6502.cpp +1609 -0
- package/src/core/cpu/cpu6502.hpp +203 -0
- package/src/core/debug/condition_evaluator.cpp +470 -0
- package/src/core/debug/condition_evaluator.hpp +87 -0
- package/src/core/disassembler/disassembler.cpp +552 -0
- package/src/core/disassembler/disassembler.hpp +171 -0
- package/src/core/disk-image/disk_image.hpp +267 -0
- package/src/core/disk-image/dsk_disk_image.cpp +827 -0
- package/src/core/disk-image/dsk_disk_image.hpp +204 -0
- package/src/core/disk-image/gcr_encoding.cpp +147 -0
- package/src/core/disk-image/gcr_encoding.hpp +78 -0
- package/src/core/disk-image/woz_disk_image.cpp +1049 -0
- package/src/core/disk-image/woz_disk_image.hpp +343 -0
- package/src/core/emulator.cpp +2126 -0
- package/src/core/emulator.hpp +434 -0
- package/src/core/filesystem/dos33.cpp +178 -0
- package/src/core/filesystem/dos33.hpp +66 -0
- package/src/core/filesystem/pascal.cpp +262 -0
- package/src/core/filesystem/pascal.hpp +87 -0
- package/src/core/filesystem/prodos.cpp +369 -0
- package/src/core/filesystem/prodos.hpp +119 -0
- package/src/core/input/keyboard.cpp +227 -0
- package/src/core/input/keyboard.hpp +111 -0
- package/src/core/mmu/mmu.cpp +1387 -0
- package/src/core/mmu/mmu.hpp +236 -0
- package/src/core/types.hpp +196 -0
- package/src/core/video/video.cpp +680 -0
- package/src/core/video/video.hpp +156 -0
- package/src/css/assembler-editor.css +1617 -0
- package/src/css/base.css +470 -0
- package/src/css/basic-debugger.css +791 -0
- package/src/css/basic-editor.css +792 -0
- package/src/css/controls.css +783 -0
- package/src/css/cpu-debugger.css +1413 -0
- package/src/css/debug-base.css +160 -0
- package/src/css/debug-windows.css +6455 -0
- package/src/css/disk-drives.css +406 -0
- package/src/css/documentation.css +392 -0
- package/src/css/file-explorer.css +867 -0
- package/src/css/hard-drive.css +180 -0
- package/src/css/layout.css +217 -0
- package/src/css/memory-windows.css +798 -0
- package/src/css/modals.css +510 -0
- package/src/css/monitor.css +425 -0
- package/src/css/release-notes.css +101 -0
- package/src/css/responsive.css +400 -0
- package/src/css/rule-builder.css +340 -0
- package/src/css/save-states.css +201 -0
- package/src/css/settings-windows.css +1231 -0
- package/src/css/window-switcher.css +150 -0
- package/src/js/agent/agent-manager.js +643 -0
- package/src/js/agent/agent-tools.js +293 -0
- package/src/js/agent/agent-version-tools.js +131 -0
- package/src/js/agent/assembler-tools.js +357 -0
- package/src/js/agent/basic-program-tools.js +894 -0
- package/src/js/agent/disk-tools.js +417 -0
- package/src/js/agent/file-explorer-tools.js +269 -0
- package/src/js/agent/index.js +13 -0
- package/src/js/agent/main-tools.js +222 -0
- package/src/js/agent/slot-tools.js +303 -0
- package/src/js/agent/smartport-tools.js +257 -0
- package/src/js/agent/window-tools.js +80 -0
- package/src/js/audio/audio-driver.js +417 -0
- package/src/js/audio/audio-worklet.js +85 -0
- package/src/js/audio/index.js +8 -0
- package/src/js/config/default-layout.js +34 -0
- package/src/js/config/version.js +8 -0
- package/src/js/data/apple2-rom-routines.js +577 -0
- package/src/js/debug/assembler-editor-window.js +2993 -0
- package/src/js/debug/basic-breakpoint-manager.js +529 -0
- package/src/js/debug/basic-program-parser.js +436 -0
- package/src/js/debug/basic-program-window.js +2594 -0
- package/src/js/debug/basic-variable-inspector.js +447 -0
- package/src/js/debug/breakpoint-manager.js +472 -0
- package/src/js/debug/cpu-debugger-window.js +2396 -0
- package/src/js/debug/index.js +22 -0
- package/src/js/debug/label-manager.js +238 -0
- package/src/js/debug/memory-browser-window.js +416 -0
- package/src/js/debug/memory-heat-map-window.js +481 -0
- package/src/js/debug/memory-map-window.js +206 -0
- package/src/js/debug/mockingboard-window.js +882 -0
- package/src/js/debug/mouse-card-window.js +355 -0
- package/src/js/debug/rule-builder-window.js +648 -0
- package/src/js/debug/soft-switch-window.js +458 -0
- package/src/js/debug/stack-viewer-window.js +221 -0
- package/src/js/debug/symbols.js +416 -0
- package/src/js/debug/trace-panel.js +291 -0
- package/src/js/debug/zero-page-watch-window.js +297 -0
- package/src/js/disk-manager/disk-drives-window.js +212 -0
- package/src/js/disk-manager/disk-operations.js +284 -0
- package/src/js/disk-manager/disk-persistence.js +301 -0
- package/src/js/disk-manager/disk-surface-renderer.js +388 -0
- package/src/js/disk-manager/drive-sounds.js +139 -0
- package/src/js/disk-manager/hard-drive-manager.js +481 -0
- package/src/js/disk-manager/hard-drive-persistence.js +187 -0
- package/src/js/disk-manager/hard-drive-window.js +57 -0
- package/src/js/disk-manager/index.js +890 -0
- package/src/js/display/display-settings-window.js +383 -0
- package/src/js/display/index.js +10 -0
- package/src/js/display/screen-window.js +342 -0
- package/src/js/display/webgl-renderer.js +705 -0
- package/src/js/file-explorer/disassembler.js +574 -0
- package/src/js/file-explorer/dos33.js +266 -0
- package/src/js/file-explorer/file-viewer.js +359 -0
- package/src/js/file-explorer/index.js +1261 -0
- package/src/js/file-explorer/prodos.js +549 -0
- package/src/js/file-explorer/utils.js +67 -0
- package/src/js/help/documentation-window.js +1096 -0
- package/src/js/help/index.js +10 -0
- package/src/js/help/release-notes-window.js +85 -0
- package/src/js/help/release-notes.js +612 -0
- package/src/js/input/gamepad-handler.js +176 -0
- package/src/js/input/index.js +12 -0
- package/src/js/input/input-handler.js +396 -0
- package/src/js/input/joystick-window.js +404 -0
- package/src/js/input/mouse-handler.js +99 -0
- package/src/js/input/text-selection.js +462 -0
- package/src/js/main.js +653 -0
- package/src/js/state/index.js +15 -0
- package/src/js/state/save-states-window.js +393 -0
- package/src/js/state/state-manager.js +409 -0
- package/src/js/state/state-persistence.js +218 -0
- package/src/js/ui/confirm.js +43 -0
- package/src/js/ui/disk-drive-positioner.js +347 -0
- package/src/js/ui/reminder-controller.js +129 -0
- package/src/js/ui/slot-configuration-window.js +560 -0
- package/src/js/ui/theme-manager.js +61 -0
- package/src/js/ui/toast.js +44 -0
- package/src/js/ui/ui-controller.js +897 -0
- package/src/js/ui/window-switcher.js +275 -0
- package/src/js/utils/basic-autocomplete.js +832 -0
- package/src/js/utils/basic-highlighting.js +473 -0
- package/src/js/utils/basic-tokenizer.js +153 -0
- package/src/js/utils/basic-tokens.js +117 -0
- package/src/js/utils/constants.js +28 -0
- package/src/js/utils/indexeddb-helper.js +225 -0
- package/src/js/utils/merlin-editor-support.js +905 -0
- package/src/js/utils/merlin-highlighting.js +551 -0
- package/src/js/utils/storage.js +125 -0
- package/src/js/utils/string-utils.js +19 -0
- package/src/js/utils/wasm-memory.js +54 -0
- package/src/js/windows/base-window.js +690 -0
- package/src/js/windows/index.js +9 -0
- package/src/js/windows/window-manager.js +375 -0
- package/tests/catch2/catch.hpp +17976 -0
- package/tests/common/basic_program_builder.cpp +119 -0
- package/tests/common/basic_program_builder.hpp +209 -0
- package/tests/common/disk_image_builder.cpp +444 -0
- package/tests/common/disk_image_builder.hpp +141 -0
- package/tests/common/test_helpers.hpp +118 -0
- package/tests/gcr/gcr-test.cpp +142 -0
- package/tests/integration/check-rom.js +70 -0
- package/tests/integration/compare-boot.js +239 -0
- package/tests/integration/crash-trace.js +102 -0
- package/tests/integration/disk-boot-test.js +264 -0
- package/tests/integration/memory-crash.js +108 -0
- package/tests/integration/nibble-read-test.js +249 -0
- package/tests/integration/phase-test.js +159 -0
- package/tests/integration/test_emulator.cpp +291 -0
- package/tests/integration/test_emulator_basic.cpp +91 -0
- package/tests/integration/test_emulator_debug.cpp +344 -0
- package/tests/integration/test_emulator_disk.cpp +153 -0
- package/tests/integration/test_emulator_state.cpp +163 -0
- package/tests/klaus/6502_functional_test.bin +0 -0
- package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
- package/tests/klaus/klaus_6502_test.cpp +184 -0
- package/tests/klaus/klaus_65c02_test.cpp +197 -0
- package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
- package/tests/thunderclock/thunderclock_test.cpp +550 -0
- package/tests/unit/test_assembler.cpp +521 -0
- package/tests/unit/test_audio.cpp +196 -0
- package/tests/unit/test_ay8910.cpp +311 -0
- package/tests/unit/test_basic_detokenizer.cpp +265 -0
- package/tests/unit/test_basic_tokenizer.cpp +382 -0
- package/tests/unit/test_block_device.cpp +259 -0
- package/tests/unit/test_condition_evaluator.cpp +219 -0
- package/tests/unit/test_cpu6502.cpp +1301 -0
- package/tests/unit/test_cpu_addressing.cpp +361 -0
- package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
- package/tests/unit/test_cpu_decimal.cpp +166 -0
- package/tests/unit/test_cpu_interrupts.cpp +285 -0
- package/tests/unit/test_disassembler.cpp +323 -0
- package/tests/unit/test_disk2_card.cpp +330 -0
- package/tests/unit/test_dos33.cpp +273 -0
- package/tests/unit/test_dsk_disk_image.cpp +315 -0
- package/tests/unit/test_expansion_card.cpp +178 -0
- package/tests/unit/test_gcr_encoding.cpp +232 -0
- package/tests/unit/test_keyboard.cpp +262 -0
- package/tests/unit/test_mmu.cpp +555 -0
- package/tests/unit/test_mmu_slots.cpp +323 -0
- package/tests/unit/test_mockingboard.cpp +352 -0
- package/tests/unit/test_mouse_card.cpp +386 -0
- package/tests/unit/test_pascal.cpp +248 -0
- package/tests/unit/test_prodos.cpp +259 -0
- package/tests/unit/test_smartport_card.cpp +321 -0
- package/tests/unit/test_thunderclock.cpp +354 -0
- package/tests/unit/test_via6522.cpp +323 -0
- package/tests/unit/test_video.cpp +319 -0
- package/tests/unit/test_woz_disk_image.cpp +257 -0
- package/vite.config.js +96 -0
- package/wiki/AI-Agent.md +372 -0
- package/wiki/Architecture-Overview.md +303 -0
- package/wiki/Audio-System.md +449 -0
- package/wiki/CPU-Emulation.md +477 -0
- package/wiki/Debugger.md +516 -0
- package/wiki/Disk-Drives.md +161 -0
- package/wiki/Disk-System-Internals.md +547 -0
- package/wiki/Display-Settings.md +88 -0
- package/wiki/Expansion-Slots.md +187 -0
- package/wiki/File-Explorer.md +259 -0
- package/wiki/Getting-Started.md +156 -0
- package/wiki/Home.md +69 -0
- package/wiki/Input-Devices.md +183 -0
- package/wiki/Keyboard-Shortcuts.md +158 -0
- package/wiki/Memory-System.md +364 -0
- package/wiki/Save-States.md +172 -0
- package/wiki/Video-Rendering.md +658 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* disk-drive-positioner.js - Disk drive window positioning
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* DiskDrivePositioner - Handles drag-to-move for the disk drives container
|
|
10
|
+
* Supports dragging to reposition and double-click to reset to default position
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class DiskDrivePositioner {
|
|
14
|
+
constructor() {
|
|
15
|
+
// Custom position (user-defined via drag)
|
|
16
|
+
this.customPosition = null; // { x, y } or null for default
|
|
17
|
+
|
|
18
|
+
// Drag state
|
|
19
|
+
this.isDragging = false;
|
|
20
|
+
this.dragStart = null;
|
|
21
|
+
|
|
22
|
+
// Bind methods for event listeners
|
|
23
|
+
this.handleDragMove = this.handleDragMove.bind(this);
|
|
24
|
+
this.handleDragEnd = this.handleDragEnd.bind(this);
|
|
25
|
+
this.handleWindowResize = this.handleWindowResize.bind(this);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize the positioner
|
|
30
|
+
*/
|
|
31
|
+
init() {
|
|
32
|
+
this.loadSavedPosition();
|
|
33
|
+
this.constrainToViewport();
|
|
34
|
+
this.setupDrag();
|
|
35
|
+
this.applyPosition();
|
|
36
|
+
this.updatePositionIndicator();
|
|
37
|
+
|
|
38
|
+
// Listen for window resize to keep drives in view
|
|
39
|
+
window.addEventListener("resize", this.handleWindowResize);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Load saved position from localStorage
|
|
44
|
+
*/
|
|
45
|
+
loadSavedPosition() {
|
|
46
|
+
const savedPosition = localStorage.getItem("a2e-drives-position");
|
|
47
|
+
if (savedPosition) {
|
|
48
|
+
try {
|
|
49
|
+
this.customPosition = JSON.parse(savedPosition);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
this.customPosition = null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Ensure the saved position is within the current viewport bounds.
|
|
58
|
+
* If the window is outside the visible area, bring it back on screen.
|
|
59
|
+
*/
|
|
60
|
+
constrainToViewport() {
|
|
61
|
+
if (!this.customPosition) return;
|
|
62
|
+
|
|
63
|
+
const container = document.querySelector(".disk-drives-container");
|
|
64
|
+
if (!container) return;
|
|
65
|
+
|
|
66
|
+
// Temporarily apply position to measure the element
|
|
67
|
+
container.classList.add("free-position");
|
|
68
|
+
container.style.left = this.customPosition.x + "px";
|
|
69
|
+
container.style.top = this.customPosition.y + "px";
|
|
70
|
+
|
|
71
|
+
const rect = container.getBoundingClientRect();
|
|
72
|
+
const header = document.querySelector("header");
|
|
73
|
+
const footer = document.querySelector("footer");
|
|
74
|
+
|
|
75
|
+
const headerHeight = header ? header.offsetHeight : 0;
|
|
76
|
+
const footerHeight = footer ? footer.offsetHeight : 0;
|
|
77
|
+
|
|
78
|
+
// Check if the element is significantly off-screen
|
|
79
|
+
// Allow some tolerance (at least 50px must be visible)
|
|
80
|
+
const minVisible = 50;
|
|
81
|
+
let needsAdjustment = false;
|
|
82
|
+
let newX = this.customPosition.x;
|
|
83
|
+
let newY = this.customPosition.y;
|
|
84
|
+
|
|
85
|
+
// Check horizontal bounds
|
|
86
|
+
if (rect.right < minVisible) {
|
|
87
|
+
// Too far left - bring right edge into view
|
|
88
|
+
newX = minVisible - rect.width;
|
|
89
|
+
needsAdjustment = true;
|
|
90
|
+
} else if (rect.left > window.innerWidth - minVisible) {
|
|
91
|
+
// Too far right - bring left edge into view
|
|
92
|
+
newX = window.innerWidth - minVisible;
|
|
93
|
+
needsAdjustment = true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check vertical bounds
|
|
97
|
+
if (rect.bottom < headerHeight + minVisible) {
|
|
98
|
+
// Too far up - bring bottom edge into view
|
|
99
|
+
newY = headerHeight + minVisible - rect.height;
|
|
100
|
+
needsAdjustment = true;
|
|
101
|
+
} else if (rect.top > window.innerHeight - footerHeight - minVisible) {
|
|
102
|
+
// Too far down - bring top edge into view
|
|
103
|
+
newY = window.innerHeight - footerHeight - minVisible;
|
|
104
|
+
needsAdjustment = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Fully constrain to keep entire element on screen
|
|
108
|
+
newX = Math.max(0, Math.min(window.innerWidth - rect.width, newX));
|
|
109
|
+
newY = Math.max(headerHeight, Math.min(window.innerHeight - footerHeight - rect.height, newY));
|
|
110
|
+
|
|
111
|
+
if (needsAdjustment || newX !== this.customPosition.x || newY !== this.customPosition.y) {
|
|
112
|
+
this.customPosition = { x: newX, y: newY };
|
|
113
|
+
localStorage.setItem("a2e-drives-position", JSON.stringify(this.customPosition));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Reset styles - applyPosition will set them properly
|
|
117
|
+
container.classList.remove("free-position");
|
|
118
|
+
container.style.left = "";
|
|
119
|
+
container.style.top = "";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Set up drag-to-move functionality
|
|
124
|
+
*/
|
|
125
|
+
setupDrag() {
|
|
126
|
+
const container = document.querySelector(".disk-drives-container");
|
|
127
|
+
if (!container) return;
|
|
128
|
+
|
|
129
|
+
// Add position indicator
|
|
130
|
+
this.createPositionIndicator(container);
|
|
131
|
+
|
|
132
|
+
// Drag on the container
|
|
133
|
+
container.addEventListener("mousedown", (e) => {
|
|
134
|
+
// Don't start drag if clicking on buttons, inputs, or dropdowns
|
|
135
|
+
if (
|
|
136
|
+
e.target.closest("button") ||
|
|
137
|
+
e.target.closest("input") ||
|
|
138
|
+
e.target.closest(".recent-dropdown") ||
|
|
139
|
+
e.target.closest(".drives-position-indicator")
|
|
140
|
+
) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
this.startDrag(e);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Double-click to reset position
|
|
149
|
+
container.addEventListener("dblclick", (e) => {
|
|
150
|
+
// Don't reset if clicking on controls
|
|
151
|
+
if (
|
|
152
|
+
e.target.closest("button") ||
|
|
153
|
+
e.target.closest("input") ||
|
|
154
|
+
e.target.closest(".recent-dropdown") ||
|
|
155
|
+
e.target.closest(".drives-position-indicator")
|
|
156
|
+
) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.resetToDefault();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
document.addEventListener("mousemove", this.handleDragMove);
|
|
164
|
+
document.addEventListener("mouseup", this.handleDragEnd);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Create the position indicator element
|
|
169
|
+
*/
|
|
170
|
+
createPositionIndicator(container) {
|
|
171
|
+
const indicator = document.createElement("button");
|
|
172
|
+
indicator.id = "drives-position-indicator";
|
|
173
|
+
indicator.className = "drives-position-indicator hidden";
|
|
174
|
+
indicator.title = "Custom position - Click to reset (or double-click drives)";
|
|
175
|
+
indicator.innerHTML = `
|
|
176
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
177
|
+
<circle cx="12" cy="12" r="3"></circle>
|
|
178
|
+
<path d="M12 2v4"></path>
|
|
179
|
+
<path d="M12 18v4"></path>
|
|
180
|
+
<path d="M2 12h4"></path>
|
|
181
|
+
<path d="M18 12h4"></path>
|
|
182
|
+
</svg>
|
|
183
|
+
`;
|
|
184
|
+
indicator.addEventListener("click", (e) => {
|
|
185
|
+
e.stopPropagation();
|
|
186
|
+
this.resetToDefault();
|
|
187
|
+
});
|
|
188
|
+
container.appendChild(indicator);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Start dragging
|
|
193
|
+
*/
|
|
194
|
+
startDrag(e) {
|
|
195
|
+
// Don't allow dragging in full-page mode or fullscreen
|
|
196
|
+
if (document.body.classList.contains("full-page-mode") || document.fullscreenElement) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const container = document.querySelector(".disk-drives-container");
|
|
201
|
+
if (!container) return;
|
|
202
|
+
|
|
203
|
+
this.isDragging = true;
|
|
204
|
+
container.classList.add("dragging");
|
|
205
|
+
|
|
206
|
+
const rect = container.getBoundingClientRect();
|
|
207
|
+
this.dragStart = {
|
|
208
|
+
mouseX: e.clientX,
|
|
209
|
+
mouseY: e.clientY,
|
|
210
|
+
elemX: rect.left,
|
|
211
|
+
elemY: rect.top,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Handle mouse move during drag
|
|
217
|
+
*/
|
|
218
|
+
handleDragMove(e) {
|
|
219
|
+
if (!this.isDragging) return;
|
|
220
|
+
|
|
221
|
+
const dx = e.clientX - this.dragStart.mouseX;
|
|
222
|
+
const dy = e.clientY - this.dragStart.mouseY;
|
|
223
|
+
|
|
224
|
+
// Calculate new position
|
|
225
|
+
let newX = this.dragStart.elemX + dx;
|
|
226
|
+
let newY = this.dragStart.elemY + dy;
|
|
227
|
+
|
|
228
|
+
// Get bounds
|
|
229
|
+
const container = document.querySelector(".disk-drives-container");
|
|
230
|
+
if (!container) return;
|
|
231
|
+
|
|
232
|
+
const rect = container.getBoundingClientRect();
|
|
233
|
+
const header = document.querySelector("header");
|
|
234
|
+
const footer = document.querySelector("footer");
|
|
235
|
+
|
|
236
|
+
const headerHeight = header ? header.offsetHeight : 0;
|
|
237
|
+
const footerHeight = footer ? footer.offsetHeight : 0;
|
|
238
|
+
|
|
239
|
+
// Constrain to viewport (keep entire element on screen)
|
|
240
|
+
newX = Math.max(0, Math.min(window.innerWidth - rect.width, newX));
|
|
241
|
+
newY = Math.max(headerHeight, Math.min(window.innerHeight - footerHeight - rect.height, newY));
|
|
242
|
+
|
|
243
|
+
this.customPosition = { x: newX, y: newY };
|
|
244
|
+
this.applyPosition();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Handle mouse up to complete drag
|
|
249
|
+
*/
|
|
250
|
+
handleDragEnd() {
|
|
251
|
+
if (!this.isDragging) return;
|
|
252
|
+
|
|
253
|
+
const container = document.querySelector(".disk-drives-container");
|
|
254
|
+
if (container) {
|
|
255
|
+
container.classList.remove("dragging");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this.isDragging = false;
|
|
259
|
+
this.dragStart = null;
|
|
260
|
+
|
|
261
|
+
if (this.customPosition) {
|
|
262
|
+
localStorage.setItem("a2e-drives-position", JSON.stringify(this.customPosition));
|
|
263
|
+
this.updatePositionIndicator();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Apply the current position
|
|
269
|
+
*/
|
|
270
|
+
applyPosition() {
|
|
271
|
+
const container = document.querySelector(".disk-drives-container");
|
|
272
|
+
if (!container) return;
|
|
273
|
+
|
|
274
|
+
if (this.customPosition) {
|
|
275
|
+
container.classList.add("free-position");
|
|
276
|
+
container.style.left = this.customPosition.x + "px";
|
|
277
|
+
container.style.top = this.customPosition.y + "px";
|
|
278
|
+
} else {
|
|
279
|
+
container.classList.remove("free-position");
|
|
280
|
+
container.style.left = "";
|
|
281
|
+
container.style.top = "";
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Reset to default position
|
|
287
|
+
*/
|
|
288
|
+
resetToDefault() {
|
|
289
|
+
this.customPosition = null;
|
|
290
|
+
localStorage.removeItem("a2e-drives-position");
|
|
291
|
+
this.applyPosition();
|
|
292
|
+
this.updatePositionIndicator();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Update the position indicator visibility
|
|
297
|
+
*/
|
|
298
|
+
updatePositionIndicator() {
|
|
299
|
+
const indicator = document.getElementById("drives-position-indicator");
|
|
300
|
+
if (!indicator) return;
|
|
301
|
+
|
|
302
|
+
if (this.customPosition) {
|
|
303
|
+
indicator.classList.remove("hidden");
|
|
304
|
+
} else {
|
|
305
|
+
indicator.classList.add("hidden");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Handle window resize - keep drives within viewport
|
|
311
|
+
*/
|
|
312
|
+
handleWindowResize() {
|
|
313
|
+
if (!this.customPosition) return;
|
|
314
|
+
|
|
315
|
+
const container = document.querySelector(".disk-drives-container");
|
|
316
|
+
if (!container) return;
|
|
317
|
+
|
|
318
|
+
const rect = container.getBoundingClientRect();
|
|
319
|
+
const header = document.querySelector("header");
|
|
320
|
+
const footer = document.querySelector("footer");
|
|
321
|
+
|
|
322
|
+
const headerHeight = header ? header.offsetHeight : 0;
|
|
323
|
+
const footerHeight = footer ? footer.offsetHeight : 0;
|
|
324
|
+
|
|
325
|
+
let newX = this.customPosition.x;
|
|
326
|
+
let newY = this.customPosition.y;
|
|
327
|
+
|
|
328
|
+
// Constrain to new viewport size
|
|
329
|
+
newX = Math.max(0, Math.min(window.innerWidth - rect.width, newX));
|
|
330
|
+
newY = Math.max(headerHeight, Math.min(window.innerHeight - footerHeight - rect.height, newY));
|
|
331
|
+
|
|
332
|
+
if (newX !== this.customPosition.x || newY !== this.customPosition.y) {
|
|
333
|
+
this.customPosition = { x: newX, y: newY };
|
|
334
|
+
this.applyPosition();
|
|
335
|
+
localStorage.setItem("a2e-drives-position", JSON.stringify(this.customPosition));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Clean up resources
|
|
341
|
+
*/
|
|
342
|
+
destroy() {
|
|
343
|
+
document.removeEventListener("mousemove", this.handleDragMove);
|
|
344
|
+
document.removeEventListener("mouseup", this.handleDragEnd);
|
|
345
|
+
window.removeEventListener("resize", this.handleWindowResize);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* reminder-controller.js - UI reminder notifications
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ReminderController - Manages floating reminder tooltips
|
|
10
|
+
* Handles power, resize, and drives toggle reminders with positioning and persistence
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class ReminderController {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.isPowerReminderVisible = false;
|
|
16
|
+
this.isBasicReminderVisible = false;
|
|
17
|
+
|
|
18
|
+
window.addEventListener("resize", () => this.repositionAll());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Position a reminder tooltip below a target element with an arrow pointing to it.
|
|
23
|
+
* Centers the reminder on the element, clamps to viewport, and sets arrow position.
|
|
24
|
+
* @param {string} reminderId - The reminder element's ID
|
|
25
|
+
* @param {string|Element} target - The target element ID or element to position below
|
|
26
|
+
* @param {number} defaultWidth - Fallback width if reminder not yet rendered
|
|
27
|
+
*/
|
|
28
|
+
positionReminderBelowElement(reminderId, target, defaultWidth = 200) {
|
|
29
|
+
const reminder = document.getElementById(reminderId);
|
|
30
|
+
const targetEl =
|
|
31
|
+
typeof target === "string" ? document.getElementById(target) : target;
|
|
32
|
+
if (!reminder || !targetEl) return;
|
|
33
|
+
|
|
34
|
+
const targetRect = targetEl.getBoundingClientRect();
|
|
35
|
+
const targetCenterX = targetRect.left + targetRect.width / 2;
|
|
36
|
+
|
|
37
|
+
const reminderRect = reminder.getBoundingClientRect();
|
|
38
|
+
const reminderWidth = reminderRect.width || defaultWidth;
|
|
39
|
+
|
|
40
|
+
// Position reminder centered below target, clamped to viewport
|
|
41
|
+
let reminderLeft = targetCenterX - reminderWidth / 2;
|
|
42
|
+
const padding = 16;
|
|
43
|
+
const maxLeft = window.innerWidth - reminderWidth - padding;
|
|
44
|
+
reminderLeft = Math.max(padding, Math.min(reminderLeft, maxLeft));
|
|
45
|
+
|
|
46
|
+
// Calculate arrow position relative to reminder
|
|
47
|
+
const arrowLeft = targetCenterX - reminderLeft;
|
|
48
|
+
|
|
49
|
+
reminder.style.left = `${reminderLeft}px`;
|
|
50
|
+
reminder.style.top = `${targetRect.bottom + 15}px`;
|
|
51
|
+
reminder.style.setProperty("--arrow-left", `${arrowLeft}px`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Power reminder methods
|
|
55
|
+
|
|
56
|
+
repositionPowerReminder() {
|
|
57
|
+
this.positionReminderBelowElement("power-reminder", "btn-power", 200);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
showPowerReminder(show) {
|
|
61
|
+
const reminder = document.getElementById("power-reminder");
|
|
62
|
+
if (!reminder) return;
|
|
63
|
+
|
|
64
|
+
// Check if already dismissed (first visit)
|
|
65
|
+
if (show && localStorage.getItem("a2e-power-reminder-dismissed")) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (show) {
|
|
70
|
+
this.isPowerReminderVisible = true;
|
|
71
|
+
reminder.classList.remove("hidden");
|
|
72
|
+
requestAnimationFrame(() => {
|
|
73
|
+
this.repositionPowerReminder();
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
this.isPowerReminderVisible = false;
|
|
77
|
+
reminder.classList.add("hidden");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
dismissPowerReminder() {
|
|
82
|
+
this.showPowerReminder(false);
|
|
83
|
+
localStorage.setItem("a2e-power-reminder-dismissed", "true");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// BASIC reminder methods (shows when powered on without a disk)
|
|
87
|
+
|
|
88
|
+
showBasicReminder(show) {
|
|
89
|
+
const reminder = document.getElementById("basic-reminder");
|
|
90
|
+
if (!reminder) return;
|
|
91
|
+
|
|
92
|
+
// Check if already dismissed
|
|
93
|
+
if (show && localStorage.getItem("a2e-basic-reminder-dismissed")) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (show) {
|
|
98
|
+
this.isBasicReminderVisible = true;
|
|
99
|
+
reminder.classList.remove("hidden");
|
|
100
|
+
requestAnimationFrame(() => {
|
|
101
|
+
this.repositionBasicReminder();
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
this.isBasicReminderVisible = false;
|
|
105
|
+
reminder.classList.add("hidden");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
repositionBasicReminder() {
|
|
110
|
+
this.positionReminderBelowElement("basic-reminder", "btn-warm-reset", 220);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
dismissBasicReminder() {
|
|
114
|
+
this.showBasicReminder(false);
|
|
115
|
+
localStorage.setItem("a2e-basic-reminder-dismissed", "true");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Reposition all visible reminders (call after resize)
|
|
120
|
+
*/
|
|
121
|
+
repositionAll() {
|
|
122
|
+
if (this.isPowerReminderVisible) {
|
|
123
|
+
requestAnimationFrame(() => this.repositionPowerReminder());
|
|
124
|
+
}
|
|
125
|
+
if (this.isBasicReminderVisible) {
|
|
126
|
+
requestAnimationFrame(() => this.repositionBasicReminder());
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|