🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@taylorvance/tv-shared-runtime

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@taylorvance/tv-shared-runtime - npm Package Compare versions

Comparing version
0.4.0
to
0.5.0
+21
dist/hotkeys.d.ts
import { type HotkeyCallback, type Keys, type Options } from 'react-hotkeys-hook';
import type { DependencyList, MutableRefObject } from 'react';
export type FormTags = 'input' | 'textarea' | 'select' | 'INPUT' | 'TEXTAREA' | 'SELECT';
export interface HotkeyBinding {
callback: HotkeyCallback;
keys: Keys;
}
export interface KonamiOptions {
document?: Document;
enableOnContentEditable?: boolean;
enableOnFormTags?: readonly FormTags[] | boolean;
enabled?: boolean;
preventDefault?: boolean;
timeoutMs?: number;
}
declare const KONAMI_CODE_SEQUENCE: readonly ["up", "up", "down", "down", "left", "right", "left", "right", "b", "a"];
export { KONAMI_CODE_SEQUENCE };
export declare function useHotkeys<T extends HTMLElement>(keys: Keys, callback: HotkeyCallback, options?: Options, deps?: DependencyList): MutableRefObject<T | null>;
export declare function useHotkeys<T extends HTMLElement>(bindings: readonly HotkeyBinding[], options?: Options, deps?: DependencyList): MutableRefObject<T | null>;
export declare function useKonami<T extends HTMLElement>(callback: (event: KeyboardEvent) => void, options?: KonamiOptions, deps?: DependencyList): MutableRefObject<T | null>;
//# sourceMappingURL=hotkeys.d.ts.map
{"version":3,"file":"hotkeys.d.ts","sourceRoot":"","sources":["../src/hotkeys.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,IAAI,EACT,KAAK,OAAO,EACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAE9D,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;AAEzF,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,cAAc,CAAC;IACzB,IAAI,EAAE,IAAI,CAAC;CACZ;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,gBAAgB,CAAC,EAAE,SAAS,QAAQ,EAAE,GAAG,OAAO,CAAC;IACjD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAgCD,QAAA,MAAM,oBAAoB,mFAWhB,CAAC;AAyHX,OAAO,EAAE,oBAAoB,EAAE,CAAC;AAEhC,wBAAgB,UAAU,CAAC,CAAC,SAAS,WAAW,EAC9C,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,cAAc,EACxB,OAAO,CAAC,EAAE,OAAO,EACjB,IAAI,CAAC,EAAE,cAAc,GACpB,gBAAgB,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AAC9B,wBAAgB,UAAU,CAAC,CAAC,SAAS,WAAW,EAC9C,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,OAAO,CAAC,EAAE,OAAO,EACjB,IAAI,CAAC,EAAE,cAAc,GACpB,gBAAgB,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AAyD9B,wBAAgB,SAAS,CAAC,CAAC,SAAS,WAAW,EAC7C,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,EACxC,OAAO,CAAC,EAAE,aAAa,EACvB,IAAI,CAAC,EAAE,cAAc,GACpB,gBAAgB,CAAC,CAAC,GAAG,IAAI,CAAC,CAwF5B"}
import { useCallback, useEffect, useRef } from 'react';
import { useHotkeys as useLibraryHotkeys, } from 'react-hotkeys-hook';
const DEFAULT_COMBINATION_KEY = '+';
const DEFAULT_SPLIT_KEY = ',';
const KONAMI_TIMEOUT_MS = 1000;
const hotkeyKeyAliases = {
' ': 'space',
',': 'comma',
'-': 'slash',
'.': 'period',
'#': 'backslash',
'+': 'bracketright',
altleft: 'alt',
altright: 'alt',
cmd: 'meta',
command: 'meta',
control: 'ctrl',
controlleft: 'ctrl',
controlright: 'ctrl',
esc: 'escape',
metaleft: 'meta',
metaright: 'meta',
os: 'meta',
osleft: 'meta',
osright: 'meta',
option: 'alt',
return: 'enter',
shiftleft: 'shift',
shiftright: 'shift',
};
const modifierKeys = new Set(['alt', 'ctrl', 'meta', 'mod', 'shift']);
const KONAMI_CODE_SEQUENCE = [
'up',
'up',
'down',
'down',
'left',
'right',
'left',
'right',
'b',
'a',
];
const isBindingArray = (value) => (Array.isArray(value)
&& value.every((item) => typeof item === 'object' && item !== null && 'callback' in item && 'keys' in item));
const isDependencyList = (value) => (Array.isArray(value));
const normalizeKey = (key) => {
const alias = hotkeyKeyAliases[key];
return (alias ?? key)
.trim()
.toLowerCase()
.replace(/key|digit|numpad|arrow/g, '');
};
const normalizeHotkey = (hotkey, combinationKey = DEFAULT_COMBINATION_KEY) => {
const keys = hotkey
.toLowerCase()
.split(combinationKey)
.map((part) => normalizeKey(part));
return {
alt: keys.includes('alt'),
ctrl: keys.includes('ctrl'),
meta: keys.includes('meta'),
mod: keys.includes('mod'),
shift: keys.includes('shift'),
keys: keys.filter((key) => !modifierKeys.has(key)),
};
};
const matchesHotkey = (event, hotkey) => {
const eventKeys = [...(event.keys ?? [])];
return event.alt === hotkey.alt
&& event.ctrl === hotkey.ctrl
&& event.meta === hotkey.meta
&& event.mod === hotkey.mod
&& event.shift === hotkey.shift
&& eventKeys.length === hotkey.keys.length
&& eventKeys.every((key, index) => key === hotkey.keys[index]);
};
const isHotkeyEnabledOnTag = (target, enabledOnTags = false) => {
const tagName = target instanceof HTMLElement ? target.tagName : null;
if (Array.isArray(enabledOnTags)) {
return Boolean(tagName
&& enabledOnTags.some((tag) => tag.toLowerCase() === tagName.toLowerCase()));
}
return Boolean(tagName && enabledOnTags);
};
const isKeyboardEventTriggeredByInput = (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return false;
}
return target.isContentEditable || ['INPUT', 'SELECT', 'TEXTAREA'].includes(target.tagName);
};
const isScopeActive = (scopeRef) => {
const scopeElement = scopeRef.current;
if (!scopeElement) {
return true;
}
const rootNode = scopeElement.getRootNode();
if (!(rootNode instanceof Document || rootNode instanceof ShadowRoot)) {
return false;
}
return rootNode.activeElement === scopeElement || scopeElement.contains(rootNode.activeElement);
};
const getBindingKeys = (keys, splitKey = DEFAULT_SPLIT_KEY) => {
const values = Array.isArray(keys) ? keys : [keys];
return values.flatMap((value) => (value.split(splitKey).map((part) => part.trim()).filter(Boolean)));
};
export { KONAMI_CODE_SEQUENCE };
export function useHotkeys(keysOrBindings, callbackOrOptions, maybeOptions, maybeDeps) {
if (!isBindingArray(keysOrBindings)) {
const options = (typeof callbackOrOptions === 'function'
? maybeOptions
: callbackOrOptions);
const deps = (typeof callbackOrOptions === 'function'
? maybeDeps
: isDependencyList(maybeOptions)
? maybeOptions
: maybeDeps);
const callback = (typeof callbackOrOptions === 'function'
? callbackOrOptions
: () => { });
return useLibraryHotkeys(keysOrBindings, callback, options, deps);
}
const options = isDependencyList(callbackOrOptions) ? undefined : callbackOrOptions;
const deps = isDependencyList(callbackOrOptions)
? callbackOrOptions
: isDependencyList(maybeOptions)
? maybeOptions
: maybeDeps;
const splitKey = options?.splitKey ?? DEFAULT_SPLIT_KEY;
const combinationKey = options?.combinationKey ?? DEFAULT_COMBINATION_KEY;
const compiledBindings = keysOrBindings.flatMap((binding) => (getBindingKeys(binding.keys, splitKey).map((key) => ({
callback: binding.callback,
hotkey: normalizeHotkey(key, combinationKey),
}))));
const flattenedKeys = keysOrBindings.flatMap((binding) => getBindingKeys(binding.keys, splitKey));
return useLibraryHotkeys(flattenedKeys, (event, hotkeyEvent) => {
const match = compiledBindings.find((binding) => matchesHotkey(hotkeyEvent, binding.hotkey));
match?.callback(event, hotkeyEvent);
}, options, deps);
}
export function useKonami(callback, options, deps) {
const scopeRef = useRef(null);
const callbackRef = useRef(callback);
const progressRef = useRef(0);
const lastKeyTimestampRef = useRef(0);
const memoizedCallback = useCallback(callback, deps ?? []);
const timeoutMs = options?.timeoutMs ?? KONAMI_TIMEOUT_MS;
callbackRef.current = deps ? memoizedCallback : callback;
useEffect(() => {
if (options?.enabled === false) {
return undefined;
}
const hotkeyDocument = options?.document ?? document;
const handleKeyDown = (event) => {
if (isKeyboardEventTriggeredByInput(event)
&& !isHotkeyEnabledOnTag(event.target, options?.enableOnFormTags)) {
return;
}
if (event.target instanceof HTMLElement
&& event.target.isContentEditable
&& !options?.enableOnContentEditable) {
return;
}
if (!isScopeActive(scopeRef)) {
return;
}
const normalizedKey = normalizeKey(event.key);
const expectedKey = KONAMI_CODE_SEQUENCE[progressRef.current];
if (!expectedKey) {
progressRef.current = 0;
lastKeyTimestampRef.current = 0;
return;
}
if (lastKeyTimestampRef.current > 0
&& Date.now() - lastKeyTimestampRef.current > timeoutMs) {
progressRef.current = 0;
}
if (normalizedKey === expectedKey) {
progressRef.current += 1;
lastKeyTimestampRef.current = Date.now();
if (progressRef.current === KONAMI_CODE_SEQUENCE.length) {
if (options?.preventDefault) {
event.preventDefault();
}
callbackRef.current(event);
progressRef.current = 0;
lastKeyTimestampRef.current = 0;
}
return;
}
progressRef.current = normalizedKey === KONAMI_CODE_SEQUENCE[0] ? 1 : 0;
lastKeyTimestampRef.current = progressRef.current > 0 ? Date.now() : 0;
};
hotkeyDocument.addEventListener('keydown', handleKeyDown);
return () => {
hotkeyDocument.removeEventListener('keydown', handleKeyDown);
};
}, [
options?.document,
options?.enableOnContentEditable,
options?.enableOnFormTags,
options?.enabled,
options?.preventDefault,
timeoutMs,
]);
return scopeRef;
}
+1
-0

