routie dev init since i didn't adhere to any proper guidance up until now

This commit is contained in:
2026-04-29 22:27:29 -06:00
commit e1dabb71e2
15301 changed files with 3562618 additions and 0 deletions
+233
View File
@@ -0,0 +1,233 @@
@layer vuetify-components {
.v-hotkey {
align-items: center;
display: inline-flex;
gap: 4px;
vertical-align: middle;
line-height: 1.5;
}
.v-hotkey--disabled {
opacity: 0.26;
}
.v-hotkey--inline {
max-height: 1lh;
vertical-align: baseline;
font-size: 1em;
line-height: inherit;
}
.v-hotkey__prefix {
opacity: var(--v-medium-emphasis-opacity);
font-weight: normal;
vertical-align: baseline;
}
.v-hotkey__suffix {
opacity: var(--v-medium-emphasis-opacity);
font-weight: normal;
vertical-align: baseline;
}
.v-hotkey--contained .v-hotkey__contained-wrapper {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 0.2rem 4px;
box-sizing: border-box;
background: unset;
box-shadow: unset;
min-height: 1em;
font-size: 0.75rem;
line-height: 1.5;
}
.v-hotkey--contained .v-hotkey__contained-wrapper .v-hotkey__prefix, .v-hotkey--contained .v-hotkey__contained-wrapper .v-hotkey__suffix {
opacity: var(--v-high-emphasis-opacity);
}
.v-hotkey--contained .v-hotkey__contained-wrapper .v-hotkey__prefix {
margin-right: 2px;
}
.v-hotkey--contained .v-hotkey__contained-wrapper .v-hotkey__suffix {
margin-left: 2px;
}
.v-hotkey--contained .v-hotkey__divider {
opacity: var(--v-medium-emphasis-opacity);
font-size: inherit;
}
.v-hotkey--contained .v-hotkey__combination {
display: inline-flex;
align-items: center;
gap: 2px;
}
.v-hotkey--contained.v-hotkey--inline .v-hotkey__contained-wrapper.v-kbd {
align-self: baseline;
align-items: baseline;
font-size: 1em;
line-height: inherit;
padding: 1px 4px 0;
gap: 2px;
margin-left: 0;
margin-right: 0;
}
.v-hotkey--contained.v-hotkey--inline .v-hotkey__divider {
font-size: 1em;
align-self: baseline;
}
.v-hotkey--contained.v-hotkey--inline .v-hotkey__combination {
gap: 2px;
align-items: baseline;
}
.v-hotkey__key.v-kbd {
min-height: unset;
font-size: 0.75rem;
line-height: 1.5;
padding: 0.2rem;
min-width: 1.5em;
}
.v-hotkey__key.v-kbd--variant-plain, .v-hotkey__key.v-kbd--variant-outlined, .v-hotkey__key.v-kbd--variant-text, .v-hotkey__key.v-kbd--variant-tonal {
background: transparent;
color: inherit;
}
.v-hotkey__key.v-kbd--variant-plain {
opacity: 0.62;
}
.v-hotkey__key.v-kbd--variant-plain:focus, .v-hotkey__key.v-kbd--variant-plain:hover {
opacity: 1;
}
.v-hotkey__key.v-kbd--variant-plain .v-hotkey__overlay {
display: none;
}
.v-hotkey__key.v-kbd--variant-elevated, .v-hotkey__key.v-kbd--variant-flat {
background: rgb(var(--v-theme-surface));
color: color-mix(in srgb, rgb(var(--v-theme-on-surface)) calc(var(--v-high-emphasis-opacity) * 100%), transparent);
}
.v-hotkey__key.v-kbd--variant-elevated {
box-shadow: 0px 1px 2px 0px rgba(var(--v-shadow-color), var(--v-shadow-key-opacity, 0.3)), 0px 1px 3px 1px rgba(var(--v-shadow-color), var(--v-shadow-ambient-opacity, 0.15));
--v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 2%, transparent);
}
.v-hotkey__key.v-kbd--variant-flat {
box-shadow: 0px 0px 0px 0px rgba(var(--v-shadow-color), var(--v-shadow-key-opacity, 0.3)), 0px 0px 0px 0px rgba(var(--v-shadow-color), var(--v-shadow-ambient-opacity, 0.15));
--v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 0%, transparent);
}
.v-hotkey__key.v-kbd--variant-outlined {
border: thin solid currentColor;
}
.v-hotkey__key.v-kbd--variant-text .v-hotkey__overlay {
background: currentColor;
}
.v-hotkey__key.v-kbd--variant-tonal .v-hotkey__underlay {
background: currentColor;
opacity: var(--v-activated-opacity);
border-radius: inherit;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
}
.v-hotkey__key.v-kbd .v-hotkey__underlay {
position: absolute;
}
.v-hotkey__key-symbol.v-kbd {
line-height: normal;
font-size: 1em;
}
.v-hotkey__key-icon .v-icon {
min-width: unset;
}
.v-hotkey__key-icon .v-icon:has(svg) {
max-width: 0.75rem;
}
.v-hotkey__key-icon .v-icon:not(:has(svg)) {
font-size: 0.75rem;
line-height: inherit;
height: inherit;
}
.v-hotkey__key--nested {
background: none;
border: none;
padding: 0;
margin: 0;
font: inherit;
color: inherit;
display: inline-flex;
align-items: center;
min-width: auto;
min-height: auto;
align-self: baseline;
}
.v-hotkey__key--nested.v-hotkey__key-icon {
align-self: center;
}
.v-hotkey__divider {
align-items: center;
display: inline-flex;
opacity: var(--v-medium-emphasis-opacity);
font-size: 1em;
}
.v-hotkey__combination {
display: flex;
gap: 2px;
}
.v-hotkey--inline .v-hotkey__key {
align-self: center;
padding: 1px 4px 0;
min-width: 20px;
height: calc(1lh - 2px);
line-height: calc(1lh - 2px);
}
.v-hotkey--inline .v-hotkey__key-icon.v-kbd {
padding-block: 0;
}
.v-hotkey--inline .v-hotkey__key-icon .v-icon {
width: min-content;
min-width: fit-content;
max-height: calc(1ex + 2px);
}
.v-hotkey--inline .v-hotkey__key-icon .v-icon .v-icon__svg {
height: 100%;
width: unset;
}
.v-hotkey--inline .v-hotkey__combination {
align-items: baseline;
gap: 1px;
}
.v-hotkey--inline .v-hotkey__divider {
font-size: 1em;
}
.v-hotkey--inline .v-hotkey__prefix, .v-hotkey--inline .v-hotkey__suffix {
align-self: baseline;
font-size: inherit;
}
.v-hotkey--variant-elevated .v-hotkey__key.v-kbd {
box-shadow: 0px 1px 2px 0px rgba(var(--v-shadow-color), var(--v-shadow-key-opacity, 0.3)), 0px 1px 3px 1px rgba(var(--v-shadow-color), var(--v-shadow-ambient-opacity, 0.15));
--v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 2%, transparent);
}
.v-hotkey--variant-flat .v-hotkey__key.v-kbd {
box-shadow: none;
}
.v-hotkey--variant-outlined .v-hotkey__key.v-kbd {
background: none;
}
.v-hotkey--variant-outlined .v-hotkey__key.v-kbd {
box-shadow: 0px 0px 0px 0px rgba(var(--v-shadow-color), var(--v-shadow-key-opacity, 0.3)), 0px 0px 0px 0px rgba(var(--v-shadow-color), var(--v-shadow-ambient-opacity, 0.15));
--v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 0%, transparent);
}
}
@layer vuetify-overrides {
.v-hotkey--variant-text .v-hotkey__key.v-kbd {
background: transparent;
border: none;
padding-inline: 0;
min-width: auto;
}
.v-hotkey--variant-text .v-hotkey__key.v-kbd {
box-shadow: 0px 0px 0px 0px rgba(var(--v-shadow-color), var(--v-shadow-key-opacity, 0.3)), 0px 0px 0px 0px rgba(var(--v-shadow-color), var(--v-shadow-ambient-opacity, 0.15));
--v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 0%, transparent);
}
}
@layer vuetify-components {
.v-hotkey--variant-text .v-hotkey__combination {
gap: 1px;
}
.v-hotkey--variant-tonal .v-hotkey__key.v-kbd {
border: unset;
box-shadow: unset;
}
}
+398
View File
@@ -0,0 +1,398 @@
/**
* VHotkey Component
*
* Purpose: Renders keyboard shortcuts in a visually consistent and accessible way.
* This component handles the complex logic of displaying keyboard combinations
* across different platforms (Mac vs PC) and display modes (icons, symbols, text).
*
* Why it exists:
* - Provides consistent visual representation of keyboard shortcuts
* - Handles platform-specific key differences (Cmd vs Ctrl, Option vs Alt)
* - Supports multiple display modes for different design needs
* - Encapsulates complex key parsing and rendering logic
* - Used throughout the command palette for instruction display
*
* Key Mapping Structure:
* The keyMap uses a simple object structure where each key has:
* - `default`: Required configuration for all platforms
* - `mac`: Optional Mac-specific overrides
* Each config can specify `symbol`, `icon`, and `text` representations.
*
* Example:
* ```
* ctrl: {
* mac: { symbol: '⌃', icon: '$ctrl', text: 'Control' },
* default: { text: 'Ctrl', icon: '$ctrl' }
* }
* ```
*/
import type { PropType } from 'vue';
type DisplayMode = 'icon' | 'symbol' | 'text';
type HotkeyVariant = 'elevated' | 'flat' | 'tonal' | 'outlined' | 'text' | 'plain' | 'contained';
type KeyConfig = {
symbol?: string;
icon?: string;
text: string;
};
type PlatformKeyConfig = {
mac?: KeyConfig;
default: KeyConfig;
};
type KeyMapConfig = Record<string, PlatformKeyConfig>;
export declare const hotkeyMap: KeyMapConfig;
export declare const makeVHotkeyProps: <Defaults extends {
theme?: unknown;
class?: unknown;
style?: unknown;
border?: unknown;
elevation?: unknown;
rounded?: unknown;
tile?: unknown;
keys?: unknown;
displayMode?: unknown;
keyMap?: unknown;
platform?: unknown;
inline?: unknown;
disabled?: unknown;
prefix?: unknown;
suffix?: unknown;
variant?: unknown;
color?: unknown;
} = {}>(defaults?: Defaults | undefined) => {
theme: unknown extends Defaults["theme"] ? StringConstructor : {
type: PropType<unknown extends Defaults["theme"] ? string : string | Defaults["theme"]>;
default: unknown extends Defaults["theme"] ? string : string | Defaults["theme"];
};
class: unknown extends Defaults["class"] ? PropType<any> : {
type: PropType<unknown extends Defaults["class"] ? any : any>;
default: unknown extends Defaults["class"] ? any : any;
};
style: unknown extends Defaults["style"] ? {
type: PropType<import("vue").StyleValue>;
default: null;
} : Omit<{
type: PropType<import("vue").StyleValue>;
default: null;
}, "default" | "type"> & {
type: PropType<unknown extends Defaults["style"] ? import("vue").StyleValue : Defaults["style"] | import("vue").StyleValue>;
default: unknown extends Defaults["style"] ? import("vue").StyleValue : Defaults["style"] | NonNullable<import("vue").StyleValue>;
};
border: unknown extends Defaults["border"] ? (BooleanConstructor | NumberConstructor | StringConstructor)[] : {
type: PropType<unknown extends Defaults["border"] ? string | number | boolean : string | number | boolean | Defaults["border"]>;
default: unknown extends Defaults["border"] ? string | number | boolean : Defaults["border"] | NonNullable<string | number | boolean>;
};
elevation: unknown extends Defaults["elevation"] ? {
type: (NumberConstructor | StringConstructor)[];
validator: (value: string | number) => boolean;
} : Omit<{
type: (NumberConstructor | StringConstructor)[];
validator: (value: string | number) => boolean;
}, "default" | "type"> & {
type: PropType<unknown extends Defaults["elevation"] ? string | number : string | number | Defaults["elevation"]>;
default: unknown extends Defaults["elevation"] ? string | number : Defaults["elevation"] | NonNullable<string | number>;
};
rounded: unknown extends Defaults["rounded"] ? {
type: (BooleanConstructor | NumberConstructor | StringConstructor)[];
default: undefined;
} : Omit<{
type: (BooleanConstructor | NumberConstructor | StringConstructor)[];
default: undefined;
}, "default" | "type"> & {
type: PropType<unknown extends Defaults["rounded"] ? string | number | boolean : string | number | boolean | Defaults["rounded"]>;
default: unknown extends Defaults["rounded"] ? string | number | boolean : Defaults["rounded"] | NonNullable<string | number | boolean>;
};
tile: unknown extends Defaults["tile"] ? BooleanConstructor : {
type: PropType<unknown extends Defaults["tile"] ? boolean : boolean | Defaults["tile"]>;
default: unknown extends Defaults["tile"] ? boolean : boolean | Defaults["tile"];
};
keys: unknown extends Defaults["keys"] ? StringConstructor : {
type: PropType<unknown extends Defaults["keys"] ? string : string | Defaults["keys"]>;
default: unknown extends Defaults["keys"] ? string : string | Defaults["keys"];
};
displayMode: unknown extends Defaults["displayMode"] ? {
type: PropType<DisplayMode>;
default: string;
} : Omit<{
type: PropType<DisplayMode>;
default: string;
}, "default" | "type"> & {
type: PropType<unknown extends Defaults["displayMode"] ? DisplayMode : Defaults["displayMode"] | DisplayMode>;
default: unknown extends Defaults["displayMode"] ? DisplayMode : Defaults["displayMode"] | NonNullable<DisplayMode>;
};
keyMap: unknown extends Defaults["keyMap"] ? {
type: PropType<KeyMapConfig>;
default: () => KeyMapConfig;
} : Omit<{
type: PropType<KeyMapConfig>;
default: () => KeyMapConfig;
}, "default" | "type"> & {
type: PropType<unknown extends Defaults["keyMap"] ? KeyMapConfig : KeyMapConfig | Defaults["keyMap"]>;
default: unknown extends Defaults["keyMap"] ? KeyMapConfig : KeyMapConfig | Defaults["keyMap"];
};
platform: unknown extends Defaults["platform"] ? {
type: PropType<'auto' | 'pc' | 'mac'>;
default: string;
} : Omit<{
type: PropType<'auto' | 'pc' | 'mac'>;
default: string;
}, "default" | "type"> & {
type: PropType<unknown extends Defaults["platform"] ? "auto" | "mac" | "pc" : "auto" | "mac" | "pc" | Defaults["platform"]>;
default: unknown extends Defaults["platform"] ? "auto" | "mac" | "pc" : Defaults["platform"] | NonNullable<"auto" | "mac" | "pc">;
};
inline: unknown extends Defaults["inline"] ? BooleanConstructor : {
type: PropType<unknown extends Defaults["inline"] ? boolean : boolean | Defaults["inline"]>;
default: unknown extends Defaults["inline"] ? boolean : boolean | Defaults["inline"];
};
disabled: unknown extends Defaults["disabled"] ? BooleanConstructor : {
type: PropType<unknown extends Defaults["disabled"] ? boolean : boolean | Defaults["disabled"]>;
default: unknown extends Defaults["disabled"] ? boolean : boolean | Defaults["disabled"];
};
prefix: unknown extends Defaults["prefix"] ? StringConstructor : {
type: PropType<unknown extends Defaults["prefix"] ? string : string | Defaults["prefix"]>;
default: unknown extends Defaults["prefix"] ? string : string | Defaults["prefix"];
};
suffix: unknown extends Defaults["suffix"] ? StringConstructor : {
type: PropType<unknown extends Defaults["suffix"] ? string : string | Defaults["suffix"]>;
default: unknown extends Defaults["suffix"] ? string : string | Defaults["suffix"];
};
variant: unknown extends Defaults["variant"] ? {
type: PropType<HotkeyVariant>;
default: 'elevated';
validator: (v: any) => boolean;
} : Omit<{
type: PropType<HotkeyVariant>;
default: 'elevated';
validator: (v: any) => boolean;
}, "default" | "type"> & {
type: PropType<unknown extends Defaults["variant"] ? HotkeyVariant : Defaults["variant"] | HotkeyVariant>;
default: unknown extends Defaults["variant"] ? HotkeyVariant : Defaults["variant"] | NonNullable<HotkeyVariant>;
};
color: unknown extends Defaults["color"] ? StringConstructor : {
type: PropType<unknown extends Defaults["color"] ? string : string | Defaults["color"]>;
default: unknown extends Defaults["color"] ? string : string | Defaults["color"];
};
};
export declare const VHotkey: {
new (...args: any[]): import("vue").CreateComponentPublicInstanceWithMixins<{
style: string | false | import("vue").StyleValue[] | import("vue").CSSProperties | null;
tile: boolean;
displayMode: DisplayMode;
keyMap: KeyMapConfig;
platform: "auto" | "mac" | "pc";
inline: boolean;
disabled: boolean;
variant: HotkeyVariant;
} & {
theme?: string | undefined;
class?: any;
border?: string | number | boolean | undefined;
elevation?: string | number | undefined;
rounded?: string | number | boolean | undefined;
keys?: string | undefined;
prefix?: string | undefined;
suffix?: string | undefined;
color?: string | undefined;
} & {
$children?: {
default?: (() => import("vue").VNodeChild) | undefined;
} | {
$stable?: boolean;
} | (() => import("vue").VNodeChild) | import("vue").VNodeChild;
'v-slots'?: {
default?: false | (() => import("vue").VNodeChild) | undefined;
} | undefined;
} & {
"v-slot:default"?: false | (() => import("vue").VNodeChild) | undefined;
}, void, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, Record<string, any>, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, {
style: import("vue").StyleValue;
rounded: string | number | boolean;
tile: boolean;
displayMode: DisplayMode;
keyMap: KeyMapConfig;
platform: "auto" | "mac" | "pc";
inline: boolean;
disabled: boolean;
variant: HotkeyVariant;
}, true, {}, import("vue").SlotsType<Partial<{
default: () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
[key: string]: any;
}>[];
}>>, import("vue").GlobalComponents, import("vue").GlobalDirectives, string, {}, any, import("vue").ComponentProvideOptions, {
P: {};
B: {};
D: {};
C: {};
M: {};
Defaults: {};
}, {
style: string | false | import("vue").StyleValue[] | import("vue").CSSProperties | null;
tile: boolean;
displayMode: DisplayMode;
keyMap: KeyMapConfig;
platform: "auto" | "mac" | "pc";
inline: boolean;
disabled: boolean;
variant: HotkeyVariant;
} & {
theme?: string | undefined;
class?: any;
border?: string | number | boolean | undefined;
elevation?: string | number | undefined;
rounded?: string | number | boolean | undefined;
keys?: string | undefined;
prefix?: string | undefined;
suffix?: string | undefined;
color?: string | undefined;
} & {
$children?: {
default?: (() => import("vue").VNodeChild) | undefined;
} | {
$stable?: boolean;
} | (() => import("vue").VNodeChild) | import("vue").VNodeChild;
'v-slots'?: {
default?: false | (() => import("vue").VNodeChild) | undefined;
} | undefined;
} & {
"v-slot:default"?: false | (() => import("vue").VNodeChild) | undefined;
}, {}, {}, {}, {}, {
style: import("vue").StyleValue;
rounded: string | number | boolean;
tile: boolean;
displayMode: DisplayMode;
keyMap: KeyMapConfig;
platform: "auto" | "mac" | "pc";
inline: boolean;
disabled: boolean;
variant: HotkeyVariant;
}>;
__isFragment?: never;
__isTeleport?: never;
__isSuspense?: never;
} & import("vue").ComponentOptionsBase<{
style: string | false | import("vue").StyleValue[] | import("vue").CSSProperties | null;
tile: boolean;
displayMode: DisplayMode;
keyMap: KeyMapConfig;
platform: "auto" | "mac" | "pc";
inline: boolean;
disabled: boolean;
variant: HotkeyVariant;
} & {
theme?: string | undefined;
class?: any;
border?: string | number | boolean | undefined;
elevation?: string | number | undefined;
rounded?: string | number | boolean | undefined;
keys?: string | undefined;
prefix?: string | undefined;
suffix?: string | undefined;
color?: string | undefined;
} & {
$children?: {
default?: (() => import("vue").VNodeChild) | undefined;
} | {
$stable?: boolean;
} | (() => import("vue").VNodeChild) | import("vue").VNodeChild;
'v-slots'?: {
default?: false | (() => import("vue").VNodeChild) | undefined;
} | undefined;
} & {
"v-slot:default"?: false | (() => import("vue").VNodeChild) | undefined;
}, void, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, Record<string, any>, string, {
style: import("vue").StyleValue;
rounded: string | number | boolean;
tile: boolean;
displayMode: DisplayMode;
keyMap: KeyMapConfig;
platform: "auto" | "mac" | "pc";
inline: boolean;
disabled: boolean;
variant: HotkeyVariant;
}, {}, string, import("vue").SlotsType<Partial<{
default: () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
[key: string]: any;
}>[];
}>>, import("vue").GlobalComponents, import("vue").GlobalDirectives, string, import("vue").ComponentProvideOptions> & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps & import("../../util/index.js").FilterPropsOptions<{
theme: StringConstructor;
class: PropType<import("../../composables/component.js").ClassValue>;
style: {
type: PropType<import("vue").StyleValue>;
default: null;
};
border: (BooleanConstructor | NumberConstructor | StringConstructor)[];
elevation: {
type: (NumberConstructor | StringConstructor)[];
validator: (value: string | number) => boolean;
};
rounded: {
type: (BooleanConstructor | NumberConstructor | StringConstructor)[];
default: undefined;
};
tile: BooleanConstructor;
keys: StringConstructor;
displayMode: {
type: PropType<DisplayMode>;
default: string;
};
keyMap: {
type: PropType<KeyMapConfig>;
default: () => KeyMapConfig;
};
platform: {
type: PropType<'auto' | 'pc' | 'mac'>;
default: string;
};
inline: BooleanConstructor;
disabled: BooleanConstructor;
prefix: StringConstructor;
suffix: StringConstructor;
variant: {
type: PropType<HotkeyVariant>;
default: 'elevated';
validator: (v: any) => boolean;
};
color: StringConstructor;
}, import("vue").ExtractPropTypes<{
theme: StringConstructor;
class: PropType<import("../../composables/component.js").ClassValue>;
style: {
type: PropType<import("vue").StyleValue>;
default: null;
};
border: (BooleanConstructor | NumberConstructor | StringConstructor)[];
elevation: {
type: (NumberConstructor | StringConstructor)[];
validator: (value: string | number) => boolean;
};
rounded: {
type: (BooleanConstructor | NumberConstructor | StringConstructor)[];
default: undefined;
};
tile: BooleanConstructor;
keys: StringConstructor;
displayMode: {
type: PropType<DisplayMode>;
default: string;
};
keyMap: {
type: PropType<KeyMapConfig>;
default: () => KeyMapConfig;
};
platform: {
type: PropType<'auto' | 'pc' | 'mac'>;
default: string;
};
inline: BooleanConstructor;
disabled: BooleanConstructor;
prefix: StringConstructor;
suffix: StringConstructor;
variant: {
type: PropType<HotkeyVariant>;
default: 'elevated';
validator: (v: any) => boolean;
};
color: StringConstructor;
}>>;
export type VHotkey = InstanceType<typeof VHotkey>;
export type { KeyConfig, PlatformKeyConfig, KeyMapConfig, DisplayMode };
+401
View File
@@ -0,0 +1,401 @@
import { createVNode as _createVNode, normalizeClass as _normalizeClass, normalizeStyle as _normalizeStyle, createElementVNode as _createElementVNode, Fragment as _Fragment, createTextVNode as _createTextVNode } from "vue";
/**
* VHotkey Component
*
* Purpose: Renders keyboard shortcuts in a visually consistent and accessible way.
* This component handles the complex logic of displaying keyboard combinations
* across different platforms (Mac vs PC) and display modes (icons, symbols, text).
*
* Why it exists:
* - Provides consistent visual representation of keyboard shortcuts
* - Handles platform-specific key differences (Cmd vs Ctrl, Option vs Alt)
* - Supports multiple display modes for different design needs
* - Encapsulates complex key parsing and rendering logic
* - Used throughout the command palette for instruction display
*
* Key Mapping Structure:
* The keyMap uses a simple object structure where each key has:
* - `default`: Required configuration for all platforms
* - `mac`: Optional Mac-specific overrides
* Each config can specify `symbol`, `icon`, and `text` representations.
*
* Example:
* ```
* ctrl: {
* mac: { symbol: '⌃', icon: '$ctrl', text: 'Control' },
* default: { text: 'Ctrl', icon: '$ctrl' }
* }
* ```
*/
// Styles
import "./VHotkey.css";
// Components
import { VIcon } from "../VIcon/index.js";
import { VKbd } from "../VKbd/index.js"; // Composables
import { makeBorderProps, useBorder } from "../../composables/border.js";
import { makeComponentProps } from "../../composables/component.js";
import { makeElevationProps, useElevation } from "../../composables/elevation.js";
import { parseKeyCombination } from "../../composables/hotkey/hotkey-parsing.js";
import { useLocale, useRtl } from "../../composables/locale.js";
import { makeRoundedProps, useRounded } from "../../composables/rounded.js";
import { makeThemeProps, provideTheme } from "../../composables/theme.js";
import { useVariant } from "../../composables/variant.js"; // Utilities
import { computed } from 'vue';
import { genericComponent, mergeDeep, propsFactory, useRender } from "../../util/index.js"; // Types
// Display mode types for different visual representations
// Extended variant type that includes our custom 'contained' variant
// Key display tuple: [mode, content] where content is string or IconValue
// Key tuple: [mode, content, keycode] where content is string or IconValue
function processKey(config, requestedMode, isMac) {
const keyCfg = isMac && config.mac ? config.mac : config.default;
// 1. Resolve the safest display mode for the current platform
const mode = (() => {
// If the requested mode lacks an asset, fall back to text
if (requestedMode === 'icon' && !keyCfg.icon) return 'text';
if (requestedMode === 'symbol' && !keyCfg.symbol) return 'text';
return requestedMode;
})();
// 2. Pick value for the chosen mode, defaulting to text representation
let value = keyCfg[mode] ?? keyCfg.text;
// 3. Guard against icon tokens leaking into text mode (e.g. "$ctrl")
if (mode === 'text' && typeof value === 'string' && value.startsWith('$') && !value.startsWith('$vuetify.')) {
value = value.slice(1).toUpperCase(); // "$ctrl" → "CTRL"
}
return mode === 'icon' ? ['icon', value] : [mode, value];
}
export const hotkeyMap = {
ctrl: {
mac: {
symbol: '⌃',
icon: '$ctrl',
text: '$vuetify.hotkey.ctrl'
},
default: {
text: 'Ctrl'
}
},
meta: {
mac: {
symbol: '⌘',
icon: '$command',
text: '$vuetify.hotkey.command'
},
default: {
text: 'Ctrl'
}
},
cmd: {
mac: {
symbol: '⌘',
icon: '$command',
text: '$vuetify.hotkey.command'
},
default: {
text: 'Ctrl'
}
},
shift: {
mac: {
symbol: '⇧',
icon: '$shift',
text: '$vuetify.hotkey.shift'
},
default: {
text: 'Shift'
}
},
alt: {
mac: {
symbol: '⌥',
icon: '$alt',
text: '$vuetify.hotkey.option'
},
default: {
text: 'Alt'
}
},
enter: {
default: {
symbol: '↵',
icon: '$enter',
text: '$vuetify.hotkey.enter'
}
},
arrowup: {
default: {
symbol: '↑',
icon: '$arrowup',
text: '$vuetify.hotkey.upArrow'
}
},
arrowdown: {
default: {
symbol: '↓',
icon: '$arrowdown',
text: '$vuetify.hotkey.downArrow'
}
},
arrowleft: {
default: {
symbol: '←',
icon: '$arrowleft',
text: '$vuetify.hotkey.leftArrow'
}
},
arrowright: {
default: {
symbol: '→',
icon: '$arrowright',
text: '$vuetify.hotkey.rightArrow'
}
},
backspace: {
default: {
symbol: '⌫',
icon: '$backspace',
text: '$vuetify.hotkey.backspace'
}
},
escape: {
default: {
text: '$vuetify.hotkey.escape'
}
},
' ': {
mac: {
symbol: '␣',
icon: '$space',
text: '$vuetify.hotkey.space'
},
default: {
text: '$vuetify.hotkey.space'
}
},
'-': {
default: {
text: '-'
}
},
'+': {
default: {
text: '+'
}
}
};
export const makeVHotkeyProps = propsFactory({
// String representing keyboard shortcuts (e.g., "ctrl+k", "meta+shift+p")
keys: String,
// How to display keys: 'symbol' uses special characters (⌘, ⌃), 'icon' uses SVG icons, 'text' uses words
displayMode: {
type: String,
default: 'icon'
},
// Custom key mapping configuration. Users can import and modify the exported hotkeyMap as needed
keyMap: {
type: Object,
default: () => hotkeyMap
},
platform: {
type: String,
default: 'auto'
},
inline: Boolean,
disabled: Boolean,
prefix: String,
suffix: String,
variant: {
type: String,
default: 'elevated',
validator: v => ['elevated', 'flat', 'tonal', 'outlined', 'text', 'plain', 'contained'].includes(v)
},
...makeComponentProps(),
...makeThemeProps(),
...makeBorderProps(),
...makeRoundedProps(),
...makeElevationProps(),
color: String
}, 'VHotkey');
const AND_DELINEATOR = Symbol('VHotkey:AND_DELINEATOR'); // For +_ separators
const OR_DELINEATOR = Symbol('VHotkey:OR_DELINEATOR'); // For / separators
const THEN_DELINEATOR = Symbol('VHotkey:THEN_DELINEATOR'); // For - separators
function getKeyText(keyMap, key, isMac) {
const lowerKey = key.toLowerCase();
if (lowerKey in keyMap) {
const result = processKey(keyMap[lowerKey], 'text', isMac);
return typeof result[1] === 'string' ? result[1] : String(result[1]);
}
return key.toUpperCase();
}
function applyDisplayModeToKey(keyMap, mode, key, isMac) {
const lowerKey = key.toLowerCase();
if (lowerKey in keyMap) {
const result = processKey(keyMap[lowerKey], mode, isMac);
if (result[0] === 'text' && typeof result[1] === 'string' && result[1].startsWith('$') && !result[1].startsWith('$vuetify.')) {
return ['text', result[1].replace('$', '').toUpperCase(), key];
}
return [...result, key];
}
return ['text', key.toUpperCase(), key];
}
export const VHotkey = genericComponent()({
name: 'VHotkey',
props: makeVHotkeyProps(),
setup(props) {
const {
t
} = useLocale();
const {
themeClasses
} = provideTheme(props);
const {
rtlClasses
} = useRtl();
const {
borderClasses
} = useBorder(props);
const {
roundedClasses
} = useRounded(props);
const {
elevationClasses
} = useElevation(props);
const {
colorClasses,
colorStyles,
variantClasses
} = useVariant(() => ({
color: props.color,
variant: props.variant === 'contained' ? 'elevated' : props.variant
}));
const isMac = computed(() => props.platform === 'auto' ? typeof navigator !== 'undefined' && /macintosh/i.test(navigator.userAgent) : props.platform === 'mac');
const keyCombinations = computed(() => {
if (!props.keys) return [];
// Split by spaces to handle multiple key combinations
// Example: "ctrl+k meta+p" -> ["ctrl+k", "meta+p"]
return props.keys.split(/\b \b/).map(combination => {
const result = [];
function visit(node) {
if (typeof node === 'string') {
if (node !== '') {
result.push(applyDisplayModeToKey(props.keyMap, props.displayMode, node, isMac.value));
}
} else {
for (let i = 0; i < node.parts.length; i++) {
if (i > 0) {
if (node.type === 'sequence') {
result.push(THEN_DELINEATOR);
} else if (node.type === 'alternate') {
result.push(OR_DELINEATOR);
} else if (node.type === 'combo') {
result.push(AND_DELINEATOR);
} else {
void node;
}
}
visit(node.parts[i]);
}
}
}
visit(parseKeyCombination(combination));
return result;
});
});
const accessibleLabel = computed(() => {
if (!props.keys) return '';
// Convert the parsed key combinations into readable text
const readableShortcuts = keyCombinations.value.map(combination => {
const readableParts = [];
for (const key of combination) {
if (Array.isArray(key)) {
// Always use text representation for screen readers
const textKey = key[0] === 'icon' || key[0] === 'symbol' ? applyDisplayModeToKey(mergeDeep(hotkeyMap, props.keyMap), 'text', String(key[1]), isMac.value)[1] : key[1];
readableParts.push(translateKey(textKey));
} else {
if (key === AND_DELINEATOR) {
readableParts.push(t('$vuetify.hotkey.plus'));
} else if (key === OR_DELINEATOR) {
readableParts.push(t('$vuetify.hotkey.or'));
} else if (key === THEN_DELINEATOR) {
readableParts.push(t('$vuetify.hotkey.then'));
}
}
}
return readableParts.join(' ');
});
const shortcutText = readableShortcuts.join(', ');
return t('$vuetify.hotkey.shortcut', shortcutText);
});
function translateKey(key) {
return key.startsWith('$vuetify.') ? t(key) : key;
}
function getKeyTooltip(key) {
if (props.displayMode === 'text') return undefined;
const textKey = getKeyText(props.keyMap, String(key[2]), isMac.value);
return translateKey(textKey);
}
function renderKey(key, keyIndex) {
const isContained = props.variant === 'contained';
const KeyComponent = isContained ? 'kbd' : VKbd;
const keyClasses = ['v-hotkey__key', `v-hotkey__key-${key[0]}`, ...(isContained ? ['v-hotkey__key--nested'] : [borderClasses.value, roundedClasses.value, elevationClasses.value, colorClasses.value])];
return _createVNode(KeyComponent, {
"key": keyIndex,
"class": _normalizeClass(keyClasses),
"style": _normalizeStyle(isContained ? undefined : colorStyles.value),
"aria-hidden": "true",
"title": getKeyTooltip(key)
}, {
default: () => [key[0] === 'icon' ? _createVNode(VIcon, {
"icon": key[1],
"aria-hidden": "true"
}, null) : translateKey(key[1])]
});
}
function renderDivider(key, keyIndex) {
return _createElementVNode("span", {
"key": keyIndex,
"class": "v-hotkey__divider",
"aria-hidden": "true"
}, [key === AND_DELINEATOR ? '+' : key === OR_DELINEATOR ? t('$vuetify.hotkey.or') : t('$vuetify.hotkey.then')]);
}
useRender(() => {
const content = _createElementVNode(_Fragment, null, [props.prefix && _createElementVNode("span", {
"key": "prefix",
"class": "v-hotkey__prefix"
}, [props.prefix]), keyCombinations.value.map((combination, comboIndex) => _createElementVNode("span", {
"class": "v-hotkey__combination",
"key": comboIndex
}, [combination.map((key, keyIndex) => Array.isArray(key) ? renderKey(key, keyIndex) : renderDivider(key, keyIndex)), comboIndex < keyCombinations.value.length - 1 && _createElementVNode("span", {
"aria-hidden": "true"
}, [_createTextVNode("\xA0")])])), props.suffix && _createElementVNode("span", {
"key": "suffix",
"class": "v-hotkey__suffix"
}, [props.suffix])]);
return _createElementVNode("div", {
"class": _normalizeClass(['v-hotkey', {
'v-hotkey--disabled': props.disabled,
'v-hotkey--inline': props.inline,
'v-hotkey--contained': props.variant === 'contained'
}, themeClasses.value, rtlClasses.value, variantClasses.value, props.class]),
"style": _normalizeStyle(props.style),
"role": "img",
"aria-label": accessibleLabel.value
}, [props.variant !== 'contained' ? content : _createVNode(VKbd, {
"key": "contained",
"class": _normalizeClass(['v-hotkey__contained-wrapper', borderClasses.value, roundedClasses.value, elevationClasses.value, colorClasses.value]),
"style": _normalizeStyle(colorStyles.value),
"aria-hidden": "true"
}, {
default: () => [content]
})]);
});
}
});
//# sourceMappingURL=VHotkey.js.map
File diff suppressed because one or more lines are too long
+227
View File
@@ -0,0 +1,227 @@
@use '../../styles/tools';
@use './variables' as *;
@include tools.layer('components') {
.v-hotkey {
align-items: center;
display: inline-flex;
gap: $hotkey-gap;
vertical-align: middle;
line-height: $hotkey-line-height;
&--disabled {
opacity: $hotkey-disabled-opacity;
}
&--inline {
max-height: 1lh;
vertical-align: baseline;
font-size: $hotkey-inline-font-size;
line-height: inherit;
}
&__prefix {
opacity: $hotkey-prefix-suffix-opacity;
font-weight: $hotkey-prefix-suffix-font-weight;
vertical-align: baseline;
}
&__suffix {
opacity: $hotkey-prefix-suffix-opacity;
font-weight: $hotkey-prefix-suffix-font-weight;
vertical-align: baseline;
}
// Contained variant
&--contained {
.v-hotkey__contained-wrapper {
display: inline-flex;
align-items: center;
gap: $hotkey-combination-gap;
padding: $hotkey-contained-padding;
box-sizing: border-box;
background: unset;
box-shadow: unset;
min-height: 1em;
font-size: $hotkey-font-size;
line-height: $hotkey-line-height;
.v-hotkey__prefix, .v-hotkey__suffix {
opacity: $hotkey-prefix-suffix-contained-opacity;
}
.v-hotkey__prefix {
margin-right: $hotkey-contained-prefix-margin-right;
}
.v-hotkey__suffix {
margin-left: $hotkey-contained-suffix-margin-left;
}
}
.v-hotkey__divider {
opacity: $hotkey-divider-opacity;
font-size: inherit;
}
.v-hotkey__combination {
display: inline-flex;
align-items: center;
gap: $hotkey-combination-gap;
}
&.v-hotkey--inline .v-hotkey__contained-wrapper.v-kbd {
align-self: baseline;
align-items: baseline;
font-size: $hotkey-inline-font-size;
line-height: inherit;
padding: $hotkey-inline-padding;
gap: $hotkey-combination-gap;
margin-left: 0;
margin-right: 0;
}
&.v-hotkey--inline .v-hotkey__divider {
font-size: $hotkey-inline-divider-font-size;
align-self: baseline;
}
&.v-hotkey--inline .v-hotkey__combination {
gap: $hotkey-combination-gap;
align-items: baseline;
}
}
&__key {
&.v-kbd {
min-height: unset;
font-size: $hotkey-font-size;
line-height: $hotkey-line-height;
padding: $hotkey-padding;
min-width: $hotkey-min-width;
@include tools.variant($hotkey-variants...);
}
&-symbol.v-kbd {
line-height: normal;
font-size: 1em;
}
&-icon .v-icon {
min-width: unset;
&:has(svg) {
max-width: $hotkey-icon-size;
}
&:not(:has(svg)) {
font-size: $hotkey-icon-size;
line-height: inherit;
height: inherit;
}
}
&--nested {
background: none;
border: none;
padding: 0;
margin: 0;
font: inherit;
color: inherit;
display: inline-flex;
align-items: center;
min-width: auto;
min-height: auto;
align-self: baseline;
&.v-hotkey__key-icon {
align-self: center;
}
}
}
&__divider {
align-items: center;
display: inline-flex;
opacity: $hotkey-divider-opacity;
font-size: $hotkey-divider-font-size;
}
&__combination {
display: flex;
gap: $hotkey-combination-gap;
}
&--inline &__key {
align-self: center;
padding: $hotkey-inline-padding;
min-width: $hotkey-inline-min-width;
height: calc(1lh - 2px);
line-height: calc(1lh - 2px);
&-icon {
&.v-kbd {
padding-block: 0;
}
.v-icon {
width: min-content;
min-width: fit-content;
max-height: $hotkey-inline-icon-max-height;
.v-icon__svg {
height: 100%;
width: unset;
}
}
}
}
&--inline &__combination {
align-items: baseline;
gap: 1px;
}
&--inline &__divider {
font-size: $hotkey-inline-divider-font-size;
}
&--inline &__prefix,
&--inline &__suffix {
align-self: baseline;
font-size: inherit;
}
&--variant-elevated &__key.v-kbd {
@include tools.elevation($hotkey-elevation);
}
&--variant-flat &__key.v-kbd {
box-shadow: none;
}
&--variant-outlined &__key.v-kbd {
background: none;
@include tools.elevation(0);
}
&--variant-text {
@include tools.layer('overrides') {
.v-hotkey__key.v-kbd {
background: transparent;
border: none;
padding-inline: 0;
min-width: auto;
@include tools.elevation(0);
}
}
.v-hotkey__combination {
gap: 1px;
}
}
&--variant-tonal &__key.v-kbd {
border: unset;
box-shadow: unset;
}
}
}
+44
View File
@@ -0,0 +1,44 @@
@forward '../VKbd/variables';
@use '../../styles/settings';
@use '../../styles/tools';
@use '../VKbd/variables' as vkbd;
$hotkey-gap: 4px !default;
$hotkey-icon-size: 0.75rem !default;
$hotkey-divider-opacity: var(--v-medium-emphasis-opacity) !default;
$hotkey-divider-font-size: 1em !default;
$hotkey-combination-gap: 2px !default;
$hotkey-font-size: 0.75rem !default;
$hotkey-line-height: 1.5 !default;
$hotkey-padding: 0.2rem !default;
$hotkey-min-width: 1.5em !default;
$hotkey-disabled-opacity: 0.26 !default;
$hotkey-inline-font-size: 1em !default;
$hotkey-inline-padding: 1px 4px 0 !default;
$hotkey-inline-min-width: vkbd.$kbd-min-width !default;
$hotkey-inline-icon-max-height: calc(1ex + 2px) !default;
$hotkey-inline-divider-font-size: 1em !default;
$hotkey-contained-padding: 0.2rem 4px !default;
$hotkey-contained-prefix-margin-right: 2px !default;
$hotkey-contained-suffix-margin-left: 2px !default;
$hotkey-prefix-suffix-opacity: var(--v-medium-emphasis-opacity) !default;
$hotkey-prefix-suffix-contained-opacity: var(--v-high-emphasis-opacity) !default;
$hotkey-prefix-suffix-font-weight: normal !default;
// Variant variables
$hotkey-background: rgb(var(--v-theme-surface)) !default;
$hotkey-color: tools.theme-color('on-surface', var(--v-high-emphasis-opacity)) !default;
$hotkey-elevation: 1 !default;
$hotkey-plain-opacity: .62 !default;
$hotkey-variants: (
$hotkey-background,
$hotkey-color,
$hotkey-elevation,
$hotkey-plain-opacity,
'v-hotkey'
) !default;
+1
View File
@@ -0,0 +1 @@
export { VHotkey } from './VHotkey.js';
+2
View File
@@ -0,0 +1,2 @@
export { VHotkey } from "./VHotkey.js";
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","names":["VHotkey"],"sources":["../../../src/components/VHotkey/index.ts"],"sourcesContent":["export { VHotkey } from './VHotkey'\n"],"mappings":"SAASA,OAAO","ignoreList":[]}