diff --git a/.github/actions/setup-python/action.yml b/.github/actions/setup-python/action.yml index f451497..a101b80 100644 --- a/.github/actions/setup-python/action.yml +++ b/.github/actions/setup-python/action.yml @@ -1,17 +1,18 @@ +--- name: "Setup Python and uv" description: "Setup Python and uv with caching" runs: using: "composite" steps: - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version-file: "pyproject.toml" + python-version-file: "pyproject.toml" - name: Install uv uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 with: - enable-cache: auto - version: 0.9.* + enable-cache: auto + version: 0.9.* - name: Install dependencies shell: bash run: uv sync --locked --all-extras --dev diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e52f25a..fe31744 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,27 +1,38 @@ +--- name: checks on: pull_request: workflow_dispatch: + +permissions: + contents: read + jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - uses: ./.github/actions/setup-python - run: | - uv run ruff check --fix - uv run ruff format + uv run ruff check --fix + uv run ruff format typecheck: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - uses: ./.github/actions/setup-python - run: uv run ty check vulture: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - uses: ./.github/actions/setup-python - run: uv run vulture --min-confidence 100 --exclude ".venv" . diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index efb0f1e..ff41564 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -1,3 +1,4 @@ +--- name: pytest on: pull_request: @@ -10,21 +11,34 @@ jobs: pytest: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - uses: ./.github/actions/setup-python - run: | - uv run pytest + uv run pytest + + # Browser/Playwright tests are excluded by default (pytest.ini) and run + # only on pull requests, where we install Chromium for them. + - name: Install Playwright browser + if: github.event_name == 'pull_request' + run: | + uv run playwright install --with-deps chromium + - name: Run browser tests + if: github.event_name == 'pull_request' + run: | + uv run pytest -m browser - name: Run tests (with coverage) run: | - uv run pytest \ - --junitxml=pytest.xml \ - --cov-report=term-missing:skip-covered \ - --cov=. \ - | tee pytest-coverage.txt + uv run pytest \ + --junitxml=pytest.xml \ + --cov-report=term-missing:skip-covered \ + --cov=. \ + | tee pytest-coverage.txt - name: Pytest coverage comment - uses: MishaKav/pytest-coverage-comment@a01708271d42c5703d489b13eb503ba47c01e82a # main + uses: MishaKav/pytest-coverage-comment@dda025d84b1dd831193ed9cd5b1d7e33c79efa9b # v1.1.59 with: - pytest-coverage-path: ./pytest-coverage.txt - junitxml-path: ./pytest.xml + pytest-coverage-path: ./pytest-coverage.txt + junitxml-path: ./pytest.xml diff --git a/.pre-commit-config.yml b/.pre-commit-config.yml new file mode 100644 index 0000000..6bf6f5e --- /dev/null +++ b/.pre-commit-config.yml @@ -0,0 +1,9 @@ +--- + +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.4 + hooks: + - id: ruff-check + args: [--fix] + - id: ruff-format diff --git a/docs/schema/cheatsheet.schema.json b/docs/schema/cheatsheet.schema.json index e255089..a988c01 100644 --- a/docs/schema/cheatsheet.schema.json +++ b/docs/schema/cheatsheet.schema.json @@ -22,6 +22,20 @@ "default": false, "description": "When true, free-form shortcut text is allowed; requires RenderKeys=false." }, + "theme": { + "type": "string", + "minLength": 1, + "default": "catppuccin", + "description": "Theme name: a built-in (e.g. 'catppuccin') or a user theme in the project's themes/ directory. Defaults to 'catppuccin'." + }, + "custom_css": { + "type": "string", + "description": "Filename of a custom CSS file (resolved from the project's styles/ directory), layered after the theme." + }, + "custom_css_inline": { + "type": "string", + "description": "Inline custom CSS, emitted last in the cascade so it overrides the theme and the custom CSS file." + }, "layout": { "type": "object", "additionalProperties": false, diff --git a/pyproject.toml b/pyproject.toml index e087dc5..e323fc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,24 +8,30 @@ dependencies = ["jinja2>=3.1.6", "python-dotenv>=1.1.1", "ruamel-yaml>=0.18.14"] [dependency-groups] dev = [ - "bandit>=1.8.6", + "prek>=0.3.6", "pytest>=8.4.2", - "pytest-cov>=7.0.0", + "pytest-cov>=7.1.0", + "pytest-playwright>=0.7.1", "ruff>=0.14.0", "ty>=0.0.1a22", "vulture>=2.14", ] +[build-system] +requires = ["uv_build>=0.10.9,<0.11.0"] +build-backend = "uv_build" + +[tool.coverage.run] +omit = ["tests/*"] + [tool.ruff] indent-width = 4 line-length = 120 [tool.ruff.lint] -# select = ["ALL"] ignore = [ "ANN", # flake8-annotations "COM", # flake8-commas - "C90", # mccabe complexity "DJ", # django "EXE", # flake8-executable "BLE", # blind except @@ -52,6 +58,24 @@ ignore = [ "TRY", "SIM105", # faster without contextlib ] +extend-select = [ + "F", # Pyflakes rules + "W", # PyCodeStyle warnings + "E", # PyCodeStyle errors + "I", # Sort imports properly + "UP", # Warn if certain things can changed due to newer Python versions + "C4", # Catch incorrect use of comprehensions, dict, list, etc + "FA", # Enforce from __future__ import annotations + "ISC", # Good use of string concatenation + "ICN", # Use common import conventions + "RET", # Good return practices + "SIM", # Common simplification rules + "TID", # Some good import practices + "TC", # Enforce importing certain types in a TYPE_CHECKING block + "PTH", # Use pathlib instead of os.path + "TD", # Be diligent with TODO comments + "C90", +] fixable = ["ALL"] unfixable = [] # Allow unused variables when underscore-prefixed. diff --git a/pytest.ini b/pytest.ini index edcbd06..19f1050 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,9 +3,12 @@ testpaths = tests python_files = test_*.py python_classes = Test* python_functions = test_* -addopts = +addopts = -v --tb=short --strict-markers --disable-warnings + -m "not browser" +markers = + browser: browser-based Playwright tests diff --git a/src/__init__.py b/src/koalakeys/__init__.py similarity index 100% rename from src/__init__.py rename to src/koalakeys/__init__.py diff --git a/src/generate_cheatsheet.py b/src/koalakeys/generate_cheatsheet.py similarity index 65% rename from src/generate_cheatsheet.py rename to src/koalakeys/generate_cheatsheet.py index 3db69e6..3c56305 100644 --- a/src/generate_cheatsheet.py +++ b/src/koalakeys/generate_cheatsheet.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import re import sys @@ -6,9 +8,10 @@ from dotenv import load_dotenv from ruamel.yaml import YAML -from logger import get_logger -from template_renderer import render_template -from validate_yaml import lint_yaml, validate_yaml +from koalakeys.logger import get_logger +from koalakeys.template_renderer import render_template +from koalakeys.theming import ThemeError, resolve_theme, sanitize_css +from koalakeys.validate_yaml import lint_yaml, validate_yaml yaml_safe = YAML(typ="safe") yaml_rw = YAML() @@ -18,22 +21,26 @@ load_dotenv() -BASE_DIR = Path(__file__).parent -PROJECT_ROOT = BASE_DIR.parent +PACKAGE_DIR = Path(__file__).parent +PROJECT_ROOT = PACKAGE_DIR.parent.parent OUTPUT_DIR = Path(os.getenv("CHEATSHEET_OUTPUT_DIR") or PROJECT_ROOT / "output") -TEMPLATES_DIR = BASE_DIR / "templates" -LAYOUTS_DIR = BASE_DIR / "layouts" CHEATSHEETS_DIR = PROJECT_ROOT / "cheatsheets" +THEMES_DIR = PROJECT_ROOT / "themes" +STYLES_DIR = PROJECT_ROOT / "styles" +LAYOUTS_DIR = PACKAGE_DIR / "layouts" OUTPUT_DIR.mkdir(exist_ok=True, parents=True) +layout_file = LAYOUTS_DIR / "keyboard_layouts.yaml" +system_mapping_file = LAYOUTS_DIR / "system_mappings.yaml" + logging = get_logger() def load_yaml(file_path: Path) -> dict | None: try: - with open(file_path, "r", encoding="utf-8") as file: + with file_path.open(encoding="utf-8") as file: return yaml_safe.load(file) except FileNotFoundError: logging.error(f"Error: YAML file '{file_path}' not found.") @@ -44,8 +51,8 @@ def load_yaml(file_path: Path) -> dict | None: def load_layout(): - keyboard_layouts = load_yaml(LAYOUTS_DIR / "keyboard_layouts.yaml") - system_mappings = load_yaml(LAYOUTS_DIR / "system_mappings.yaml") + keyboard_layouts = load_yaml(layout_file) + system_mappings = load_yaml(system_mapping_file) if keyboard_layouts is None or system_mappings is None: logging.error("Failed to load configuration files.") @@ -54,47 +61,51 @@ def load_layout(): return keyboard_layouts, system_mappings -def replace_shortcut_names(shortcut, system_mappings): +def consume_separator(shortcut: str, index: int) -> tuple[str, int]: + current = shortcut[index] + next_index = index + 1 + + if next_index < len(shortcut): + next_char = shortcut[next_index] + if current == "+" and next_char == "+": + return "+", index + 2 + if current == "+" and next_char == ">": + return ">", index + 2 + if current == ">" and next_char == ">": + return ">", index + 2 + if current == ">" and next_char == "+": + return "+", index + 2 + + return ("", index + 1) if current == "+" else ("", index + 1) + + +def format_shortcut_part(part: str, system_mappings: dict) -> str: arrow_key_mappings = {"Up": "↑", "Down": "↓", "Left": "←", "Right": "→"} + + mapped_part = system_mappings.get(part.lower(), part) + if mapped_part in ["⌘", "⌥", "⌃", "⇧"]: + mapped_part = f'{mapped_part}' + + return arrow_key_mappings.get(mapped_part, mapped_part) + + +def replace_shortcut_names(shortcut, system_mappings): try: processed_parts = [] i = 0 shortcut = re.sub(r"(\+|\>)\s*(\+|\>)", r"\g<1>\g<2>", shortcut) while i < len(shortcut): - if shortcut[i] == "+": - if i + 1 < len(shortcut) and shortcut[i + 1] == "+": - processed_parts.append("+") - i += 2 - elif i + 1 < len(shortcut) and shortcut[i + 1] == ">": - processed_parts.append(">") - i += 2 - else: - processed_parts.append("") - i += 1 - elif shortcut[i] == ">": - if i + 1 < len(shortcut) and shortcut[i + 1] == ">": - processed_parts.append(">") - i += 2 - elif i + 1 < len(shortcut) and shortcut[i + 1] == "+": - processed_parts.append("+") - i += 2 - else: - processed_parts.append("") - i += 1 + if shortcut[i] in ("+", ">"): + separator, i = consume_separator(shortcut, i) + processed_parts.append(separator) else: current_part = "" while i < len(shortcut) and shortcut[i] not in ("+", ">"): current_part += shortcut[i] i += 1 if current_part.strip(): - part = current_part.strip() - part = system_mappings.get(part.lower(), part) - if part in ["⌘", "⌥", "⌃", "⇧"]: - part = f'{part}' - - part = arrow_key_mappings.get(part, part) - processed_parts.append(part) + processed_parts.append(format_shortcut_part(current_part.strip(), system_mappings)) return "".join(processed_parts) except Exception as e: @@ -127,6 +138,41 @@ def get_layout_info(data): } +def load_custom_css_file(filename): + if not filename: + return "" + path = STYLES_DIR / filename + try: + return sanitize_css(path.read_text(encoding="utf-8")) + except FileNotFoundError: + logging.error(f"Custom CSS file not found: {path}") + return "" + except OSError as e: + logging.error(f"Error reading custom CSS file '{path}': {e}") + return "" + + +def apply_styling(data): + """Resolve the cheatsheet's theme and custom CSS into render-ready context. + + Returns True on success; False if the theme cannot be resolved. + """ + try: + theme = resolve_theme(data.get("theme"), themes_dir=THEMES_DIR) + except ThemeError as e: + logging.error(f"Theme error: {e}") + return False + + data["theme_token_css"] = theme.render_token_css() + data["theme_font_url"] = theme.font_url + data["theme_default_is_dark"] = theme.default_is_dark + data["theme_both_modes"] = theme.both_modes + data["theme_custom_css"] = theme.custom_css + data["custom_css_file"] = load_custom_css_file(data.get("custom_css")) + data["custom_css_inline"] = sanitize_css(str(data.get("custom_css_inline") or "")) + return True + + def generate_html(data, keyboard_layouts, system_mappings): template_path = "cheatsheets/cheatsheet-template.html" layout_info = get_layout_info(data) @@ -136,6 +182,9 @@ def generate_html(data, keyboard_layouts, system_mappings): data["render_keys"] = data.get("RenderKeys", True) data["allow_text"] = data.get("AllowText", False) + if not apply_styling(data): + return None + return render_template(template_path, data) @@ -159,7 +208,7 @@ def write_html_content(html_output, html_content): try: with open(html_output, "w", encoding="utf-8") as file: file.write(html_content) - except IOError as e: + except OSError as e: logging.error(f"Error writing to output file: {e}") return False return True diff --git a/src/koalakeys/layouts/keyboard_layouts.yaml b/src/koalakeys/layouts/keyboard_layouts.yaml new file mode 100644 index 0000000..9818479 --- /dev/null +++ b/src/koalakeys/layouts/keyboard_layouts.yaml @@ -0,0 +1,54 @@ +--- +US: + layout: + - ["Esc", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"] + - ["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "Backspace"] + - ["Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "[", "]", "\\"] + - ["CapsLock", "A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "'", "Enter"] + - ["Shift", "Z", "X", "C", "V", "B", "N", "M", ",", ".", "/", "Shift"] + - ["Ctrl", "Alt", "Cmd", "Space", "Cmd", "Alt", "Ctrl", "Left", "Up", "Down", "Right"] + +UK: + layout: + - ["Esc", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"] + - ["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "Backspace"] + - ["Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "[", "]", "\\"] + - ["CapsLock", "A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "'", "Enter"] + - ["Shift", "\\", "Z", "X", "C", "V", "B", "N", "M", ",", ".", "/", "Shift"] + - ["Ctrl", "Alt", "Cmd", "Space", "Cmd", "Alt", "Ctrl"] + +DE: + layout: + - ["Esc", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"] + - ["^", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "ß", "´", "Backspace"] + - ["Tab", "Q", "W", "E", "R", "T", "Z", "U", "I", "O", "P", "Ü", "+", "Enter"] + - ["CapsLock", "A", "S", "D", "F", "G", "H", "J", "K", "L", "Ö", "Ä", "#"] + - ["Shift", "Y", "X", "C", "V", "B", "N", "M", ",", ".", "-", "Shift"] + - ["Ctrl", "Alt", "Cmd", "Space", "Cmd", "Alt", "Ctrl"] + +FR: + layout: + - ["Esc", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"] + - ["²", "&", "é", '"', "'", "(", "-", "è", "_", "ç", "à", ")", "=", "Backspace"] + - ["Tab", "A", "Z", "E", "R", "T", "Y", "U", "I", "O", "P", "^", "$", "\\"] + - ["CapsLock", "Q", "S", "D", "F", "G", "H", "J", "K", "L", "M", "ù", "*", "Enter"] + - ["Shift", "W", "X", "C", "V", "B", "N", ",", ";", ":", "!", "Shift"] + - ["Ctrl", "Alt", "Cmd", "Space", "Cmd", "Alt", "Ctrl"] + +ES: + layout: + - ["Esc", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"] + - ["º", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "'", "¡", "Backspace"] + - ["Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "`", "+", "\\"] + - ["CapsLock", "A", "S", "D", "F", "G", "H", "J", "K", "L", "Ñ", "´", "Enter"] + - ["Shift", ">", "Z", "X", "C", "V", "B", "N", "M", ",", ".", "-", "Shift"] + - ["Ctrl", "Alt", "Cmd", "Space", "Cmd", "Alt", "Ctrl"] + +DVORAK: + layout: + - ["Esc", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"] + - ["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "[", "]", "Backspace"] + - ["Tab", "'", ",", ".", "P", "Y", "F", "G", "C", "R", "L", "/", "=", "\\"] + - ["CapsLock", "A", "O", "E", "U", "I", "D", "H", "T", "N", "S", "-", "Enter"] + - ["Shift", ";", "Q", "J", "K", "X", "B", "M", "W", "V", "Z", "Shift"] + - ["Ctrl", "Alt", "Cmd", "Space", "Cmd", "Alt", "Ctrl", "Left", "Up", "Down", "Right"] diff --git a/src/koalakeys/layouts/system_mappings.yaml b/src/koalakeys/layouts/system_mappings.yaml new file mode 100644 index 0000000..03d4a55 --- /dev/null +++ b/src/koalakeys/layouts/system_mappings.yaml @@ -0,0 +1,21 @@ +--- +Darwin: + cmd: "⌘" + alt: "⌥" + ctrl: "⌃" + shift: "Shift" + space: "Space" + +Linux: + cmd: "Super" + alt: "Alt" + ctrl: "Ctrl" + shift: "Shift" + space: "Space" + +Windows: + cmd: "Win" + alt: "Alt" + ctrl: "Ctrl" + shift: "Shift" + space: "Space" diff --git a/src/logger.py b/src/koalakeys/logger.py similarity index 94% rename from src/logger.py rename to src/koalakeys/logger.py index 4a9524e..0102e51 100644 --- a/src/logger.py +++ b/src/koalakeys/logger.py @@ -1,15 +1,16 @@ +from __future__ import annotations + import logging import os import sys from logging.handlers import RotatingFileHandler -from typing import Optional DEFAULT_LOG_FILE = "app.log" DEFAULT_MAX_BYTES = 10 * 1024 * 1024 DEFAULT_BACKUP_COUNT = 5 DEFAULT_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" -_logger: Optional[logging.Logger] = None +_logger: logging.Logger | None = None def setup_logging( diff --git a/src/template_renderer.py b/src/koalakeys/template_renderer.py similarity index 93% rename from src/template_renderer.py rename to src/koalakeys/template_renderer.py index 0c04792..f951b7a 100644 --- a/src/template_renderer.py +++ b/src/koalakeys/template_renderer.py @@ -1,7 +1,9 @@ -from jinja2 import Environment, FileSystemLoader -from logger import get_logger from pathlib import Path +from jinja2 import Environment, FileSystemLoader + +from koalakeys.logger import get_logger + logging = get_logger() diff --git a/src/templates/base.html b/src/koalakeys/templates/base.html similarity index 74% rename from src/templates/base.html rename to src/koalakeys/templates/base.html index 26c756c..3f2b25b 100644 --- a/src/templates/base.html +++ b/src/koalakeys/templates/base.html @@ -4,11 +4,11 @@ - + {% block fonts %}{% endblock %} {% block title %}{% endblock %} {% block page_styles %}{% endblock %} - + {% block content %}{% endblock %} {% block javascript %}{% endblock %} diff --git a/src/koalakeys/templates/cheatsheets/assets/cheatsheets.css b/src/koalakeys/templates/cheatsheets/assets/cheatsheets.css new file mode 100644 index 0000000..d3c5aa9 --- /dev/null +++ b/src/koalakeys/templates/cheatsheets/assets/cheatsheets.css @@ -0,0 +1,468 @@ +/* + * Component stylesheet — consumes only the --kk-* semantic token contract. + * Token VALUES are emitted per-theme by the generator (see koalakeys/themes.py); + * this file never references palette-specific variables or mode overrides. + * Token reference: docs/themes/tokens.md + */ + +body { + font-family: var(--kk-font); + font-size: var(--kk-font-size); + line-height: var(--kk-line-height); + color: var(--kk-text); + max-width: 1400px; + margin: 0 auto; + padding: 0 10px 20px 90px; + background-color: var(--kk-bg); + transition: + background-color 0.3s, + color 0.3s; +} +#content-wrapper { + transition: padding-left 0.3s ease; + padding-left: 0; +} +body.nav-active #content-wrapper { + padding-left: 200px; +} +h1 { + color: var(--kk-accent); + text-align: center; + margin-bottom: 10px; + font-weight: 700; + font-size: 2.2rem; + position: absolute; + left: 50%; + right: 0; + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100vw; + padding: 0; + transform: translateX(-50%); +} +.section { + background-color: var(--kk-surface); + border-radius: var(--kk-radius); + padding: 8px; + margin-bottom: var(--kk-section-gap); + box-shadow: 0 4px 6px var(--kk-shadow); + transition: + background-color 0.3s, + color 0.3s, + box-shadow 0.3s; +} +.section h2 { + color: var(--kk-accent-2); + border-bottom: 1px solid var(--kk-accent-2); + padding-bottom: 5px; + margin-top: 0; + margin-bottom: 10px; + font-weight: 600; + font-size: 1.3rem; +} +.shortcuts-container { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--kk-gap); +} +@media (max-width: 1200px) { + .shortcuts-container { + grid-template-columns: repeat(3, 1fr); + } +} +@media (max-width: 900px) { + .shortcuts-container { + grid-template-columns: repeat(2, 1fr); + } +} +@media (max-width: 600px) { + .shortcuts-container { + grid-template-columns: 1fr; + } +} +.shortcut { + background-color: var(--kk-card); + border-radius: var(--kk-radius); + cursor: pointer; + transition: + background-color 0.3s, + transform 0.2s, + box-shadow 0.2s; + box-shadow: 0 2px 4px var(--kk-shadow); + display: flex; + justify-content: space-between; + align-items: center; + min-height: 40px; + overflow: hidden; +} +.shortcut:hover { + background-color: var(--kk-card-hover); + transform: translateY(-2px); + box-shadow: 0 4px 8px var(--kk-shadow-strong); +} +.shortcut:active { + transform: translateY(0); +} +.shortcut-key { + font-weight: 600; + font-size: 0.95rem; + color: var(--kk-key-text); + font-family: var(--kk-font); + transition: + color 0.3s, + font-size 0.3s; + flex: 1 1 50%; + word-wrap: break-word; + overflow-wrap: break-word; + display: flex; + align-items: center; + height: 100%; +} + +.text-shortcut { + font-size: 0.95rem; + color: var(--kk-text); +} + +.shortcut-key .key-part { + color: var(--kk-key-text); +} + +.shortcut-key .separator { + color: var(--kk-text-muted); + font-weight: normal; + margin-right: 0.3em; + margin-left: 0.3em; +} + +.shortcut-key .sequence-separator { + white-space-collapse: preserve; +} + +.shortcut-key .modifier-symbol { + font-size: 1.5em; + display: inline-block; + vertical-align: middle; +} + +.shortcut-key span:has(> span.modifier-symbol) { + line-height: 1; +} + +.shortcut-description { + font-size: 0.85rem; + text-align: right; + flex: 1 1 50%; + word-wrap: break-word; + overflow-wrap: break-word; + transition: font-size 0.3s; + display: flex; + align-items: center; + justify-content: flex-end; + height: 100%; +} +@media (max-width: 600px) { + .shortcuts-container { + grid-template-columns: 1fr; + } +} + +/* Keyboard styles */ +#keyboard-container { + display: flex; + justify-content: center; + margin-bottom: 25px; +} +#keyboard { + width: 100%; + max-width: 1000px; + margin: 0 auto; + border-radius: 10px; + background: var(--kk-keyboard-bg); + padding: 5px; + transition: + background-color 0.3s, + box-shadow 0.3s; + box-shadow: 0 6px 12px var(--kk-shadow); +} +.row { + display: flex; + justify-content: space-between; + margin-bottom: 5px; +} +.key { + width: var(--kk-key-size); + height: var(--kk-key-size); + background: var(--kk-key-bg); + border: 1px solid var(--kk-border); + border-radius: calc(var(--kk-radius) - 2px); + display: flex; + justify-content: center; + align-items: center; + font-size: 14px; + transition: + all 0.2s, + background-color 0.3s, + color 0.3s, + border-color 0.3s, + box-shadow 0.3s; + color: var(--kk-text); + box-shadow: 0 2px 4px var(--kk-shadow); + cursor: pointer; + flex-grow: 1; + margin-right: 4px; +} +.key:last-child { + margin-right: 0; +} +.key.active, +.key.active-step-1 { + background: var(--kk-step-1); + color: var(--kk-step-text); + box-shadow: 0 0 8px var(--kk-step-1); +} +.key.active-step-2 { + background: var(--kk-step-2); + color: var(--kk-step-text); + box-shadow: 0 0 8px var(--kk-step-2); +} +.key.active-step-3 { + background: var(--kk-step-3); + color: var(--kk-step-text); + box-shadow: 0 0 8px var(--kk-step-3); +} +.key.active-step-4 { + background: var(--kk-step-4); + color: var(--kk-step-text); + box-shadow: 0 0 8px var(--kk-step-4); +} +.key.active-step-5 { + background: var(--kk-step-5); + color: var(--kk-step-text); + box-shadow: 0 0 8px var(--kk-step-5); +} +/* Default style for steps greater than 5 */ +.key[class*="active-step-"]:not(.active-step-1):not(.active-step-2):not( + .active-step-3 + ):not(.active-step-4):not(.active-step-5) { + background: var(--kk-step-overflow); + color: var(--kk-step-text); + box-shadow: 0 0 8px var(--kk-step-overflow); +} +.key__wide { + width: 75px; +} +.key__wider { + width: 90px; +} +.key__widest { + width: 110px; +} +.key__spacebar { + width: 300px; +} +.key__enter { + width: 112px; +} +.key__shift-left { + width: 100px; +} +.key__backspace { + width: 100px; +} +.key__tab { + width: 75px; +} +.key__caps { + width: 90px; +} +.key__right-shift { + width: 125px; +} +.key__bottom-funct { + width: 70px; +} +.key__arrow { + width: 40px; + font-size: 18px; +} +.key-stack { + display: flex; + flex-direction: column; + width: 40px; +} +.key__arrow-half { + height: 25px; + font-size: 14px; +} +.wide { + width: 65px; +} +.wider { + width: 85px; +} +.widest { + width: 110px; +} + +.layout-info { + text-align: center; + margin-bottom: 20px; + font-size: 1.1rem; + color: var(--kk-text-muted); +} + +.top-content { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 10px; + margin-bottom: 5px; + width: 100%; + position: relative; + box-sizing: border-box; + z-index: 1000; +} + +.search-container { + flex: 0 1 200px; + margin: 0 20px; + transition: margin-left 0.3s ease; +} + +body.nav-active .search-container { + margin-left: 220px; +} + +#shortcut-search { + width: 100%; + padding: 10px; + font-size: 16px; + border: 2px solid var(--kk-border-input); + border-radius: var(--kk-radius); + background-color: var(--kk-bg); + color: var(--kk-text); + transition: + border-color 0.3s, + background-color 0.3s, + color 0.3s, + box-shadow 0.3s, + width 0.3s ease; + box-sizing: border-box; + position: relative; + z-index: 1001; +} + +body.nav-active #shortcut-search { + width: calc(100% - 200px); +} + +#shortcut-search:focus { + outline: none; + border-color: var(--kk-accent-2); + box-shadow: 0 0 5px var(--kk-accent-2); +} + +#content-wrapper { + padding-top: 10px; + max-width: var(--kk-max-width); + margin-left: auto; + margin-right: auto; + transition: padding-left 0.3s ease; +} + +body.nav-active #content-wrapper { + padding-left: 200px; +} + +#dark-mode-toggle { + flex: 0 0 auto; + position: relative; + z-index: 1002; + background: none; + border: none; + cursor: pointer; + font-size: 24px; + color: var(--kk-text); + transition: color 0.3s; +} + +#category-nav-toggle { + flex: 0 0 auto; +} +#category-nav-toggle { + position: fixed; + top: 20px; + left: 10px; + z-index: 1000; + background: var(--kk-card-hover); + border: none; + border-radius: 50%; + width: 40px; + height: 40px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 5px var(--kk-shadow-strong); + transition: + background-color 0.3s, + transform 0.2s, + left 0.3s; +} +#category-nav-toggle:hover { + transform: scale(1.1); +} +#category-nav-toggle svg { + transition: transform 0.3s; +} +#category-nav-toggle.active svg { + transform: rotate(90deg); +} +#category-nav-toggle.active { + left: 270px; +} +#category-nav { + position: fixed; + top: 0; + left: -200px; + width: 200px; + height: 100vh; + background: var(--kk-surface); + overflow-y: auto; + transition: left 0.3s ease; + z-index: 998; + padding: 70px 15px 20px; + box-shadow: 2px 0 5px var(--kk-shadow); +} +#category-nav.active { + left: 0; +} +#category-nav ul { + display: none; +} +#category-nav.active ul { + display: block; +} +#category-nav-toggle { + z-index: 999; +} +#category-nav ul { + list-style-type: none; + padding: 0; + margin: 0; +} +#category-nav li { + margin-bottom: 10px; +} +#category-nav a { + color: var(--kk-text); + text-decoration: none; + font-size: 1rem; + transition: color 0.2s; +} +#category-nav a:hover { + color: var(--kk-accent-2); +} diff --git a/src/koalakeys/templates/cheatsheets/cheatsheet-template.html b/src/koalakeys/templates/cheatsheets/cheatsheet-template.html new file mode 100644 index 0000000..b9aee56 --- /dev/null +++ b/src/koalakeys/templates/cheatsheets/cheatsheet-template.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} + +{% block title %}{{ title }} Cheatsheet{% endblock %} + +{% block body_class %}{% if theme_default_is_dark %}dark-mode{% endif %}{% endblock %} + +{% block fonts %}{% if theme_font_url %}{% endif %}{% endblock %} + +{% block page_styles %} + +{% endblock %} + + +{% block content %} +{% include 'cheatsheets/components/body.html' %} +{% endblock %} + +{% block javascript %} + +{% endblock %} diff --git a/src/templates/cheatsheets/components/body.html b/src/koalakeys/templates/cheatsheets/components/body.html similarity index 99% rename from src/templates/cheatsheets/components/body.html rename to src/koalakeys/templates/cheatsheets/components/body.html index db6d7ff..7e1f06f 100644 --- a/src/templates/cheatsheets/components/body.html +++ b/src/koalakeys/templates/cheatsheets/components/body.html @@ -18,11 +18,13 @@

{{ title }}

+ {% if theme_both_modes %} + {% endif %}