@@ -5,2 +5,3 @@ export { BrandBadge, brandBadgeClassNames, type BrandBadgeProps, } from './BrandBadge.js';

export { createProjectStorage, type ProjectStorageEntry, type ProjectStorage, type ProjectStorageOptions, type StorageKeyPart, type StorageLike, } from './storage.js';
export { KONAMI_CODE_SEQUENCE, useHotkeys, useKonami, type HotkeyBinding, type KonamiOptions, } from './hotkeys.js';
//# sourceMappingURL=index.d.ts.map
+1
-1

@@ -1,1 +0,1 @@

{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,oBAAoB,EACpB,KAAK,eAAe,GACrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,cAAc,GACf,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,WAAW,GACjB,MAAM,cAAc,CAAC"}
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,oBAAoB,EACpB,KAAK,eAAe,GACrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,cAAc,GACf,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,WAAW,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,oBAAoB,EACpB,UAAU,EACV,SAAS,EACT,KAAK,aAAa,EAClB,KAAK,aAAa,GACnB,MAAM,cAAc,CAAC"}

@@ -5,1 +5,2 @@ export { BrandBadge, brandBadgeClassNames, } from './BrandBadge.js';

export { createProjectStorage, } from './storage.js';
export { KONAMI_CODE_SEQUENCE, useHotkeys, useKonami, } from './hotkeys.js';
{
"name": "@taylorvance/tv-shared-runtime",
"version": "0.4.0",
"version": "0.5.0",
"description": "Shared React runtime primitives for Taylor Vance portfolio projects.",

@@ -31,2 +31,7 @@ "type": "module",

},
"./hotkeys": {
"types": "./dist/hotkeys.d.ts",
"import": "./dist/hotkeys.js",
"default": "./dist/hotkeys.js"
},
"./storage-dev": {

@@ -58,2 +63,5 @@ "types": "./dist/storage-dev.d.ts",

},
"dependencies": {
"react-hotkeys-hook": "^5.1.0"
},
"peerDependencies": {

@@ -60,0 +68,0 @@ "react": "^18.3.0 || ^19.0.0"

@@ -18,2 +18,5 @@ # `@taylorvance/tv-shared-runtime`

- `createProjectStorage`
- `useHotkeys`
- `useKonami`
- `KONAMI_CODE_SEQUENCE`

@@ -24,2 +27,3 @@ Explicit subpaths:

- `@taylorvance/tv-shared-runtime/assets`
- `@taylorvance/tv-shared-runtime/hotkeys`
- `@taylorvance/tv-shared-runtime/storage`

@@ -135,1 +139,47 @@ - `@taylorvance/tv-shared-runtime/storage-dev`

This inspector is meant for local tooling and debug screens, not default production UI.
## Hotkeys
Use `useHotkeys` for shared app shortcuts. It supports the library's normal global behavior by default, and it becomes element-scoped when you attach the returned ref to a focusable container.
```tsx
import { useHotkeys } from '@taylorvance/tv-shared-runtime';
export function SessionPanel() {
const hotkeyRef = useHotkeys<HTMLDivElement>([
{ keys: 'r', callback: () => resetGame() },
{ keys: 'z', callback: () => undoMove() },
{ keys: 'x', callback: () => redoMove() },
]);
return (
<section ref={hotkeyRef} tabIndex={-1}>
...
</section>
);
}
```
If you do not attach the returned ref, the hotkeys are global for the current document.
The hook keeps the default input-safe behavior from `react-hotkeys-hook`, so shortcuts do not fire while a user is typing into an `input`, `textarea`, or `select` unless you opt in through `enableOnFormTags`.
For an easy easter-egg path, `useKonami` exposes a built-in Konami listener with the same optional scoping model:
```tsx
import { useKonami } from '@taylorvance/tv-shared-runtime';
export function SessionPanel() {
const hotkeyRef = useKonami<HTMLDivElement>(() => {
setDebugMode(true);
});
return (
<section ref={hotkeyRef} tabIndex={-1}>
...
</section>
);
}
```
The shared Konami sequence is also exported as `KONAMI_CODE_SEQUENCE`.