mirror of
https://github.com/qmk/qmk_userspace.git
synced 2026-03-07 00:01:09 -05:00
Create documentation website
Builds a static documentation website using Eleventy to visualize the keymap with comprehensive symbol tables and SVG layer diagrams. Components: - layout.yaml: Source of truth defining keymap layers and key positions - Eleventy website: Generates static HTML documentation - SVG diagrams: Visual layer representations via keymap-drawer - Symbol tables: Shows all ways to type each character, organized by category - Physical position notation: LPH, RTI+RMT format for describing key positions Features: - 8 layers documented: BASE, BASE_SHIFT, BASE_ALT, BASE_SHIFT_ALT, NAV, ALT_NAV, SYMBOL, SHIFT_SYMBOL - 180 unique symbols categorized across 10 groups (letters, numbers, punctuation, common, invisible, navigation, commands, special, non-printing, rare) - Light/dark mode theming with system preference detection - Real layer grouping: shows access methods per physical layer - Comprehensive symbol access methods: displays all key combinations Build pipeline: 1. scripts/generate-svgs.js: Generates SVG diagrams from layout.yaml 2. website/src/_data/keymaps.js: Parses layout.yaml and builds symbol index 3. Eleventy: Renders HTML with templates and symbol tables The documentation focuses on the user perspective (what can I type and how) rather than implementation details.
This commit is contained in:
parent
4fda78704a
commit
a5d434e44d
18 changed files with 3771 additions and 0 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -14,3 +14,6 @@
|
|||
compile_commands.json
|
||||
.clangd/
|
||||
.cache/
|
||||
|
||||
# Eleventy build output
|
||||
_site/
|
||||
|
|
|
|||
47
keyboards/ferris/sweep/keymaps/qwerty/layout.yaml
Normal file
47
keyboards/ferris/sweep/keymaps/qwerty/layout.yaml
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
layout:
|
||||
qmk_keyboard: ferris/sweep
|
||||
layout_name: LAYOUT_split_3x5_2
|
||||
layers:
|
||||
BASE:
|
||||
- [q, w, e, r, t, y, u, i, o, p]
|
||||
- [a, s, {h: Gui, t: d}, {h: Ctrl, t: f}, g, h, {h: Ctrl, t: j}, {h: Gui, t: k}, l, ";"]
|
||||
- [z, x, c, v, b, n, m, Repeat, ., /]
|
||||
- [{t: TO(NAV)}, {t: OSM Shift}, {t: OSM Alt}, Space]
|
||||
BASE_SHIFT:
|
||||
- [Q, W, E, R, T, Y, U, I, O, P]
|
||||
- [A, S, {h: Gui, t: D}, {h: Ctrl, t: F}, G, H, {h: Ctrl, t: J}, {h: Gui, t: K}, L, ":"]
|
||||
- [Z, X, C, V, B, N, M, "Alt Repeat", ",", "?"]
|
||||
- [{t: TO(NAV)}, {type: held}, {t: OSM Alt}, Space]
|
||||
BASE_ALT:
|
||||
- [Esc, "@", "#", "$", "%", "^", "&", "*", "-", BSpace]
|
||||
- [Tab, "`", "'", '"', null, "\\", "[", "|", "]", Enter]
|
||||
- ["~", "-", "+", "=", "_", null, "(", null, ")", "Del Word"]
|
||||
- [{t: TO(NAV)}, {t: OSM Shift}, {type: held}, Space]
|
||||
BASE_SHIFT_ALT:
|
||||
- [null, "€", null, "£", null, null, null, null, null, null]
|
||||
- [null, null, null, null, null, null, "{", "!", "}", null]
|
||||
- [null, "“", "‘", "’", "”", null, "<", null, ">", null]
|
||||
- [{t: TO(NAV)}, {type: held}, {type: held}, Space]
|
||||
NAV:
|
||||
- [Esc, "KP_7", "KP_8", "KP_9", null, "Cmd+[", "Ctrl+Shift+Tab", "Ctrl+Tab", "Cmd+]", BSpace]
|
||||
- [Tab, "KP_4", {h: Gui, t: "KP_5"}, {h: Ctrl, t: "KP_6"}, null, Left, {h: Ctrl, t: Down}, {h: Gui, t: Up}, Right, Enter]
|
||||
- ["KP_0", "KP_1", "KP_2", "KP_3", null, null, null, Repeat, ".", null]
|
||||
- [{t: TO(SYMBOL)}, Trans, Trans, {t: TO(BASE)}]
|
||||
ALT_NAV:
|
||||
- [Esc, F7, F8, F9, null, "Cmd+[", "Ctrl+Shift+Tab", "Ctrl+Tab", "Cmd+]", BSpace]
|
||||
- [Tab, F4, {h: Gui, t: F5}, {h: Ctrl, t: F6}, null, Left, {h: Ctrl, t: Down}, {h: Gui, t: Up}, Right, Enter]
|
||||
- [F10, F1, F2, F3, null, null, null, Repeat, ".", null]
|
||||
- [{t: TO(SYMBOL)}, Trans, {type: held}, {t: TO(BASE)}]
|
||||
SYMBOL:
|
||||
- ["œ", "∑", "´", "®", "†", "¥", "¨", "ˆ", "ø", "π"]
|
||||
- ["å", "ß", "∂", "ƒ", "©", "˙", "∆", "˚", "¬", "…"]
|
||||
- ["Ω", "≈", "ç", "√", "∫", "~", "µ", "≤", "≥", "÷"]
|
||||
- [{t: TO(BASE)}, Trans, Trans, {t: TO(BASE)}]
|
||||
SHIFT_SYMBOL:
|
||||
- ["Œ", "„", "‰", "Â", "Ê", "Á", "Ë", "ˆ", "Ø", "∏"]
|
||||
- ["Å", "Í", "Î", "Ï", "Ì", "Ó", "Ô", "", "Ò", "Ú"]
|
||||
- ["Û", "Ù", "Ç", "◊", "ı", "ˆ", "˜", "¯", "˘", "¿"]
|
||||
- [{t: TO(BASE)}, {type: held}, Trans, {t: TO(BASE)}]
|
||||
combos:
|
||||
- {p: [0, 1], k: "Esc+Tab combo", l: ["BASE"], a: top}
|
||||
- {p: [8, 9], k: "Special combo", l: ["BASE"], a: top}
|
||||
32
project/keymap-cheatsheets.md
Normal file
32
project/keymap-cheatsheets.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Keymap cheetsheets
|
||||
|
||||
Create a website containing visualisations of my keymaps. The website should be able to run locally with a webserver, and should also be able to be published to Github pages. The website should have an index page, containing links to each keymap, grouped by keyboard. Initially, there will only be a single keymap, that being qwerty for my Ferris Sweep. Later, I may add a variation for Colemak.
|
||||
|
||||
The webpage for a keymap should feature:
|
||||
|
||||
* SVG diagrams for each layer
|
||||
* index tables showing all symbols that can be typed, and where on the keyboard they can be found
|
||||
|
||||
Note that some symbols may appear multiple times.
|
||||
|
||||
## Creating SVG diagrams
|
||||
|
||||
The keymap-drawer app can create SVG diagrams from a layout.yaml file. This is a good starting point. We shall define one layout.yaml file for each keymap. There should be a build step that generates all SVG files. The layout.yaml source files should be co-located with the actual source code for the keymap itself. The generated SVG files should appear in the source directory for the website.
|
||||
|
||||
## Creating index tables
|
||||
|
||||
The index tables should be grouped by category. For example, we'll start with the most common symbols (lowercase letters, uppercase letters, punctuation, numbers), we'll also include non-printing characters (including function keys F1-F12, command+{symbol} combos, and so on), and finally we'll include the rare symbols, such as those accessed on os x with the alt key.
|
||||
|
||||
The index table should include several columns:
|
||||
|
||||
1. Showing the character (where possible)
|
||||
2. Describing the symbol (e.g. "Lowercase a", "Tab", "Function key 1")
|
||||
3. Layer 0: which keys to press to produce that symbol
|
||||
4. Layer 1: which keys to press to produce that symbol
|
||||
5. Layer 2: which keys to press to produce that symbol
|
||||
|
||||
The purpose of these index tables is to make it easy to look up a symbol/character, and figure out how to type it.
|
||||
|
||||
## Theming
|
||||
|
||||
The site should support both light mode and dark mode. By default it should follow the user's system preferences.
|
||||
160
project/keymap-notation.md
Normal file
160
project/keymap-notation.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# Keymap Notation System
|
||||
|
||||
## Overview
|
||||
|
||||
This notation system provides a clear, concise way to describe which physical keys to press on a split keyboard, avoiding tautological descriptions like "press the 'a' key to type 'a'".
|
||||
|
||||
The notation describes the **physical position** of keys using a coordinate system based on:
|
||||
- Which hand (left or right)
|
||||
- Which finger is used
|
||||
- Which row the key is on
|
||||
|
||||
## Coordinate Format
|
||||
|
||||
### Regular Keys (Non-Thumb)
|
||||
|
||||
Format: `[L|R][p|r|m|i|e][h|t|b]`
|
||||
|
||||
**Hand:**
|
||||
- `L` = Left hand
|
||||
- `R` = Right hand
|
||||
|
||||
**Finger:**
|
||||
- `p` = Pinky
|
||||
- `r` = Ring finger
|
||||
- `m` = Middle finger
|
||||
- `i` = Index finger
|
||||
- `e` = Empty column (index finger reach - the 5th column)
|
||||
|
||||
**Row:**
|
||||
- `h` = Home row
|
||||
- `t` = Top row
|
||||
- `b` = Bottom row
|
||||
|
||||
### Thumb Keys
|
||||
|
||||
Format: `[L|R]t[i|o]`
|
||||
|
||||
**Hand:**
|
||||
- `L` = Left hand
|
||||
- `R` = Right hand
|
||||
|
||||
**Finger:**
|
||||
- `t` = Thumb
|
||||
|
||||
**Position:**
|
||||
- `i` = Inside (thumb key closer to center/index finger)
|
||||
- `o` = Outside (thumb key further from center)
|
||||
|
||||
## Examples
|
||||
|
||||
### Single Key Press
|
||||
|
||||
| Symbol | Notation | Description |
|
||||
|--------|----------|-------------|
|
||||
| a | `LPH` | Left pinky, home row |
|
||||
| A | `LPH` (with shift) | Left pinky, home row + modifier |
|
||||
| s | `LRH` | Left ring, home row |
|
||||
| d | `LMH` | Left middle, home row |
|
||||
| f | `LIH` | Left index, home row |
|
||||
| g | `LEH` | Left index reach (empty column), home row |
|
||||
| q | `LPT` | Left pinky, top row |
|
||||
| z | `LPB` | Left pinky, bottom row |
|
||||
| ; | `RPH` | Right pinky, home row |
|
||||
| Space | `LTO` | Left thumb, outside |
|
||||
| Nav layer | `RTO` | Right thumb, outside (TO(NAV)) |
|
||||
|
||||
### Key Combinations (Modifiers + Key)
|
||||
|
||||
Format: `MODIFIER+KEY`
|
||||
|
||||
| Symbol | Notation | Description |
|
||||
|--------|----------|-------------|
|
||||
| * | `RTI+RMT` | Right thumb inside (Alt) + Right middle top (i key) |
|
||||
| @ | `RTI+LRT` | Right thumb inside (Alt) + Left ring top (w key) |
|
||||
| % | `RTI+RRT` | Right thumb inside (Alt) + Right ring top (t key) |
|
||||
| Backspace (Alt+P) | `RTI+RPT` | Right thumb inside (Alt) + Right pinky top |
|
||||
|
||||
## Physical Keyboard Layout (Ferris Sweep)
|
||||
|
||||
### Left Hand
|
||||
|
||||
```
|
||||
Row: Top (t) Home (h) Bottom (b)
|
||||
Pinky: q a z (p)
|
||||
Ring: w s x (r)
|
||||
Middle: e d c (m)
|
||||
Index: r f v (i)
|
||||
Reach: t g b (e)
|
||||
|
||||
Thumbs: Space (o) OSM Shift (i)
|
||||
```
|
||||
|
||||
### Right Hand
|
||||
|
||||
```
|
||||
Row: Top (t) Home (h) Bottom (b)
|
||||
Reach: y h n (e)
|
||||
Index: u j m (i)
|
||||
Middle: i k Repeat (m)
|
||||
Ring: o l . (r)
|
||||
Pinky: p ; / (p)
|
||||
|
||||
Thumbs: OSM Alt (i) TO(NAV) (o)
|
||||
```
|
||||
|
||||
## Layer Types
|
||||
|
||||
### Real Layers
|
||||
Accessed with layer-switch keys (e.g., `TO(NAV)`). No modifier keys need to be held.
|
||||
|
||||
- **Layer 0**: Base layer (default)
|
||||
- **Layer 1**: Nav layer (navigation and numbers)
|
||||
- **Layer 2**: Alt-symbol layer (future/TBD)
|
||||
|
||||
### Ghost Layers
|
||||
Virtual layers accessed by holding modifier keys. Not separate physical layers.
|
||||
|
||||
- **SHIFT**: Accessed by holding Shift modifier
|
||||
- **ALT**: Accessed by holding Alt modifier
|
||||
- **SHIFT_ALT**: Accessed by holding both Shift and Alt modifiers
|
||||
|
||||
## Usage in Documentation
|
||||
|
||||
When documenting symbol access methods in the index tables:
|
||||
|
||||
1. **Symbol column**: The character itself (e.g., `a`, `*`, `@`)
|
||||
2. **Description column**: Human-readable name (e.g., "Lowercase a", "Asterisk", "At sign")
|
||||
3. **Layer columns**: One column per **real layer** (Layer 0, Layer 1, Layer 2)
|
||||
- Shows all ways to access the symbol from that layer
|
||||
- Includes both direct key presses and modifier combinations
|
||||
- Uses coordinate notation for clarity
|
||||
|
||||
### Example Table Entries
|
||||
|
||||
| Symbol | Description | Layer 0 (Base) | Layer 1 (Nav) | Layer 2 (Alt-Sym) |
|
||||
|--------|-------------|----------------|---------------|-------------------|
|
||||
| a | Lowercase a | `LPH` | — | — |
|
||||
| A | Uppercase A | `LTI+LPH` | — | — |
|
||||
| * | Asterisk | `RTI+RMT` | — | — |
|
||||
| Space | Space | `LTO` | — | — |
|
||||
| ↵ | Enter | `RTI+RPH` | `RPH` | — |
|
||||
|
||||
## Notes
|
||||
|
||||
- This notation describes **physical key positions**, not the symbols printed on keycaps
|
||||
- The same physical key may produce different symbols depending on:
|
||||
- The active layer
|
||||
- Which modifiers are held
|
||||
- Modifier combinations are shown with `+` (e.g., `LTI+LPH` = left thumb inside + left pinky home)
|
||||
- The notation is independent of the keymap layout (QWERTY, Colemak, etc.)
|
||||
- `—` indicates the symbol is not accessible from that layer
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Language-independent**: Describes physical positions, not key labels
|
||||
2. **Layout-agnostic**: Works with any keymap (QWERTY, Colemak, Dvorak, etc.)
|
||||
3. **Muscle memory focused**: Emphasizes finger positions rather than abstract key names
|
||||
4. **Concise**: Short notation (3-4 characters per key)
|
||||
5. **Unambiguous**: Each physical key has exactly one notation
|
||||
6. **Avoids tautology**: Describes *how* to type a symbol, not just "press the X key"
|
||||
255
project/website-plan.md
Normal file
255
project/website-plan.md
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
Key Changes:
|
||||
1. ✅ Directory name: Changed from docs/ to website/ throughout
|
||||
2. ✅ Documentation philosophy: layout.yaml is the SOURCE OF TRUTH - shows user perspective, not implementation details
|
||||
3. ✅ Symbol tables: Will show ALL methods comprehensively from user perspective
|
||||
4. ✅ Implementation approach: Incremental with checkpoints after each milestone
|
||||
5. ✅ Categories: Using the proposed categories (letters, numbers, punctuation, common, non-printing, rare)
|
||||
6. ✅ Removed: No parsing of keymap.c - documentation driven purely by layout.yaml
|
||||
|
||||
The plan is ready for implementation. We'll proceed incrementally:
|
||||
- Milestone 1: SVG generation → CHECKPOINT
|
||||
- Milestone 2: Data processing → CHECKPOINT
|
||||
- Milestone 3: Templates & symbol tables → CHECKPOINT
|
||||
- Milestone 4: Theming → CHECKPOINT
|
||||
- Milestone 5: GitHub Actions deployment → CHECKPOINT
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Ready to code?
|
||||
|
||||
Here is Claude's plan:
|
||||
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
|
||||
Eleventy Documentation Site for QMK Keymaps
|
||||
|
||||
Overview
|
||||
|
||||
Build a static documentation website using Eleventy to visualize QMK keymaps with:
|
||||
- SVG layer diagrams (generated by keymap-drawer from layout.yaml)
|
||||
- Symbol index tables (showing which key combinations produce each symbol)
|
||||
- Light/dark mode theming
|
||||
- Local development server
|
||||
- GitHub Pages deployment
|
||||
|
||||
Repository Structure
|
||||
|
||||
qmk_userspace/
|
||||
├── keyboards/ferris/sweep/keymaps/qwerty/
|
||||
│ ├── keymap.c # Existing - implementation
|
||||
│ └── layout.yaml # Existing - SOURCE OF TRUTH (5 layers)
|
||||
├── website/ # NEW - Eleventy project root
|
||||
│ ├── .eleventy.js # Configuration
|
||||
│ ├── package.json # Dependencies: @11ty/eleventy, js-yaml
|
||||
│ ├── src/
|
||||
│ │ ├── _data/
|
||||
│ │ │ ├── keymaps.js # Parses layout.yaml (user perspective)
|
||||
│ │ │ └── symbolMapping.json # Keycode → description mapping
|
||||
│ │ ├── _includes/
|
||||
│ │ │ ├── layouts/base.njk # HTML template
|
||||
│ │ │ ├── layouts/keymap.njk # Per-keymap page
|
||||
│ │ │ └── partials/symbol-table.njk
|
||||
│ │ └── index.njk # Landing page
|
||||
│ ├── public/
|
||||
│ │ ├── css/theme.css # Light/dark mode variables
|
||||
│ │ └── generated/ # SVGs from keymap-drawer
|
||||
│ └── _site/ # Build output (gitignored)
|
||||
├── scripts/ # NEW - Build orchestration
|
||||
│ ├── generate-svgs.js # Runs keymap-drawer
|
||||
│ └── discover-keymaps.js # Finds all layout.yaml files
|
||||
└── .github/workflows/
|
||||
└── build_website.yaml # NEW - CI/CD for website
|
||||
|
||||
Build Pipeline
|
||||
|
||||
Phase 1: SVG Generation (Node.js → Python)
|
||||
|
||||
1. scripts/generate-svgs.js finds all layout.yaml files in keyboards/
|
||||
2. For each file, runs: keymap-drawer parse -c <layout.yaml> -o website/public/generated/
|
||||
3. Generates SVG for each layer (BASE, SHIFT, ALT, SHIFT_ALT, NAV)
|
||||
|
||||
Phase 2: Data Processing (Eleventy)
|
||||
|
||||
1. src/_data/keymaps.js parses all layout.yaml files using js-yaml
|
||||
2. Source of Truth: layout.yaml defines the complete keymap from user perspective
|
||||
3. Builds symbol accessibility map showing ALL ways to type each symbol
|
||||
4. Returns array of keymap objects for template use
|
||||
|
||||
Phase 3: Site Build (Eleventy)
|
||||
|
||||
1. Index page lists all keyboards/keymaps
|
||||
2. Keymap pages show layer diagrams + symbol tables
|
||||
3. Templates use symbolMapping.json to convert keycodes to descriptions
|
||||
|
||||
Key Technical Decisions
|
||||
|
||||
Documentation Philosophy
|
||||
|
||||
User Perspective, Not Implementation:
|
||||
- layout.yaml is the SOURCE OF TRUTH defining how the keymap works
|
||||
- Show ALL ways to type each symbol (comprehensive symbol tables)
|
||||
- Hide implementation details (don't reference keymap.c internals)
|
||||
- Focus on "what can I type and how" rather than "how is this coded"
|
||||
|
||||
Symbol Mapping Strategy
|
||||
|
||||
Approach: Manual JSON file mapping QMK keycodes to metadata
|
||||
- Maps KC_A → {symbol: "a", description: "Lowercase a", category: "letters"}
|
||||
- Categories: letters, numbers, punctuation, common, non-printing, rare
|
||||
- Fallback heuristics for unmapped codes
|
||||
|
||||
Why: QMK has 500+ keycodes; manual curation ensures accuracy for commonly used ones
|
||||
|
||||
Data Source
|
||||
|
||||
layout.yaml as Single Source:
|
||||
- Parse layout.yaml to extract all layers and key definitions
|
||||
- Show all symbol access methods defined in the YAML
|
||||
- Symbol tables comprehensive: show every way to type each character
|
||||
- NO parsing of keymap.c (implementation detail)
|
||||
|
||||
Theming Implementation
|
||||
|
||||
Approach: CSS custom properties with prefers-color-scheme media query
|
||||
:root { --color-bg: #ffffff; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --color-bg: #1a1a1a; }
|
||||
}
|
||||
SVG theming: Override CSS classes in generated SVGs or use CSS filters
|
||||
|
||||
Implementation Sequence (Incremental with Checkpoints)
|
||||
|
||||
Milestone 1: Foundation & SVG Generation
|
||||
|
||||
Goal: Generate visual layer diagrams
|
||||
|
||||
1. Create website/ directory structure
|
||||
2. Initialize package.json with Eleventy + js-yaml
|
||||
3. Create basic .eleventy.js config
|
||||
4. Implement scripts/generate-svgs.js
|
||||
5. Install keymap-drawer (Python: pip install keymap-drawer)
|
||||
6. Generate SVGs for existing qwerty keymap
|
||||
|
||||
Output: SVG files in website/public/generated/ferris-sweep-qwerty/
|
||||
CHECKPOINT: Review SVG diagrams before proceeding
|
||||
|
||||
Milestone 2: Data Processing
|
||||
|
||||
Goal: Parse layout.yaml into structured data
|
||||
|
||||
1. Create symbolMapping.json with initial 50-100 keycodes
|
||||
2. Implement scripts/discover-keymaps.js to find all layout.yaml files
|
||||
3. Implement src/_data/keymaps.js to parse layout.yaml
|
||||
4. Build data structure: array of keymaps with layers and accessible symbols
|
||||
|
||||
Output: Data available to templates via keymaps global
|
||||
CHECKPOINT: Review data structure and symbol extraction
|
||||
|
||||
Milestone 3: Templates & Symbol Tables
|
||||
|
||||
Goal: Create web pages with comprehensive symbol tables
|
||||
|
||||
1. Create base.njk HTML layout
|
||||
2. Create index.njk landing page (list keyboards)
|
||||
3. Create keymap.njk page (embed SVGs + symbol tables)
|
||||
4. Implement symbol-table.njk partial with category grouping
|
||||
5. Add filters: formatKeyCombination, keycodeDesc
|
||||
|
||||
Output: Functional site with diagrams and comprehensive symbol tables
|
||||
CHECKPOINT: Review symbol table accuracy and completeness
|
||||
|
||||
Milestone 4: Theming
|
||||
|
||||
Goal: Light/dark mode support
|
||||
|
||||
1. Create public/css/theme.css with light/dark variables
|
||||
2. Style base layout, tables, and typography
|
||||
3. Test SVG rendering in both modes
|
||||
4. Implement SVG theming (CSS classes or filters)
|
||||
|
||||
Output: Working light/dark mode with system preference detection
|
||||
CHECKPOINT: Review visual design in both themes
|
||||
|
||||
Milestone 5: GitHub Actions & Deployment
|
||||
|
||||
Goal: Automated publishing to GitHub Pages
|
||||
|
||||
1. Create .github/workflows/build_website.yaml
|
||||
2. Configure workflow to install Python + Node.js
|
||||
3. Run SVG generation → Eleventy build → deploy to Pages
|
||||
4. Create .nojekyll file in root
|
||||
5. Enable GitHub Pages in repo settings
|
||||
|
||||
Output: Live website automatically deployed on push
|
||||
CHECKPOINT: Verify live deployment works
|
||||
|
||||
Critical Files
|
||||
|
||||
Existing (to read):
|
||||
- keyboards/ferris/sweep/keymaps/qwerty/layout.yaml - Source data
|
||||
- keyboards/ferris/sweep/keymaps/qwerty/keymap.c - Key override definitions
|
||||
|
||||
To create:
|
||||
- website/.eleventy.js - Core configuration
|
||||
- website/package.json - Dependencies
|
||||
- website/src/_data/keymaps.js - Data processing pipeline (layout.yaml only)
|
||||
- website/src/_data/symbolMapping.json - Keycode mappings
|
||||
- scripts/generate-svgs.js - SVG generation orchestration
|
||||
- scripts/discover-keymaps.js - Find all layout.yaml files
|
||||
- .github/workflows/build_website.yaml - CI/CD pipeline
|
||||
|
||||
Local Development Workflow
|
||||
|
||||
# First-time setup
|
||||
cd website
|
||||
npm install
|
||||
pip install keymap-drawer # or: pipx install keymap-drawer
|
||||
|
||||
# Daily development
|
||||
npm run dev # Runs prebuild (SVG gen) + Eleventy dev server
|
||||
# Open http://localhost:8080
|
||||
|
||||
# Production build
|
||||
npm run build
|
||||
|
||||
Note: Implementation will be incremental with checkpoints after each milestone for review.
|
||||
|
||||
Challenges & Solutions
|
||||
|
||||
Challenge 1: Complex Keycode Syntax
|
||||
- layout.yaml uses {h: Gui, t: d}, OSM Shift, TO(NAV), custom labels
|
||||
- Solution: Parser function handles each syntax type differently
|
||||
|
||||
Challenge 2: Multiple Symbol Access Methods
|
||||
- Some symbols accessible via multiple layers or modifier combinations
|
||||
- Solution: Show all methods in table - comprehensive from user perspective
|
||||
|
||||
Challenge 3: Symbol Categorization
|
||||
- Need to group 200+ symbols into meaningful categories
|
||||
- Solution: Manual curation (letters/numbers/punctuation/common/non-printing/rare) with fallback heuristics
|
||||
|
||||
Challenge 4: Keeping layout.yaml as Source of Truth
|
||||
- Must ensure layout.yaml is complete and doesn't require parsing keymap.c
|
||||
- Solution: Document only what's in layout.yaml; update YAML if needed to be comprehensive
|
||||
|
||||
Success Criteria
|
||||
|
||||
Implementation complete when:
|
||||
1. ✓ SVG diagrams display for all 5 layers
|
||||
2. ✓ Symbol table shows 100+ categorized symbols
|
||||
3. ✓ "Which keys to press" is accurate (spot-check 20 symbols)
|
||||
4. ✓ Light/dark mode works via system preference
|
||||
5. ✓ Local dev server runs with hot reload
|
||||
6. ✓ GitHub Actions deploys to Pages automatically
|
||||
7. ✓ Documentation includes setup instructions
|
||||
|
||||
Dependencies
|
||||
|
||||
Node.js (package.json):
|
||||
- @11ty/eleventy - Static site generator
|
||||
- js-yaml - YAML parsing
|
||||
|
||||
Python (pip/pipx):
|
||||
- keymap-drawer - SVG generation tool
|
||||
|
||||
GitHub Actions:
|
||||
- Python 3.12+ for keymap-drawer
|
||||
- Node.js 20 for Eleventy
|
||||
- GitHub Pages enabled in repo settings
|
||||
88
scripts/discover-keymaps.js
Normal file
88
scripts/discover-keymaps.js
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Recursively find all layout.yaml files in a directory
|
||||
*/
|
||||
function findLayoutYamls(dir) {
|
||||
const results = [];
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...findLayoutYamls(fullPath));
|
||||
} else if (entry.name === 'layout.yaml' || entry.name === 'keymap.yaml') {
|
||||
results.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract keyboard/keymap identifier from path
|
||||
* Example: keyboards/ferris/sweep/keymaps/qwerty/layout.yaml
|
||||
* Returns: { id: 'ferris-sweep-qwerty', keyboard: 'ferris/sweep', keymap: 'qwerty' }
|
||||
*/
|
||||
function parseKeymapPath(layoutPath) {
|
||||
const parts = layoutPath.split(path.sep);
|
||||
const keyboardsIdx = parts.indexOf('keyboards');
|
||||
|
||||
if (keyboardsIdx === -1) {
|
||||
throw new Error(`Invalid path: ${layoutPath} (no 'keyboards' directory found)`);
|
||||
}
|
||||
|
||||
// Find 'keymaps' directory
|
||||
const keymapsIdx = parts.indexOf('keymaps');
|
||||
if (keymapsIdx === -1) {
|
||||
throw new Error(`Invalid path: ${layoutPath} (no 'keymaps' directory found)`);
|
||||
}
|
||||
|
||||
// Extract keyboard parts (between keyboards/ and keymaps/)
|
||||
const keyboardParts = parts.slice(keyboardsIdx + 1, keymapsIdx);
|
||||
const keyboard = keyboardParts.join('/');
|
||||
|
||||
// Extract keymap name
|
||||
const keymapName = parts[keymapsIdx + 1];
|
||||
|
||||
// Create ID: keyboard-keymap (with dashes)
|
||||
const id = [...keyboardParts, keymapName].join('-');
|
||||
|
||||
return {
|
||||
id,
|
||||
keyboard,
|
||||
keymap: keymapName,
|
||||
path: layoutPath
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover all keymaps in the keyboards directory
|
||||
*/
|
||||
function discoverKeymaps(keyboardsDir) {
|
||||
const layoutFiles = findLayoutYamls(keyboardsDir);
|
||||
return layoutFiles.map(parseKeymapPath);
|
||||
}
|
||||
|
||||
// Run if executed directly
|
||||
if (require.main === module) {
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
const keyboardsDir = path.join(rootDir, 'keyboards');
|
||||
|
||||
console.log('Discovering keymaps...\n');
|
||||
const keymaps = discoverKeymaps(keyboardsDir);
|
||||
|
||||
console.log(`Found ${keymaps.length} keymap(s):\n`);
|
||||
keymaps.forEach(km => {
|
||||
console.log(` ${km.id}`);
|
||||
console.log(` Keyboard: ${km.keyboard}`);
|
||||
console.log(` Keymap: ${km.keymap}`);
|
||||
console.log(` Path: ${km.path}\n`);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { findLayoutYamls, parseKeymapPath, discoverKeymaps };
|
||||
188
scripts/generate-svgs.js
Normal file
188
scripts/generate-svgs.js
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Try to require js-yaml from website/node_modules when run from website/
|
||||
let yaml;
|
||||
try {
|
||||
yaml = require('js-yaml');
|
||||
} catch (error) {
|
||||
// If not found, try from website/node_modules (when run from root)
|
||||
const yamlPath = path.join(__dirname, '..', 'website', 'node_modules', 'js-yaml');
|
||||
yaml = require(yamlPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find all layout.yaml files in keyboards/ directory
|
||||
*/
|
||||
function findLayoutYamls(dir) {
|
||||
const results = [];
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...findLayoutYamls(fullPath));
|
||||
} else if (entry.name === 'layout.yaml' || entry.name === 'keymap.yaml') {
|
||||
results.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract keyboard/keymap identifier from path
|
||||
* Example: keyboards/ferris/sweep/keymaps/qwerty/layout.yaml
|
||||
* Returns: ferris-sweep-qwerty
|
||||
*/
|
||||
function extractKeymapId(layoutPath) {
|
||||
const parts = layoutPath.split(path.sep);
|
||||
const keyboardsIdx = parts.indexOf('keyboards');
|
||||
|
||||
if (keyboardsIdx === -1) {
|
||||
throw new Error(`Invalid path: ${layoutPath} (no 'keyboards' directory found)`);
|
||||
}
|
||||
|
||||
// Find 'keymaps' directory
|
||||
const keymapsIdx = parts.indexOf('keymaps');
|
||||
if (keymapsIdx === -1) {
|
||||
throw new Error(`Invalid path: ${layoutPath} (no 'keymaps' directory found)`);
|
||||
}
|
||||
|
||||
// Extract keyboard parts (between keyboards/ and keymaps/)
|
||||
const keyboardParts = parts.slice(keyboardsIdx + 1, keymapsIdx);
|
||||
|
||||
// Extract keymap name
|
||||
const keymapName = parts[keymapsIdx + 1];
|
||||
|
||||
// Create ID: keyboard-keymap
|
||||
return [...keyboardParts, keymapName].join('-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract layer names from layout.yaml
|
||||
*/
|
||||
function getLayerNames(layoutPath) {
|
||||
try {
|
||||
const fileContents = fs.readFileSync(layoutPath, 'utf8');
|
||||
const data = yaml.load(fileContents);
|
||||
|
||||
if (!data.layers) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.keys(data.layers);
|
||||
} catch (error) {
|
||||
console.error(`Error parsing ${layoutPath}: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SVGs for a layout.yaml file using keymap-drawer
|
||||
*/
|
||||
function generateSvgs(layoutPath, outputDir) {
|
||||
const keymapId = extractKeymapId(layoutPath);
|
||||
const svgOutputDir = path.join(outputDir, keymapId);
|
||||
|
||||
// Create output directory
|
||||
if (!fs.existsSync(svgOutputDir)) {
|
||||
fs.mkdirSync(svgOutputDir, { recursive: true });
|
||||
}
|
||||
|
||||
console.log(`Generating SVGs for: ${keymapId}`);
|
||||
console.log(` Input: ${layoutPath}`);
|
||||
console.log(` Output: ${svgOutputDir}`);
|
||||
|
||||
// Get layer names from layout.yaml
|
||||
const layerNames = getLayerNames(layoutPath);
|
||||
|
||||
if (layerNames.length === 0) {
|
||||
console.error(` ✗ No layers found in ${layoutPath}\n`);
|
||||
throw new Error('No layers found');
|
||||
}
|
||||
|
||||
console.log(` Layers: ${layerNames.join(', ')}`);
|
||||
|
||||
// Generate SVG for each layer
|
||||
let generatedCount = 0;
|
||||
for (const layerName of layerNames) {
|
||||
const outputFile = path.join(svgOutputDir, `${layerName.toLowerCase()}.svg`);
|
||||
|
||||
try {
|
||||
// Run keymap draw command for this layer
|
||||
const cmd = `keymap draw "${layoutPath}" -s ${layerName} -o "${outputFile}"`;
|
||||
execSync(cmd, { stdio: 'pipe' });
|
||||
generatedCount++;
|
||||
} catch (error) {
|
||||
console.error(` ✗ Error generating ${layerName}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(` ✓ Generated ${generatedCount} SVG(s) successfully\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
function main() {
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
const keyboardsDir = path.join(rootDir, 'keyboards');
|
||||
const outputDir = path.join(rootDir, 'website', 'public', 'generated');
|
||||
|
||||
console.log('=== Keymap SVG Generator ===\n');
|
||||
|
||||
// Check if keymap is installed (from keymap-drawer package)
|
||||
try {
|
||||
execSync('keymap --version', { stdio: 'pipe' });
|
||||
} catch (error) {
|
||||
console.error('ERROR: keymap (from keymap-drawer) is not installed!');
|
||||
console.error('Install it with: pip install keymap-drawer');
|
||||
console.error('Or with pipx: pipx install keymap-drawer\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Find all layout.yaml files
|
||||
console.log(`Searching for layout.yaml files in: ${keyboardsDir}\n`);
|
||||
const layoutFiles = findLayoutYamls(keyboardsDir);
|
||||
|
||||
if (layoutFiles.length === 0) {
|
||||
console.log('No layout.yaml files found.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${layoutFiles.length} layout file(s):\n`);
|
||||
|
||||
// Generate SVGs for each layout
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const layoutPath of layoutFiles) {
|
||||
try {
|
||||
generateSvgs(layoutPath, outputDir);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('=== Summary ===');
|
||||
console.log(`✓ Success: ${successCount}`);
|
||||
console.log(`✗ Errors: ${errorCount}`);
|
||||
|
||||
if (errorCount > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if executed directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { findLayoutYamls, extractKeymapId, generateSvgs };
|
||||
69
website/.eleventy.js
Normal file
69
website/.eleventy.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
module.exports = function(eleventyConfig) {
|
||||
// Pass through static assets
|
||||
eleventyConfig.addPassthroughCopy("public");
|
||||
|
||||
// Watch for changes in keyboard configuration files during dev
|
||||
eleventyConfig.addWatchTarget("../keyboards/**/*.yaml");
|
||||
|
||||
// Custom filter: Get keycode description from symbol mapping
|
||||
eleventyConfig.addFilter("keycodeDesc", function(keycode) {
|
||||
const mapping = this.ctx.symbolMapping || {};
|
||||
return mapping[keycode] || {
|
||||
symbol: null,
|
||||
description: keycode,
|
||||
category: 'unknown'
|
||||
};
|
||||
});
|
||||
|
||||
// Custom filter: Find item in array by property
|
||||
eleventyConfig.addFilter("find", function(array, property, value) {
|
||||
if (!Array.isArray(array)) return null;
|
||||
return array.find(item => item[property] === value);
|
||||
});
|
||||
|
||||
// Custom filter: Format key combination for display
|
||||
eleventyConfig.addFilter("formatKeyCombination", function(combo) {
|
||||
if (!combo) return '—';
|
||||
|
||||
let parts = [];
|
||||
|
||||
// Add modifiers
|
||||
if (combo.mods && combo.mods.length > 0) {
|
||||
parts.push(...combo.mods);
|
||||
}
|
||||
|
||||
// Add base key
|
||||
if (combo.baseKey) {
|
||||
parts.push(combo.baseKey);
|
||||
}
|
||||
|
||||
// Handle special cases
|
||||
if (combo.modTapHold) {
|
||||
return `Hold ${combo.modTapHold}${combo.withKey ? ' + ' + combo.withKey : ''}`;
|
||||
}
|
||||
|
||||
if (combo.layerSwitch) {
|
||||
return `Layer ${combo.layerSwitch}${combo.baseKey ? ' → ' + combo.baseKey : ''}`;
|
||||
}
|
||||
|
||||
return parts.join(' + ') || '—';
|
||||
});
|
||||
|
||||
// Custom filter: Replace text
|
||||
eleventyConfig.addFilter("replace", function(str, search, replace) {
|
||||
if (typeof str !== 'string') return str;
|
||||
return str.replace(new RegExp(search, 'g'), replace);
|
||||
});
|
||||
|
||||
return {
|
||||
dir: {
|
||||
input: "src",
|
||||
output: "_site",
|
||||
includes: "_includes",
|
||||
data: "_data"
|
||||
},
|
||||
templateFormats: ["njk", "md", "html"],
|
||||
htmlTemplateEngine: "njk",
|
||||
markdownTemplateEngine: "njk"
|
||||
};
|
||||
};
|
||||
16
website/.gitignore
vendored
Normal file
16
website/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Build output
|
||||
_site/
|
||||
|
||||
# Generated SVG files (built from layout.yaml via scripts/generate-svgs.js)
|
||||
public/generated/
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
1713
website/package-lock.json
generated
Normal file
1713
website/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
18
website/package.json
Normal file
18
website/package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "qmk-keymap-docs",
|
||||
"version": "1.0.0",
|
||||
"description": "Documentation website for QMK keymaps",
|
||||
"scripts": {
|
||||
"prebuild": "node ../scripts/generate-svgs.js",
|
||||
"build": "eleventy",
|
||||
"dev": "npm run prebuild && eleventy --serve",
|
||||
"clean": "rm -rf _site public/generated/*"
|
||||
},
|
||||
"keywords": ["qmk", "keyboard", "keymap", "documentation"],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@11ty/eleventy": "^3.1.0",
|
||||
"js-yaml": "^4.1.0"
|
||||
}
|
||||
}
|
||||
103
website/public/css/theme.css
Normal file
103
website/public/css/theme.css
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/**
|
||||
* QMK Keymap Documentation Theme
|
||||
* Light/Dark mode support using CSS custom properties
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Light mode colors (default) */
|
||||
--color-bg: #ffffff;
|
||||
--color-text: #1a1a1a;
|
||||
--color-heading: #2c3e50;
|
||||
--color-muted: #6c757d;
|
||||
--color-border: #dee2e6;
|
||||
|
||||
/* Links and accents */
|
||||
--color-link: #007bff;
|
||||
--color-link-hover: #0056b3;
|
||||
--color-accent: #007bff;
|
||||
|
||||
/* Buttons */
|
||||
--color-primary: #007bff;
|
||||
--color-primary-hover: #0056b3;
|
||||
--color-secondary: #6c757d;
|
||||
--color-secondary-hover: #5a6268;
|
||||
|
||||
/* Cards and surfaces */
|
||||
--color-card-bg: #f8f9fa;
|
||||
--color-layer-bg: #ffffff;
|
||||
|
||||
/* Tables */
|
||||
--color-table-bg: #ffffff;
|
||||
--color-table-header-bg: #e9ecef;
|
||||
--color-table-border: #dee2e6;
|
||||
--color-table-row-hover: #f8f9fa;
|
||||
|
||||
/* Code blocks */
|
||||
--color-code-bg: #f8f9fa;
|
||||
--color-code-text: #e83e8c;
|
||||
|
||||
/* SVG diagram colors */
|
||||
--svg-filter: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
/* Dark mode colors */
|
||||
--color-bg: #1a1a1a;
|
||||
--color-text: #e9ecef;
|
||||
--color-heading: #f8f9fa;
|
||||
--color-muted: #adb5bd;
|
||||
--color-border: #495057;
|
||||
|
||||
/* Links and accents */
|
||||
--color-link: #66b3ff;
|
||||
--color-link-hover: #99ccff;
|
||||
--color-accent: #66b3ff;
|
||||
|
||||
/* Buttons */
|
||||
--color-primary: #0d6efd;
|
||||
--color-primary-hover: #0a58ca;
|
||||
--color-secondary: #6c757d;
|
||||
--color-secondary-hover: #5a6268;
|
||||
|
||||
/* Cards and surfaces */
|
||||
--color-card-bg: #212529;
|
||||
--color-layer-bg: #2b3035;
|
||||
|
||||
/* Tables */
|
||||
--color-table-bg: #212529;
|
||||
--color-table-header-bg: #343a40;
|
||||
--color-table-border: #495057;
|
||||
--color-table-row-hover: #2c3136;
|
||||
|
||||
/* Code blocks */
|
||||
--color-code-bg: #2d3238;
|
||||
--color-code-text: #ff6b9d;
|
||||
|
||||
/* SVG diagram theming - invert colors for dark mode */
|
||||
--svg-filter: invert(0.9) hue-rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Force dark mode for testing - uncomment to test */
|
||||
/*
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
*/
|
||||
|
||||
/* SVG theming */
|
||||
svg,
|
||||
object[type="image/svg+xml"] {
|
||||
filter: var(--svg-filter);
|
||||
}
|
||||
|
||||
/* Ensure smooth transitions when switching themes */
|
||||
* {
|
||||
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Prevent transition on page load */
|
||||
.preload * {
|
||||
transition: none !important;
|
||||
}
|
||||
590
website/src/_data/keymaps.js
Normal file
590
website/src/_data/keymaps.js
Normal file
|
|
@ -0,0 +1,590 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
// Import discovery utilities
|
||||
const discoverPath = path.join(__dirname, '../../../scripts/discover-keymaps.js');
|
||||
const { discoverKeymaps } = require(discoverPath);
|
||||
|
||||
// Load symbol mapping
|
||||
const symbolMapping = require('./symbolMapping.json');
|
||||
|
||||
// Position-to-coordinate mapping for LAYOUT_split_3x5_2 (Ferris Sweep)
|
||||
// Layout is stored as rows: [L L L L L R R R R R] × 3 rows + [thumb thumb thumb thumb]
|
||||
// 34 keys total: 3 rows × 10 keys (5L + 5R) + 4 thumb keys (2L + 2R)
|
||||
const POSITION_TO_COORDINATE = [
|
||||
// Row 1 (top): positions 0-9
|
||||
'LPT', 'LRT', 'LMT', 'LIT', 'LET', // Left hand (0-4): q w e r t
|
||||
'RET', 'RIT', 'RMT', 'RRT', 'RPT', // Right hand (5-9): y u i o p
|
||||
|
||||
// Row 2 (home): positions 10-19
|
||||
'LPH', 'LRH', 'LMH', 'LIH', 'LEH', // Left hand (10-14): a s d f g
|
||||
'REH', 'RIH', 'RMH', 'RRH', 'RPH', // Right hand (15-19): h j k l ;
|
||||
|
||||
// Row 3 (bottom): positions 20-29
|
||||
'LPB', 'LRB', 'LMB', 'LIB', 'LEB', // Left hand (20-24): z x c v b
|
||||
'REB', 'RIB', 'RMB', 'RRB', 'RPB', // Right hand (25-29): n m Repeat . /
|
||||
|
||||
// Thumbs: positions 30-33
|
||||
'LTO', 'LTI', // Left thumbs (30-31): TO(NAV), OSM Shift
|
||||
'RTI', 'RTO' // Right thumbs (32-33): OSM Alt, Space
|
||||
];
|
||||
|
||||
// Layer type classification
|
||||
const LAYER_TYPES = {
|
||||
real: ['BASE', 'NAV', 'ALT_NAV', 'SYMBOL', 'SHIFT_SYMBOL'], // Physical layers accessed via layer-switch keys
|
||||
ghost: ['BASE_SHIFT', 'BASE_ALT', 'BASE_SHIFT_ALT'] // Virtual layers accessed via modifiers
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a key definition from layout.yaml
|
||||
* Handles: simple keys, labeled keys, mod-tap, one-shot mods, layer switches
|
||||
*/
|
||||
function parseKeyDefinition(keyDef) {
|
||||
if (keyDef === null) {
|
||||
return { type: 'empty', keycode: null, label: '—' };
|
||||
}
|
||||
|
||||
// Handle string definitions
|
||||
if (typeof keyDef === 'string') {
|
||||
// Custom label (quoted in YAML)
|
||||
if (keyDef.includes('+') || keyDef.includes(' ')) {
|
||||
return { type: 'labeled', keycode: null, label: keyDef };
|
||||
}
|
||||
|
||||
// Map common key names to keycodes (check this first, before pattern matching)
|
||||
const keycodeMap = {
|
||||
'Space': 'KC_SPACE',
|
||||
'Esc': 'KC_ESC',
|
||||
'Tab': 'KC_TAB',
|
||||
'Enter': 'KC_ENTER',
|
||||
'BSpace': 'KC_BSPC',
|
||||
'Repeat': 'QK_REPEAT_KEY',
|
||||
'Trans': 'KC_TRNS',
|
||||
'Left': 'KC_LEFT',
|
||||
'Right': 'KC_RIGHT',
|
||||
'Up': 'KC_UP',
|
||||
'Down': 'KC_DOWN'
|
||||
};
|
||||
|
||||
if (keycodeMap[keyDef]) {
|
||||
return { type: 'keycode', keycode: keycodeMap[keyDef], label: keyDef };
|
||||
}
|
||||
|
||||
// Check if it's a known keycode pattern (all uppercase)
|
||||
if (keyDef.match(/^[A-Z_]+$/)) {
|
||||
const mappedKeycode = `KC_${keyDef.toUpperCase()}`;
|
||||
return { type: 'keycode', keycode: mappedKeycode, label: keyDef };
|
||||
}
|
||||
|
||||
// Single character
|
||||
if (keyDef.length === 1) {
|
||||
const keycode = `KC_${keyDef.toUpperCase()}`;
|
||||
return { type: 'char', keycode, label: keyDef };
|
||||
}
|
||||
|
||||
// Fallback: treat as label
|
||||
return { type: 'labeled', keycode: null, label: keyDef };
|
||||
}
|
||||
|
||||
// Handle object definitions (mod-tap, one-shot, layer switches, held keys)
|
||||
if (typeof keyDef === 'object') {
|
||||
// Check for "held" type (modifier keys that enable the current layer)
|
||||
if (keyDef.type === 'held') {
|
||||
return {
|
||||
type: 'held',
|
||||
keycode: null,
|
||||
label: '(held)'
|
||||
};
|
||||
}
|
||||
|
||||
// Handle tap behavior (t: value)
|
||||
if (keyDef.t !== undefined) {
|
||||
const tapValue = keyDef.t;
|
||||
|
||||
// Mod-tap with hold modifier
|
||||
if (keyDef.h !== undefined) {
|
||||
const holdMod = keyDef.h;
|
||||
const tapKey = parseKeyDefinition(tapValue);
|
||||
return {
|
||||
type: 'mod-tap',
|
||||
holdMod,
|
||||
tapKey,
|
||||
label: `${tapValue} (${holdMod} hold)`
|
||||
};
|
||||
}
|
||||
|
||||
// One-shot modifier or layer switch (just tap behavior)
|
||||
if (tapValue.startsWith('OSM ')) {
|
||||
const modifier = tapValue.substring(4);
|
||||
return {
|
||||
type: 'one-shot-mod',
|
||||
modifier,
|
||||
keycode: `OSM(MOD_L${modifier.toUpperCase()})`,
|
||||
label: `OSM ${modifier}`
|
||||
};
|
||||
}
|
||||
|
||||
if (tapValue.startsWith('TO(')) {
|
||||
const layerName = tapValue.substring(3, tapValue.length - 1);
|
||||
return {
|
||||
type: 'layer-switch',
|
||||
targetLayer: layerName,
|
||||
keycode: tapValue,
|
||||
label: `→ ${layerName}`
|
||||
};
|
||||
}
|
||||
|
||||
// Simple tap value
|
||||
return parseKeyDefinition(tapValue);
|
||||
}
|
||||
}
|
||||
|
||||
return { type: 'unknown', keycode: null, label: String(keyDef) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable description for a shifted symbol
|
||||
*/
|
||||
function getShiftedDescription(symbol, originalDescription) {
|
||||
// For uppercase letters, use "Uppercase X" instead of "Shifted Lowercase x"
|
||||
if (symbol && symbol.length === 1 && symbol >= 'A' && symbol <= 'Z') {
|
||||
return `Uppercase ${symbol}`;
|
||||
}
|
||||
// For other symbols, use the symbol itself as description
|
||||
return symbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the modifier needed to access a layer
|
||||
*/
|
||||
function getLayerModifier(layerName) {
|
||||
if (layerName === 'BASE_SHIFT') return 'LTI'; // OSM Shift on left thumb inside
|
||||
if (layerName === 'BASE_ALT') return 'RTI'; // OSM Alt on right thumb inside
|
||||
if (layerName === 'BASE_SHIFT_ALT') return 'LTI+RTI'; // Both modifiers
|
||||
if (layerName === 'SHIFT_SYMBOL') return 'LTI'; // Shift on SYMBOL layer
|
||||
if (layerName === 'ALT_NAV') return 'RTI'; // Alt on NAV layer
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize a labeled symbol that doesn't have a keycode mapping
|
||||
*/
|
||||
function categorizeLabeledSymbol(label, layerName) {
|
||||
// Symbol descriptions map
|
||||
const symbolDescriptions = {
|
||||
'`': 'Backtick',
|
||||
'@': 'At sign',
|
||||
'#': 'Hash',
|
||||
'$': 'Dollar sign',
|
||||
'%': 'Percent sign',
|
||||
'^': 'Caret',
|
||||
'&': 'Ampersand',
|
||||
'*': 'Asterisk',
|
||||
'-': 'Hyphen',
|
||||
'+': 'Plus sign',
|
||||
'=': 'Equals sign',
|
||||
'_': 'Underscore',
|
||||
'~': 'Tilde',
|
||||
"'": 'Single quote',
|
||||
'"': 'Double quote',
|
||||
'(': 'Left parenthesis',
|
||||
')': 'Right parenthesis',
|
||||
'[': 'Left bracket',
|
||||
']': 'Right bracket',
|
||||
'{': 'Left brace',
|
||||
'}': 'Right brace',
|
||||
'<': 'Less than',
|
||||
'>': 'Greater than',
|
||||
'/': 'Forward slash',
|
||||
'\\': 'Backslash',
|
||||
'|': 'Pipe',
|
||||
',': 'Comma',
|
||||
'.': 'Period',
|
||||
':': 'Colon',
|
||||
';': 'Semicolon',
|
||||
'?': 'Question mark',
|
||||
'!': 'Exclamation mark',
|
||||
'€': 'Euro sign',
|
||||
'£': 'Pound sterling',
|
||||
'\u201C': 'Opening double quote',
|
||||
'\u201D': 'Closing double quote',
|
||||
'\u2018': 'Opening single quote',
|
||||
'\u2019': 'Closing single quote',
|
||||
// macOS Alt+letter symbols (SYMBOL layer)
|
||||
'œ': 'Latin ligature oe',
|
||||
'∑': 'Summation',
|
||||
'´': 'Acute accent',
|
||||
'®': 'Registered trademark',
|
||||
'†': 'Dagger',
|
||||
'¥': 'Yen sign',
|
||||
'¨': 'Diaeresis',
|
||||
'ˆ': 'Circumflex',
|
||||
'ø': 'O with stroke',
|
||||
'π': 'Pi',
|
||||
'å': 'A with ring',
|
||||
'ß': 'Sharp S (eszett)',
|
||||
'∂': 'Partial derivative',
|
||||
'ƒ': 'Function (florin)',
|
||||
'©': 'Copyright',
|
||||
'˙': 'Dot above',
|
||||
'∆': 'Delta',
|
||||
'˚': 'Ring above',
|
||||
'¬': 'Not sign',
|
||||
'…': 'Ellipsis',
|
||||
'Ω': 'Omega',
|
||||
'≈': 'Approximately equal',
|
||||
'ç': 'C cedilla',
|
||||
'√': 'Square root',
|
||||
'∫': 'Integral',
|
||||
'˜': 'Small tilde',
|
||||
'~': 'Tilde',
|
||||
'µ': 'Micro sign',
|
||||
'≤': 'Less than or equal',
|
||||
'≥': 'Greater than or equal',
|
||||
'÷': 'Division sign',
|
||||
// macOS Shift+Alt+letter symbols (SHIFT_SYMBOL layer)
|
||||
'Œ': 'Latin ligature OE',
|
||||
'„': 'Double low quote',
|
||||
'‰': 'Per mille',
|
||||
'‡': 'Double dagger',
|
||||
'ˇ': 'Caron',
|
||||
'Á': 'A acute',
|
||||
'Â': 'A circumflex',
|
||||
'Ê': 'E circumflex',
|
||||
'Ë': 'E diaeresis',
|
||||
'¯': 'Macron',
|
||||
'ˆ': 'Circumflex',
|
||||
'Ø': 'O with stroke',
|
||||
'∏': 'Product',
|
||||
'Å': 'A with ring',
|
||||
'Í': 'I acute',
|
||||
'Î': 'I circumflex',
|
||||
'Ï': 'I diaeresis',
|
||||
'Ì': 'I grave',
|
||||
'˝': 'Double acute',
|
||||
'Ó': 'O acute',
|
||||
'Ô': 'O circumflex',
|
||||
'Ò': 'O grave',
|
||||
'Ú': 'U acute',
|
||||
'Û': 'U circumflex',
|
||||
'Ù': 'U grave',
|
||||
'Æ': 'Latin ligature AE',
|
||||
'¸': 'Cedilla',
|
||||
'⁄': 'Fraction slash',
|
||||
'Ç': 'C cedilla',
|
||||
'◊': 'Lozenge',
|
||||
'ı': 'Dotless i',
|
||||
'˘': 'Breve',
|
||||
'¿': 'Inverted question mark'
|
||||
};
|
||||
|
||||
// Keypad numbers (already formatted as "KP_X")
|
||||
if (/^KP_\d+$/.test(label)) {
|
||||
return { category: 'numbers', priority: 1, description: `Keypad ${label.substring(3)}` };
|
||||
}
|
||||
|
||||
// Function keys
|
||||
if (/^F\d+$/.test(label)) {
|
||||
return { category: 'non-printing', priority: 3, description: `Function key ${label}` };
|
||||
}
|
||||
|
||||
// Command combinations
|
||||
if (label.includes('Cmd+') || label.includes('Ctrl+') || label.includes('Alt+') || label.includes('Shift+')) {
|
||||
return { category: 'commands', priority: 4, description: label };
|
||||
}
|
||||
|
||||
// Special words/labels
|
||||
if (label === 'Repeat') {
|
||||
return { category: 'special', priority: 3, description: 'Repeat last key' };
|
||||
}
|
||||
if (label === 'Alt Repeat') {
|
||||
return { category: 'special', priority: 3, description: 'Alternate repeat' };
|
||||
}
|
||||
if (label.includes('Word')) {
|
||||
return { category: 'special', priority: 3, description: label };
|
||||
}
|
||||
|
||||
// Symbols from SYMBOL and SHIFT_SYMBOL layers are always rare
|
||||
if (layerName === 'SYMBOL' || layerName === 'SHIFT_SYMBOL') {
|
||||
const description = symbolDescriptions[label] || label;
|
||||
return { category: 'rare', priority: 4, description };
|
||||
}
|
||||
|
||||
// £ and € from BASE_ALT or BASE_SHIFT_ALT layers are common (not rare)
|
||||
const baseLayerSymbols = ['€', '£'];
|
||||
const isBaseLayer = layerName === 'BASE_ALT' || layerName === 'BASE_SHIFT_ALT' || layerName === 'BASE' || layerName === 'BASE_SHIFT';
|
||||
const isCurlyQuote = ['\u201C', '\u201D', '\u2018', '\u2019'].includes(label);
|
||||
|
||||
// Check if we have a description for this symbol
|
||||
if (symbolDescriptions[label]) {
|
||||
let category = 'common';
|
||||
let priority = 2;
|
||||
|
||||
// Curly quotes are always rare
|
||||
if (isCurlyQuote) {
|
||||
category = 'rare';
|
||||
priority = 4;
|
||||
}
|
||||
// If £ or € are on base layers (BASE_ALT/BASE_SHIFT_ALT), they're common
|
||||
// Otherwise (like if they appear on SYMBOL layer), they're rare
|
||||
else if (baseLayerSymbols.includes(label) && !isBaseLayer) {
|
||||
category = 'rare';
|
||||
priority = 4;
|
||||
}
|
||||
|
||||
return { category, priority, description: symbolDescriptions[label] };
|
||||
}
|
||||
|
||||
// Default to common with the label as description
|
||||
return { category: 'common', priority: 2, description: label };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all symbols that can be typed from a parsed key
|
||||
*/
|
||||
function extractSymbolsFromKey(parsedKey, layerName, position) {
|
||||
const symbols = [];
|
||||
|
||||
// Don't extract symbols from empty keys or held modifier keys
|
||||
if (parsedKey.type === 'empty' || parsedKey.type === 'held') {
|
||||
return symbols;
|
||||
}
|
||||
|
||||
// For mod-tap keys, extract symbols from the tap behavior
|
||||
if (parsedKey.type === 'mod-tap' && parsedKey.tapKey) {
|
||||
return extractSymbolsFromKey(parsedKey.tapKey, layerName, position);
|
||||
}
|
||||
|
||||
// Get position coordinate
|
||||
const coordinate = POSITION_TO_COORDINATE[position] || `pos${position}`;
|
||||
|
||||
// Get layer modifier (for ghost layers)
|
||||
const layerModifier = getLayerModifier(layerName);
|
||||
|
||||
// Get symbol mapping if available
|
||||
const mapping = parsedKey.keycode ? symbolMapping[parsedKey.keycode] : null;
|
||||
|
||||
if (mapping) {
|
||||
// Determine which symbol to extract based on the key label
|
||||
// If label is uppercase (e.g., 'A'), extract only the shifted symbol
|
||||
// If label is lowercase (e.g., 'a'), extract only the unshifted symbol
|
||||
const isUppercase = parsedKey.label && parsedKey.label.length === 1 &&
|
||||
parsedKey.label === parsedKey.label.toUpperCase() &&
|
||||
parsedKey.label !== parsedKey.label.toLowerCase();
|
||||
|
||||
if (isUppercase && mapping.shiftedSymbol) {
|
||||
// Extract shifted symbol (uppercase letter)
|
||||
const shiftedDescription = getShiftedDescription(mapping.shiftedSymbol, mapping.description);
|
||||
const notation = layerModifier ? `${layerModifier}+${coordinate}` : coordinate;
|
||||
|
||||
symbols.push({
|
||||
symbol: mapping.shiftedSymbol,
|
||||
description: shiftedDescription,
|
||||
category: mapping.category,
|
||||
priority: mapping.priority,
|
||||
layer: layerName,
|
||||
position,
|
||||
coordinate,
|
||||
notation,
|
||||
method: layerModifier ? 'modifier' : 'direct',
|
||||
keyLabel: parsedKey.label
|
||||
});
|
||||
} else if (!isUppercase && mapping.symbol) {
|
||||
// Extract unshifted symbol (lowercase letter or other)
|
||||
const notation = layerModifier ? `${layerModifier}+${coordinate}` : coordinate;
|
||||
|
||||
symbols.push({
|
||||
symbol: mapping.symbol,
|
||||
description: mapping.description,
|
||||
category: mapping.category,
|
||||
priority: mapping.priority,
|
||||
layer: layerName,
|
||||
position,
|
||||
coordinate,
|
||||
notation,
|
||||
method: layerModifier ? 'modifier' : 'direct',
|
||||
keyLabel: parsedKey.label
|
||||
});
|
||||
}
|
||||
} else if ((parsedKey.type === 'labeled' || parsedKey.type === 'char') && parsedKey.label && parsedKey.label !== '—') {
|
||||
// For labeled keys or char keys without keycode mapping, use the label itself
|
||||
const notation = layerModifier ? `${layerModifier}+${coordinate}` : coordinate;
|
||||
const { category, priority, description } = categorizeLabeledSymbol(parsedKey.label, layerName);
|
||||
|
||||
symbols.push({
|
||||
symbol: parsedKey.label,
|
||||
description,
|
||||
category,
|
||||
priority,
|
||||
layer: layerName,
|
||||
position,
|
||||
coordinate,
|
||||
notation,
|
||||
method: layerModifier ? 'modifier' : 'direct',
|
||||
keyLabel: parsedKey.label
|
||||
});
|
||||
}
|
||||
|
||||
return symbols;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse layer data from layout.yaml
|
||||
*/
|
||||
function parseLayerData(layoutData) {
|
||||
const layers = [];
|
||||
|
||||
for (const [layerName, layerKeys] of Object.entries(layoutData.layers || {})) {
|
||||
const keys = [];
|
||||
const allSymbols = [];
|
||||
let position = 0;
|
||||
|
||||
// Flatten the layer key array (it's a 2D array representing rows)
|
||||
for (const row of layerKeys) {
|
||||
for (const keyDef of row) {
|
||||
const parsedKey = parseKeyDefinition(keyDef);
|
||||
keys.push(parsedKey);
|
||||
|
||||
// Extract symbols from this key
|
||||
const symbols = extractSymbolsFromKey(parsedKey, layerName, position);
|
||||
allSymbols.push(...symbols);
|
||||
|
||||
position++;
|
||||
}
|
||||
}
|
||||
|
||||
layers.push({
|
||||
name: layerName,
|
||||
keys,
|
||||
symbols: allSymbols
|
||||
});
|
||||
}
|
||||
|
||||
return layers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the real layer that a ghost layer belongs to
|
||||
*/
|
||||
function getRealLayer(layerName) {
|
||||
if (LAYER_TYPES.ghost.includes(layerName)) {
|
||||
return 'Layer 0 (Base)'; // Ghost layers belong to Layer 0
|
||||
}
|
||||
if (layerName === 'BASE') return 'Layer 0 (Base)';
|
||||
if (layerName === 'NAV') return 'Layer 1 (Nav)';
|
||||
if (layerName === 'ALT_NAV') return 'Layer 1 (Nav)'; // Alt on NAV layer
|
||||
if (layerName === 'SYMBOL') return 'Layer 2 (Symbol)';
|
||||
if (layerName === 'SHIFT_SYMBOL') return 'Layer 2 (Symbol)'; // Shift on SYMBOL layer
|
||||
return layerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build keymap data structure
|
||||
*/
|
||||
function buildKeymapData(keymapInfo) {
|
||||
try {
|
||||
// Read and parse layout.yaml
|
||||
const fileContents = fs.readFileSync(keymapInfo.path, 'utf8');
|
||||
const layoutData = yaml.load(fileContents);
|
||||
|
||||
// Parse layers
|
||||
const layers = parseLayerData(layoutData);
|
||||
|
||||
// Collect all unique symbols grouped by real layers
|
||||
const allSymbolsMap = new Map();
|
||||
|
||||
for (const layer of layers) {
|
||||
for (const symbol of layer.symbols) {
|
||||
const key = `${symbol.symbol}|${symbol.description}`;
|
||||
if (!allSymbolsMap.has(key)) {
|
||||
allSymbolsMap.set(key, {
|
||||
symbol: symbol.symbol,
|
||||
description: symbol.description,
|
||||
category: symbol.category,
|
||||
priority: symbol.priority,
|
||||
accessMethods: {} // Keyed by real layer name
|
||||
});
|
||||
}
|
||||
|
||||
const symbolData = allSymbolsMap.get(key);
|
||||
const realLayer = getRealLayer(symbol.layer);
|
||||
|
||||
// Initialize array for this real layer if needed
|
||||
if (!symbolData.accessMethods[realLayer]) {
|
||||
symbolData.accessMethods[realLayer] = [];
|
||||
}
|
||||
|
||||
// Add this access method
|
||||
symbolData.accessMethods[realLayer].push({
|
||||
layer: symbol.layer,
|
||||
position: symbol.position,
|
||||
coordinate: symbol.coordinate,
|
||||
notation: symbol.notation,
|
||||
method: symbol.method,
|
||||
keyLabel: symbol.keyLabel
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Convert map to sorted array
|
||||
const allSymbols = Array.from(allSymbolsMap.values()).sort((a, b) => {
|
||||
// Sort by: priority, then category, then symbol
|
||||
if (a.priority !== b.priority) return a.priority - b.priority;
|
||||
if (a.category !== b.category) return a.category.localeCompare(b.category);
|
||||
return (a.symbol || '').localeCompare(b.symbol || '');
|
||||
});
|
||||
|
||||
// Define available real layers
|
||||
const realLayers = [
|
||||
{ id: 'layer0', name: 'Layer 0 (Base)' },
|
||||
{ id: 'layer1', name: 'Layer 1 (Nav)' },
|
||||
{ id: 'layer2', name: 'Layer 2 (Symbol)' }
|
||||
];
|
||||
|
||||
return {
|
||||
id: keymapInfo.id,
|
||||
keyboard: keymapInfo.keyboard,
|
||||
keymapName: keymapInfo.keymap,
|
||||
layoutFile: keymapInfo.path,
|
||||
layers,
|
||||
allSymbols,
|
||||
realLayers,
|
||||
svgPaths: layers.map(layer => ({
|
||||
name: layer.name,
|
||||
path: `/public/generated/${keymapInfo.id}/${layer.name.toLowerCase()}.svg`
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error parsing keymap ${keymapInfo.id}:`, error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main data export for Eleventy
|
||||
*/
|
||||
module.exports = function() {
|
||||
const rootDir = path.join(__dirname, '../../..');
|
||||
const keyboardsDir = path.join(rootDir, 'keyboards');
|
||||
|
||||
console.log('\n[Eleventy Data] Discovering keymaps...');
|
||||
|
||||
// Discover all keymaps
|
||||
const keymapInfos = discoverKeymaps(keyboardsDir);
|
||||
console.log(`[Eleventy Data] Found ${keymapInfos.length} keymap(s)`);
|
||||
|
||||
// Build data for each keymap
|
||||
const keymaps = keymapInfos
|
||||
.map(buildKeymapData)
|
||||
.filter(km => km !== null);
|
||||
|
||||
console.log(`[Eleventy Data] Processed ${keymaps.length} keymap(s)`);
|
||||
|
||||
// Log some stats
|
||||
keymaps.forEach(km => {
|
||||
console.log(` ${km.id}: ${km.layers.length} layers, ${km.allSymbols.length} unique symbols`);
|
||||
});
|
||||
|
||||
return keymaps;
|
||||
};
|
||||
117
website/src/_data/symbolMapping.json
Normal file
117
website/src/_data/symbolMapping.json
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
{
|
||||
"_comment": "Maps QMK keycodes to human-readable descriptions and categories",
|
||||
|
||||
"KC_A": { "symbol": "a", "shiftedSymbol": "A", "description": "Lowercase a", "category": "letters", "priority": 1 },
|
||||
"KC_B": { "symbol": "b", "shiftedSymbol": "B", "description": "Lowercase b", "category": "letters", "priority": 1 },
|
||||
"KC_C": { "symbol": "c", "shiftedSymbol": "C", "description": "Lowercase c", "category": "letters", "priority": 1 },
|
||||
"KC_D": { "symbol": "d", "shiftedSymbol": "D", "description": "Lowercase d", "category": "letters", "priority": 1 },
|
||||
"KC_E": { "symbol": "e", "shiftedSymbol": "E", "description": "Lowercase e", "category": "letters", "priority": 1 },
|
||||
"KC_F": { "symbol": "f", "shiftedSymbol": "F", "description": "Lowercase f", "category": "letters", "priority": 1 },
|
||||
"KC_G": { "symbol": "g", "shiftedSymbol": "G", "description": "Lowercase g", "category": "letters", "priority": 1 },
|
||||
"KC_H": { "symbol": "h", "shiftedSymbol": "H", "description": "Lowercase h", "category": "letters", "priority": 1 },
|
||||
"KC_I": { "symbol": "i", "shiftedSymbol": "I", "description": "Lowercase i", "category": "letters", "priority": 1 },
|
||||
"KC_J": { "symbol": "j", "shiftedSymbol": "J", "description": "Lowercase j", "category": "letters", "priority": 1 },
|
||||
"KC_K": { "symbol": "k", "shiftedSymbol": "K", "description": "Lowercase k", "category": "letters", "priority": 1 },
|
||||
"KC_L": { "symbol": "l", "shiftedSymbol": "L", "description": "Lowercase l", "category": "letters", "priority": 1 },
|
||||
"KC_M": { "symbol": "m", "shiftedSymbol": "M", "description": "Lowercase m", "category": "letters", "priority": 1 },
|
||||
"KC_N": { "symbol": "n", "shiftedSymbol": "N", "description": "Lowercase n", "category": "letters", "priority": 1 },
|
||||
"KC_O": { "symbol": "o", "shiftedSymbol": "O", "description": "Lowercase o", "category": "letters", "priority": 1 },
|
||||
"KC_P": { "symbol": "p", "shiftedSymbol": "P", "description": "Lowercase p", "category": "letters", "priority": 1 },
|
||||
"KC_Q": { "symbol": "q", "shiftedSymbol": "Q", "description": "Lowercase q", "category": "letters", "priority": 1 },
|
||||
"KC_R": { "symbol": "r", "shiftedSymbol": "R", "description": "Lowercase r", "category": "letters", "priority": 1 },
|
||||
"KC_S": { "symbol": "s", "shiftedSymbol": "S", "description": "Lowercase s", "category": "letters", "priority": 1 },
|
||||
"KC_T": { "symbol": "t", "shiftedSymbol": "T", "description": "Lowercase t", "category": "letters", "priority": 1 },
|
||||
"KC_U": { "symbol": "u", "shiftedSymbol": "U", "description": "Lowercase u", "category": "letters", "priority": 1 },
|
||||
"KC_V": { "symbol": "v", "shiftedSymbol": "V", "description": "Lowercase v", "category": "letters", "priority": 1 },
|
||||
"KC_W": { "symbol": "w", "shiftedSymbol": "W", "description": "Lowercase w", "category": "letters", "priority": 1 },
|
||||
"KC_X": { "symbol": "x", "shiftedSymbol": "X", "description": "Lowercase x", "category": "letters", "priority": 1 },
|
||||
"KC_Y": { "symbol": "y", "shiftedSymbol": "Y", "description": "Lowercase y", "category": "letters", "priority": 1 },
|
||||
"KC_Z": { "symbol": "z", "shiftedSymbol": "Z", "description": "Lowercase z", "category": "letters", "priority": 1 },
|
||||
|
||||
"KC_1": { "symbol": "1", "shiftedSymbol": "!", "description": "Number 1", "category": "numbers", "priority": 1 },
|
||||
"KC_2": { "symbol": "2", "shiftedSymbol": "@", "description": "Number 2", "category": "numbers", "priority": 1 },
|
||||
"KC_3": { "symbol": "3", "shiftedSymbol": "#", "description": "Number 3", "category": "numbers", "priority": 1 },
|
||||
"KC_4": { "symbol": "4", "shiftedSymbol": "$", "description": "Number 4", "category": "numbers", "priority": 1 },
|
||||
"KC_5": { "symbol": "5", "shiftedSymbol": "%", "description": "Number 5", "category": "numbers", "priority": 1 },
|
||||
"KC_6": { "symbol": "6", "shiftedSymbol": "^", "description": "Number 6", "category": "numbers", "priority": 1 },
|
||||
"KC_7": { "symbol": "7", "shiftedSymbol": "&", "description": "Number 7", "category": "numbers", "priority": 1 },
|
||||
"KC_8": { "symbol": "8", "shiftedSymbol": "*", "description": "Number 8", "category": "numbers", "priority": 1 },
|
||||
"KC_9": { "symbol": "9", "shiftedSymbol": "(", "description": "Number 9", "category": "numbers", "priority": 1 },
|
||||
"KC_0": { "symbol": "0", "shiftedSymbol": ")", "description": "Number 0", "category": "numbers", "priority": 1 },
|
||||
|
||||
"KC_KP_0": { "symbol": "0", "description": "Keypad 0", "category": "numbers", "priority": 1 },
|
||||
"KC_KP_1": { "symbol": "1", "description": "Keypad 1", "category": "numbers", "priority": 1 },
|
||||
"KC_KP_2": { "symbol": "2", "description": "Keypad 2", "category": "numbers", "priority": 1 },
|
||||
"KC_KP_3": { "symbol": "3", "description": "Keypad 3", "category": "numbers", "priority": 1 },
|
||||
"KC_KP_4": { "symbol": "4", "description": "Keypad 4", "category": "numbers", "priority": 1 },
|
||||
"KC_KP_5": { "symbol": "5", "description": "Keypad 5", "category": "numbers", "priority": 1 },
|
||||
"KC_KP_6": { "symbol": "6", "description": "Keypad 6", "category": "numbers", "priority": 1 },
|
||||
"KC_KP_7": { "symbol": "7", "description": "Keypad 7", "category": "numbers", "priority": 1 },
|
||||
"KC_KP_8": { "symbol": "8", "description": "Keypad 8", "category": "numbers", "priority": 1 },
|
||||
"KC_KP_9": { "symbol": "9", "description": "Keypad 9", "category": "numbers", "priority": 1 },
|
||||
|
||||
"KC_DOT": { "symbol": ".", "shiftedSymbol": ">", "description": "Period", "category": "punctuation", "priority": 2 },
|
||||
"KC_COMMA": { "symbol": ",", "shiftedSymbol": "<", "description": "Comma", "category": "punctuation", "priority": 2 },
|
||||
"KC_SCLN": { "symbol": ";", "shiftedSymbol": ":", "description": "Semicolon", "category": "punctuation", "priority": 2 },
|
||||
"KC_SLSH": { "symbol": "/", "shiftedSymbol": "?", "description": "Forward slash", "category": "punctuation", "priority": 2 },
|
||||
"KC_QUOTE": { "symbol": "'", "shiftedSymbol": "\"", "description": "Single quote", "category": "punctuation", "priority": 2 },
|
||||
"KC_GRAVE": { "symbol": "`", "shiftedSymbol": "~", "description": "Backtick", "category": "punctuation", "priority": 2 },
|
||||
"KC_MINUS": { "symbol": "-", "shiftedSymbol": "_", "description": "Minus/Underscore", "category": "punctuation", "priority": 2 },
|
||||
"KC_EQUAL": { "symbol": "=", "shiftedSymbol": "+", "description": "Equals/Plus", "category": "punctuation", "priority": 2 },
|
||||
"KC_LBRC": { "symbol": "[", "shiftedSymbol": "{", "description": "Left bracket", "category": "punctuation", "priority": 2 },
|
||||
"KC_RBRC": { "symbol": "]", "shiftedSymbol": "}", "description": "Right bracket", "category": "punctuation", "priority": 2 },
|
||||
"KC_BACKSLASH": { "symbol": "\\", "shiftedSymbol": "|", "description": "Backslash/Pipe", "category": "punctuation", "priority": 2 },
|
||||
|
||||
"KC_AT": { "symbol": "@", "description": "At sign", "category": "common", "priority": 2 },
|
||||
"KC_HASH": { "symbol": "#", "description": "Hash/Pound", "category": "common", "priority": 2 },
|
||||
"KC_DOLLAR": { "symbol": "$", "description": "Dollar sign", "category": "common", "priority": 2 },
|
||||
"KC_PERCENT": { "symbol": "%", "description": "Percent", "category": "common", "priority": 2 },
|
||||
"KC_CIRCUMFLEX": { "symbol": "^", "description": "Caret", "category": "common", "priority": 2 },
|
||||
"KC_AMPERSAND": { "symbol": "&", "description": "Ampersand", "category": "common", "priority": 2 },
|
||||
"KC_ASTERISK": { "symbol": "*", "description": "Asterisk", "category": "common", "priority": 2 },
|
||||
"KC_LPRN": { "symbol": "(", "description": "Left parenthesis", "category": "common", "priority": 2 },
|
||||
"KC_RPRN": { "symbol": ")", "description": "Right parenthesis", "category": "common", "priority": 2 },
|
||||
"KC_UNDERSCORE": { "symbol": "_", "description": "Underscore", "category": "common", "priority": 2 },
|
||||
"KC_PLUS": { "symbol": "+", "description": "Plus", "category": "common", "priority": 2 },
|
||||
"KC_LCBR": { "symbol": "{", "description": "Left curly brace", "category": "common", "priority": 2 },
|
||||
"KC_RCBR": { "symbol": "}", "description": "Right curly brace", "category": "common", "priority": 2 },
|
||||
"KC_PIPE": { "symbol": "|", "description": "Pipe", "category": "common", "priority": 2 },
|
||||
"KC_COLON": { "symbol": ":", "description": "Colon", "category": "common", "priority": 2 },
|
||||
"KC_DQUO": { "symbol": "\"", "description": "Double quote", "category": "common", "priority": 2 },
|
||||
"KC_TILDE": { "symbol": "~", "description": "Tilde", "category": "common", "priority": 2 },
|
||||
"KC_LABK": { "symbol": "<", "description": "Less than", "category": "common", "priority": 2 },
|
||||
"KC_RABK": { "symbol": ">", "description": "Greater than", "category": "common", "priority": 2 },
|
||||
"KC_QUESTION": { "symbol": "?", "description": "Question mark", "category": "common", "priority": 2 },
|
||||
"KC_EXCLAIM": { "symbol": "!", "description": "Exclamation mark", "category": "common", "priority": 2 },
|
||||
|
||||
"KC_SPACE": { "symbol": "␣", "description": "Space", "category": "invisible", "priority": 2 },
|
||||
"KC_ENTER": { "symbol": "↵", "description": "Enter", "category": "invisible", "priority": 2 },
|
||||
"KC_ESC": { "symbol": "⎋", "description": "Escape", "category": "invisible", "priority": 2 },
|
||||
"KC_BSPC": { "symbol": "⌫", "description": "Backspace", "category": "invisible", "priority": 3 },
|
||||
"KC_TAB": { "symbol": "⇥", "description": "Tab", "category": "invisible", "priority": 2 },
|
||||
"KC_DELETE": { "symbol": "⌦", "description": "Delete", "category": "invisible", "priority": 3 },
|
||||
|
||||
"KC_LEFT": { "symbol": "←", "description": "Left arrow", "category": "navigation", "priority": 3 },
|
||||
"KC_RIGHT": { "symbol": "→", "description": "Right arrow", "category": "navigation", "priority": 3 },
|
||||
"KC_UP": { "symbol": "↑", "description": "Up arrow", "category": "navigation", "priority": 3 },
|
||||
"KC_DOWN": { "symbol": "↓", "description": "Down arrow", "category": "navigation", "priority": 3 },
|
||||
|
||||
"KC_F1": { "symbol": null, "description": "Function key F1", "category": "non-printing", "priority": 3 },
|
||||
"KC_F2": { "symbol": null, "description": "Function key F2", "category": "non-printing", "priority": 3 },
|
||||
"KC_F3": { "symbol": null, "description": "Function key F3", "category": "non-printing", "priority": 3 },
|
||||
"KC_F4": { "symbol": null, "description": "Function key F4", "category": "non-printing", "priority": 3 },
|
||||
"KC_F5": { "symbol": null, "description": "Function key F5", "category": "non-printing", "priority": 3 },
|
||||
"KC_F6": { "symbol": null, "description": "Function key F6", "category": "non-printing", "priority": 3 },
|
||||
"KC_F7": { "symbol": null, "description": "Function key F7", "category": "non-printing", "priority": 3 },
|
||||
"KC_F8": { "symbol": null, "description": "Function key F8", "category": "non-printing", "priority": 3 },
|
||||
"KC_F9": { "symbol": null, "description": "Function key F9", "category": "non-printing", "priority": 3 },
|
||||
"KC_F10": { "symbol": null, "description": "Function key F10", "category": "non-printing", "priority": 3 },
|
||||
"KC_F11": { "symbol": null, "description": "Function key F11", "category": "non-printing", "priority": 3 },
|
||||
"KC_F12": { "symbol": null, "description": "Function key F12", "category": "non-printing", "priority": 3 },
|
||||
|
||||
"QK_REPEAT_KEY": { "symbol": null, "description": "Repeat last key", "category": "non-printing", "priority": 3 },
|
||||
"QK_ALT_REPEAT_KEY": { "symbol": null, "description": "Alt repeat", "category": "non-printing", "priority": 3 },
|
||||
|
||||
"KC_EURO": { "symbol": "€", "description": "Euro sign", "category": "rare", "priority": 4 },
|
||||
"KC_UK_POUND": { "symbol": "£", "description": "Pound sterling", "category": "rare", "priority": 4 }
|
||||
}
|
||||
206
website/src/_includes/layouts/base.njk
Normal file
206
website/src/_includes/layouts/base.njk
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ title }} | QMK Keymap Documentation</title>
|
||||
<link rel="stylesheet" href="/public/css/theme.css">
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
line-height: 1.6;
|
||||
background: var(--color-bg, #fff);
|
||||
color: var(--color-text, #1a1a1a);
|
||||
}
|
||||
|
||||
header {
|
||||
border-bottom: 2px solid var(--color-border, #e0e0e0);
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: var(--color-heading, #333);
|
||||
}
|
||||
|
||||
nav {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: var(--color-link, #007bff);
|
||||
text-decoration: none;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main {
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 4rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--color-border, #e0e0e0);
|
||||
text-align: center;
|
||||
color: var(--color-muted, #666);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.keyboard-group {
|
||||
margin: 2rem 0;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--color-border, #ddd);
|
||||
border-radius: 8px;
|
||||
background: var(--color-card-bg, #f9f9f9);
|
||||
}
|
||||
|
||||
.keyboard-group h2, .keyboard-group h3 {
|
||||
margin-top: 0;
|
||||
color: var(--color-heading, #333);
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0.5rem 0.5rem 0.5rem 0;
|
||||
background: var(--color-primary, #007bff);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: var(--color-primary-hover, #0056b3);
|
||||
}
|
||||
|
||||
.button.secondary {
|
||||
background: var(--color-secondary, #6c757d);
|
||||
}
|
||||
|
||||
.button.secondary:hover {
|
||||
background: var(--color-secondary-hover, #5a6268);
|
||||
}
|
||||
|
||||
.stats {
|
||||
color: var(--color-muted, #666);
|
||||
font-size: 0.95rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.layer-diagrams {
|
||||
margin: 3rem 0;
|
||||
}
|
||||
|
||||
.layer {
|
||||
margin: 2rem 0;
|
||||
padding: 1.5rem;
|
||||
background: var(--color-layer-bg, #fff);
|
||||
border: 1px solid var(--color-border, #ddd);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.layer h3 {
|
||||
margin-top: 0;
|
||||
color: var(--color-heading, #333);
|
||||
}
|
||||
|
||||
.layer svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 1200px;
|
||||
display: block;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.5rem 0;
|
||||
font-size: 0.95rem;
|
||||
background: var(--color-table-bg, #fff);
|
||||
}
|
||||
|
||||
thead {
|
||||
background: var(--color-table-header-bg, #f0f0f0);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
border: 1px solid var(--color-table-border, #ddd);
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
color: var(--color-heading, #333);
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background: var(--color-table-row-hover, #f5f5f5);
|
||||
}
|
||||
|
||||
.symbol-char {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace;
|
||||
}
|
||||
|
||||
.not-available {
|
||||
color: var(--color-muted, #999);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.category-section {
|
||||
margin: 3rem 0;
|
||||
}
|
||||
|
||||
.category-section h3 {
|
||||
color: var(--color-heading, #333);
|
||||
border-bottom: 2px solid var(--color-accent, #007bff);
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
code {
|
||||
background: var(--color-code-bg, #f5f5f5);
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>QMK Keymap Documentation</h1>
|
||||
<nav>
|
||||
<a href="/">Home</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{{ content | safe }}
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>Generated with <a href="https://www.11ty.dev/">Eleventy</a> and <a href="https://github.com/caksoylar/keymap-drawer">keymap-drawer</a></p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
77
website/src/_includes/partials/symbol-table.njk
Normal file
77
website/src/_includes/partials/symbol-table.njk
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
{# Symbol table component - displays all symbols grouped by category #}
|
||||
|
||||
{% set categories = {
|
||||
'letters': 'Letters (a-z, A-Z)',
|
||||
'numbers': 'Numbers (0-9)',
|
||||
'punctuation': 'Punctuation',
|
||||
'common': 'Common Symbols',
|
||||
'invisible': 'Invisible Characters',
|
||||
'navigation': 'Navigation',
|
||||
'commands': 'Common Commands',
|
||||
'special': 'Special Keys',
|
||||
'non-printing': 'Function Keys & Non-Printing',
|
||||
'rare': 'Rare & Unicode Symbols'
|
||||
} %}
|
||||
|
||||
{% for categoryKey, categoryName in categories %}
|
||||
{# Filter symbols by this category #}
|
||||
{% set categorySymbols = [] %}
|
||||
{% for symbol in keymap.allSymbols %}
|
||||
{% if symbol.category == categoryKey %}
|
||||
{% set categorySymbols = (categorySymbols.push(symbol), categorySymbols) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if categorySymbols.length > 0 %}
|
||||
<div class="category-section">
|
||||
<h4>{{ categoryName }}</h4>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 80px;">Symbol</th>
|
||||
<th style="width: 200px;">Description</th>
|
||||
{% for realLayer in keymap.realLayers %}
|
||||
<th>{{ realLayer.name }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for symbol in categorySymbols %}
|
||||
<tr>
|
||||
<td class="symbol-char">
|
||||
{% if symbol.symbol %}
|
||||
{{ symbol.symbol }}
|
||||
{% else %}
|
||||
<span class="not-available">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ symbol.description }}</td>
|
||||
|
||||
{# Show how to access this symbol from each real layer #}
|
||||
{% for realLayer in keymap.realLayers %}
|
||||
<td>
|
||||
{% set accessMethods = symbol.accessMethods[realLayer.name] %}
|
||||
|
||||
{% if accessMethods and accessMethods.length > 0 %}
|
||||
{% for method in accessMethods %}
|
||||
<div style="margin: 0.25rem 0;">
|
||||
<code>{{ method.notation }}</code>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="not-available">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p style="color: var(--color-muted, #666); font-size: 0.9rem; margin-top: 0.5rem;">
|
||||
{{ categorySymbols.length }} {{ 'symbol' if categorySymbols.length == 1 else 'symbols' }} in this category
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
34
website/src/index.njk
Normal file
34
website/src/index.njk
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
title: Home
|
||||
---
|
||||
|
||||
<h2>QMK Keymaps</h2>
|
||||
|
||||
<p>Browse visual documentation and comprehensive symbol tables for all configured QMK keymaps.</p>
|
||||
|
||||
{% if keymaps.length > 0 %}
|
||||
<h2>Available Keymaps</h2>
|
||||
|
||||
{% for keymap in keymaps %}
|
||||
<div class="keyboard-group">
|
||||
<h3>{{ keymap.keyboard }}</h3>
|
||||
<p><strong>Keymap:</strong> {{ keymap.keymapName }}</p>
|
||||
|
||||
<a href="/keymaps/{{ keymap.id }}/index.html" class="button">View Keymap Documentation</a>
|
||||
|
||||
<div class="stats">
|
||||
<p>
|
||||
<strong>Layers:</strong> {{ keymap.layers | length }}
|
||||
({% for layer in keymap.layers %}{{ layer.name }}{% if not loop.last %}, {% endif %}{% endfor %})
|
||||
</p>
|
||||
<p>
|
||||
<strong>Unique Symbols:</strong> {{ keymap.allSymbols.length }} symbols and keys documented
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% else %}
|
||||
<p><em>No keymaps found. Make sure you have layout.yaml files in your keyboards/ directory.</em></p>
|
||||
{% endif %}
|
||||
55
website/src/keymaps.njk
Normal file
55
website/src/keymaps.njk
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
pagination:
|
||||
data: keymaps
|
||||
size: 1
|
||||
alias: keymap
|
||||
permalink: "/keymaps/{{ keymap.id }}/index.html"
|
||||
eleventyComputed:
|
||||
title: "{{ keymap.keyboard }} - {{ keymap.keymapName }}"
|
||||
---
|
||||
|
||||
<h2>{{ keymap.keyboard }} - {{ keymap.keymapName }}</h2>
|
||||
|
||||
<p class="stats">
|
||||
<strong>{{ keymap.layers.length }} layers</strong> with <strong>{{ keymap.allSymbols.length }} unique symbols</strong>
|
||||
</p>
|
||||
|
||||
<nav style="margin: 2rem 0;">
|
||||
<a href="#diagrams" class="button">Layer Diagrams</a>
|
||||
<a href="#symbols" class="button">Symbol Index</a>
|
||||
<a href="/" class="button secondary">← Back to Home</a>
|
||||
</nav>
|
||||
|
||||
<section id="diagrams" class="layer-diagrams">
|
||||
<h3>Layer Diagrams</h3>
|
||||
|
||||
{% for layer in keymap.layers %}
|
||||
<div class="layer">
|
||||
<h4>Layer: {{ layer.name }}</h4>
|
||||
{% set svgPath = keymap.svgPaths | find('name', layer.name) %}
|
||||
{% if svgPath %}
|
||||
<div class="svg-container">
|
||||
<object data="{{ svgPath.path }}" type="image/svg+xml" style="width: 100%; max-width: 1200px;">
|
||||
<img src="{{ svgPath.path }}" alt="{{ layer.name }} layer diagram" />
|
||||
</object>
|
||||
</div>
|
||||
{% else %}
|
||||
<p><em>SVG diagram not available for this layer.</em></p>
|
||||
{% endif %}
|
||||
|
||||
<details style="margin-top: 1rem;">
|
||||
<summary>Layer details ({{ layer.keys.length }} keys, {{ layer.symbols.length }} symbols)</summary>
|
||||
<p style="margin-top: 1rem;">This layer defines {{ layer.symbols.length }} accessible symbols/keys.</p>
|
||||
</details>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<section id="symbols" class="symbol-tables">
|
||||
<h3>Symbol Index</h3>
|
||||
|
||||
<p>Find any symbol and see how to type it on this keyboard. Symbols are organized by category for easy reference.</p>
|
||||
|
||||
{% include "partials/symbol-table.njk" %}
|
||||
</section>
|
||||
Loading…
Add table
Add a link
Reference in a new issue