diff --git a/oled-emulator/.gitignore b/oled-emulator/.gitignore
new file mode 100644
index 000000000..8f2999efe
--- /dev/null
+++ b/oled-emulator/.gitignore
@@ -0,0 +1,30 @@
+# Dependencies
+node_modules/
+
+# Build output
+dist/
+
+# Vite cache
+.vite/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
+npm-debug.log*
+
+# Local env files
+.env
+.env.local
+.env.*.local
diff --git a/oled-emulator/README.md b/oled-emulator/README.md
new file mode 100644
index 000000000..1ce2b581c
--- /dev/null
+++ b/oled-emulator/README.md
@@ -0,0 +1,149 @@
+# Meshtastic OLED Emulator
+
+A Vue.js component and TypeScript library for emulating SSD1306/SH1106 OLED displays used in Meshtastic devices. This allows firmware UI development and testing directly in the browser.
+
+## Features
+
+- **Same API as firmware**: Uses the same `OLEDDisplay` API as esp8266-oled-ssd1306 library
+- **Multiple display sizes**: 128x64, 128x32, 64x48, 128x128
+- **Realistic rendering**: Pixel glow, OLED color tints (blue, white, yellow/blue dual)
+- **XBM image support**: Render firmware icons and images
+- **Font rendering**: Bitmap font support with text alignment
+- **Vue 3 component**: Easy integration with Vue applications
+
+## Installation
+
+```bash
+npm install @meshtastic/oled-emulator
+```
+
+## Quick Start
+
+```vue
+
+
+
+
+
+```
+
+## OLEDDisplay API
+
+The `OLEDDisplay` class provides the same methods as the firmware's display library:
+
+```typescript
+const display = new OLEDDisplay("128x64");
+
+// Basic drawing
+display.clear();
+display.setPixel(x, y);
+display.drawLine(x0, y0, x1, y1);
+display.drawRect(x, y, width, height);
+display.fillRect(x, y, width, height);
+display.drawCircle(x, y, radius);
+display.fillCircle(x, y, radius);
+
+// Text
+display.setFont(ArialMT_Plain_10);
+display.setTextAlignment("TEXT_ALIGN_CENTER");
+display.drawString(x, y, "Hello");
+display.drawStringMaxWidth(x, y, maxWidth, "Long text...");
+const width = display.getStringWidth("text");
+
+// Images (XBM format)
+display.drawXbm(x, y, width, height, imageData);
+
+// Colors
+display.setColor("WHITE"); // or 'BLACK', 'INVERSE'
+
+// Buffer access (for custom rendering)
+const buffer = display.getBuffer(); // Uint8Array
+```
+
+## Display Presets
+
+The emulator includes several realistic display presets:
+
+| Preset | Description |
+| --------------------- | ------------------------------- |
+| `ssd1306-blue` | Classic blue OLED (default) |
+| `ssd1306-white` | White OLED |
+| `ssd1306-yellow-blue` | Dual-color (top 16 rows yellow) |
+| `sh1106` | SH1106 style |
+| `sh1107` | SH1107 128x128 |
+| `ssd1306-64x48` | Small 64x48 display |
+| `ssd1306-128x32` | Wide 128x32 display |
+
+## Screen Renderers
+
+Pre-built screen renderers matching firmware UI:
+
+```typescript
+import { ScreenRenderers } from "@meshtastic/oled-emulator";
+
+// Boot screen
+ScreenRenderers.drawBootScreen(display, "2.5.0");
+
+// Node info screen
+ScreenRenderers.drawNodeInfoScreen(display, {
+ shortName: "MESH",
+ longName: "My Node",
+ nodeId: "!abcd1234",
+ lastHeard: "2m ago",
+ snr: 12.5,
+});
+
+// Message screen
+ScreenRenderers.drawMessageScreen(display, {
+ from: "Alice",
+ text: "Hello from the mesh!",
+ time: "14:32",
+ channel: "LongFast",
+});
+
+// And more: drawGPSScreen, drawNodeListScreen, drawSystemScreen, drawCompassScreen
+```
+
+## Development
+
+```bash
+# Install dependencies
+npm install
+
+# Run development server
+npm run dev
+
+# Build library
+npm run build
+```
+
+## Future: Protocol Bridge
+
+A future enhancement will allow connecting this emulator to a real Meshtastic firmware instance (running on native/portduino), enabling live framebuffer streaming over WebSocket for real-time display mirroring.
+
+## License
+
+GPL-3.0 - Same as Meshtastic firmware
diff --git a/oled-emulator/index.html b/oled-emulator/index.html
new file mode 100644
index 000000000..b0acab187
--- /dev/null
+++ b/oled-emulator/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+ Meshtastic OLED Emulator
+
+
+
+
+
+
+
diff --git a/oled-emulator/package-lock.json b/oled-emulator/package-lock.json
new file mode 100644
index 000000000..7d1977bcc
--- /dev/null
+++ b/oled-emulator/package-lock.json
@@ -0,0 +1,2211 @@
+{
+ "name": "@meshtastic/oled-emulator",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@meshtastic/oled-emulator",
+ "version": "0.1.0",
+ "license": "GPL-3.0",
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.0.0",
+ "typescript": "^5.3.0",
+ "vite": "^5.0.0",
+ "vite-plugin-dts": "^3.7.0",
+ "vue": "^3.4.0",
+ "vue-tsc": "^2.0.0"
+ },
+ "peerDependencies": {
+ "vue": "^3.3.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@microsoft/api-extractor": {
+ "version": "7.43.0",
+ "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.43.0.tgz",
+ "integrity": "sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@microsoft/api-extractor-model": "7.28.13",
+ "@microsoft/tsdoc": "0.14.2",
+ "@microsoft/tsdoc-config": "~0.16.1",
+ "@rushstack/node-core-library": "4.0.2",
+ "@rushstack/rig-package": "0.5.2",
+ "@rushstack/terminal": "0.10.0",
+ "@rushstack/ts-command-line": "4.19.1",
+ "lodash": "~4.17.15",
+ "minimatch": "~3.0.3",
+ "resolve": "~1.22.1",
+ "semver": "~7.5.4",
+ "source-map": "~0.6.1",
+ "typescript": "5.4.2"
+ },
+ "bin": {
+ "api-extractor": "bin/api-extractor"
+ }
+ },
+ "node_modules/@microsoft/api-extractor-model": {
+ "version": "7.28.13",
+ "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.13.tgz",
+ "integrity": "sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@microsoft/tsdoc": "0.14.2",
+ "@microsoft/tsdoc-config": "~0.16.1",
+ "@rushstack/node-core-library": "4.0.2"
+ }
+ },
+ "node_modules/@microsoft/api-extractor/node_modules/typescript": {
+ "version": "5.4.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
+ "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/@microsoft/tsdoc": {
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz",
+ "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@microsoft/tsdoc-config": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz",
+ "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@microsoft/tsdoc": "0.14.2",
+ "ajv": "~6.12.6",
+ "jju": "~1.4.0",
+ "resolve": "~1.19.0"
+ }
+ },
+ "node_modules/@microsoft/tsdoc-config/node_modules/resolve": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz",
+ "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.1.0",
+ "path-parse": "^1.0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/@rollup/pluginutils": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
+ "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "estree-walker": "^2.0.2",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
+ "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
+ "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
+ "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
+ "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
+ "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
+ "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
+ "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
+ "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
+ "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
+ "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
+ "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
+ "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
+ "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
+ "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
+ "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
+ "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
+ "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
+ "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
+ "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
+ "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
+ "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
+ "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
+ "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
+ "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
+ "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rushstack/node-core-library": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.0.2.tgz",
+ "integrity": "sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fs-extra": "~7.0.1",
+ "import-lazy": "~4.0.0",
+ "jju": "~1.4.0",
+ "resolve": "~1.22.1",
+ "semver": "~7.5.4",
+ "z-schema": "~5.0.2"
+ },
+ "peerDependencies": {
+ "@types/node": "*"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rushstack/rig-package": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.2.tgz",
+ "integrity": "sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve": "~1.22.1",
+ "strip-json-comments": "~3.1.1"
+ }
+ },
+ "node_modules/@rushstack/terminal": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.10.0.tgz",
+ "integrity": "sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rushstack/node-core-library": "4.0.2",
+ "supports-color": "~8.1.1"
+ },
+ "peerDependencies": {
+ "@types/node": "*"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rushstack/ts-command-line": {
+ "version": "4.19.1",
+ "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.19.1.tgz",
+ "integrity": "sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rushstack/terminal": "0.10.0",
+ "@types/argparse": "1.0.38",
+ "argparse": "~1.0.9",
+ "string-argv": "~0.3.1"
+ }
+ },
+ "node_modules/@types/argparse": {
+ "version": "1.0.38",
+ "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz",
+ "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+ "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@volar/language-core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz",
+ "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/source-map": "1.11.1"
+ }
+ },
+ "node_modules/@volar/source-map": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz",
+ "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "muggle-string": "^0.3.1"
+ }
+ },
+ "node_modules/@volar/typescript": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz",
+ "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "path-browserify": "^1.0.1",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@volar/typescript/node_modules/@volar/language-core": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz",
+ "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/source-map": "2.4.15"
+ }
+ },
+ "node_modules/@volar/typescript/node_modules/@volar/source-map": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz",
+ "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.28.tgz",
+ "integrity": "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@vue/shared": "3.5.28",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz",
+ "integrity": "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.28",
+ "@vue/shared": "3.5.28"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz",
+ "integrity": "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@vue/compiler-core": "3.5.28",
+ "@vue/compiler-dom": "3.5.28",
+ "@vue/compiler-ssr": "3.5.28",
+ "@vue/shared": "3.5.28",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.6",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz",
+ "integrity": "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.28",
+ "@vue/shared": "3.5.28"
+ }
+ },
+ "node_modules/@vue/compiler-vue2": {
+ "version": "2.7.16",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
+ "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "de-indent": "^1.0.2",
+ "he": "^1.2.0"
+ }
+ },
+ "node_modules/@vue/language-core": {
+ "version": "1.8.27",
+ "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz",
+ "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "~1.11.1",
+ "@volar/source-map": "~1.11.1",
+ "@vue/compiler-dom": "^3.3.0",
+ "@vue/shared": "^3.3.0",
+ "computeds": "^0.0.1",
+ "minimatch": "^9.0.3",
+ "muggle-string": "^0.3.1",
+ "path-browserify": "^1.0.1",
+ "vue-template-compiler": "^2.7.14"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/language-core/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@vue/language-core/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.28.tgz",
+ "integrity": "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.28"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.28.tgz",
+ "integrity": "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.28",
+ "@vue/shared": "3.5.28"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.28.tgz",
+ "integrity": "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.28",
+ "@vue/runtime-core": "3.5.28",
+ "@vue/shared": "3.5.28",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.28.tgz",
+ "integrity": "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.28",
+ "@vue/shared": "3.5.28"
+ },
+ "peerDependencies": {
+ "vue": "3.5.28"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz",
+ "integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/alien-signals": {
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
+ "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/commander": {
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
+ "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": "^12.20.0 || >=14"
+ }
+ },
+ "node_modules/computeds": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz",
+ "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/de-indent": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
+ "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fs-extra": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
+ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/import-lazy": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz",
+ "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/jju": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz",
+ "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "dev": true,
+ "license": "MIT",
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/kolorist": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
+ "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash": {
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
+ "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+ "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
+ "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/muggle-string": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz",
+ "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
+ "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.57.1",
+ "@rollup/rollup-android-arm64": "4.57.1",
+ "@rollup/rollup-darwin-arm64": "4.57.1",
+ "@rollup/rollup-darwin-x64": "4.57.1",
+ "@rollup/rollup-freebsd-arm64": "4.57.1",
+ "@rollup/rollup-freebsd-x64": "4.57.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.57.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.57.1",
+ "@rollup/rollup-linux-arm64-musl": "4.57.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.57.1",
+ "@rollup/rollup-linux-loong64-musl": "4.57.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.57.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.57.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.57.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.57.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.57.1",
+ "@rollup/rollup-linux-x64-gnu": "4.57.1",
+ "@rollup/rollup-linux-x64-musl": "4.57.1",
+ "@rollup/rollup-openbsd-x64": "4.57.1",
+ "@rollup/rollup-openharmony-arm64": "4.57.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.57.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.57.1",
+ "@rollup/rollup-win32-x64-gnu": "4.57.1",
+ "@rollup/rollup-win32-x64-msvc": "4.57.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/string-argv": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
+ "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.19"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/validator": {
+ "version": "13.15.26",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz",
+ "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-plugin-dts": {
+ "version": "3.9.1",
+ "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-3.9.1.tgz",
+ "integrity": "sha512-rVp2KM9Ue22NGWB8dNtWEr+KekN3rIgz1tWD050QnRGlriUCmaDwa7qA5zDEjbXg5lAXhYMSBJtx3q3hQIJZSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@microsoft/api-extractor": "7.43.0",
+ "@rollup/pluginutils": "^5.1.0",
+ "@vue/language-core": "^1.8.27",
+ "debug": "^4.3.4",
+ "kolorist": "^1.8.0",
+ "magic-string": "^0.30.8",
+ "vue-tsc": "^1.8.27"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "typescript": "*",
+ "vite": "*"
+ },
+ "peerDependenciesMeta": {
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-plugin-dts/node_modules/@volar/typescript": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz",
+ "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "1.11.1",
+ "path-browserify": "^1.0.1"
+ }
+ },
+ "node_modules/vite-plugin-dts/node_modules/vue-tsc": {
+ "version": "1.8.27",
+ "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.27.tgz",
+ "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/typescript": "~1.11.1",
+ "@vue/language-core": "1.8.27",
+ "semver": "^7.5.4"
+ },
+ "bin": {
+ "vue-tsc": "bin/vue-tsc.js"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ }
+ },
+ "node_modules/vscode-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
+ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vue": {
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz",
+ "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.28",
+ "@vue/compiler-sfc": "3.5.28",
+ "@vue/runtime-dom": "3.5.28",
+ "@vue/server-renderer": "3.5.28",
+ "@vue/shared": "3.5.28"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-template-compiler": {
+ "version": "2.7.16",
+ "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
+ "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "de-indent": "^1.0.2",
+ "he": "^1.2.0"
+ }
+ },
+ "node_modules/vue-tsc": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz",
+ "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/typescript": "2.4.15",
+ "@vue/language-core": "2.2.12"
+ },
+ "bin": {
+ "vue-tsc": "bin/vue-tsc.js"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.0.0"
+ }
+ },
+ "node_modules/vue-tsc/node_modules/@volar/language-core": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz",
+ "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/source-map": "2.4.15"
+ }
+ },
+ "node_modules/vue-tsc/node_modules/@volar/source-map": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz",
+ "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vue-tsc/node_modules/@vue/language-core": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz",
+ "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "@vue/compiler-dom": "^3.5.0",
+ "@vue/compiler-vue2": "^2.7.16",
+ "@vue/shared": "^3.5.0",
+ "alien-signals": "^1.0.3",
+ "minimatch": "^9.0.3",
+ "muggle-string": "^0.4.1",
+ "path-browserify": "^1.0.1"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-tsc/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/vue-tsc/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/vue-tsc/node_modules/muggle-string": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/z-schema": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
+ "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lodash.get": "^4.4.2",
+ "lodash.isequal": "^4.5.0",
+ "validator": "^13.7.0"
+ },
+ "bin": {
+ "z-schema": "bin/z-schema"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "commander": "^9.4.1"
+ }
+ }
+ }
+}
diff --git a/oled-emulator/package.json b/oled-emulator/package.json
new file mode 100644
index 000000000..439e6d5ec
--- /dev/null
+++ b/oled-emulator/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "@meshtastic/oled-emulator",
+ "version": "0.1.0",
+ "description": "Vue.js OLED display emulator for Meshtastic firmware UI development",
+ "keywords": [
+ "meshtastic",
+ "oled",
+ "ssd1306",
+ "sh1106",
+ "display",
+ "emulator",
+ "vue",
+ "vue3"
+ ],
+ "author": "Meshtastic",
+ "license": "GPL-3.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/meshtastic/firmware.git",
+ "directory": "oled-emulator"
+ },
+ "main": "./dist/index.js",
+ "module": "./dist/index.mjs",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.js",
+ "types": "./dist/index.d.ts"
+ },
+ "./style.css": "./dist/style.css"
+ },
+ "files": [
+ "dist",
+ "src"
+ ],
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc && vite build",
+ "preview": "vite preview",
+ "type-check": "vue-tsc --noEmit"
+ },
+ "peerDependencies": {
+ "vue": "^3.3.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.0.0",
+ "typescript": "^5.3.0",
+ "vite": "^5.0.0",
+ "vite-plugin-dts": "^3.7.0",
+ "vue": "^3.4.0",
+ "vue-tsc": "^2.0.0"
+ }
+}
diff --git a/oled-emulator/src/Demo.vue b/oled-emulator/src/Demo.vue
new file mode 100644
index 000000000..c60d3b18e
--- /dev/null
+++ b/oled-emulator/src/Demo.vue
@@ -0,0 +1,1001 @@
+
+
+
+
+
Meshtastic OLED Emulator
+
Vue.js component for rendering firmware UI screens
+
+
+
+
+
Display Settings
+
+
+ Display Type:
+
+
+ {{ preset.name }}
+
+
+
+
+
+ Resolution:
+
+
+ {{ geo }}
+
+
+
+
+
+ Pixel Scale:
+
+ {{ pixelScale }}x
+
+
+
+
+
+ Enable Pixel Glow
+
+
+
+
+
+
+ Show Pixel Grid
+
+
+
+
+
+
+ Dual Color (Yellow/Blue)
+
+
+
+
Screen Demo
+
+
+ Screen:
+
+
+ {{ screen.name }}
+
+
+
+
+
Refresh Display
+
+
Code Playground
+
+ {{ playgroundExpanded ? '▼ Hide Editor' : '▶ Show Editor' }}
+
+
+
+
+
+
+
+
+ {{ display.getWidth() }}×{{ display.getHeight() }} pixels
+ {{ display.getBufferSize() }} bytes framebuffer
+
+
+
+
+
+
+
+
+
Load Example:
+
+
+ {{ example.name }}
+
+
+
+
+
+
+ Error: {{ playgroundError }}
+
+
+ Press Ctrl+Enter (Cmd+Enter on Mac) to run • Examples based on real firmware code from src/graphics/
+
+
+
+
+
+
+
+
Usage Example
+
import { OLEDDisplay } from './OLEDDisplay';
+import { Font_6x8 } from './OLEDFonts';
+import OLEDEmulator from './OLEDEmulator.vue';
+
+// Create display (matches firmware OLEDDisplay API)
+const display = new OLEDDisplay('128x64');
+display.setFont(Font_6x8);
+
+// Draw using same API as firmware
+display.clear();
+display.drawString(0, 0, 'Hello Mesh!');
+display.drawRect(0, 16, 100, 30);
+display.fillCircle(64, 40, 10);
+
+// In Vue template:
+<OLEDEmulator
+ :display="display"
+ :pixel-scale="4"
+ preset="ssd1306-blue"
+/>
+
+
+
+
About
+
+ This emulator provides a TypeScript port of the OLEDDisplay API used in
+ Meshtastic firmware. It allows you to prototype and test screen layouts in the browser
+ before deploying to hardware.
+
+
Features:
+
+ Same drawing API as esp8266-oled-ssd1306 library
+ Multiple display sizes (128x64, 128x32, 64x48, 128x128)
+ Realistic OLED pixel effects (glow, color temperature)
+ Support for dual-color yellow/blue displays
+ XBM image rendering for icons
+ Font rendering with alignment support
+
+
Future: Protocol Bridge (Approach 3)
+
+ A future enhancement could connect this emulator to a real firmware instance
+ running on native/portduino, streaming the live framebuffer over WebSocket
+ for real-time display mirroring.
+
+
+
+
+
+
diff --git a/oled-emulator/src/DisplayPresets.ts b/oled-emulator/src/DisplayPresets.ts
new file mode 100644
index 000000000..b35a8e603
--- /dev/null
+++ b/oled-emulator/src/DisplayPresets.ts
@@ -0,0 +1,82 @@
+/**
+ * DisplayPresets.ts - OLED display presets for emulator
+ */
+
+import type { OLEDDISPLAY_GEOMETRY } from "./OLEDDisplay";
+
+// Display type presets with realistic OLED colors
+export interface DisplayPreset {
+ name: string;
+ geometry: OLEDDISPLAY_GEOMETRY;
+ pixelOn: { r: number; g: number; b: number };
+ pixelOff: { r: number; g: number; b: number };
+ glow: number;
+ contrast: number;
+}
+
+export const DISPLAY_PRESETS: Record = {
+ // Classic blue OLED (SSD1306)
+ "ssd1306-blue": {
+ name: "SSD1306 Blue",
+ geometry: "128x64",
+ pixelOn: { r: 100, g: 180, b: 255 },
+ pixelOff: { r: 2, g: 3, b: 8 },
+ glow: 0.4,
+ contrast: 1.0,
+ },
+ // White OLED (SSD1306)
+ "ssd1306-white": {
+ name: "SSD1306 White",
+ geometry: "128x64",
+ pixelOn: { r: 255, g: 255, b: 245 },
+ pixelOff: { r: 5, g: 5, b: 5 },
+ glow: 0.3,
+ contrast: 1.0,
+ },
+ // Yellow-blue dual color OLED
+ "ssd1306-yellow-blue": {
+ name: "SSD1306 Yellow/Blue",
+ geometry: "128x64",
+ // Yellow section is top 16 rows, blue is rest
+ pixelOn: { r: 255, g: 220, b: 50 }, // Will be overridden per-pixel
+ pixelOff: { r: 3, g: 3, b: 5 },
+ glow: 0.35,
+ contrast: 1.0,
+ },
+ // SH1106 (slightly different characteristics)
+ sh1106: {
+ name: "SH1106",
+ geometry: "128x64",
+ pixelOn: { r: 120, g: 200, b: 255 },
+ pixelOff: { r: 2, g: 2, b: 6 },
+ glow: 0.45,
+ contrast: 0.95,
+ },
+ // SH1107 128x128
+ sh1107: {
+ name: "SH1107 128x128",
+ geometry: "128x128",
+ pixelOn: { r: 255, g: 255, b: 240 },
+ pixelOff: { r: 4, g: 4, b: 6 },
+ glow: 0.3,
+ contrast: 1.0,
+ },
+ // Small 64x48 OLED
+ "ssd1306-64x48": {
+ name: "SSD1306 64x48",
+ geometry: "64x48",
+ pixelOn: { r: 100, g: 180, b: 255 },
+ pixelOff: { r: 2, g: 3, b: 8 },
+ glow: 0.4,
+ contrast: 1.0,
+ },
+ // Mini 128x32
+ "ssd1306-128x32": {
+ name: "SSD1306 128x32",
+ geometry: "128x32",
+ pixelOn: { r: 100, g: 180, b: 255 },
+ pixelOff: { r: 2, g: 3, b: 8 },
+ glow: 0.4,
+ contrast: 1.0,
+ },
+};
diff --git a/oled-emulator/src/OLEDDisplay.ts b/oled-emulator/src/OLEDDisplay.ts
new file mode 100644
index 000000000..247929083
--- /dev/null
+++ b/oled-emulator/src/OLEDDisplay.ts
@@ -0,0 +1,557 @@
+/**
+ * OLEDDisplay.ts - TypeScript port of esp8266-oled-ssd1306 OLEDDisplay API
+ *
+ * This provides the same drawing API as the firmware uses, allowing
+ * Meshtastic screen rendering code to be ported to the web.
+ *
+ * Buffer format: Vertical byte packing (8 vertical pixels per byte)
+ * Same as SSD1306/SH1106 native format.
+ */
+
+export type OLEDDISPLAY_GEOMETRY =
+ | "128x64"
+ | "128x32"
+ | "64x48"
+ | "64x32"
+ | "128x128"
+ | "96x16";
+
+export type OLEDDISPLAY_COLOR = "WHITE" | "BLACK" | "INVERSE";
+
+export type OLEDDISPLAY_TEXT_ALIGNMENT =
+ | "TEXT_ALIGN_LEFT"
+ | "TEXT_ALIGN_CENTER"
+ | "TEXT_ALIGN_RIGHT";
+
+export interface OLEDFont {
+ height: number;
+ firstChar: number;
+ charCount: number;
+ widths: number[];
+ data: Uint8Array;
+}
+
+// Resolution info based on geometry
+const GEOMETRY_SIZES: Record<
+ OLEDDISPLAY_GEOMETRY,
+ { width: number; height: number }
+> = {
+ "128x64": { width: 128, height: 64 },
+ "128x32": { width: 128, height: 32 },
+ "64x48": { width: 64, height: 48 },
+ "64x32": { width: 64, height: 32 },
+ "128x128": { width: 128, height: 128 },
+ "96x16": { width: 96, height: 16 },
+};
+
+export class OLEDDisplay {
+ private buffer: Uint8Array;
+ private _width: number;
+ private _height: number;
+ private color: OLEDDISPLAY_COLOR = "WHITE";
+ private textAlignment: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_LEFT";
+ private font: OLEDFont | null = null;
+
+ constructor(geometry: OLEDDISPLAY_GEOMETRY = "128x64") {
+ const size = GEOMETRY_SIZES[geometry];
+ this._width = size.width;
+ this._height = size.height;
+ // Buffer size: width * (height / 8) - vertical byte packing
+ this.buffer = new Uint8Array((this._width * this._height) / 8);
+ }
+
+ get width(): number {
+ return this._width;
+ }
+
+ get height(): number {
+ return this._height;
+ }
+
+ getWidth(): number {
+ return this._width;
+ }
+
+ getHeight(): number {
+ return this._height;
+ }
+
+ /**
+ * Get the raw framebuffer (vertical byte packing format)
+ */
+ getBuffer(): Uint8Array {
+ return this.buffer;
+ }
+
+ getBufferSize(): number {
+ return this.buffer.length;
+ }
+
+ /**
+ * Clear the display buffer
+ */
+ clear(): void {
+ this.buffer.fill(0);
+ }
+
+ /**
+ * Set the drawing color
+ */
+ setColor(color: OLEDDISPLAY_COLOR): void {
+ this.color = color;
+ }
+
+ /**
+ * Set text alignment for drawString
+ */
+ setTextAlignment(align: OLEDDISPLAY_TEXT_ALIGNMENT): void {
+ this.textAlignment = align;
+ }
+
+ /**
+ * Set the current font
+ */
+ setFont(font: OLEDFont): void {
+ this.font = font;
+ }
+
+ /**
+ * Set a single pixel
+ */
+ setPixel(x: number, y: number): void {
+ if (x < 0 || x >= this._width || y < 0 || y >= this._height) return;
+
+ // Vertical byte packing: each byte represents 8 vertical pixels
+ const byteIndex = x + Math.floor(y / 8) * this._width;
+ const bitMask = 1 << (y & 7);
+
+ switch (this.color) {
+ case "WHITE":
+ this.buffer[byteIndex] |= bitMask;
+ break;
+ case "BLACK":
+ this.buffer[byteIndex] &= ~bitMask;
+ break;
+ case "INVERSE":
+ this.buffer[byteIndex] ^= bitMask;
+ break;
+ }
+ }
+
+ /**
+ * Clear a single pixel (set to black)
+ */
+ clearPixel(x: number, y: number): void {
+ if (x < 0 || x >= this._width || y < 0 || y >= this._height) return;
+ const byteIndex = x + Math.floor(y / 8) * this._width;
+ const bitMask = 1 << (y & 7);
+ this.buffer[byteIndex] &= ~bitMask;
+ }
+
+ /**
+ * Get a pixel value (true = on, false = off)
+ */
+ getPixel(x: number, y: number): boolean {
+ if (x < 0 || x >= this._width || y < 0 || y >= this._height) return false;
+ const byteIndex = x + Math.floor(y / 8) * this._width;
+ const bitMask = 1 << (y & 7);
+ return (this.buffer[byteIndex] & bitMask) !== 0;
+ }
+
+ /**
+ * Draw a line using Bresenham's algorithm
+ */
+ drawLine(x0: number, y0: number, x1: number, y1: number): void {
+ const dx = Math.abs(x1 - x0);
+ const dy = Math.abs(y1 - y0);
+ const sx = x0 < x1 ? 1 : -1;
+ const sy = y0 < y1 ? 1 : -1;
+ let err = dx - dy;
+
+ while (true) {
+ this.setPixel(x0, y0);
+ if (x0 === x1 && y0 === y1) break;
+ const e2 = 2 * err;
+ if (e2 > -dy) {
+ err -= dy;
+ x0 += sx;
+ }
+ if (e2 < dx) {
+ err += dx;
+ y0 += sy;
+ }
+ }
+ }
+
+ /**
+ * Draw horizontal line (optimized)
+ */
+ drawHorizontalLine(x: number, y: number, length: number): void {
+ if (y < 0 || y >= this._height) return;
+ for (let i = 0; i < length; i++) {
+ this.setPixel(x + i, y);
+ }
+ }
+
+ /**
+ * Draw vertical line (optimized)
+ */
+ drawVerticalLine(x: number, y: number, length: number): void {
+ if (x < 0 || x >= this._width) return;
+ for (let i = 0; i < length; i++) {
+ this.setPixel(x, y + i);
+ }
+ }
+
+ /**
+ * Draw a rectangle outline
+ */
+ drawRect(x: number, y: number, width: number, height: number): void {
+ this.drawHorizontalLine(x, y, width);
+ this.drawHorizontalLine(x, y + height - 1, width);
+ this.drawVerticalLine(x, y, height);
+ this.drawVerticalLine(x + width - 1, y, height);
+ }
+
+ /**
+ * Draw a filled rectangle
+ */
+ fillRect(x: number, y: number, width: number, height: number): void {
+ for (let i = 0; i < width; i++) {
+ for (let j = 0; j < height; j++) {
+ this.setPixel(x + i, y + j);
+ }
+ }
+ }
+
+ /**
+ * Draw a circle outline using midpoint algorithm
+ */
+ drawCircle(x0: number, y0: number, radius: number): void {
+ let x = radius;
+ let y = 0;
+ let err = 0;
+
+ while (x >= y) {
+ this.setPixel(x0 + x, y0 + y);
+ this.setPixel(x0 + y, y0 + x);
+ this.setPixel(x0 - y, y0 + x);
+ this.setPixel(x0 - x, y0 + y);
+ this.setPixel(x0 - x, y0 - y);
+ this.setPixel(x0 - y, y0 - x);
+ this.setPixel(x0 + y, y0 - x);
+ this.setPixel(x0 + x, y0 - y);
+
+ y++;
+ if (err <= 0) {
+ err += 2 * y + 1;
+ }
+ if (err > 0) {
+ x--;
+ err -= 2 * x + 1;
+ }
+ }
+ }
+
+ /**
+ * Draw a filled circle
+ */
+ fillCircle(x0: number, y0: number, radius: number): void {
+ let x = radius;
+ let y = 0;
+ let err = 0;
+
+ while (x >= y) {
+ this.drawHorizontalLine(x0 - x, y0 + y, 2 * x + 1);
+ this.drawHorizontalLine(x0 - y, y0 + x, 2 * y + 1);
+ this.drawHorizontalLine(x0 - x, y0 - y, 2 * x + 1);
+ this.drawHorizontalLine(x0 - y, y0 - x, 2 * y + 1);
+
+ y++;
+ if (err <= 0) {
+ err += 2 * y + 1;
+ }
+ if (err > 0) {
+ x--;
+ err -= 2 * x + 1;
+ }
+ }
+ }
+
+ /**
+ * Draw a quarter circle (quadrant 0-3)
+ */
+ drawCircleQuads(x0: number, y0: number, radius: number, quads: number): void {
+ let x = radius;
+ let y = 0;
+ let err = 0;
+
+ while (x >= y) {
+ if (quads & 0x1) {
+ this.setPixel(x0 + x, y0 - y);
+ this.setPixel(x0 + y, y0 - x);
+ }
+ if (quads & 0x2) {
+ this.setPixel(x0 - y, y0 - x);
+ this.setPixel(x0 - x, y0 - y);
+ }
+ if (quads & 0x4) {
+ this.setPixel(x0 - x, y0 + y);
+ this.setPixel(x0 - y, y0 + x);
+ }
+ if (quads & 0x8) {
+ this.setPixel(x0 + y, y0 + x);
+ this.setPixel(x0 + x, y0 + y);
+ }
+
+ y++;
+ if (err <= 0) {
+ err += 2 * y + 1;
+ }
+ if (err > 0) {
+ x--;
+ err -= 2 * x + 1;
+ }
+ }
+ }
+
+ /**
+ * Draw an XBM image (like firmware's drawXbm)
+ * XBM format: LSB first, horizontal
+ */
+ drawXbm(
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+ xbm: Uint8Array | number[],
+ ): void {
+ const widthBytes = Math.ceil(width / 8);
+ for (let j = 0; j < height; j++) {
+ for (let i = 0; i < width; i++) {
+ const byteIndex = j * widthBytes + Math.floor(i / 8);
+ const bitIndex = i % 8;
+ if (xbm[byteIndex] & (1 << bitIndex)) {
+ this.setPixel(x + i, y + j);
+ }
+ }
+ }
+ }
+
+ /**
+ * Draw a PROGMEM-style image (vertical byte format, like some firmware icons)
+ */
+ drawFastImage(
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+ image: Uint8Array | number[],
+ ): void {
+ const heightInBytes = Math.ceil(height / 8);
+ for (let i = 0; i < width; i++) {
+ for (let j = 0; j < heightInBytes; j++) {
+ const imageByte = image[i + j * width];
+ for (let bit = 0; bit < 8; bit++) {
+ if (imageByte & (1 << bit)) {
+ this.setPixel(x + i, y + j * 8 + bit);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Get the width of a string with the current font
+ */
+ getStringWidth(text: string): number {
+ if (!this.font) return 0;
+ let width = 0;
+ for (const char of text) {
+ const charCode = char.charCodeAt(0);
+ if (
+ charCode >= this.font.firstChar &&
+ charCode < this.font.firstChar + this.font.charCount
+ ) {
+ width += this.font.widths[charCode - this.font.firstChar];
+ } else {
+ // Unknown char - use space width or default
+ width += this.font.widths[0] || 4;
+ }
+ }
+ return width;
+ }
+
+ /**
+ * Draw a string at the given position
+ */
+ drawString(x: number, y: number, text: string): void {
+ if (!this.font) return;
+
+ // Adjust x based on alignment
+ let startX = x;
+ if (this.textAlignment === "TEXT_ALIGN_CENTER") {
+ startX = x - Math.floor(this.getStringWidth(text) / 2);
+ } else if (this.textAlignment === "TEXT_ALIGN_RIGHT") {
+ startX = x - this.getStringWidth(text);
+ }
+
+ let cursorX = startX;
+ for (const char of text) {
+ const charCode = char.charCodeAt(0);
+ if (
+ charCode >= this.font.firstChar &&
+ charCode < this.font.firstChar + this.font.charCount
+ ) {
+ const charIndex = charCode - this.font.firstChar;
+ const charWidth = this.font.widths[charIndex];
+
+ // Find the offset into the font data
+ let offset = 0;
+ const bytesPerColumn = Math.ceil(this.font.height / 8);
+ for (let i = 0; i < charIndex; i++) {
+ offset += this.font.widths[i] * bytesPerColumn;
+ }
+
+ // Draw the character
+ this.drawFontChar(
+ cursorX,
+ y,
+ charWidth,
+ this.font.height,
+ this.font.data,
+ offset,
+ );
+ cursorX += charWidth;
+ } else {
+ // Unknown character - advance by space
+ cursorX += this.font.widths[0] || 4;
+ }
+ }
+ }
+
+ /**
+ * Draw a single font character
+ */
+ private drawFontChar(
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+ data: Uint8Array,
+ offset: number,
+ ): void {
+ const bytesPerColumn = Math.ceil(height / 8);
+ for (let col = 0; col < width; col++) {
+ for (let byteRow = 0; byteRow < bytesPerColumn; byteRow++) {
+ const dataByte = data[offset + col * bytesPerColumn + byteRow];
+ for (let bit = 0; bit < 8; bit++) {
+ const pixelY = byteRow * 8 + bit;
+ if (pixelY < height && dataByte & (1 << bit)) {
+ this.setPixel(x + col, y + pixelY);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Draw a string that wraps within maxWidth
+ */
+ drawStringMaxWidth(
+ x: number,
+ y: number,
+ maxWidth: number,
+ text: string,
+ ): void {
+ if (!this.font) return;
+
+ const words = text.split(" ");
+ let line = "";
+ let lineY = y;
+
+ for (const word of words) {
+ const testLine = line + (line ? " " : "") + word;
+ const testWidth = this.getStringWidth(testLine);
+
+ if (testWidth > maxWidth && line) {
+ this.drawString(x, lineY, line);
+ line = word;
+ lineY += this.font.height;
+ } else {
+ line = testLine;
+ }
+ }
+
+ if (line) {
+ this.drawString(x, lineY, line);
+ }
+ }
+
+ /**
+ * Draw a progress bar
+ */
+ drawProgressBar(
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+ progress: number,
+ ): void {
+ // Clamp progress 0-100
+ progress = Math.max(0, Math.min(100, progress));
+
+ // Draw outline
+ this.drawRect(x, y, width, height);
+
+ // Draw fill
+ const fillWidth = Math.floor(((width - 4) * progress) / 100);
+ this.fillRect(x + 2, y + 2, fillWidth, height - 4);
+ }
+
+ /**
+ * Display - no-op for emulator (would send buffer to hardware)
+ */
+ display(): void {
+ // In real hardware this sends the buffer to the display
+ // For emulator, this is a no-op - we render from getBuffer()
+ }
+
+ /**
+ * Flip the display (useful for some mounting orientations)
+ */
+ flipScreenVertically(): void {
+ // Rotate buffer 180 degrees
+ const newBuffer = new Uint8Array(this.buffer.length);
+ const heightBytes = this._height / 8;
+
+ for (let x = 0; x < this._width; x++) {
+ for (let yByte = 0; yByte < heightBytes; yByte++) {
+ let srcByte = this.buffer[x + yByte * this._width];
+ // Reverse the bits
+ let reversed = 0;
+ for (let i = 0; i < 8; i++) {
+ if (srcByte & (1 << i)) {
+ reversed |= 1 << (7 - i);
+ }
+ }
+ // Place in mirrored position
+ const newX = this._width - 1 - x;
+ const newYByte = heightBytes - 1 - yByte;
+ newBuffer[newX + newYByte * this._width] = reversed;
+ }
+ }
+
+ this.buffer = newBuffer;
+ }
+}
+
+// Color constants for compatibility with firmware code style
+export const WHITE: OLEDDISPLAY_COLOR = "WHITE";
+export const BLACK: OLEDDISPLAY_COLOR = "BLACK";
+export const INVERSE: OLEDDISPLAY_COLOR = "INVERSE";
+
+export const TEXT_ALIGN_LEFT: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_LEFT";
+export const TEXT_ALIGN_CENTER: OLEDDISPLAY_TEXT_ALIGNMENT =
+ "TEXT_ALIGN_CENTER";
+export const TEXT_ALIGN_RIGHT: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_RIGHT";
diff --git a/oled-emulator/src/OLEDEmulator.vue b/oled-emulator/src/OLEDEmulator.vue
new file mode 100644
index 000000000..862b6f35b
--- /dev/null
+++ b/oled-emulator/src/OLEDEmulator.vue
@@ -0,0 +1,327 @@
+
+
+
+
+
+
+
diff --git a/oled-emulator/src/OLEDFonts.ts b/oled-emulator/src/OLEDFonts.ts
new file mode 100644
index 000000000..8a45ad02e
--- /dev/null
+++ b/oled-emulator/src/OLEDFonts.ts
@@ -0,0 +1,467 @@
+/**
+ * OLEDFonts.ts - Font data for OLED display emulator
+ *
+ * These are simplified bitmap fonts matching the style used in
+ * Meshtastic firmware (based on ArialMT from esp8266-oled-ssd1306).
+ *
+ * Font format: Vertical byte packing, column-major order
+ * Each character is drawn column by column, with each byte representing
+ * 8 vertical pixels.
+ */
+
+import type { OLEDFont } from "./OLEDDisplay";
+
+/**
+ * ArialMT Plain 10 - Small font (height ~13 pixels)
+ * Covers ASCII 32-126 (space through ~)
+ */
+export const ArialMT_Plain_10: OLEDFont = {
+ height: 13,
+ firstChar: 32,
+ charCount: 95,
+ widths: [
+ 3, // space
+ 3, // !
+ 4, // "
+ 6, // #
+ 6, // $
+ 9, // %
+ 7, // &
+ 2, // '
+ 4, // (
+ 4, // )
+ 5, // *
+ 6, // +
+ 3, // ,
+ 4, // -
+ 3, // .
+ 4, // /
+ 6, // 0
+ 6, // 1
+ 6, // 2
+ 6, // 3
+ 6, // 4
+ 6, // 5
+ 6, // 6
+ 6, // 7
+ 6, // 8
+ 6, // 9
+ 3, // :
+ 3, // ;
+ 6, // <
+ 6, // =
+ 6, // >
+ 6, // ?
+ 11, // @
+ 8, // A
+ 7, // B
+ 7, // C
+ 7, // D
+ 6, // E
+ 6, // F
+ 8, // G
+ 7, // H
+ 3, // I
+ 5, // J
+ 7, // K
+ 6, // L
+ 9, // M
+ 7, // N
+ 8, // O
+ 6, // P
+ 8, // Q
+ 7, // R
+ 6, // S
+ 6, // T
+ 7, // U
+ 7, // V
+ 11, // W
+ 7, // X
+ 7, // Y
+ 6, // Z
+ 4, // [
+ 4, // \
+ 4, // ]
+ 6, // ^
+ 6, // _
+ 4, // `
+ 6, // a
+ 6, // b
+ 5, // c
+ 6, // d
+ 6, // e
+ 4, // f
+ 6, // g
+ 6, // h
+ 3, // i
+ 3, // j
+ 6, // k
+ 3, // l
+ 9, // m
+ 6, // n
+ 6, // o
+ 6, // p
+ 6, // q
+ 4, // r
+ 5, // s
+ 4, // t
+ 6, // u
+ 6, // v
+ 9, // w
+ 6, // x
+ 6, // y
+ 5, // z
+ 4, // {
+ 3, // |
+ 4, // }
+ 6, // ~
+ ],
+ data: new Uint8Array([
+ // Space (32)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // ! (33)
+ 0x00, 0x00, 0xbe, 0x00, 0x00, 0x00,
+ // " (34)
+ 0x00, 0x0e, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00,
+ // # (35)
+ 0x00, 0x48, 0xf8, 0x4e, 0xf8, 0x4e, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // $ (36)
+ 0x00, 0x9c, 0x92, 0xff, 0x92, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // % (37)
+ 0x0c, 0x12, 0x12, 0x8c, 0x60, 0x18, 0xc6, 0x20, 0x20, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // & (38)
+ 0x00, 0x60, 0x9c, 0x92, 0x6a, 0xa4, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00,
+ // ' (39)
+ 0x00, 0x0e, 0x00, 0x00,
+ // ( (40)
+ 0x00, 0x7c, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // ) (41)
+ 0x00, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // * (42)
+ 0x14, 0x08, 0x3e, 0x08, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // + (43)
+ 0x10, 0x10, 0x7c, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // , (44)
+ 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00,
+ // - (45)
+ 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00,
+ // . (46)
+ 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
+ // / (47)
+ 0x00, 0xc0, 0x38, 0x06, 0x00, 0x00, 0x00, 0x00,
+ // 0 (48)
+ 0x7c, 0x82, 0x82, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 1 (49)
+ 0x00, 0x04, 0x02, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 2 (50)
+ 0x84, 0xc2, 0xa2, 0x92, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 3 (51)
+ 0x44, 0x82, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 4 (52)
+ 0x30, 0x28, 0x24, 0xfe, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 5 (53)
+ 0x4e, 0x8a, 0x8a, 0x8a, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 6 (54)
+ 0x78, 0x94, 0x92, 0x92, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 7 (55)
+ 0x02, 0xc2, 0x32, 0x0a, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 8 (56)
+ 0x6c, 0x92, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 9 (57)
+ 0x0c, 0x92, 0x92, 0x52, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // : (58)
+ 0x00, 0x00, 0x88, 0x00, 0x00, 0x00,
+ // ; (59)
+ 0x00, 0x00, 0xc8, 0x00, 0x00, 0x00,
+ // < (60)
+ 0x10, 0x28, 0x28, 0x44, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // = (61)
+ 0x28, 0x28, 0x28, 0x28, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // > (62)
+ 0x44, 0x44, 0x28, 0x28, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // ? (63)
+ 0x04, 0x02, 0xa2, 0x12, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // @ (64)
+ 0xf0, 0x08, 0xe4, 0x12, 0x12, 0x12, 0xe2, 0x14, 0x08, 0xf0, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // A (65)
+ 0x00, 0xc0, 0x38, 0x26, 0x26, 0x38, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ // B (66)
+ 0xfe, 0x92, 0x92, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00,
+ // C (67)
+ 0x7c, 0x82, 0x82, 0x82, 0x82, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00,
+ // D (68)
+ 0xfe, 0x82, 0x82, 0x82, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00,
+ // E (69)
+ 0xfe, 0x92, 0x92, 0x92, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // F (70)
+ 0xfe, 0x12, 0x12, 0x12, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // G (71)
+ 0x7c, 0x82, 0x82, 0x92, 0x92, 0x74, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ // H (72)
+ 0xfe, 0x10, 0x10, 0x10, 0x10, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00,
+ // I (73)
+ 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00,
+ // J (74)
+ 0x40, 0x80, 0x80, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // K (75)
+ 0xfe, 0x10, 0x28, 0x44, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00,
+ // L (76)
+ 0xfe, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // M (77)
+ 0xfe, 0x04, 0x18, 0x60, 0x60, 0x18, 0x04, 0xfe, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // N (78)
+ 0xfe, 0x04, 0x08, 0x30, 0x40, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00,
+ // O (79)
+ 0x7c, 0x82, 0x82, 0x82, 0x82, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ // P (80)
+ 0xfe, 0x12, 0x12, 0x12, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // Q (81)
+ 0x7c, 0x82, 0x82, 0x82, 0x42, 0xc2, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ // R (82)
+ 0xfe, 0x12, 0x12, 0x32, 0x4c, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00,
+ // S (83)
+ 0x4c, 0x92, 0x92, 0x92, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // T (84)
+ 0x02, 0x02, 0xfe, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // U (85)
+ 0x7e, 0x80, 0x80, 0x80, 0x80, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00,
+ // V (86)
+ 0x06, 0x18, 0x60, 0x80, 0x60, 0x18, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00,
+ // W (87)
+ 0x06, 0x38, 0xc0, 0x38, 0x06, 0x38, 0xc0, 0x38, 0x06, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // X (88)
+ 0x82, 0x44, 0x28, 0x10, 0x28, 0x44, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00,
+ // Y (89)
+ 0x02, 0x04, 0x08, 0xf0, 0x08, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00,
+ // Z (90)
+ 0xc2, 0xa2, 0x92, 0x8a, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // [ (91)
+ 0x00, 0xfe, 0x82, 0x82, 0x00, 0x00, 0x00, 0x00,
+ // \ (92)
+ 0x06, 0x38, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // ] (93)
+ 0x82, 0x82, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // ^ (94)
+ 0x08, 0x04, 0x02, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // _ (95)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // ` (96)
+ 0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // a (97)
+ 0x40, 0xa8, 0xa8, 0xa8, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // b (98)
+ 0xfe, 0x88, 0x88, 0x88, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // c (99)
+ 0x70, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // d (100)
+ 0x70, 0x88, 0x88, 0x88, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // e (101)
+ 0x70, 0xa8, 0xa8, 0xa8, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // f (102)
+ 0x08, 0xfc, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // g (103)
+ 0x30, 0x48, 0x48, 0x48, 0xf8, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00,
+ // h (104)
+ 0xfe, 0x08, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // i (105)
+ 0x00, 0xfa, 0x00, 0x00, 0x00, 0x00,
+ // j (106)
+ 0x00, 0x00, 0xfa, 0x01, 0x00, 0x00,
+ // k (107)
+ 0xfe, 0x20, 0x50, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // l (108)
+ 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00,
+ // m (109)
+ 0xf8, 0x08, 0x08, 0xf0, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // n (110)
+ 0xf8, 0x08, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // o (111)
+ 0x70, 0x88, 0x88, 0x88, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // p (112)
+ 0xf8, 0x48, 0x48, 0x48, 0x30, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // q (113)
+ 0x30, 0x48, 0x48, 0x48, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
+ // r (114)
+ 0xf8, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // s (115)
+ 0x90, 0xa8, 0xa8, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // t (116)
+ 0x08, 0x7e, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // u (117)
+ 0x78, 0x80, 0x80, 0x80, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // v (118)
+ 0x18, 0x60, 0x80, 0x60, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // w (119)
+ 0x78, 0x80, 0x60, 0x18, 0x60, 0x80, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // x (120)
+ 0x88, 0x50, 0x20, 0x50, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // y (121)
+ 0x18, 0x60, 0x80, 0x60, 0x18, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // z (122)
+ 0xc8, 0xa8, 0x98, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // { (123)
+ 0x10, 0x6c, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // | (124)
+ 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00,
+ // } (125)
+ 0x82, 0x6c, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // ~ (126)
+ 0x10, 0x08, 0x10, 0x20, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ ]),
+};
+
+/**
+ * ArialMT Plain 16 - Medium font (height ~19 pixels)
+ * Simplified subset covering 0-9, A-Z, a-z, common punctuation
+ */
+export const ArialMT_Plain_16: OLEDFont = {
+ height: 19,
+ firstChar: 32,
+ charCount: 95,
+ widths: [
+ 4, // space
+ 4, // !
+ 5, // "
+ 8, // #
+ 8, // $
+ 12, // %
+ 10, // &
+ 2, // '
+ 5, // (
+ 5, // )
+ 6, // *
+ 8, // +
+ 4, // ,
+ 5, // -
+ 4, // .
+ 4, // /
+ 8, // 0
+ 8, // 1
+ 8, // 2
+ 8, // 3
+ 8, // 4
+ 8, // 5
+ 8, // 6
+ 8, // 7
+ 8, // 8
+ 8, // 9
+ 4, // :
+ 4, // ;
+ 8, // <
+ 8, // =
+ 8, // >
+ 8, // ?
+ 15, // @
+ 10, // A
+ 9, // B
+ 10, // C
+ 10, // D
+ 9, // E
+ 8, // F
+ 11, // G
+ 9, // H
+ 4, // I
+ 6, // J
+ 9, // K
+ 8, // L
+ 11, // M
+ 9, // N
+ 11, // O
+ 9, // P
+ 11, // Q
+ 10, // R
+ 9, // S
+ 8, // T
+ 9, // U
+ 10, // V
+ 14, // W
+ 10, // X
+ 10, // Y
+ 9, // Z
+ 4, // [
+ 4, // \
+ 4, // ]
+ 7, // ^
+ 8, // _
+ 5, // `
+ 8, // a
+ 8, // b
+ 7, // c
+ 8, // d
+ 8, // e
+ 4, // f
+ 8, // g
+ 8, // h
+ 4, // i
+ 4, // j
+ 8, // k
+ 4, // l
+ 12, // m
+ 8, // n
+ 8, // o
+ 8, // p
+ 8, // q
+ 5, // r
+ 7, // s
+ 4, // t
+ 8, // u
+ 8, // v
+ 12, // w
+ 8, // x
+ 8, // y
+ 7, // z
+ 5, // {
+ 4, // |
+ 5, // }
+ 8, // ~
+ ],
+ // Font data would be similar pattern but larger - using placeholder
+ data: new Uint8Array(2500).fill(0xff), // Placeholder - would contain actual font bitmap data
+};
+
+/**
+ * Helper to get FONT_HEIGHT_SMALL equivalent
+ */
+export const FONT_HEIGHT_SMALL = 13;
+export const FONT_HEIGHT_MEDIUM = 19;
+export const FONT_HEIGHT_LARGE = 28;
+
+/**
+ * Helper function to create a font from raw byte data (for importing firmware fonts)
+ */
+export function createFontFromBytes(
+ height: number,
+ firstChar: number,
+ widths: number[],
+ data: number[],
+): OLEDFont {
+ return {
+ height,
+ firstChar,
+ charCount: widths.length,
+ widths,
+ data: new Uint8Array(data),
+ };
+}
diff --git a/oled-emulator/src/OLEDImages.ts b/oled-emulator/src/OLEDImages.ts
new file mode 100644
index 000000000..e81a2cc27
--- /dev/null
+++ b/oled-emulator/src/OLEDImages.ts
@@ -0,0 +1,255 @@
+/**
+ * OLEDImages.ts - Icon and image data for OLED display emulator
+ *
+ * These are direct ports of the XBM/bitmap icons from the Meshtastic firmware.
+ * Format: XBM style (LSB first, horizontal scanning)
+ */
+
+// Satellite icon (8x8)
+export const imgSatellite_width = 8;
+export const imgSatellite_height = 8;
+export const imgSatellite = new Uint8Array([
+ 0b00000000, 0b00000000, 0b00000000, 0b00011000, 0b11011011, 0b11111111,
+ 0b11011011, 0b00011000,
+]);
+
+// USB icon (10x8)
+export const imgUSB = new Uint8Array([
+ 0x00, 0xfc, 0xf0, 0xfc, 0x88, 0xff, 0x86, 0xfe, 0x85, 0xfe, 0x89, 0xff, 0xf1,
+ 0xfc, 0x00, 0xfc,
+]);
+
+// Power icon (8x16)
+export const imgPower = new Uint8Array([
+ 0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08, 0x1c, 0x22, 0x22, 0x41, 0x7f,
+ 0x22, 0x22, 0x22,
+]);
+
+// User icon (8x8)
+export const imgUser = new Uint8Array([
+ 0x3c, 0x42, 0x99, 0xa5, 0xa5, 0x99, 0x42, 0x3c,
+]);
+
+// Position empty arrow (6x8)
+export const imgPositionEmpty = new Uint8Array([
+ 0x20, 0x30, 0x28, 0x24, 0x42, 0xff,
+]);
+
+// Position solid arrow (6x8)
+export const imgPositionSolid = new Uint8Array([
+ 0x20, 0x30, 0x38, 0x3c, 0x7e, 0xff,
+]);
+
+// Mail icon (10x7)
+export const mail_width = 10;
+export const mail_height = 7;
+export const mail = new Uint8Array([
+ 0b11111111,
+ 0b00, // Top line
+ 0b10000001,
+ 0b00, // Edges
+ 0b11000011,
+ 0b00, // Diagonals start
+ 0b10100101,
+ 0b00, // Inner M part
+ 0b10011001,
+ 0b00, // Inner M part
+ 0b10000001,
+ 0b00, // Edges
+ 0b11111111,
+ 0b00, // Bottom line
+]);
+
+// Hop icon (9x10)
+export const hop_width = 9;
+export const hop_height = 10;
+export const hop = new Uint8Array([
+ 0x05, 0x00, 0x07, 0x00, 0x05, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, 0x00, 0xc0,
+ 0x01, 0x40, 0x01, 0xc0, 0x01, 0x40, 0x00,
+]);
+
+// Mail icon (8x8)
+export const icon_mail = new Uint8Array([
+ 0b11111111, // ████████ top border
+ 0b10000001, // █ █ sides
+ 0b11000011, // ██ ██ diagonal
+ 0b10100101, // █ █ █ █ inner M
+ 0b10011001, // █ ██ █ inner M
+ 0b10000001, // █ █ sides
+ 0b10000001, // █ █ sides
+ 0b11111111, // ████████ bottom
+]);
+
+// Compass icon (8x8)
+export const icon_compass = new Uint8Array([
+ 0x3c, // Row 0: ..####..
+ 0x52, // Row 1: .#..#.#.
+ 0x91, // Row 2: #...#..#
+ 0x91, // Row 3: #...#..#
+ 0x91, // Row 4: #...#..#
+ 0x81, // Row 5: #......#
+ 0x42, // Row 6: .#....#.
+ 0x3c, // Row 7: ..####..
+]);
+
+// Radio icon (8x8)
+export const icon_radio = new Uint8Array([
+ 0x0f, // Row 0: ####....
+ 0x10, // Row 1: ....#...
+ 0x27, // Row 2: ###..#..
+ 0x48, // Row 3: ...#..#.
+ 0x93, // Row 4: ##..#..#
+ 0xa4, // Row 5: ..#..#.#
+ 0xa8, // Row 6: ...#.#.#
+ 0xa9, // Row 7: #..#.#.#
+]);
+
+// System/settings icon (8x8)
+export const icon_system = new Uint8Array([
+ 0x24, // Row 0: ..#..#..
+ 0x3c, // Row 1: ..####..
+ 0xc3, // Row 2: ##....##
+ 0x5a, // Row 3: .#.##.#.
+ 0x5a, // Row 4: .#.##.#.
+ 0xc3, // Row 5: ##....##
+ 0x3c, // Row 6: ..####..
+ 0x24, // Row 7: ..#..#..
+]);
+
+// WiFi icon (8x8)
+export const icon_wifi = new Uint8Array([
+ 0b00000000, 0b00011000, 0b00111100, 0b01111110, 0b11011011, 0b00011000,
+ 0b00011000, 0b00000000,
+]);
+
+// Nodes icon (8x8)
+export const icon_nodes = new Uint8Array([
+ 0xf9, // Row 0: #..#####
+ 0x00, // Row 1
+ 0xf9, // Row 2: #..#####
+ 0x00, // Row 3
+ 0xf9, // Row 4: #..#####
+ 0x00, // Row 5
+ 0xf9, // Row 6: #..#####
+ 0x00, // Row 7
+]);
+
+// Battery vertical (7x11)
+export const batteryBitmap_v = new Uint8Array([
+ 0b00011100, 0b00111110, 0b01000001, 0b01000001, 0b00000000, 0b00000000,
+ 0b00000000, 0b01000001, 0b01000001, 0b01000001, 0b00111110,
+]);
+
+// Battery side gaps (8x3)
+export const batteryBitmap_sidegaps_v = new Uint8Array([
+ 0b10000010, 0b10000010, 0b10000010,
+]);
+
+// Lightning bolt vertical (5x5)
+export const lightning_bolt_v = new Uint8Array([
+ 0b00000100, 0b00000110, 0b00011111, 0b00001100, 0b00000100,
+]);
+
+// Horizontal battery bottom (13x9)
+export const batteryBitmap_h_bottom = new Uint8Array([
+ 0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
+ 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
+ 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
+ 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
+ 0b00011110, 0b00000000,
+]);
+
+// Horizontal battery top (13x9)
+export const batteryBitmap_h_top = new Uint8Array([
+ 0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000,
+ 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b11000000, 0b00000000,
+ 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000,
+ 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000,
+ 0b00111100, 0b00000000,
+]);
+
+// Lightning bolt horizontal (13x9)
+export const lightning_bolt_h = new Uint8Array([
+ 0b00000000, 0b00000000, 0b00100000, 0b00000000, 0b00110000, 0b00000000,
+ 0b00111000, 0b00000000, 0b00111100, 0b00000000, 0b00011110, 0b00000000,
+ 0b11111111, 0b00000000, 0b01111000, 0b00000000, 0b00111100, 0b00000000,
+ 0b00011100, 0b00000000, 0b00001100, 0b00000000, 0b00000100, 0b00000000,
+ 0b00000000, 0b00000000,
+]);
+
+// Signal bars icon (various signal strengths)
+export const signal_bars = {
+ width: 12,
+ height: 8,
+ // 0 bars
+ none: new Uint8Array([
+ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
+ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
+ 0b00000000, 0b00000000, 0b00000000, 0b00000000,
+ ]),
+ // 1 bar
+ low: new Uint8Array([
+ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
+ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
+ 0b00000011, 0b00000000, 0b00000011, 0b00000000,
+ ]),
+ // 2 bars
+ medium: new Uint8Array([
+ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
+ 0b00000000, 0b00000000, 0b00001100, 0b00000000, 0b00001100, 0b00000000,
+ 0b00001111, 0b00000000, 0b00001111, 0b00000000,
+ ]),
+ // 3 bars
+ high: new Uint8Array([
+ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00110000, 0b00000000,
+ 0b00110000, 0b00000000, 0b00111100, 0b00000000, 0b00111100, 0b00000000,
+ 0b00111111, 0b00000000, 0b00111111, 0b00000000,
+ ]),
+ // 4 bars (full)
+ full: new Uint8Array([
+ 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11110000, 0b00000000,
+ 0b11110000, 0b00000000, 0b11111100, 0b00000000, 0b11111100, 0b00000000,
+ 0b11111111, 0b00000000, 0b11111111, 0b00000000,
+ ]),
+};
+
+// Meshtastic logo (simplified 32x32)
+export const logo_width = 32;
+export const logo_height = 32;
+export const meshtastic_logo = new Uint8Array([
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00,
+ 0x03, 0xc0, 0x00, 0x80, 0x01, 0x80, 0x01, 0xc0, 0x00, 0x00, 0x03, 0x60, 0x00,
+ 0x00, 0x06, 0x30, 0x00, 0x00, 0x0c, 0x18, 0xf8, 0x1f, 0x18, 0x08, 0x04, 0x20,
+ 0x10, 0x0c, 0x02, 0x40, 0x30, 0x04, 0x01, 0x80, 0x20, 0x06, 0x01, 0x80, 0x60,
+ 0x02, 0xe1, 0x87, 0x40, 0x02, 0x11, 0x88, 0x40, 0x03, 0x09, 0x90, 0xc0, 0x03,
+ 0x09, 0x90, 0xc0, 0x02, 0x11, 0x88, 0x40, 0x02, 0xe1, 0x87, 0x40, 0x06, 0x01,
+ 0x80, 0x60, 0x04, 0x01, 0x80, 0x20, 0x0c, 0x02, 0x40, 0x30, 0x08, 0x04, 0x20,
+ 0x10, 0x18, 0xf8, 0x1f, 0x18, 0x30, 0x00, 0x00, 0x0c, 0x60, 0x00, 0x00, 0x06,
+ 0xc0, 0x00, 0x00, 0x03, 0x80, 0x01, 0x80, 0x01, 0x00, 0x03, 0xc0, 0x00, 0x00,
+ 0x06, 0x60, 0x00, 0x00, 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00,
+]);
+
+// Bluetooth icon (8x11)
+export const icon_bluetooth = new Uint8Array([
+ 0b00000000, 0b10000100, 0b01001010, 0b00101010, 0b00010100, 0b11111111,
+ 0b00010100, 0b00101010, 0b01001010, 0b10000100, 0b00000000,
+]);
+
+// GPS/Location pin icon (8x12)
+export const icon_gps_pin = new Uint8Array([
+ 0b00111100, 0b01111110, 0b11111111, 0b11100111, 0b11100111, 0b11111111,
+ 0b01111110, 0b00111100, 0b00011000, 0b00011000, 0b00001000, 0b00000000,
+]);
+
+// Message/chat bubble icon (10x8)
+export const icon_message = new Uint8Array([
+ 0b11111111, 0b00, 0b10000001, 0b00, 0b10000001, 0b00, 0b10000001, 0b00,
+ 0b10000001, 0b00, 0b11111111, 0b00, 0b00000110, 0b00, 0b00000010, 0b00,
+]);
+
+/**
+ * Helper to convert icon data to XBM format if needed
+ */
+export function toXbm(data: Uint8Array | number[]): Uint8Array {
+ return data instanceof Uint8Array ? data : new Uint8Array(data);
+}
diff --git a/oled-emulator/src/ScreenRenderers.ts b/oled-emulator/src/ScreenRenderers.ts
new file mode 100644
index 000000000..4dee89f4a
--- /dev/null
+++ b/oled-emulator/src/ScreenRenderers.ts
@@ -0,0 +1,627 @@
+/**
+ * ScreenRenderers.ts - Sample screen rendering functions
+ *
+ * These demonstrate how to port Meshtastic firmware UI screens
+ * to the OLED emulator. The API matches the firmware's drawing code.
+ */
+
+import {
+ OLEDDisplay,
+ WHITE,
+ BLACK,
+ TEXT_ALIGN_LEFT,
+ TEXT_ALIGN_CENTER,
+ TEXT_ALIGN_RIGHT,
+} from "./OLEDDisplay";
+import { Font_6x8 } from "./SimpleFonts";
+import * as images from "./OLEDImages";
+
+// Font height for simple 6x8 font
+const FONT_HEIGHT_SMALL = 8;
+
+// Screen resolution detection (matches SharedUIDisplay.cpp)
+export type ScreenResolution = "UltraLow" | "Low" | "High";
+
+export function determineScreenResolution(
+ width: number,
+ height: number,
+): ScreenResolution {
+ if (width <= 64 || height <= 48) {
+ return "UltraLow";
+ }
+ if (width > 128) {
+ return "High";
+ }
+ return "Low";
+}
+
+// Helper for consistent line positioning
+export function getTextPositions(display: OLEDDisplay): number[] {
+ const fontHeight = FONT_HEIGHT_SMALL;
+ return [
+ 0, // textZeroLine
+ fontHeight - 1, // textFirstLine
+ fontHeight - 1 + (fontHeight - 5), // textSecondLine
+ fontHeight - 1 + 2 * (fontHeight - 5), // textThirdLine
+ fontHeight - 1 + 3 * (fontHeight - 5), // textFourthLine
+ fontHeight - 1 + 4 * (fontHeight - 5), // textFifthLine
+ ];
+}
+
+/**
+ * Draw a rounded highlight box (used for inverted headers)
+ */
+export function drawRoundedHighlight(
+ display: OLEDDisplay,
+ x: number,
+ y: number,
+ w: number,
+ h: number,
+ r: number,
+): void {
+ // Draw the center and side rectangles
+ display.fillRect(x + r, y, w - 2 * r, h); // center bar
+ display.fillRect(x, y + r, r, h - 2 * r); // left edge
+ display.fillRect(x + w - r, y + r, r, h - 2 * r); // right edge
+
+ // Draw the rounded corners using filled circles
+ display.fillCircle(x + r + 1, y + r, r); // top-left
+ display.fillCircle(x + w - r - 1, y + r, r); // top-right
+ display.fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left
+ display.fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right
+}
+
+/**
+ * Draw the common header bar (battery, title, icons)
+ */
+export function drawCommonHeader(
+ display: OLEDDisplay,
+ x: number,
+ y: number,
+ titleStr: string,
+ options: {
+ inverted?: boolean;
+ batteryPercent?: number;
+ isCharging?: boolean;
+ hasUSB?: boolean;
+ hasUnreadMessage?: boolean;
+ } = {},
+): void {
+ const {
+ inverted = true,
+ batteryPercent = 75,
+ isCharging = false,
+ hasUSB = false,
+ hasUnreadMessage = false,
+ } = options;
+
+ const HEADER_OFFSET_Y = 1;
+ y += HEADER_OFFSET_Y;
+
+ display.setFont(Font_6x8);
+ const highlightHeight = FONT_HEIGHT_SMALL - 1;
+ const screenW = display.getWidth();
+
+ if (inverted) {
+ // Draw inverted header background
+ display.setColor(WHITE);
+ drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
+ display.setColor(BLACK);
+ } else {
+ // Draw line under header
+ display.setColor(WHITE);
+ display.drawLine(0, 14, screenW, 14);
+ }
+
+ // Draw title centered
+ display.setTextAlignment(TEXT_ALIGN_CENTER);
+ display.drawString(screenW / 2, y, titleStr);
+
+ // Reset color for remaining elements
+ if (inverted) {
+ display.setColor(WHITE);
+ }
+
+ // Draw battery on left
+ let batteryX = 1;
+ const batteryY = HEADER_OFFSET_Y + 1;
+
+ if (hasUSB && !isCharging) {
+ // Draw USB icon
+ display.drawXbm(batteryX + 1, batteryY + 2, 10, 8, images.imgUSB);
+ batteryX += 11;
+ } else {
+ // Draw battery outline
+ display.drawXbm(batteryX, batteryY, 7, 11, images.batteryBitmap_v);
+
+ if (isCharging) {
+ // Draw lightning bolt
+ display.drawXbm(
+ batteryX + 1,
+ batteryY + 3,
+ 5,
+ 5,
+ images.lightning_bolt_v,
+ );
+ } else {
+ // Draw battery level
+ display.drawXbm(
+ batteryX - 1,
+ batteryY + 4,
+ 8,
+ 3,
+ images.batteryBitmap_sidegaps_v,
+ );
+ const fillHeight = Math.floor((8 * batteryPercent) / 100);
+ const fillY = batteryY - fillHeight + 10;
+ display.fillRect(batteryX + 1, fillY, 5, fillHeight);
+ }
+ batteryX += 9;
+ }
+
+ // Draw mail icon on right if unread message
+ if (hasUnreadMessage) {
+ const mailX = screenW - images.mail_width - 2;
+ display.drawXbm(
+ mailX,
+ y + 2,
+ images.mail_width,
+ images.mail_height,
+ images.mail,
+ );
+ }
+
+ display.setTextAlignment(TEXT_ALIGN_LEFT);
+ display.setColor(WHITE);
+}
+
+/**
+ * Draw a boot/splash screen with logo
+ */
+export function drawBootScreen(
+ display: OLEDDisplay,
+ appVersion: string = "2.5.0",
+): void {
+ display.clear();
+ display.setFont(Font_6x8);
+ display.setColor(WHITE);
+
+ const centerX = display.getWidth() / 2;
+ const centerY = display.getHeight() / 2;
+
+ // Draw logo centered
+ const logoX = centerX - images.logo_width / 2;
+ const logoY = centerY - images.logo_height / 2 - 8;
+ display.drawXbm(
+ logoX,
+ logoY,
+ images.logo_width,
+ images.logo_height,
+ images.meshtastic_logo,
+ );
+
+ // Draw version below
+ display.setTextAlignment(TEXT_ALIGN_CENTER);
+ display.drawString(
+ centerX,
+ centerY + images.logo_height / 2,
+ `v${appVersion}`,
+ );
+
+ display.setTextAlignment(TEXT_ALIGN_LEFT);
+}
+
+/**
+ * Draw a node info screen
+ */
+export function drawNodeInfoScreen(
+ display: OLEDDisplay,
+ nodeInfo: {
+ shortName: string;
+ longName: string;
+ nodeId: string;
+ batteryLevel?: number;
+ lastHeard?: string;
+ snr?: number;
+ hopsAway?: number;
+ },
+): void {
+ display.clear();
+
+ // Draw header
+ drawCommonHeader(display, 0, 0, "Node Info", {
+ inverted: true,
+ batteryPercent: 85,
+ });
+
+ display.setFont(Font_6x8);
+ display.setColor(WHITE);
+ display.setTextAlignment(TEXT_ALIGN_LEFT);
+
+ const lines = getTextPositions(display);
+ const screenW = display.getWidth();
+
+ // Node name (bolded by drawing twice offset)
+ display.drawString(0, lines[1], nodeInfo.longName);
+ display.drawString(1, lines[1], nodeInfo.longName);
+
+ // Short name and ID
+ display.drawString(0, lines[2], `${nodeInfo.shortName} • ${nodeInfo.nodeId}`);
+
+ // Last heard
+ if (nodeInfo.lastHeard) {
+ display.drawString(0, lines[3], `Heard: ${nodeInfo.lastHeard}`);
+ }
+
+ // Signal info on right side
+ if (nodeInfo.snr !== undefined) {
+ const snrStr = `SNR: ${nodeInfo.snr}dB`;
+ display.setTextAlignment(TEXT_ALIGN_RIGHT);
+ display.drawString(screenW, lines[3], snrStr);
+ }
+
+ // Hops
+ if (nodeInfo.hopsAway !== undefined) {
+ const hopStr =
+ nodeInfo.hopsAway === 0 ? "Direct" : `${nodeInfo.hopsAway} hops`;
+ display.setTextAlignment(TEXT_ALIGN_LEFT);
+ display.drawString(0, lines[4], hopStr);
+ }
+
+ display.setTextAlignment(TEXT_ALIGN_LEFT);
+}
+
+/**
+ * Draw a message screen
+ */
+export function drawMessageScreen(
+ display: OLEDDisplay,
+ message: {
+ from: string;
+ text: string;
+ time: string;
+ channel?: string;
+ },
+): void {
+ display.clear();
+
+ // Draw header with channel name if provided
+ const headerTitle = message.channel || "Message";
+ drawCommonHeader(display, 0, 0, headerTitle, {
+ inverted: true,
+ batteryPercent: 72,
+ hasUnreadMessage: true,
+ });
+
+ display.setFont(Font_6x8);
+ display.setColor(WHITE);
+ display.setTextAlignment(TEXT_ALIGN_LEFT);
+
+ const lines = getTextPositions(display);
+ const screenW = display.getWidth();
+
+ // From line
+ display.drawString(0, lines[1], `From: ${message.from}`);
+
+ // Time on right
+ display.setTextAlignment(TEXT_ALIGN_RIGHT);
+ display.drawString(screenW, lines[1], message.time);
+
+ // Message text (with word wrap)
+ display.setTextAlignment(TEXT_ALIGN_LEFT);
+ display.drawStringMaxWidth(0, lines[2], screenW, message.text);
+}
+
+/**
+ * Draw a GPS/location screen
+ */
+export function drawGPSScreen(
+ display: OLEDDisplay,
+ gpsInfo: {
+ latitude?: number;
+ longitude?: number;
+ altitude?: number;
+ satellites?: number;
+ hasLock: boolean;
+ speed?: number;
+ },
+): void {
+ display.clear();
+
+ drawCommonHeader(display, 0, 0, "GPS", {
+ inverted: true,
+ batteryPercent: 90,
+ });
+
+ display.setFont(Font_6x8);
+ display.setColor(WHITE);
+ display.setTextAlignment(TEXT_ALIGN_LEFT);
+
+ const lines = getTextPositions(display);
+
+ // Draw satellite icon and count
+ display.drawXbm(
+ 0,
+ lines[1],
+ images.imgSatellite_width,
+ images.imgSatellite_height,
+ images.imgSatellite,
+ );
+
+ if (!gpsInfo.hasLock) {
+ display.drawString(12, lines[1], "No Lock");
+ } else {
+ display.drawString(12, lines[1], `${gpsInfo.satellites || 0} sats`);
+ }
+
+ if (
+ gpsInfo.hasLock &&
+ gpsInfo.latitude !== undefined &&
+ gpsInfo.longitude !== undefined
+ ) {
+ // Coordinates
+ display.drawString(0, lines[2], `Lat: ${gpsInfo.latitude.toFixed(6)}`);
+ display.drawString(0, lines[3], `Lon: ${gpsInfo.longitude.toFixed(6)}`);
+
+ // Altitude
+ if (gpsInfo.altitude !== undefined) {
+ display.drawString(0, lines[4], `Alt: ${gpsInfo.altitude.toFixed(0)}m`);
+ }
+
+ // Speed
+ if (gpsInfo.speed !== undefined) {
+ const screenW = display.getWidth();
+ display.setTextAlignment(TEXT_ALIGN_RIGHT);
+ display.drawString(screenW, lines[4], `${gpsInfo.speed.toFixed(1)} km/h`);
+ }
+ }
+
+ display.setTextAlignment(TEXT_ALIGN_LEFT);
+}
+
+/**
+ * Draw a node list screen
+ */
+export function drawNodeListScreen(
+ display: OLEDDisplay,
+ nodes: Array<{
+ shortName: string;
+ longName: string;
+ lastHeard: string;
+ snr?: number;
+ }>,
+ selectedIndex: number = 0,
+): void {
+ display.clear();
+
+ drawCommonHeader(display, 0, 0, `Nodes (${nodes.length})`, {
+ inverted: true,
+ batteryPercent: 80,
+ });
+
+ display.setFont(Font_6x8);
+ display.setColor(WHITE);
+ display.setTextAlignment(TEXT_ALIGN_LEFT);
+
+ const lines = getTextPositions(display);
+ const screenW = display.getWidth();
+ const maxVisibleNodes = 4;
+ const startIndex = Math.max(
+ 0,
+ selectedIndex - Math.floor(maxVisibleNodes / 2),
+ );
+
+ for (let i = 0; i < maxVisibleNodes && startIndex + i < nodes.length; i++) {
+ const node = nodes[startIndex + i];
+ const lineY = lines[i + 1];
+ const isSelected = startIndex + i === selectedIndex;
+
+ // Highlight selected row
+ if (isSelected) {
+ display.fillRect(0, lineY - 1, screenW, FONT_HEIGHT_SMALL);
+ display.setColor(BLACK);
+ }
+
+ // Node short name
+ display.drawString(0, lineY, node.shortName);
+
+ // Last heard on right
+ display.setTextAlignment(TEXT_ALIGN_RIGHT);
+ display.drawString(screenW, lineY, node.lastHeard);
+ display.setTextAlignment(TEXT_ALIGN_LEFT);
+
+ if (isSelected) {
+ display.setColor(WHITE);
+ }
+ }
+}
+
+/**
+ * Draw a system info screen
+ */
+export function drawSystemScreen(
+ display: OLEDDisplay,
+ sysInfo: {
+ uptime: string;
+ channelUtil: number;
+ airUtil: number;
+ batteryVoltage?: number;
+ nodes: number;
+ freeMemory?: number;
+ },
+): void {
+ display.clear();
+
+ drawCommonHeader(display, 0, 0, "System", {
+ inverted: true,
+ batteryPercent: 95,
+ });
+
+ display.setFont(Font_6x8);
+ display.setColor(WHITE);
+ display.setTextAlignment(TEXT_ALIGN_LEFT);
+
+ const lines = getTextPositions(display);
+ const screenW = display.getWidth();
+
+ // Uptime
+ display.drawString(0, lines[1], `Uptime: ${sysInfo.uptime}`);
+
+ // Channel utilization
+ display.drawString(0, lines[2], `ChUtil: ${sysInfo.channelUtil.toFixed(1)}%`);
+
+ // Air utilization
+ display.setTextAlignment(TEXT_ALIGN_RIGHT);
+ display.drawString(
+ screenW,
+ lines[2],
+ `AirTx: ${sysInfo.airUtil.toFixed(1)}%`,
+ );
+ display.setTextAlignment(TEXT_ALIGN_LEFT);
+
+ // Nodes
+ display.drawString(0, lines[3], `Nodes: ${sysInfo.nodes}`);
+
+ // Battery voltage
+ if (sysInfo.batteryVoltage !== undefined) {
+ display.setTextAlignment(TEXT_ALIGN_RIGHT);
+ display.drawString(
+ screenW,
+ lines[3],
+ `${sysInfo.batteryVoltage.toFixed(2)}V`,
+ );
+ }
+
+ // Free memory
+ if (sysInfo.freeMemory !== undefined) {
+ display.setTextAlignment(TEXT_ALIGN_LEFT);
+ display.drawString(
+ 0,
+ lines[4],
+ `Free: ${(sysInfo.freeMemory / 1024).toFixed(0)}KB`,
+ );
+ }
+
+ display.setTextAlignment(TEXT_ALIGN_LEFT);
+}
+
+/**
+ * Draw a compass screen with bearing arrow
+ */
+export function drawCompassScreen(
+ display: OLEDDisplay,
+ compassInfo: {
+ heading: number; // Device heading in degrees
+ bearing: number; // Bearing to target in degrees
+ distance?: number; // Distance to target in meters
+ targetName?: string;
+ },
+): void {
+ display.clear();
+
+ drawCommonHeader(display, 0, 0, compassInfo.targetName || "Compass", {
+ inverted: true,
+ batteryPercent: 70,
+ });
+
+ display.setFont(Font_6x8);
+ display.setColor(WHITE);
+
+ const screenW = display.getWidth();
+ const screenH = display.getHeight();
+
+ // Compass center and radius
+ const compassRadius = Math.min(screenW, screenH - 20) / 2 - 4;
+ const compassX = screenW / 2;
+ const compassY = (screenH + 16) / 2;
+
+ // Draw compass circle
+ display.drawCircle(compassX, compassY, compassRadius);
+
+ // Draw cardinal directions
+ display.setTextAlignment(TEXT_ALIGN_CENTER);
+ display.drawString(compassX, compassY - compassRadius - 10, "N");
+
+ // Calculate relative bearing (bearing - heading)
+ const relativeBearing =
+ ((compassInfo.bearing - compassInfo.heading) * Math.PI) / 180;
+
+ // Draw bearing arrow
+ const arrowLength = compassRadius - 6;
+ const arrowX = compassX + Math.sin(relativeBearing) * arrowLength;
+ const arrowY = compassY - Math.cos(relativeBearing) * arrowLength;
+
+ display.drawLine(compassX, compassY, arrowX, arrowY);
+
+ // Draw arrowhead
+ const headAngle = 0.4;
+ const headLength = 8;
+ const angle1 = relativeBearing + Math.PI - headAngle;
+ const angle2 = relativeBearing + Math.PI + headAngle;
+ display.drawLine(
+ arrowX,
+ arrowY,
+ arrowX + Math.sin(angle1) * headLength,
+ arrowY - Math.cos(angle1) * headLength,
+ );
+ display.drawLine(
+ arrowX,
+ arrowY,
+ arrowX + Math.sin(angle2) * headLength,
+ arrowY - Math.cos(angle2) * headLength,
+ );
+
+ // Draw distance at bottom
+ if (compassInfo.distance !== undefined) {
+ let distStr: string;
+ if (compassInfo.distance >= 1000) {
+ distStr = `${(compassInfo.distance / 1000).toFixed(1)} km`;
+ } else {
+ distStr = `${Math.round(compassInfo.distance)} m`;
+ }
+ display.drawString(compassX, screenH - FONT_HEIGHT_SMALL, distStr);
+ }
+
+ display.setTextAlignment(TEXT_ALIGN_LEFT);
+}
+
+/**
+ * Draw a progress/loading screen
+ */
+export function drawProgressScreen(
+ display: OLEDDisplay,
+ title: string,
+ progress: number,
+ statusText?: string,
+): void {
+ display.clear();
+ display.setFont(Font_6x8);
+ display.setColor(WHITE);
+ display.setTextAlignment(TEXT_ALIGN_CENTER);
+
+ const screenW = display.getWidth();
+ const screenH = display.getHeight();
+ const centerX = screenW / 2;
+ const centerY = screenH / 2;
+
+ // Title
+ display.drawString(centerX, centerY - 20, title);
+
+ // Progress bar
+ const barWidth = screenW - 20;
+ const barHeight = 10;
+ const barX = 10;
+ const barY = centerY - barHeight / 2;
+
+ display.drawProgressBar(barX, barY, barWidth, barHeight, progress);
+
+ // Progress percentage
+ display.drawString(centerX, barY + barHeight + 4, `${Math.round(progress)}%`);
+
+ // Status text
+ if (statusText) {
+ display.drawString(centerX, screenH - FONT_HEIGHT_SMALL - 2, statusText);
+ }
+
+ display.setTextAlignment(TEXT_ALIGN_LEFT);
+}
diff --git a/oled-emulator/src/SimpleFonts.ts b/oled-emulator/src/SimpleFonts.ts
new file mode 100644
index 000000000..f3629fa82
--- /dev/null
+++ b/oled-emulator/src/SimpleFonts.ts
@@ -0,0 +1,421 @@
+/**
+ * SimpleFonts.ts - Simple working bitmap fonts for OLED emulator
+ *
+ * These are basic monospace bitmap fonts that are easy to render correctly.
+ * Format: Each character is stored in column-major order, 1 byte per column.
+ * For fonts taller than 8px, multiple bytes per column are used.
+ */
+
+import type { OLEDFont } from "./OLEDDisplay";
+
+/**
+ * Simple 6x8 monospace font
+ * Each character is 6 columns wide, 8 rows tall
+ * 1 byte per column (8 vertical pixels)
+ * Includes ASCII 32-126
+ */
+export const Font_6x8: OLEDFont = {
+ height: 8,
+ firstChar: 32,
+ charCount: 95,
+ widths: new Array(95).fill(6), // Fixed width font
+ data: new Uint8Array([
+ // Space (32)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // ! (33)
+ 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00,
+ // " (34)
+ 0x00, 0x07, 0x00, 0x07, 0x00, 0x00,
+ // # (35)
+ 0x14, 0x7f, 0x14, 0x7f, 0x14, 0x00,
+ // $ (36)
+ 0x24, 0x2a, 0x7f, 0x2a, 0x12, 0x00,
+ // % (37)
+ 0x23, 0x13, 0x08, 0x64, 0x62, 0x00,
+ // & (38)
+ 0x36, 0x49, 0x55, 0x22, 0x50, 0x00,
+ // ' (39)
+ 0x00, 0x05, 0x03, 0x00, 0x00, 0x00,
+ // ( (40)
+ 0x00, 0x1c, 0x22, 0x41, 0x00, 0x00,
+ // ) (41)
+ 0x00, 0x41, 0x22, 0x1c, 0x00, 0x00,
+ // * (42)
+ 0x08, 0x2a, 0x1c, 0x2a, 0x08, 0x00,
+ // + (43)
+ 0x08, 0x08, 0x3e, 0x08, 0x08, 0x00,
+ // , (44)
+ 0x00, 0x50, 0x30, 0x00, 0x00, 0x00,
+ // - (45)
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x00,
+ // . (46)
+ 0x00, 0x60, 0x60, 0x00, 0x00, 0x00,
+ // / (47)
+ 0x20, 0x10, 0x08, 0x04, 0x02, 0x00,
+ // 0 (48)
+ 0x3e, 0x51, 0x49, 0x45, 0x3e, 0x00,
+ // 1 (49)
+ 0x00, 0x42, 0x7f, 0x40, 0x00, 0x00,
+ // 2 (50)
+ 0x42, 0x61, 0x51, 0x49, 0x46, 0x00,
+ // 3 (51)
+ 0x21, 0x41, 0x45, 0x4b, 0x31, 0x00,
+ // 4 (52)
+ 0x18, 0x14, 0x12, 0x7f, 0x10, 0x00,
+ // 5 (53)
+ 0x27, 0x45, 0x45, 0x45, 0x39, 0x00,
+ // 6 (54)
+ 0x3c, 0x4a, 0x49, 0x49, 0x30, 0x00,
+ // 7 (55)
+ 0x01, 0x71, 0x09, 0x05, 0x03, 0x00,
+ // 8 (56)
+ 0x36, 0x49, 0x49, 0x49, 0x36, 0x00,
+ // 9 (57)
+ 0x06, 0x49, 0x49, 0x29, 0x1e, 0x00,
+ // : (58)
+ 0x00, 0x36, 0x36, 0x00, 0x00, 0x00,
+ // ; (59)
+ 0x00, 0x56, 0x36, 0x00, 0x00, 0x00,
+ // < (60)
+ 0x00, 0x08, 0x14, 0x22, 0x41, 0x00,
+ // = (61)
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x00,
+ // > (62)
+ 0x41, 0x22, 0x14, 0x08, 0x00, 0x00,
+ // ? (63)
+ 0x02, 0x01, 0x51, 0x09, 0x06, 0x00,
+ // @ (64)
+ 0x32, 0x49, 0x79, 0x41, 0x3e, 0x00,
+ // A (65)
+ 0x7e, 0x11, 0x11, 0x11, 0x7e, 0x00,
+ // B (66)
+ 0x7f, 0x49, 0x49, 0x49, 0x36, 0x00,
+ // C (67)
+ 0x3e, 0x41, 0x41, 0x41, 0x22, 0x00,
+ // D (68)
+ 0x7f, 0x41, 0x41, 0x22, 0x1c, 0x00,
+ // E (69)
+ 0x7f, 0x49, 0x49, 0x49, 0x41, 0x00,
+ // F (70)
+ 0x7f, 0x09, 0x09, 0x01, 0x01, 0x00,
+ // G (71)
+ 0x3e, 0x41, 0x41, 0x51, 0x32, 0x00,
+ // H (72)
+ 0x7f, 0x08, 0x08, 0x08, 0x7f, 0x00,
+ // I (73)
+ 0x00, 0x41, 0x7f, 0x41, 0x00, 0x00,
+ // J (74)
+ 0x20, 0x40, 0x41, 0x3f, 0x01, 0x00,
+ // K (75)
+ 0x7f, 0x08, 0x14, 0x22, 0x41, 0x00,
+ // L (76)
+ 0x7f, 0x40, 0x40, 0x40, 0x40, 0x00,
+ // M (77)
+ 0x7f, 0x02, 0x04, 0x02, 0x7f, 0x00,
+ // N (78)
+ 0x7f, 0x04, 0x08, 0x10, 0x7f, 0x00,
+ // O (79)
+ 0x3e, 0x41, 0x41, 0x41, 0x3e, 0x00,
+ // P (80)
+ 0x7f, 0x09, 0x09, 0x09, 0x06, 0x00,
+ // Q (81)
+ 0x3e, 0x41, 0x51, 0x21, 0x5e, 0x00,
+ // R (82)
+ 0x7f, 0x09, 0x19, 0x29, 0x46, 0x00,
+ // S (83)
+ 0x46, 0x49, 0x49, 0x49, 0x31, 0x00,
+ // T (84)
+ 0x01, 0x01, 0x7f, 0x01, 0x01, 0x00,
+ // U (85)
+ 0x3f, 0x40, 0x40, 0x40, 0x3f, 0x00,
+ // V (86)
+ 0x1f, 0x20, 0x40, 0x20, 0x1f, 0x00,
+ // W (87)
+ 0x7f, 0x20, 0x18, 0x20, 0x7f, 0x00,
+ // X (88)
+ 0x63, 0x14, 0x08, 0x14, 0x63, 0x00,
+ // Y (89)
+ 0x03, 0x04, 0x78, 0x04, 0x03, 0x00,
+ // Z (90)
+ 0x61, 0x51, 0x49, 0x45, 0x43, 0x00,
+ // [ (91)
+ 0x00, 0x00, 0x7f, 0x41, 0x41, 0x00,
+ // \ (92)
+ 0x02, 0x04, 0x08, 0x10, 0x20, 0x00,
+ // ] (93)
+ 0x41, 0x41, 0x7f, 0x00, 0x00, 0x00,
+ // ^ (94)
+ 0x04, 0x02, 0x01, 0x02, 0x04, 0x00,
+ // _ (95)
+ 0x40, 0x40, 0x40, 0x40, 0x40, 0x00,
+ // ` (96)
+ 0x00, 0x01, 0x02, 0x04, 0x00, 0x00,
+ // a (97)
+ 0x20, 0x54, 0x54, 0x54, 0x78, 0x00,
+ // b (98)
+ 0x7f, 0x48, 0x44, 0x44, 0x38, 0x00,
+ // c (99)
+ 0x38, 0x44, 0x44, 0x44, 0x20, 0x00,
+ // d (100)
+ 0x38, 0x44, 0x44, 0x48, 0x7f, 0x00,
+ // e (101)
+ 0x38, 0x54, 0x54, 0x54, 0x18, 0x00,
+ // f (102)
+ 0x08, 0x7e, 0x09, 0x01, 0x02, 0x00,
+ // g (103)
+ 0x08, 0x14, 0x54, 0x54, 0x3c, 0x00,
+ // h (104)
+ 0x7f, 0x08, 0x04, 0x04, 0x78, 0x00,
+ // i (105)
+ 0x00, 0x44, 0x7d, 0x40, 0x00, 0x00,
+ // j (106)
+ 0x20, 0x40, 0x44, 0x3d, 0x00, 0x00,
+ // k (107)
+ 0x00, 0x7f, 0x10, 0x28, 0x44, 0x00,
+ // l (108)
+ 0x00, 0x41, 0x7f, 0x40, 0x00, 0x00,
+ // m (109)
+ 0x7c, 0x04, 0x18, 0x04, 0x78, 0x00,
+ // n (110)
+ 0x7c, 0x08, 0x04, 0x04, 0x78, 0x00,
+ // o (111)
+ 0x38, 0x44, 0x44, 0x44, 0x38, 0x00,
+ // p (112)
+ 0x7c, 0x14, 0x14, 0x14, 0x08, 0x00,
+ // q (113)
+ 0x08, 0x14, 0x14, 0x18, 0x7c, 0x00,
+ // r (114)
+ 0x7c, 0x08, 0x04, 0x04, 0x08, 0x00,
+ // s (115)
+ 0x48, 0x54, 0x54, 0x54, 0x20, 0x00,
+ // t (116)
+ 0x04, 0x3f, 0x44, 0x40, 0x20, 0x00,
+ // u (117)
+ 0x3c, 0x40, 0x40, 0x20, 0x7c, 0x00,
+ // v (118)
+ 0x1c, 0x20, 0x40, 0x20, 0x1c, 0x00,
+ // w (119)
+ 0x3c, 0x40, 0x30, 0x40, 0x3c, 0x00,
+ // x (120)
+ 0x44, 0x28, 0x10, 0x28, 0x44, 0x00,
+ // y (121)
+ 0x0c, 0x50, 0x50, 0x50, 0x3c, 0x00,
+ // z (122)
+ 0x44, 0x64, 0x54, 0x4c, 0x44, 0x00,
+ // { (123)
+ 0x00, 0x08, 0x36, 0x41, 0x00, 0x00,
+ // | (124)
+ 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00,
+ // } (125)
+ 0x00, 0x41, 0x36, 0x08, 0x00, 0x00,
+ // ~ (126)
+ 0x08, 0x08, 0x2a, 0x1c, 0x08, 0x00,
+ ]),
+};
+
+/**
+ * Simple 5x7 monospace font (more compact)
+ * Each character is 5 columns wide, 7 rows tall
+ * 1 byte per column
+ */
+export const Font_5x7: OLEDFont = {
+ height: 7,
+ firstChar: 32,
+ charCount: 95,
+ widths: new Array(95).fill(5),
+ data: new Uint8Array([
+ // Space (32)
+ 0x00, 0x00, 0x00, 0x00, 0x00,
+ // ! (33)
+ 0x00, 0x00, 0x2f, 0x00, 0x00,
+ // " (34)
+ 0x00, 0x07, 0x00, 0x07, 0x00,
+ // # (35)
+ 0x14, 0x3e, 0x14, 0x3e, 0x14,
+ // $ (36)
+ 0x24, 0x2a, 0x7f, 0x2a, 0x12,
+ // % (37)
+ 0x23, 0x13, 0x08, 0x64, 0x62,
+ // & (38)
+ 0x36, 0x49, 0x55, 0x22, 0x50,
+ // ' (39)
+ 0x00, 0x05, 0x03, 0x00, 0x00,
+ // ( (40)
+ 0x00, 0x1c, 0x22, 0x41, 0x00,
+ // ) (41)
+ 0x00, 0x41, 0x22, 0x1c, 0x00,
+ // * (42)
+ 0x14, 0x08, 0x3e, 0x08, 0x14,
+ // + (43)
+ 0x08, 0x08, 0x3e, 0x08, 0x08,
+ // , (44)
+ 0x00, 0x50, 0x30, 0x00, 0x00,
+ // - (45)
+ 0x08, 0x08, 0x08, 0x08, 0x08,
+ // . (46)
+ 0x00, 0x30, 0x30, 0x00, 0x00,
+ // / (47)
+ 0x20, 0x10, 0x08, 0x04, 0x02,
+ // 0 (48)
+ 0x3e, 0x51, 0x49, 0x45, 0x3e,
+ // 1 (49)
+ 0x00, 0x42, 0x7f, 0x40, 0x00,
+ // 2 (50)
+ 0x42, 0x61, 0x51, 0x49, 0x46,
+ // 3 (51)
+ 0x21, 0x41, 0x45, 0x4b, 0x31,
+ // 4 (52)
+ 0x18, 0x14, 0x12, 0x7f, 0x10,
+ // 5 (53)
+ 0x27, 0x45, 0x45, 0x45, 0x39,
+ // 6 (54)
+ 0x3c, 0x4a, 0x49, 0x49, 0x30,
+ // 7 (55)
+ 0x01, 0x71, 0x09, 0x05, 0x03,
+ // 8 (56)
+ 0x36, 0x49, 0x49, 0x49, 0x36,
+ // 9 (57)
+ 0x06, 0x49, 0x49, 0x29, 0x1e,
+ // : (58)
+ 0x00, 0x36, 0x36, 0x00, 0x00,
+ // ; (59)
+ 0x00, 0x56, 0x36, 0x00, 0x00,
+ // < (60)
+ 0x08, 0x14, 0x22, 0x41, 0x00,
+ // = (61)
+ 0x14, 0x14, 0x14, 0x14, 0x14,
+ // > (62)
+ 0x00, 0x41, 0x22, 0x14, 0x08,
+ // ? (63)
+ 0x02, 0x01, 0x51, 0x09, 0x06,
+ // @ (64)
+ 0x32, 0x49, 0x79, 0x41, 0x3e,
+ // A (65)
+ 0x7e, 0x11, 0x11, 0x11, 0x7e,
+ // B (66)
+ 0x7f, 0x49, 0x49, 0x49, 0x36,
+ // C (67)
+ 0x3e, 0x41, 0x41, 0x41, 0x22,
+ // D (68)
+ 0x7f, 0x41, 0x41, 0x22, 0x1c,
+ // E (69)
+ 0x7f, 0x49, 0x49, 0x49, 0x41,
+ // F (70)
+ 0x7f, 0x09, 0x09, 0x09, 0x01,
+ // G (71)
+ 0x3e, 0x41, 0x49, 0x49, 0x7a,
+ // H (72)
+ 0x7f, 0x08, 0x08, 0x08, 0x7f,
+ // I (73)
+ 0x00, 0x41, 0x7f, 0x41, 0x00,
+ // J (74)
+ 0x20, 0x40, 0x41, 0x3f, 0x01,
+ // K (75)
+ 0x7f, 0x08, 0x14, 0x22, 0x41,
+ // L (76)
+ 0x7f, 0x40, 0x40, 0x40, 0x40,
+ // M (77)
+ 0x7f, 0x02, 0x0c, 0x02, 0x7f,
+ // N (78)
+ 0x7f, 0x04, 0x08, 0x10, 0x7f,
+ // O (79)
+ 0x3e, 0x41, 0x41, 0x41, 0x3e,
+ // P (80)
+ 0x7f, 0x09, 0x09, 0x09, 0x06,
+ // Q (81)
+ 0x3e, 0x41, 0x51, 0x21, 0x5e,
+ // R (82)
+ 0x7f, 0x09, 0x19, 0x29, 0x46,
+ // S (83)
+ 0x46, 0x49, 0x49, 0x49, 0x31,
+ // T (84)
+ 0x01, 0x01, 0x7f, 0x01, 0x01,
+ // U (85)
+ 0x3f, 0x40, 0x40, 0x40, 0x3f,
+ // V (86)
+ 0x1f, 0x20, 0x40, 0x20, 0x1f,
+ // W (87)
+ 0x3f, 0x40, 0x38, 0x40, 0x3f,
+ // X (88)
+ 0x63, 0x14, 0x08, 0x14, 0x63,
+ // Y (89)
+ 0x07, 0x08, 0x70, 0x08, 0x07,
+ // Z (90)
+ 0x61, 0x51, 0x49, 0x45, 0x43,
+ // [ (91)
+ 0x00, 0x7f, 0x41, 0x41, 0x00,
+ // \ (92)
+ 0x02, 0x04, 0x08, 0x10, 0x20,
+ // ] (93)
+ 0x00, 0x41, 0x41, 0x7f, 0x00,
+ // ^ (94)
+ 0x04, 0x02, 0x01, 0x02, 0x04,
+ // _ (95)
+ 0x40, 0x40, 0x40, 0x40, 0x40,
+ // ` (96)
+ 0x00, 0x01, 0x02, 0x04, 0x00,
+ // a (97)
+ 0x20, 0x54, 0x54, 0x54, 0x78,
+ // b (98)
+ 0x7f, 0x48, 0x44, 0x44, 0x38,
+ // c (99)
+ 0x38, 0x44, 0x44, 0x44, 0x20,
+ // d (100)
+ 0x38, 0x44, 0x44, 0x48, 0x7f,
+ // e (101)
+ 0x38, 0x54, 0x54, 0x54, 0x18,
+ // f (102)
+ 0x08, 0x7e, 0x09, 0x01, 0x02,
+ // g (103)
+ 0x0c, 0x52, 0x52, 0x52, 0x3e,
+ // h (104)
+ 0x7f, 0x08, 0x04, 0x04, 0x78,
+ // i (105)
+ 0x00, 0x44, 0x7d, 0x40, 0x00,
+ // j (106)
+ 0x20, 0x40, 0x44, 0x3d, 0x00,
+ // k (107)
+ 0x7f, 0x10, 0x28, 0x44, 0x00,
+ // l (108)
+ 0x00, 0x41, 0x7f, 0x40, 0x00,
+ // m (109)
+ 0x7c, 0x04, 0x18, 0x04, 0x78,
+ // n (110)
+ 0x7c, 0x08, 0x04, 0x04, 0x78,
+ // o (111)
+ 0x38, 0x44, 0x44, 0x44, 0x38,
+ // p (112)
+ 0x7c, 0x14, 0x14, 0x14, 0x08,
+ // q (113)
+ 0x08, 0x14, 0x14, 0x18, 0x7c,
+ // r (114)
+ 0x7c, 0x08, 0x04, 0x04, 0x08,
+ // s (115)
+ 0x48, 0x54, 0x54, 0x54, 0x20,
+ // t (116)
+ 0x04, 0x3f, 0x44, 0x40, 0x20,
+ // u (117)
+ 0x3c, 0x40, 0x40, 0x20, 0x7c,
+ // v (118)
+ 0x1c, 0x20, 0x40, 0x20, 0x1c,
+ // w (119)
+ 0x3c, 0x40, 0x30, 0x40, 0x3c,
+ // x (120)
+ 0x44, 0x28, 0x10, 0x28, 0x44,
+ // y (121)
+ 0x0c, 0x50, 0x50, 0x50, 0x3c,
+ // z (122)
+ 0x44, 0x64, 0x54, 0x4c, 0x44,
+ // { (123)
+ 0x00, 0x08, 0x36, 0x41, 0x00,
+ // | (124)
+ 0x00, 0x00, 0x7f, 0x00, 0x00,
+ // } (125)
+ 0x00, 0x41, 0x36, 0x08, 0x00,
+ // ~ (126)
+ 0x10, 0x08, 0x08, 0x10, 0x08,
+ ]),
+};
+
+// Alias for compatibility
+export const SimpleFont = Font_6x8;
diff --git a/oled-emulator/src/index.ts b/oled-emulator/src/index.ts
new file mode 100644
index 000000000..44de34217
--- /dev/null
+++ b/oled-emulator/src/index.ts
@@ -0,0 +1,50 @@
+/**
+ * Meshtastic OLED Emulator
+ *
+ * A Vue.js component and TypeScript library for emulating SSD1306/SH1106 OLED displays.
+ * Provides the same OLEDDisplay API as the Meshtastic firmware, enabling
+ * screen development and testing in the browser.
+ */
+
+// Core display class
+export {
+ OLEDDisplay,
+ type OLEDDISPLAY_GEOMETRY,
+ type OLEDDISPLAY_COLOR,
+ type OLEDDISPLAY_TEXT_ALIGNMENT,
+ type OLEDFont,
+} from "./OLEDDisplay";
+export {
+ WHITE,
+ BLACK,
+ INVERSE,
+ TEXT_ALIGN_LEFT,
+ TEXT_ALIGN_CENTER,
+ TEXT_ALIGN_RIGHT,
+} from "./OLEDDisplay";
+
+// Fonts
+export {
+ ArialMT_Plain_10,
+ ArialMT_Plain_16,
+ FONT_HEIGHT_SMALL,
+ FONT_HEIGHT_MEDIUM,
+ FONT_HEIGHT_LARGE,
+ createFontFromBytes,
+} from "./OLEDFonts";
+
+// Simple working fonts (recommended)
+export { Font_6x8, Font_5x7, SimpleFont } from "./SimpleFonts";
+
+// Images and icons
+export * as OLEDImages from "./OLEDImages";
+
+// Screen renderers (firmware UI ports)
+export * as ScreenRenderers from "./ScreenRenderers";
+
+// Vue component
+export { default as OLEDEmulator } from "./OLEDEmulator.vue";
+export { DISPLAY_PRESETS, type DisplayPreset } from "./DisplayPresets";
+
+// Demo component
+export { default as OLEDEmulatorDemo } from "./Demo.vue";
diff --git a/oled-emulator/src/main.ts b/oled-emulator/src/main.ts
new file mode 100644
index 000000000..6f2bfadfa
--- /dev/null
+++ b/oled-emulator/src/main.ts
@@ -0,0 +1,4 @@
+import { createApp } from "vue";
+import Demo from "./Demo.vue";
+
+createApp(Demo).mount("#app");
diff --git a/oled-emulator/tsconfig.json b/oled-emulator/tsconfig.json
new file mode 100644
index 000000000..881b6989a
--- /dev/null
+++ b/oled-emulator/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "preserve",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+
+ /* Vue */
+ "types": ["vite/client"]
+ },
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/oled-emulator/vite.config.ts b/oled-emulator/vite.config.ts
new file mode 100644
index 000000000..2a5b862d6
--- /dev/null
+++ b/oled-emulator/vite.config.ts
@@ -0,0 +1,35 @@
+import { defineConfig } from "vite";
+import vue from "@vitejs/plugin-vue";
+import dts from "vite-plugin-dts";
+import { resolve } from "path";
+
+export default defineConfig({
+ plugins: [
+ vue(),
+ dts({
+ insertTypesEntry: true,
+ }),
+ ],
+ build: {
+ lib: {
+ entry: resolve(__dirname, "src/index.ts"),
+ name: "MeshtasticOLEDEmulator",
+ formats: ["es", "cjs"],
+ fileName: (format) => `index.${format === "es" ? "mjs" : "js"}`,
+ },
+ rollupOptions: {
+ external: ["vue"],
+ output: {
+ globals: {
+ vue: "Vue",
+ },
+ },
+ },
+ cssCodeSplit: false,
+ },
+ resolve: {
+ alias: {
+ "@": resolve(__dirname, "src"),
+ },
+ },
+});