From a8ebac9702d59f0a4fddb80cb71b9b75811f6434 Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Wed, 21 Oct 2026 23:30:30 -0700 Subject: [PATCH] feat: add terminal keyboard editor and community gallery --- .github/workflows/ci.yml | 12 +- README.md | 2 + docs/keyboard-editor.md | 67 + eslint.config.mjs | 4 + package.json | 8 +- playwright.config.ts | 5 +- pnpm-lock.yaml | 316 ++++- public/fonts/keyboard-roboto-LICENSE.txt | 93 ++ public/fonts/keyboard-roboto-medium.ttf | Bin 0 -> 123904 bytes public/keyboards/README.md | 34 + scripts/keyboards/check.ts | 4 + src/app/styles.css | 4 +- .../keyboards/GoogleKeyboardPreview.tsx | 39 + src/components/keyboards/KeyboardCanvas.tsx | 255 ++++ src/components/keyboards/KeyboardEditor.tsx | 921 ++++++++++++++ src/components/keyboards/KeyboardGallery.tsx | 163 +++ src/components/keyboards/KeyboardLibrary.tsx | 6 + src/components/keyboards/MacroLibrary.tsx | 301 +++++ src/components/keyboards/download.ts | 8 + src/components/keyboards/keyboard.css | 1084 +++++++++++++++++ src/content/_meta.ts | 6 + src/content/guides/index.mdx | 4 + src/content/guides/special-keys.mdx | 2 + src/content/guides/terminal-keyboard.mdx | 57 + src/content/keyboards/_meta.ts | 8 + src/content/keyboards/editor.mdx | 8 + src/content/keyboards/index.mdx | 8 + src/keyboards/gallery.ts | 78 ++ src/keyboards/geometry.ts | 24 + src/keyboards/model.ts | 417 +++++++ src/keyboards/presets.ts | 64 + src/navigation/guides.ts | 1 + tests/keyboards/editor.spec.ts | 312 +++++ tests/keyboards/model.test.ts | 256 ++++ 34 files changed, 4551 insertions(+), 20 deletions(-) create mode 100644 docs/keyboard-editor.md create mode 100644 public/fonts/keyboard-roboto-LICENSE.txt create mode 100644 public/fonts/keyboard-roboto-medium.ttf create mode 100644 public/keyboards/README.md create mode 100644 scripts/keyboards/check.ts create mode 100644 src/components/keyboards/GoogleKeyboardPreview.tsx create mode 100644 src/components/keyboards/KeyboardCanvas.tsx create mode 100644 src/components/keyboards/KeyboardEditor.tsx create mode 100644 src/components/keyboards/KeyboardGallery.tsx create mode 100644 src/components/keyboards/KeyboardLibrary.tsx create mode 100644 src/components/keyboards/MacroLibrary.tsx create mode 100644 src/components/keyboards/download.ts create mode 100644 src/components/keyboards/keyboard.css create mode 100644 src/content/guides/terminal-keyboard.mdx create mode 100644 src/content/keyboards/_meta.ts create mode 100644 src/content/keyboards/editor.mdx create mode 100644 src/content/keyboards/index.mdx create mode 100644 src/keyboards/gallery.ts create mode 100644 src/keyboards/geometry.ts create mode 100644 src/keyboards/model.ts create mode 100644 src/keyboards/presets.ts create mode 100644 tests/keyboards/editor.spec.ts create mode 100644 tests/keyboards/model.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b2a62ac..5c405c18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,8 @@ concurrency: jobs: build: - runs-on: ubuntu-latest + # Match the OS supported by the pinned Playwright version. + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -49,9 +50,18 @@ jobs: - name: Type Check run: pnpm run check:types + - name: Keyboard contract tests + run: pnpm test:keyboards + - name: Build run: pnpm run build + - name: Install keyboard test browser + run: pnpm exec playwright install --with-deps chromium + + - name: Keyboard browser tests + run: pnpm exec playwright test tests/keyboards --project=chromium + - name: Deploy to Cloudflare Pages if: github.ref == 'refs/heads/develop' && github.repository == 'connectbot/connectbot.github.io' && github.event_name == 'push' uses: cloudflare/wrangler-action@953926a2e2182532811c01a25e53647d93bf07c0 # v4.1.3 diff --git a/README.md b/README.md index 352518a6..7a23321c 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ Web page for [ConnectBot](https://connectbot.org/), the first open source SSH cl ## Contributor documentation +- [Terminal keyboard editor](docs/keyboard-editor.md): format compatibility, previews, tests, and community layout contributions. + - [Development](docs/development.md): project structure, Git hooks, LAN access, checks, and deployment. - [Parameterized commands in MDX](docs/parameterized-commands.md): shared inputs, templates, and validation. - [Authoring feature guides](docs/guide-authoring.md): scenarios, translations, capture, narration, and publication. diff --git a/docs/keyboard-editor.md b/docs/keyboard-editor.md new file mode 100644 index 00000000..3fc9aa53 --- /dev/null +++ b/docs/keyboard-editor.md @@ -0,0 +1,67 @@ +# Terminal keyboard editor + +The keyboard library at `/keyboards/` and editor at `/keyboards/editor/` run on +this site's existing static export. Imported files, drafts, and simulated actions +stay in the browser. Community JSON is read from `public/keyboards/` at build time; +see the README there for the pull request workflow. + +## Compatibility + +The baseline is ConnectBot commit `a754a104`, schema 17. The reference is +`docs/terminal-keyboard.md`, `KeyboardDefaults`, `KeyboardPresets`, +`KeyboardLayers`, `KeyboardRegions`, `KeyboardGrid`, `MacroFormat`, and +`SchemaBasedExporter` in the Android repository. The PNGs under +`app/build/reports/keyboard-presets/` establish preset key order and appearance. + +`src/keyboards/model.ts` defines normalized browser types, JSON5 parsing, semantic +validation, and configuration serialization. Wire files use camelCase properties +and integer booleans. Macro documents remain strings, with their independent +version 1 protocol. Unknown row fields and unsupported macro documents survive +import/export. Unsupported keys remain visible but are disabled in test mode. +Malformed structure does not replace the current document; invalid geometry can +be corrected in the inspector or JSON editor and blocks testing/export. + +Local drafts and the JSON editor retain unassigned library macros. A downloaded +export contains the selected layout and the closure of its referenced macros. +It never includes host/profile data or keyboard settings. New-copy exports remap +all identities, preserving shared references. Identical repeated downloads reuse +the copy identities until the document or export options change. Preserve UUIDs +is intended for gallery maintenance: ConnectBot skips existing UUIDs, rather than +overwriting them. + +## Preview behavior + +The shared renderer in `src/components/keyboards/` uses pure geometry calculations +from `src/keyboards/geometry.ts`. All layers and hidden keys reserve geometry. +Pinned regions use independent coordinates; fitted mode appends their columns. +Keys retain row spans when edited, unlike the current Android editor's flattening +behavior. Labels use locally hosted Roboto with its license in `public/fonts/`. + +Phone (390×844), tablet (800×1280), and desktop (1440×900) presets describe logical +app and input area dimensions. Orientation sorts/swaps the short and long sides. Custom +sizes range from 240 to 3000 dp. One dp equals one logical CSS pixel before the +whole preview is scaled to fit. The keyboard occupies at most half the console +height remaining above the input method. Phone and tablet previews include a +simulated Google Keyboard (Gboard), with a tablet number row and a shorter +landscape keyboard. The Show Google Keyboard checkbox and test-mode IME toggle +show or hide it. Desktop and custom previews omit it. These preview controls do +not change exported JSON. The simulated keys are decorative; actual Android IME +geometry, device decorations, dynamic colors, and font rendering may differ. + +Test mode simulates layers, one-shot/locked modifiers, IME/compose toggles, and +hold-to-repeat. Other app controls are logged. Semantic output is an action log, +not terminal escape encoding or a live shell. Haptic preferences are exported +without vibrating the browser device. + +## Checks + +- `pnpm check:keyboards`: validate contributed files and built-in templates. +- `pnpm test:keyboards`: Node tests for formats, identity, macros, and geometry. +- `pnpm test:keyboards:browser --project=chromium`: editor/gallery behavior against + the production export. First build with `pnpm build` and install browsers using + `PLAYWRIGHT_BROWSERS_PATH=local/playwright pnpm exec playwright install chromium`. +- `pnpm check:types`, `pnpm lint`, and `pnpm build`: standard repository checks. + +The production prebuild validates contributions. CI also runs contract tests and +Chromium browser tests. The Playwright configuration retains all three browser +projects; install their binaries to run the keyboard suite across all engines. diff --git a/eslint.config.mjs b/eslint.config.mjs index b2cce9d0..596f1ffb 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -34,6 +34,10 @@ export default antfu( ], ...playwright.configs['flat/recommended'], }, + { + files: ['tests/keyboards/*.test.ts'], + rules: { 'test/no-import-node-test': 'off' }, + }, // --- Custom Rule Overrides --- { rules: { diff --git a/package.json b/package.json index 3a99b70b..94b7c8cd 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,14 @@ "prune:guides": "python3 scripts/guides/prune.py", "check:guides": "UV_CACHE_DIR=local/uv-cache uv run --no-project --with-requirements scripts/guides/requirements.lock python scripts/guides/cli.py check", "test:guides": "UV_CACHE_DIR=local/uv-cache uv run --no-project --with-requirements scripts/guides/requirements.lock python -m unittest discover -s scripts/guides/tests -v", - "test:guides:browser": "UV_CACHE_DIR=local/uv-cache uv run --no-project --with-requirements scripts/guides/requirements.lock python scripts/guides/browser_fixtures.py && PLAYWRIGHT_BROWSERS_PATH=local/playwright playwright test" + "test:guides:browser": "UV_CACHE_DIR=local/uv-cache uv run --no-project --with-requirements scripts/guides/requirements.lock python scripts/guides/browser_fixtures.py && PLAYWRIGHT_BROWSERS_PATH=local/playwright playwright test tests/guides", + "check:keyboards": "node --import tsx scripts/keyboards/check.ts", + "test:keyboards": "node --import tsx tests/keyboards/model.test.ts", + "test:keyboards:browser": "PLAYWRIGHT_BROWSERS_PATH=local/playwright playwright test tests/keyboards", + "prebuild": "pnpm check:keyboards" }, "dependencies": { + "json5": "^2.2.3", "next": "^16.2.3", "next-themes": "^0.4.6", "nextra": "^4.6.1", @@ -51,6 +56,7 @@ "lefthook": "^2.1.5", "next-sitemap": "^4.2.3", "pagefind": "^1.5.2", + "tsx": "^4.23.15", "typescript": "^5.9.3", "wrangler": "^4.83.0" } diff --git a/playwright.config.ts b/playwright.config.ts index e1c53d24..ed8c2949 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,10 +1,13 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ - testDir: './tests/guides', + testDir: './tests', + testMatch: '**/*.spec.ts', fullyParallel: true, use: { baseURL: 'http://127.0.0.1:3012', contextOptions: { reducedMotion: 'reduce' } }, webServer: { + stdout: 'ignore', + stderr: 'ignore', command: 'python3 -m http.server 3012 --bind 127.0.0.1 --directory out', url: 'http://127.0.0.1:3012/guides/', reuseExistingServer: process.env.CI !== 'true', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de9dd005..ab3f127e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + json5: + specifier: ^2.2.3 + version: 2.2.3 next: specifier: ^16.2.3 version: 16.2.3(@babel/core@7.28.5)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -93,6 +96,9 @@ importers: pagefind: specifier: ^1.5.2 version: 1.5.2 + tsx: + specifier: ^4.23.15 + version: 4.23.15 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -441,6 +447,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.27.3': resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} engines: {node: '>=18'} @@ -453,6 +465,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.27.3': resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} engines: {node: '>=18'} @@ -465,6 +483,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.27.3': resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} engines: {node: '>=18'} @@ -477,6 +501,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.27.3': resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} engines: {node: '>=18'} @@ -489,6 +519,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.27.3': resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} engines: {node: '>=18'} @@ -501,6 +537,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.27.3': resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} engines: {node: '>=18'} @@ -513,6 +555,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.3': resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} engines: {node: '>=18'} @@ -525,6 +573,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.27.3': resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} engines: {node: '>=18'} @@ -537,6 +591,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.27.3': resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} engines: {node: '>=18'} @@ -549,6 +609,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.27.3': resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} engines: {node: '>=18'} @@ -561,6 +627,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.27.3': resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} engines: {node: '>=18'} @@ -573,6 +645,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.27.3': resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} engines: {node: '>=18'} @@ -585,6 +663,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.27.3': resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} engines: {node: '>=18'} @@ -597,6 +681,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.27.3': resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} engines: {node: '>=18'} @@ -609,6 +699,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.27.3': resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} engines: {node: '>=18'} @@ -621,6 +717,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.27.3': resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} engines: {node: '>=18'} @@ -633,6 +735,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.27.3': resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} engines: {node: '>=18'} @@ -645,6 +753,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.27.3': resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} engines: {node: '>=18'} @@ -657,6 +771,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.27.3': resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} engines: {node: '>=18'} @@ -669,6 +789,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.27.3': resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} engines: {node: '>=18'} @@ -681,6 +807,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.27.3': resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} engines: {node: '>=18'} @@ -693,6 +825,12 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.27.3': resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} engines: {node: '>=18'} @@ -705,6 +843,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.27.3': resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} engines: {node: '>=18'} @@ -717,6 +861,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.27.3': resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} engines: {node: '>=18'} @@ -729,6 +879,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.27.3': resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} engines: {node: '>=18'} @@ -741,6 +897,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-plugin-eslint-comments@4.6.0': resolution: {integrity: sha512-2EX2bBQq1ez++xz2o9tEeEQkyvfieWgUFMH4rtJJri2q0Azvhja3hZGXsjPXs31R4fQkZDtWzNDDK2zQn5UE5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2940,6 +3102,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -5081,6 +5248,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.23.15: + resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==} + engines: {node: '>=18.0.0'} + hasBin: true + twoslash-protocol@0.3.6: resolution: {integrity: sha512-FHGsJ9Q+EsNr5bEbgG3hnbkvEBdW5STgPU824AHUjB4kw0Dn4p8tABT7Ncg1Ie6V0+mDg3Qpy41VafZXcQhWMA==} @@ -5885,156 +6057,234 @@ snapshots: '@esbuild/aix-ppc64@0.27.7': optional: true + '@esbuild/aix-ppc64@0.28.2': + optional: true + '@esbuild/android-arm64@0.27.3': optional: true '@esbuild/android-arm64@0.27.7': optional: true + '@esbuild/android-arm64@0.28.2': + optional: true + '@esbuild/android-arm@0.27.3': optional: true '@esbuild/android-arm@0.27.7': optional: true + '@esbuild/android-arm@0.28.2': + optional: true + '@esbuild/android-x64@0.27.3': optional: true '@esbuild/android-x64@0.27.7': optional: true + '@esbuild/android-x64@0.28.2': + optional: true + '@esbuild/darwin-arm64@0.27.3': optional: true '@esbuild/darwin-arm64@0.27.7': optional: true + '@esbuild/darwin-arm64@0.28.2': + optional: true + '@esbuild/darwin-x64@0.27.3': optional: true '@esbuild/darwin-x64@0.27.7': optional: true + '@esbuild/darwin-x64@0.28.2': + optional: true + '@esbuild/freebsd-arm64@0.27.3': optional: true '@esbuild/freebsd-arm64@0.27.7': optional: true + '@esbuild/freebsd-arm64@0.28.2': + optional: true + '@esbuild/freebsd-x64@0.27.3': optional: true '@esbuild/freebsd-x64@0.27.7': optional: true + '@esbuild/freebsd-x64@0.28.2': + optional: true + '@esbuild/linux-arm64@0.27.3': optional: true '@esbuild/linux-arm64@0.27.7': optional: true + '@esbuild/linux-arm64@0.28.2': + optional: true + '@esbuild/linux-arm@0.27.3': optional: true '@esbuild/linux-arm@0.27.7': optional: true + '@esbuild/linux-arm@0.28.2': + optional: true + '@esbuild/linux-ia32@0.27.3': optional: true '@esbuild/linux-ia32@0.27.7': optional: true + '@esbuild/linux-ia32@0.28.2': + optional: true + '@esbuild/linux-loong64@0.27.3': optional: true '@esbuild/linux-loong64@0.27.7': optional: true + '@esbuild/linux-loong64@0.28.2': + optional: true + '@esbuild/linux-mips64el@0.27.3': optional: true '@esbuild/linux-mips64el@0.27.7': optional: true + '@esbuild/linux-mips64el@0.28.2': + optional: true + '@esbuild/linux-ppc64@0.27.3': optional: true '@esbuild/linux-ppc64@0.27.7': optional: true + '@esbuild/linux-ppc64@0.28.2': + optional: true + '@esbuild/linux-riscv64@0.27.3': optional: true '@esbuild/linux-riscv64@0.27.7': optional: true + '@esbuild/linux-riscv64@0.28.2': + optional: true + '@esbuild/linux-s390x@0.27.3': optional: true '@esbuild/linux-s390x@0.27.7': optional: true + '@esbuild/linux-s390x@0.28.2': + optional: true + '@esbuild/linux-x64@0.27.3': optional: true '@esbuild/linux-x64@0.27.7': optional: true + '@esbuild/linux-x64@0.28.2': + optional: true + '@esbuild/netbsd-arm64@0.27.3': optional: true '@esbuild/netbsd-arm64@0.27.7': optional: true + '@esbuild/netbsd-arm64@0.28.2': + optional: true + '@esbuild/netbsd-x64@0.27.3': optional: true '@esbuild/netbsd-x64@0.27.7': optional: true + '@esbuild/netbsd-x64@0.28.2': + optional: true + '@esbuild/openbsd-arm64@0.27.3': optional: true '@esbuild/openbsd-arm64@0.27.7': optional: true + '@esbuild/openbsd-arm64@0.28.2': + optional: true + '@esbuild/openbsd-x64@0.27.3': optional: true '@esbuild/openbsd-x64@0.27.7': optional: true + '@esbuild/openbsd-x64@0.28.2': + optional: true + '@esbuild/openharmony-arm64@0.27.3': optional: true '@esbuild/openharmony-arm64@0.27.7': optional: true + '@esbuild/openharmony-arm64@0.28.2': + optional: true + '@esbuild/sunos-x64@0.27.3': optional: true '@esbuild/sunos-x64@0.27.7': optional: true + '@esbuild/sunos-x64@0.28.2': + optional: true + '@esbuild/win32-arm64@0.27.3': optional: true '@esbuild/win32-arm64@0.27.7': optional: true + '@esbuild/win32-arm64@0.28.2': + optional: true + '@esbuild/win32-ia32@0.27.3': optional: true '@esbuild/win32-ia32@0.27.7': optional: true + '@esbuild/win32-ia32@0.28.2': + optional: true + '@esbuild/win32-x64@0.27.3': optional: true '@esbuild/win32-x64@0.27.7': optional: true + '@esbuild/win32-x64@0.28.2': + optional: true + '@eslint-community/eslint-plugin-eslint-comments@4.6.0(eslint@9.39.2(jiti@2.6.1))': dependencies: escape-string-regexp: 4.0.0 @@ -7322,13 +7572,13 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitest/browser-playwright@4.0.16(playwright@1.57.0)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3))(vitest@4.0.16)': + '@vitest/browser-playwright@4.0.16(playwright@1.57.0)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.23.15)(yaml@2.8.3))(vitest@4.0.16)': dependencies: - '@vitest/browser': 4.0.16(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3))(vitest@4.0.16) - '@vitest/mocker': 4.0.16(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)) + '@vitest/browser': 4.0.16(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.23.15)(yaml@2.8.3))(vitest@4.0.16) + '@vitest/mocker': 4.0.16(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.23.15)(yaml@2.8.3)) playwright: 1.57.0 tinyrainbow: 3.1.0 - vitest: 4.0.16(@types/node@25.6.0)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) + vitest: 4.0.16(@types/node@25.6.0)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.23.15)(yaml@2.8.3) transitivePeerDependencies: - bufferutil - msw @@ -7336,16 +7586,16 @@ snapshots: - vite optional: true - '@vitest/browser@4.0.16(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3))(vitest@4.0.16)': + '@vitest/browser@4.0.16(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.23.15)(yaml@2.8.3))(vitest@4.0.16)': dependencies: - '@vitest/mocker': 4.0.16(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)) + '@vitest/mocker': 4.0.16(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.23.15)(yaml@2.8.3)) '@vitest/utils': 4.0.16 magic-string: 0.30.21 pixelmatch: 7.1.0 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.0.16(@types/node@25.6.0)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) + vitest: 4.0.16(@types/node@25.6.0)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.23.15)(yaml@2.8.3) ws: 8.20.0 transitivePeerDependencies: - bufferutil @@ -7361,7 +7611,7 @@ snapshots: eslint: 9.39.2(jiti@2.6.1) optionalDependencies: typescript: 5.9.3 - vitest: 4.0.16(@types/node@25.6.0)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) + vitest: 4.0.16(@types/node@25.6.0)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.23.15)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -7375,13 +7625,13 @@ snapshots: tinyrainbow: 3.1.0 optional: true - '@vitest/mocker@4.0.16(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3))': + '@vitest/mocker@4.0.16(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.23.15)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.0.16 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) + vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.23.15)(yaml@2.8.3) optional: true '@vitest/pretty-format@4.0.16': @@ -8220,6 +8470,35 @@ snapshots: '@esbuild/win32-x64': 0.27.7 optional: true + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + escalade@3.2.0: {} escape-string-regexp@1.0.5: {} @@ -11156,6 +11435,12 @@ snapshots: tslib@2.8.1: {} + tsx@4.23.15: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + twoslash-protocol@0.3.6: {} twoslash@0.3.6(typescript@5.9.3): @@ -11334,7 +11619,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3): + vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.23.15)(yaml@2.8.3): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) @@ -11347,13 +11632,14 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 + tsx: 4.23.15 yaml: 2.8.3 optional: true - vitest@4.0.16(@types/node@25.6.0)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3): + vitest@4.0.16(@types/node@25.6.0)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.23.15)(yaml@2.8.3): dependencies: '@vitest/expect': 4.0.16 - '@vitest/mocker': 4.0.16(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)) + '@vitest/mocker': 4.0.16(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.23.15)(yaml@2.8.3)) '@vitest/pretty-format': 4.0.16 '@vitest/runner': 4.0.16 '@vitest/snapshot': 4.0.16 @@ -11370,11 +11656,11 @@ snapshots: tinyexec: 1.1.1 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) + vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.23.15)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.6.0 - '@vitest/browser-playwright': 4.0.16(playwright@1.57.0)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3))(vitest@4.0.16) + '@vitest/browser-playwright': 4.0.16(playwright@1.57.0)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.23.15)(yaml@2.8.3))(vitest@4.0.16) transitivePeerDependencies: - jiti - less diff --git a/public/fonts/keyboard-roboto-LICENSE.txt b/public/fonts/keyboard-roboto-LICENSE.txt new file mode 100644 index 00000000..65a3057b --- /dev/null +++ b/public/fonts/keyboard-roboto-LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/fonts/keyboard-roboto-medium.ttf b/public/fonts/keyboard-roboto-medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..310c9e48da2e3652f024ae84f84b10a2addbac3d GIT binary patch literal 123904 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 LBeU(#(0Bg_nPOco literal 0 HcmV?d00001 diff --git a/public/keyboards/README.md b/public/keyboards/README.md new file mode 100644 index 00000000..f96cb596 --- /dev/null +++ b/public/keyboards/README.md @@ -0,0 +1,34 @@ +# Share a ConnectBot terminal keyboard + +Create or import your layout at https://connectbot.org/keyboards/editor/ and choose +**Share your layout**. Download the JSON, then use GitHub's **Add file → Upload +files** here. GitHub will guide you through forking the repository and opening a +pull request against `develop`. + +One file is all you need: `.json`. The gallery discovers +JSON files here automatically; no index or screenshot needs updating. The layout +name inside the JSON becomes its gallery title. Credit and discussion remain in +the pull request and the file's Git history, linked from the gallery. + +## Before opening the pull request + +- Include exactly one named layout and only its keys and referenced named macros. +- Export a **new copy** for a new contribution, so its UUIDs do not collide with + another layout. Preserve UUIDs when maintaining an existing contributed file. +- Use standard JSON, containing only `version`, `keyboard_layouts`, + `keyboard_macros`, and `keyboard_items`. Exported boolean fields must use integer + `0`/`1` values. Do not contribute host configurations. +- Test the layout at phone and tablet sizes in portrait and landscape. Check layer + navigation, shortcuts, hidden keys, and pinned controls. +- Import the downloaded file into a current ConnectBot build via **Import Hosts**, + then select it under **Settings → Terminal keyboard**. +- Describe the intended workflow and any noteworthy macros in your pull request. + Use generic example text in macros. All contributed content is public. + +The website checks files against the current keyboard contract during CI and the +production build. Run `pnpm check:keyboards` locally to see diagnostics. Compatibility +is based on ConnectBot schema 17, commit `a754a104`. + +For existing layouts, ConnectBot skips records whose UUIDs already exist. Updating +a gallery file does not overwrite an installed copy; users can customize and +export it as a new copy when they want to import the updated version. diff --git a/scripts/keyboards/check.ts b/scripts/keyboards/check.ts new file mode 100644 index 00000000..061445ad --- /dev/null +++ b/scripts/keyboards/check.ts @@ -0,0 +1,4 @@ +import { readGallery } from '../../src/keyboards/gallery'; + +const entries = await readGallery(); +console.log(`Validated ${entries.filter(e => !e.template).length} contributed keyboards and ${entries.filter(e => e.template).length} templates.`); diff --git a/src/app/styles.css b/src/app/styles.css index b9df4f4e..7ed0e795 100644 --- a/src/app/styles.css +++ b/src/app/styles.css @@ -598,10 +598,10 @@ html:has(.hero-brand):not([data-brand-docked='true']) a:has(> .navbar-brand) { align-items: center; justify-content: center; min-height: 44px; - padding: 0 0.5rem; + padding: 0 0.3rem; border-radius: 0.5rem; color: inherit; - font-size: 0.9375rem; + font-size: 0.875rem; font-weight: 600; text-decoration: none; } diff --git a/src/components/keyboards/GoogleKeyboardPreview.tsx b/src/components/keyboards/GoogleKeyboardPreview.tsx new file mode 100644 index 00000000..56933f21 --- /dev/null +++ b/src/components/keyboards/GoogleKeyboardPreview.tsx @@ -0,0 +1,39 @@ +const letterRows = ['qwertyuiop', 'asdfghjkl', 'zxcvbnm']; + +/** A visual stand-in for the Android input method; never part of the exported layout. */ +export function GoogleKeyboardPreview({ height, tablet }: Readonly<{ height: number; tablet: boolean }>) { + return ( +
+ +
+ ); +} diff --git a/src/components/keyboards/KeyboardCanvas.tsx b/src/components/keyboards/KeyboardCanvas.tsx new file mode 100644 index 00000000..51dc5bb7 --- /dev/null +++ b/src/components/keyboards/KeyboardCanvas.tsx @@ -0,0 +1,255 @@ +'use client'; + +import type { CSSProperties, PointerEvent } from 'react'; +import type { Rect, Region } from '@/keyboards/geometry'; +import type { Item, Layout, Macro } from '@/keyboards/model'; +import { useEffect, useRef } from 'react'; +import { geometry } from '@/keyboards/geometry'; +import { accessibleLabel, colorCss, label, populated, supported } from '@/keyboards/model'; +import './keyboard.css'; + +export type TestState = { modifiers: Record; ime: boolean; compose: boolean }; +function KeyIcon({ target, active }: Readonly<{ target: string; active: boolean }>) { + const paths: Record = { + arrow_up: 'm7 14 5-5 5 5', + arrow_down: 'm7 10 5 5 5-5', + arrow_left: 'm14 7-5 5 5 5', + arrow_right: 'm10 7 5 5-5 5', + paste: 'M9 5H5v16h14V5h-4M9 3h6v4H9z', + text_input: 'm4 16 12-12 4 4L8 20H4zm10-10 4 4', + toggle_ime: active ? 'M3 4h18v13H3zM7 8h1m3 0h1m3 0h1M7 11h1m3 0h1m3 0h1m-8 9 3 3 3-3' : 'M3 5h18v14H3zM6 9h1m3 0h1m3 0h1m3 0h1M6 12h1m3 0h1m3 0h1m3 0h1M8 16h8', + }; + const path = paths[target]; + return path !== undefined ? : null; +} +function faceOpacity(item: Item, active: number, enabled: boolean): number { + if (active > 0) { + return 1; + } + const opacity = item.backgroundOpacity ?? 0.85; + return enabled && item.visible ? opacity : opacity * 0.35 / 0.85; +} +function KeyFace({ item, macros, active, enabled }: Readonly<{ item: Item; macros: Macro[]; active: number; enabled: boolean }>) { + const text = useRef(null); + useEffect(() => { + const span = text.current; + if (!span?.parentElement) { + return; + } + const fit = () => { + const width = Math.max(0, span.parentElement!.clientWidth - 4); + const height = span.parentElement!.clientHeight; + span.style.transform = `scale(${Math.min(1, width / Math.max(1, span.scrollWidth))}, ${Math.min(1, height / Math.max(1, span.scrollHeight))})`; + }; + const observer = new ResizeObserver(fit); + observer.observe(span.parentElement); + + fit(); + + document.fonts.ready.then(fit).catch(() => {}); + return () => observer.disconnect(); + }, [item, macros]); + const style: CSSProperties = { + '--kb-background': item.backgroundColor === null ? undefined : colorCss(item.backgroundColor), + 'color': item.textColor === null ? undefined : `${colorCss(item.textColor)}${((item.textColor >>> 24) & 255).toString(16).padStart(2, '0')}`, + 'borderColor': item.outlineColor === null ? undefined : `${colorCss(item.outlineColor)}${((item.outlineColor >>> 24) & 255).toString(16).padStart(2, '0')}`, + 'borderWidth': item.outlineWidth ?? 1, + '--kb-opacity': faceOpacity(item, active, enabled), + } as CSSProperties; + const icon = item.kind !== 'macro' && ['arrow_up', 'arrow_down', 'arrow_left', 'arrow_right', 'paste', 'text_input', 'toggle_ime'].includes(item.target); + return ( + + {icon ? : {label(item, macros)}} + + ); +} +type CanvasProps = Readonly<{ + layout: Layout; + items: Item[]; + macros: Macro[]; + width: number; + maxHeight: number; + layer: number; + editing?: boolean; + selectedId?: string; + testState?: TestState; + onPress?: (item: Item) => void; + onMove?: (id: string, row: number, column: number) => void; +}>; +function activeLevel(item: Item, items: Item[], layer: number, state?: TestState): number { + const initial = populated(items)[0] ?? 0; + const selectedLayer = item.target === String(layer) || (item.target === 'next' && layer !== initial); + if (item.kind === 'layer' && selectedLayer) { + return 2; + } + if (item.target === 'toggle_ime') { + return state?.ime ? 1 : 0; + } + if (item.target === 'toggle_compose') { + return state?.compose ? 2 : 0; + } + return state?.modifiers[item.target] ?? 0; +} +function keyDescription(item: Item, macros: Macro[], editing: boolean): string { + const name = accessibleLabel(item, macros); + if (!editing) { + return name; + } + const position = `, row ${item.row}, column ${item.column}`; + const visibility = item.visible ? '' : ', hidden'; + return name + position + visibility; +} +function keyTitle(item: Item, macros: Macro[], editing: boolean, active: number): string { + const action = `${label(item, macros)} · ${item.kind}: ${item.target || item.macroId}`; + if (editing || item.kind !== 'modifier') { + return action; + } + const state = ['off', 'one-shot', 'locked'][active]; + return `${action} · ${state}`; +} +type Gesture = { x: number; y: number; moved: boolean; timer?: ReturnType; interval?: ReturnType }; +function clearGesture(gesture: Gesture | null): void { + if (gesture === null) { + return; + } + clearTimeout(gesture.timer); + clearInterval(gesture.interval); +} +function startRepeating(gesture: Gesture, press: () => void): void { + gesture.timer = setTimeout(() => { + press(); + gesture.timer = setTimeout(() => { + press(); + gesture.interval = setInterval(press, 50); + }, 400); + }, 100); +} +function KeyboardButton({ item, rect, region, items, macros, layer, editing, selectedId, testState, onPress, onMove }: Readonly<{ + item: Item; + rect: Rect; + region: Region; + items: Item[]; + macros: Macro[]; + layer: number; + editing: boolean; + selectedId?: string; + testState?: TestState; + onPress?: (item: Item) => void; + onMove?: (id: string, row: number, column: number) => void; +}>) { + const gesture = useRef(null); + const suppressClick = useRef(false); + const cleanup = () => { + clearGesture(gesture.current); + gesture.current = null; + }; + useEffect(() => () => clearGesture(gesture.current), []); + const enabled = supported(item, items, macros); + const active = activeLevel(item, items, layer, testState); + const defaultRepeat = item.kind === 'key' && item.target.startsWith('arrow_'); + const repeating = !editing && ['key', 'macro'].includes(item.kind) && (item.repeatOnHold ?? defaultRepeat); + const pointerDown = (event: PointerEvent) => { + if (event.button !== 0 || (!onMove && !repeating)) { + return; + } + cleanup(); + suppressClick.current = false; + const current: Gesture = { x: event.clientX, y: event.clientY, moved: false }; + gesture.current = current; + if (editing) { + event.currentTarget.setPointerCapture(event.pointerId); + } + if (repeating) { + startRepeating(current, () => { + onPress?.(item); + suppressClick.current = true; + }); + } + }; + const pointerMove = (event: PointerEvent) => { + const current = gesture.current; + if (!current || !editing || !onMove) { + return; + } + if (Math.hypot(event.clientX - current.x, event.clientY - current.y) > 6) { + current.moved = true; + } + }; + const pointerUp = (event: PointerEvent) => { + const current = gesture.current; + if (current?.moved && editing && onMove) { + const scale = event.currentTarget.getBoundingClientRect().width / Math.max(1, rect.width); + const dx = (event.clientX - current.x) / scale; + const dy = (event.clientY - current.y) / scale; + const row = Math.max(0, Math.round(item.row + dy / Math.max(1, region.cellHeight))); + const column = Math.max(0, Math.round(item.column + dx / Math.max(1, region.width / Math.max(1, region.columns)))); + onMove(item.id, row, column); + suppressClick.current = true; + } + cleanup(); + }; + const classes = ['kb-key']; + if (editing) { + classes.push('kb-editable'); + } + if (selectedId === item.id) { + classes.push('kb-selected'); + } + if (!item.visible) { + classes.push('kb-hidden-key'); + } + return ( + + ); +} +export function KeyboardCanvas({ layout, items, macros, width, maxHeight, layer, editing = false, selectedId, testState, onPress, onMove }: CanvasProps) { + const regions = geometry(layout, items, width, maxHeight); + const height = regions.length ? Math.max(...regions.map(r => r.viewportHeight)) : 30; + return ( +
+ {regions.map(region => ( +
+
+ {region.rects.filter(r => r.item.layer === layer && (editing || r.item.visible)).map(rect => ( + i.id === rect.item.id)!} rect={rect} region={region} items={items} macros={macros} layer={layer} editing={editing} selectedId={selectedId} testState={testState} onPress={onPress} onMove={onMove} /> + ))} +
+
+ ))} + {regions.filter(r => r.pinned && r.x > 0).map(r =>
+ ); +} diff --git a/src/components/keyboards/KeyboardEditor.tsx b/src/components/keyboards/KeyboardEditor.tsx new file mode 100644 index 00000000..3a46df6b --- /dev/null +++ b/src/components/keyboards/KeyboardEditor.tsx @@ -0,0 +1,921 @@ +'use client'; + +/* eslint-disable react/no-array-index-key -- Diagnostic and action-log entries are display-only, with no component state. */ + +import type { ChangeEvent } from 'react'; +import type { TestState } from './KeyboardCanvas'; +import type { Action, Configuration, Item, Layout, Macro } from '@/keyboards/model'; +import Link from 'next/link'; +import { useEffect, useRef, useState } from 'react'; +import { appActions, colorCss, colorValue, decodeMacro, destination, keys, label, MAX_BYTES, modifiers, newId, newItem, overlaps, parseConfiguration, populated, serialize, slugify, supported, validate, validateContribution, warnings } from '@/keyboards/model'; +import { createPreset, templates } from '@/keyboards/presets'; +import { downloadJson } from './download'; +import { GoogleKeyboardPreview } from './GoogleKeyboardPreview'; +import { KeyboardCanvas } from './KeyboardCanvas'; +import { MacroLibrary } from './MacroLibrary'; +import './keyboard.css'; + +const devices = { phone: [390, 844], tablet: [800, 1280], desktop: [1440, 900], custom: [390, 844] } as const; +const storageKey = 'connectbot-keyboard-draft-v1'; +type History = { past: Configuration[]; present: Configuration; future: Configuration[] }; +type Simulation = TestState & { log: string[] }; +const initialSimulation = (ime = true): Simulation => ({ modifiers: { ctrl: 0, alt: 0, shift: 0 }, ime, compose: false, log: [] }); + +function previewImeHeight(device: keyof typeof devices, height: number, visible: boolean): number { + if (!visible || (device !== 'phone' && device !== 'tablet')) { + return 0; + } + const preferredHeight = device === 'tablet' ? 360 : 280; + return Math.min(preferredHeight, Math.round(height * 0.42)); +} + +const actionTargets = new Map([['key', 'escape'], ['modifier', 'ctrl'], ['app', 'toggle_ime'], ['layer', 'next']]); +const actionOptions = new Map([['key', keys], ['modifier', modifiers], ['app', appActions]]); +const appearanceLabels = { backgroundColor: 'Background', textColor: 'Text color', outlineColor: 'Outline color' }; +function formatAction(action: Action): string { + if (action.type === 'text') { + return `text ${JSON.stringify(action.text)}`; + } + if (action.type === 'bytes') { + return `bytes ${action.hex}`; + } + return `key ${[...action.modifiers, action.key ?? JSON.stringify(action.character)].join('+')}`; +} +function draftStatus(ready: boolean, recovery: string | null, storageError: string): string { + if (!ready) { + return 'Loading editor…'; + } + if (recovery === null && storageError === '') { + return 'Draft saved locally'; + } + return 'Local workspace'; +} +async function initialConfiguration(search: string): Promise { + const query = new URLSearchParams(search); + const template = query.get('template'); + if (template !== null && [...templates.map(t => t.key), 'blank'].includes(template)) { + return createPreset(template); + } + const slug = query.get('layout'); + if (slug === null || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) { + return undefined; + } + const response = await fetch(`/keyboards/${slug}.json`); + if (!response.ok) { + throw new Error('Could not load this gallery layout.'); + } + return parseConfiguration(await response.text()); +} +function Numeric({ title, value, onChange, min = 0, max = 1000, step = 1 }: Readonly<{ title: string; value: number; onChange: (value: number) => void; min?: number; max?: number; step?: number }>) { + return ( + + ); +} +function TriState({ title, value, onChange }: Readonly<{ title: string; value: boolean | null; onChange: (value: boolean | null) => void }>) { + return ( + + ); +} +function Inspector({ item, config, onChange, onDuplicate, onDelete }: Readonly<{ item: Item; config: Configuration; onChange: (item: Item) => void; onDuplicate: () => void; onDelete: () => void }>) { + const update = (patch: Partial) => onChange({ ...item, ...patch }); + const options = actionOptions.get(item.kind) ?? ['next', '0', '1', '2', '3', '4']; + return ( +
+
+

{label(item, config.macros)}

+ Selected key +
+ + {item.kind !== 'macro' && ( + + )} + {['key', 'macro'].includes(item.kind) && ( + + )} +

For a custom label or key combination, create a named macro and assign it here.

+
+ Placement +
+ update({ row })} /> + update({ column })} /> + update({ rowSpan })} /> + update({ columnSpan })} /> + update({ layer })} /> +
+
+ + +
+

