Major overhaul to the theme and now functioning.

This commit is contained in:
2026-01-05 16:07:30 -05:00
parent 5823ca3c39
commit a475dfeab6
5218 changed files with 1643729 additions and 13229 deletions
@@ -0,0 +1,19 @@
// Types
export declare const MODIFIERS: string[];
/**
* Splits a single combination string into individual key parts.
*
* A combination is a set of keys that must be pressed simultaneously.
* e.g. `ctrl+k`, `shift--`
*/
export declare function splitKeyCombination(combination: string, isInternal?: boolean): {
keys: string[];
separators: string[];
};
/**
* Splits a hotkey string into its constituent combination groups.
*
* A sequence is a series of combinations that must be pressed in order.
* e.g. `a-b`, `ctrl+k-p`
*/
export declare function splitKeySequence(str: string): string[];
@@ -0,0 +1,164 @@
// Utilities
import { normalizeKey } from "./key-aliases.js";
import { consoleWarn } from "../../util/console.js"; // Types
export const MODIFIERS = ['ctrl', 'shift', 'alt', 'meta', 'cmd'];
/**
* Splits a single combination string into individual key parts.
*
* A combination is a set of keys that must be pressed simultaneously.
* e.g. `ctrl+k`, `shift--`
*/
export function splitKeyCombination(combination) {
let isInternal = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
const emptyResult = {
keys: [],
separators: []
};
if (!combination) {
if (!isInternal) consoleWarn('Invalid hotkey combination: empty string provided');
return emptyResult;
}
// --- VALIDATION ---
const hasInvalidLeadingSeparator = combination.length > 1 &&
// Starts with a single separator followed by a non-separator character (e.g. '+a', '_a')
['+', '/', '_'].some(v => combination.startsWith(v)) && !['++', '//', '__'].some(v => combination.startsWith(v));
const hasInvalidStructure = hasInvalidLeadingSeparator ||
// Disallow literal + or _ keys (they require shift)
combination.includes('++') || combination.includes('__') || combination === '+' || combination === '_' ||
// Ends with a separator that is not part of a doubled literal
combination.length > 1 && (combination.endsWith('+') || combination.endsWith('_')) && combination.at(-2) !== combination.at(-1) ||
// Stand-alone doubled separators (dangling)
combination === '++' || combination === '--' || combination === '__';
if (hasInvalidStructure) {
if (!isInternal) consoleWarn(`Invalid hotkey combination: "${combination}" has invalid structure`);
return emptyResult;
}
const keys = [];
const separators = [];
let buffer = '';
const flushBuffer = separator => {
if (buffer) {
separator && separators.push(separator);
keys.push(normalizeKey(buffer));
buffer = '';
}
};
for (let i = 0; i < combination.length; i++) {
const char = combination[i];
const nextChar = combination[i + 1];
if (['+', '/', '_', '-'].includes(char)) {
if (char === nextChar) {
flushBuffer(char);
keys.push(char);
i++;
} else if (['+', '/', '_'].includes(char)) {
flushBuffer(char);
} else {
buffer += char;
}
} else {
buffer += char;
}
}
flushBuffer();
// Within a combination, `-` is only valid as a literal key (e.g., `ctrl+-`).
// `-` cannot be part of a longer key name within a combination.
const hasInvalidMinus = keys.some(key => key.length > 1 && key.includes('-') && key !== '--');
if (hasInvalidMinus) {
if (!isInternal) consoleWarn(`Invalid hotkey combination: "${combination}" has invalid structure`);
return emptyResult;
}
if (keys.length === 0 && combination) {
return {
keys: [normalizeKey(combination)],
separators
};
}
return {
keys,
separators
};
}
/**
* Splits a hotkey string into its constituent combination groups.
*
* A sequence is a series of combinations that must be pressed in order.
* e.g. `a-b`, `ctrl+k-p`
*/
export function splitKeySequence(str) {
if (!str) {
consoleWarn('Invalid hotkey sequence: empty string provided');
return [];
}
// A sequence is invalid if it starts or ends with a separator,
// unless it is part of a combination (e.g., `shift+-`).
const hasInvalidStart = str.startsWith('-') && !['---', '--+'].includes(str);
const hasInvalidEnd = str.endsWith('-') && !str.endsWith('+-') && !str.endsWith('_-') && str !== '-' && str !== '---';
if (hasInvalidStart || hasInvalidEnd) {
consoleWarn(`Invalid hotkey sequence: "${str}" contains invalid combinations`);
return [];
}
const result = [];
let buffer = '';
let i = 0;
while (i < str.length) {
const char = str[i];
if (char === '-') {
// Determine if this hyphen is part of the current combination
const prevChar = str[i - 1];
const prevPrevChar = i > 1 ? str[i - 2] : undefined;
const precededBySeparator = ['+', '_'].includes(prevChar) && !['+', '/'].includes(prevPrevChar ?? '');
if (precededBySeparator) {
// Treat as part of the combination (e.g., 'ctrl+-')
buffer += char;
i++;
} else {
// Treat as sequence separator
if (buffer) {
result.push(buffer);
buffer = '';
} else {
// Empty buffer means we have a literal '-' key
result.push('-');
}
i++;
}
} else {
buffer += char;
i++;
}
}
// Add final buffer if it exists
if (buffer) {
result.push(buffer);
}
// Collapse runs of '-' so that every second '-' is removed
const collapsed = [];
let minusCount = 0;
for (const part of result) {
if (part === '-') {
if (minusCount % 2 === 0) collapsed.push('-');
minusCount++;
} else {
minusCount = 0;
collapsed.push(part);
}
}
// Validate that each part of the sequence is a valid combination
const areAllValid = collapsed.every(s => splitKeyCombination(s, true).keys.length > 0);
if (!areAllValid) {
consoleWarn(`Invalid hotkey sequence: "${str}" contains invalid combinations`);
return [];
}
return collapsed;
}
//# sourceMappingURL=hotkey-parsing.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
// Types
import type { MaybeRef } from '../../util/index.js';
interface HotkeyOptions {
event?: MaybeRef<'keydown' | 'keyup'>;
inputs?: MaybeRef<boolean>;
preventDefault?: MaybeRef<boolean>;
sequenceTimeout?: MaybeRef<number>;
}
export declare function useHotkey(keys: MaybeRef<string | undefined>, callback: (e: KeyboardEvent) => void, options?: HotkeyOptions): () => void;
@@ -0,0 +1,114 @@
// Composables
import { splitKeyCombination, splitKeySequence } from "./hotkey-parsing.js"; // Utilities
import { onScopeDispose, toValue, watch } from 'vue';
import { IN_BROWSER } from "../../util/index.js"; // Types
export function useHotkey(keys, callback) {
let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
if (!IN_BROWSER) return function () {};
const {
event = 'keydown',
inputs = false,
preventDefault = true,
sequenceTimeout = 1000
} = options;
const isMac = navigator?.userAgent?.includes('Macintosh') ?? false;
let timeout = 0;
let keyGroups;
let isSequence = false;
let groupIndex = 0;
function isInputFocused() {
if (toValue(inputs)) return false;
const activeElement = document.activeElement;
return activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable || activeElement.contentEditable === 'true');
}
function resetSequence() {
groupIndex = 0;
clearTimeout(timeout);
}
function handler(e) {
const group = keyGroups[groupIndex];
if (!group || isInputFocused()) return;
if (!matchesKeyGroup(e, group, isMac)) {
if (isSequence) resetSequence();
return;
}
if (toValue(preventDefault)) e.preventDefault();
if (!isSequence) {
callback(e);
return;
}
clearTimeout(timeout);
groupIndex++;
if (groupIndex === keyGroups.length) {
callback(e);
resetSequence();
return;
}
timeout = window.setTimeout(resetSequence, toValue(sequenceTimeout));
}
function cleanup() {
window.removeEventListener(toValue(event), handler);
clearTimeout(timeout);
}
watch(() => toValue(keys), newKeys => {
cleanup();
if (newKeys) {
const groups = splitKeySequence(newKeys.toLowerCase());
isSequence = groups.length > 1;
keyGroups = groups;
resetSequence();
window.addEventListener(toValue(event), handler);
}
}, {
immediate: true
});
// Watch for changes in the event type to re-register the listener
watch(() => toValue(event), (newEvent, oldEvent) => {
if (oldEvent && keyGroups && keyGroups.length > 0) {
window.removeEventListener(oldEvent, handler);
window.addEventListener(newEvent, handler);
}
});
onScopeDispose(cleanup, true);
return cleanup;
}
function matchesKeyGroup(e, group, isMac) {
const {
modifiers,
actualKey
} = parseKeyGroup(group);
const expectCtrl = modifiers.ctrl || !isMac && (modifiers.cmd || modifiers.meta);
const expectMeta = isMac && (modifiers.cmd || modifiers.meta);
return e.ctrlKey === expectCtrl && e.metaKey === expectMeta && e.shiftKey === modifiers.shift && e.altKey === modifiers.alt && e.key.toLowerCase() === actualKey?.toLowerCase();
}
function parseKeyGroup(group) {
const MODIFIERS = ['ctrl', 'shift', 'alt', 'meta', 'cmd'];
// Use the shared combination splitting logic
const {
keys: parts
} = splitKeyCombination(group.toLowerCase());
// If the combination is invalid, return empty result
if (parts.length === 0) {
return {
modifiers: Object.fromEntries(MODIFIERS.map(m => [m, false])),
actualKey: undefined
};
}
const modifiers = Object.fromEntries(MODIFIERS.map(m => [m, false]));
let actualKey;
for (const part of parts) {
if (MODIFIERS.includes(part)) {
modifiers[part] = true;
} else {
actualKey = part;
}
}
return {
modifiers,
actualKey
};
}
//# sourceMappingURL=hotkey.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
export { useHotkey } from './hotkey.js';
@@ -0,0 +1,2 @@
export { useHotkey } from "./hotkey.js";
//# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
{"version":3,"file":"index.js","names":["useHotkey"],"sources":["../../../src/composables/hotkey/index.ts"],"sourcesContent":["export { useHotkey } from './hotkey'\n"],"mappings":"SAASA,SAAS","ignoreList":[]}
@@ -0,0 +1,14 @@
/**
* Centralized key alias mapping for consistent key normalization across the hotkey system.
*
* This maps various user-friendly aliases to canonical key names that match
* KeyboardEvent.key values (in lowercase) where possible.
*/
export declare const keyAliasMap: Record<string, string>;
/**
* Normalizes a key string to its canonical form using the alias map.
*
* @param key - The key string to normalize
* @returns The canonical key name in lowercase
*/
export declare function normalizeKey(key: string): string;
@@ -0,0 +1,38 @@
/**
* Centralized key alias mapping for consistent key normalization across the hotkey system.
*
* This maps various user-friendly aliases to canonical key names that match
* KeyboardEvent.key values (in lowercase) where possible.
*/
export const keyAliasMap = {
// Modifier aliases (from vue-use, other libraries, and current implementation)
control: 'ctrl',
command: 'cmd',
option: 'alt',
// Arrow key aliases (common abbreviations)
up: 'arrowup',
down: 'arrowdown',
left: 'arrowleft',
right: 'arrowright',
// Other common key aliases
esc: 'escape',
spacebar: ' ',
space: ' ',
return: 'enter',
del: 'delete',
// Symbol aliases (existing from hotkey-parsing.ts)
minus: '-',
hyphen: '-'
};
/**
* Normalizes a key string to its canonical form using the alias map.
*
* @param key - The key string to normalize
* @returns The canonical key name in lowercase
*/
export function normalizeKey(key) {
const lowerKey = key.toLowerCase();
return keyAliasMap[lowerKey] || lowerKey;
}
//# sourceMappingURL=key-aliases.js.map
@@ -0,0 +1 @@
{"version":3,"file":"key-aliases.js","names":["keyAliasMap","control","command","option","up","down","left","right","esc","spacebar","space","return","del","minus","hyphen","normalizeKey","key","lowerKey","toLowerCase"],"sources":["../../../src/composables/hotkey/key-aliases.ts"],"sourcesContent":["/**\n * Centralized key alias mapping for consistent key normalization across the hotkey system.\n *\n * This maps various user-friendly aliases to canonical key names that match\n * KeyboardEvent.key values (in lowercase) where possible.\n */\nexport const keyAliasMap: Record<string, string> = {\n // Modifier aliases (from vue-use, other libraries, and current implementation)\n control: 'ctrl',\n command: 'cmd',\n option: 'alt',\n\n // Arrow key aliases (common abbreviations)\n up: 'arrowup',\n down: 'arrowdown',\n left: 'arrowleft',\n right: 'arrowright',\n\n // Other common key aliases\n esc: 'escape',\n spacebar: ' ',\n space: ' ',\n return: 'enter',\n del: 'delete',\n\n // Symbol aliases (existing from hotkey-parsing.ts)\n minus: '-',\n hyphen: '-',\n}\n\n/**\n * Normalizes a key string to its canonical form using the alias map.\n *\n * @param key - The key string to normalize\n * @returns The canonical key name in lowercase\n */\nexport function normalizeKey (key: string): string {\n const lowerKey = key.toLowerCase()\n return keyAliasMap[lowerKey] || lowerKey\n}\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMA,WAAmC,GAAG;EACjD;EACAC,OAAO,EAAE,MAAM;EACfC,OAAO,EAAE,KAAK;EACdC,MAAM,EAAE,KAAK;EAEb;EACAC,EAAE,EAAE,SAAS;EACbC,IAAI,EAAE,WAAW;EACjBC,IAAI,EAAE,WAAW;EACjBC,KAAK,EAAE,YAAY;EAEnB;EACAC,GAAG,EAAE,QAAQ;EACbC,QAAQ,EAAE,GAAG;EACbC,KAAK,EAAE,GAAG;EACVC,MAAM,EAAE,OAAO;EACfC,GAAG,EAAE,QAAQ;EAEb;EACAC,KAAK,EAAE,GAAG;EACVC,MAAM,EAAE;AACV,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,YAAYA,CAAEC,GAAW,EAAU;EACjD,MAAMC,QAAQ,GAAGD,GAAG,CAACE,WAAW,CAAC,CAAC;EAClC,OAAOlB,WAAW,CAACiB,QAAQ,CAAC,IAAIA,QAAQ;AAC1C","ignoreList":[]}