Pinned keys have their own coordinates. Fitted layouts append them to the main grid.

+
+
+ Appearance + {(['backgroundColor', 'textColor', 'outlineColor'] as const).map(field => ( +
+ + +
+ ))} + update({ backgroundOpacity })} /> + update({ outlineWidth })} /> + +
+
+ Behavior + update({ repeatOnHold })} /> + update({ hapticFeedback })} /> +

By default arrows repeat. Haptics are saved for ConnectBot; the preview does not vibrate.

+
+
+ + +
+
+ ); +} +export function KeyboardEditor() { + const [history, setHistory] = useState(() => ({ past: [], present: createPreset('citrus'), future: [] })); + const config = history.present; + const [layoutId, setLayoutId] = useState(config.layouts[0]!.id); + const layout = config.layouts.find(l => l.id === layoutId) ?? config.layouts[0]!; + const items = config.items.filter(i => i.layoutId === layout.id); + const [selectedId, setSelectedId] = useState(); + const selected = items.find(i => i.id === selectedId); + const [layer, setLayer] = useState(0); + const [mode, setMode] = useState<'edit' | 'test'>('edit'); + const [device, setDevice] = useState('phone'); + const [orientation, setOrientation] = useState<'portrait' | 'landscape'>('portrait'); + const [custom, setCustom] = useState<[number, number]>([390, 844]); + const [theme, setTheme] = useState('light'); + const [simulation, setSimulation] = useState(() => initialSimulation()); + const [notice, setNotice] = useState(''); + const [jsonDraft, setJsonDraft] = useState(null); + const [preserve, setPreserve] = useState(false); + const [share, setShare] = useState(false); + const [ready, setReady] = useState(false); + const [recovery, setRecovery] = useState(null); + const [storageError, setStorageError] = useState(''); + const stage = useRef(null); + const [availableWidth, setAvailableWidth] = useState(700); + const exported = useRef<{ key: string; source: string } | null>(null); + const source = serialize(config); + const errors = validate(config, layout.id); + const warn = warnings(config, layout.id); + const contributionErrors = share ? validateContribution(parseConfiguration(serialize(config, layout.id))) : []; + const dimensions = device === 'custom' ? custom : devices[device]; + const short = Math.min(...dimensions); + const long = Math.max(...dimensions); + const width = orientation === 'portrait' ? short : long; + const height = orientation === 'portrait' ? long : short; + const scale = Math.min(1, Math.max(100, availableWidth - 32) / width, 650 / height); + const commit = (next: Configuration) => setHistory(h => ({ past: [...h.past.slice(-49), h.present], present: next, future: [] })); + const mobileDevice = device === 'phone' || device === 'tablet'; + const imeHeight = previewImeHeight(device, height, simulation.ime); + const consoleHeight = height - imeHeight; + const reset = (nextDevice = device) => setSimulation(initialSimulation(nextDevice === 'phone' || nextDevice === 'tablet')); + const load = (next: Configuration) => { + commit(next); + setLayoutId(next.layouts[0]!.id); + setLayer(populated(next.items.filter(i => i.layoutId === next.layouts[0]!.id))[0] ?? 0); + setSelectedId(undefined); + reset(); + setJsonDraft(null); + }; + useEffect(() => { + const element = stage.current; + if (!element) { + return; + } + const observer = new ResizeObserver(entries => setAvailableWidth(entries[0]?.contentRect.width ?? 700)); + observer.observe(element); + return () => observer.disconnect(); + }, []); + useEffect(() => { + let cancelled = false; + const initialize = async () => { + try { + setRecovery(localStorage.getItem(storageKey)); + } catch { + setStorageError('Browser storage is unavailable. Download your layout to keep your work.'); + } + try { + const next = await initialConfiguration(window.location.search); + if (next !== undefined && !cancelled) { + setHistory({ past: [], present: next, future: [] }); + setLayoutId(next.layouts[0]!.id); + setLayer(populated(next.items.filter(i => i.layoutId === next.layouts[0]!.id))[0] ?? 0); + } + } catch (e) { + if (!cancelled) { + setNotice(e instanceof Error ? e.message : String(e)); + } + } + if (!cancelled) { + setReady(true); + } + }; + void initialize(); + return () => { + cancelled = true; + }; + }, []); + useEffect(() => { + if (!ready || recovery !== null) { + return; + } + const timer = setTimeout(() => { + try { + localStorage.setItem(storageKey, source); + } catch { + setStorageError('Draft could not be saved in this browser. Download your work.'); + } + }, 300); + return () => clearTimeout(timer); + }, [source, ready, recovery]); + const updateLayout = (patch: Partial) => commit({ ...config, layouts: config.layouts.map(l => l.id === layout.id ? { ...l, ...patch } : l) }); + const updateItem = (next: Item, moving = false) => { + if (moving && items.some(i => i.id !== next.id && overlaps(i, next))) { + setNotice('That cell is occupied. Move the key to an empty area.'); + return; + } + if (moving) { + const candidate = { ...config, items: config.items.map(i => i.id === next.id ? next : i) }; + const geometryError = validate(candidate, layout.id).filter(e => !e.includes('route back')); + if (geometryError.length) { + setNotice(geometryError[0]!); + return; + } + } + commit({ ...config, items: config.items.map(i => i.id === next.id ? next : i) }); + setNotice(''); + }; + const addKey = (copy?: Item) => { + if (items.length >= 256) { + setNotice('A layout supports at most 256 keys.'); + return; + } + let item = newItem(layout.id, copy ? { ...copy, id: newId(), layer } : { layer }); + let found = false; + const columns = Math.max(1, ...items.map(i => i.column + i.columnSpan)); + for (let row = 0; row < Math.floor(3000 / layout.buttonHeight) && !found; row++) { + for (let column = 0; column < columns && !found; column++) { + const candidate = { ...item, row, column }; + if ((column + item.columnSpan) * layout.buttonWidth <= 3000 && (row + item.rowSpan) * layout.buttonHeight <= 3000 && !items.some(i => overlaps(i, candidate))) { + item = candidate; + found = true; + } + } + } + if (!found) { + setNotice('No free cells remain within ConnectBot’s grid limits.'); + return; + } + commit({ ...config, items: [...config.items, item] }); + setSelectedId(item.id); + setNotice(''); + }; + const press = (item: Item) => { + if (mode === 'edit') { + setSelectedId(item.id); + return; + } + if (!supported(item, items, config.macros)) { + return; + } + if (item.kind === 'layer') { + const next = destination(items, layer, item.target); + if (next !== undefined) { + setLayer(next); + } + return; + } + setSimulation((previous) => { + const next = { ...previous, modifiers: { ...previous.modifiers } }; + let event = ''; + if (item.kind === 'modifier') { + next.modifiers[item.target] = ((previous.modifiers[item.target] ?? 0) + 1) % 3; + } else if (item.kind === 'app') { + if (item.target === 'toggle_ime') { + next.ime = !next.ime; + } + if (item.target === 'toggle_compose') { + next.compose = !next.compose; + } + event = `App: ${label(item, config.macros)}`; + } else { + const macro = config.macros.find(m => m.id === item.macroId); + if (macro) { + event = decodeMacro(macro.document).map(formatAction).join(' → '); + } else { + event = `key ${[...modifiers.filter(m => (next.modifiers[m] ?? 0) > 0), item.target].join('+')}`; + } + modifiers.forEach((m) => { + if (next.modifiers[m] === 1) { + next.modifiers[m] = 0; + } + }); + } + if (event) { + next.log = [...previous.log.slice(-99), event]; + } + return next; + }); + }; + const download = () => { + if (errors.length) { + setNotice('Resolve the layout errors before downloading.'); + return; + } + const key = `${source}:${layout.id}:${preserve}`; + if (exported.current?.key !== key) { + exported.current = { key, source: serialize(config, layout.id, !preserve) }; + } + downloadJson(exported.current.source, `${slugify(layout.name)}.json`); + setNotice('Downloaded. In ConnectBot, use Import Hosts, then choose the layout in Settings → Terminal keyboard.'); + }; + const importFile = async (event: ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) { + return; + } + try { + if (file.size > MAX_BYTES) { + throw new Error('Files must be 8 MiB or smaller.'); + } + load(parseConfiguration(await file.text())); + setNotice('Loaded keyboard data locally. Other configuration tables are excluded from downloads.'); + } catch (e) { + setNotice(e instanceof Error ? e.message : String(e)); + } + event.target.value = ''; + }; + const assignMacro = (macro: Macro) => { + if (!selected) { + return; + } + const actions = decodeMacro(macro.document); + const shortcut = actions.length === 1 && actions[0]?.type === 'key' ? actions[0] : undefined; + updateItem({ ...selected, kind: shortcut?.key !== undefined ? 'key' : 'macro', target: shortcut?.key ?? '', macroId: macro.id }); + }; + return ( +
+
+
+
+ ← Keyboard library +

Make room for your workflow.

+

Build a terminal keyboard. Preview it on any screen. Take it into ConnectBot.

+
+ + + {' '} + All editing stays in your browser + +
+ {recovery !== null && ( +
+ A previous keyboard draft is available. + + +
+ )} + {storageError && {storageError}} +
+ + + + + + + + +
+ {notice || `${items.length} keys · ${populated(items).length} populated layers · ${draftStatus(ready, recovery, storageError)}`} + {share && ( +
+

Give someone a better keyboard.

+ {contributionErrors.length > 0 && ( +
+ Before contributing +
    {contributionErrors.map(message =>
  • {message}
  • )}
+
+ )} +

Download your layout, then contribute the JSON file to the ConnectBot website repository. One file is all it takes.

+
    +
  1. Give the layout a clear name and resolve any errors.
  2. +
  3. + +
  4. +
  5. + Upload the JSON on GitHub ↗ + , then follow GitHub’s prompts to fork and propose a pull request against + {' '} + develop + . +
  6. +
+

+ The gallery updates after your pull request is merged. + {' '} + Contribution guide ↗ +

+
+ )} + {!!errors.length && ( +
+ Resolve these before testing or exporting +
    {errors.slice(0, 10).map(error =>
  • {error}
  • )}
+ {errors.length > 10 && ( +

+ And + {errors.length - 10} + {' '} + more errors. +

+ )} +
+ )} + {jsonDraft !== null && ( +
+

Configuration JSON

+

JSON5 is accepted. Apply validates the structure; layout errors remain visible for correction. Downloads contain the selected layout and its referenced macros.

+