🚀 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.6.0
to
0.7.0
+14
dist/a11y.d.ts
import { type ReactNode } from 'react';
export type AnnouncementPriority = 'assertive' | 'polite';
export interface LiveAnnouncerValue {
announce: (message: string, priority?: AnnouncementPriority) => void;
}
export interface LiveAnnouncerProps {
children?: ReactNode;
}
export declare const usePrefersReducedMotion: (options?: {
window?: Window | null;
}) => boolean;
export declare function LiveAnnouncer({ children }: LiveAnnouncerProps): import("react/jsx-runtime").JSX.Element;
export declare const useLiveAnnouncer: () => LiveAnnouncerValue;
//# sourceMappingURL=a11y.d.ts.map
{"version":3,"file":"a11y.d.ts","sourceRoot":"","sources":["../src/a11y.tsx"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAgBf,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE1D,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,oBAAoB,KAAK,IAAI,CAAC;CACtE;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AA4BD,eAAO,MAAM,uBAAuB,GAAI,UAAS;IAC/C,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnB,YA0BL,CAAC;AAEF,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,kBAAkB,2CAgC7D;AAED,eAAO,MAAM,gBAAgB,0BAAyC,CAAC"}
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
const REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)';
const visuallyHiddenStyle = {
border: 0,
clip: 'rect(0 0 0 0)',
height: '1px',
margin: '-1px',
overflow: 'hidden',
padding: 0,
position: 'absolute',
whiteSpace: 'nowrap',
width: '1px',
};
const LiveAnnouncerContext = createContext({
announce: () => { },
});
const resolveWindow = (providedWindow) => {
if (providedWindow !== undefined) {
return providedWindow;
}
if (typeof window === 'undefined') {
return null;
}
return window;
};
const getInitialReducedMotion = (providedWindow) => {
const activeWindow = resolveWindow(providedWindow);
if (!activeWindow?.matchMedia) {
return false;
}
return activeWindow.matchMedia(REDUCED_MOTION_QUERY).matches;
};
export const usePrefersReducedMotion = (options = {}) => {
const [prefersReducedMotion, setPrefersReducedMotion] = useState(() => (getInitialReducedMotion(options.window)));
useEffect(() => {
const activeWindow = resolveWindow(options.window);
if (!activeWindow?.matchMedia) {
return undefined;
}
const mediaQuery = activeWindow.matchMedia(REDUCED_MOTION_QUERY);
const handleChange = () => {
setPrefersReducedMotion(mediaQuery.matches);
};
handleChange();
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}, [options.window]);
return prefersReducedMotion;
};
export function LiveAnnouncer({ children }) {
const [politeAnnouncement, setPoliteAnnouncement] = useState({ id: 0, message: '' });
const [assertiveAnnouncement, setAssertiveAnnouncement] = useState({ id: 0, message: '' });
const announce = useCallback((message, priority = 'polite') => {
if (priority === 'assertive') {
setAssertiveAnnouncement((previousAnnouncement) => ({
id: previousAnnouncement.id + 1,
message,
}));
return;
}
setPoliteAnnouncement((previousAnnouncement) => ({
id: previousAnnouncement.id + 1,
message,
}));
}, []);
const value = useMemo(() => ({ announce }), [announce]);
return (_jsxs(LiveAnnouncerContext.Provider, { value: value, children: [children, _jsxs("div", { style: visuallyHiddenStyle, children: [_jsx("div", { "aria-live": "polite", role: "status", children: politeAnnouncement.message }, `polite-${politeAnnouncement.id}`), _jsx("div", { "aria-live": "assertive", role: "alert", children: assertiveAnnouncement.message }, `assertive-${assertiveAnnouncement.id}`)] })] }));
}
export const useLiveAnnouncer = () => useContext(LiveAnnouncerContext);
export interface ValueCodec<T> {
equals?: (left: T, right: T) => boolean;
parse: (raw: string) => T;
serialize: (value: T) => string;
}
export declare const valueCodecEquals: <T>(codec: ValueCodec<T>, left: T, right: T) => boolean;
export declare const createStringCodec: () => ValueCodec<string>;
export declare const createJsonCodec: <T>() => ValueCodec<T>;
export declare const createNumberCodec: () => ValueCodec<number>;
export declare const createBooleanCodec: () => ValueCodec<boolean>;
export declare const createStringUnionCodec: <const T extends readonly [string, ...string[]]>(values: T) => ValueCodec<T[number]>;
//# sourceMappingURL=codecs.d.ts.map
{"version":3,"file":"codecs.d.ts","sourceRoot":"","sources":["../src/codecs.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC;IACxC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,CAAC,CAAC;IAC1B,SAAS,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;CACjC;AAID,eAAO,MAAM,gBAAgB,GAAI,CAAC,EAChC,OAAO,UAAU,CAAC,CAAC,CAAC,EACpB,MAAM,CAAC,EACP,OAAO,CAAC,YACuC,CAAC;AAElD,eAAO,MAAM,iBAAiB,QAAO,UAAU,CAAC,MAAM,CAGpD,CAAC;AAEH,eAAO,MAAM,eAAe,GAAI,CAAC,OAAM,UAAU,CAAC,CAAC,CAGjD,CAAC;AAEH,eAAO,MAAM,iBAAiB,QAAO,UAAU,CAAC,MAAM,CAWpD,CAAC;AAEH,eAAO,MAAM,kBAAkB,QAAO,UAAU,CAAC,OAAO,CAatD,CAAC;AAEH,eAAO,MAAM,sBAAsB,GAAI,KAAK,CAAC,CAAC,SAAS,SAAS,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,EACnF,QAAQ,CAAC,KACR,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CASrB,CAAC"}
const defaultEquals = (left, right) => Object.is(left, right);
export const valueCodecEquals = (codec, left, right) => (codec.equals ?? defaultEquals)(left, right);
export const createStringCodec = () => ({
parse: (raw) => raw,
serialize: (value) => value,
});
export const createJsonCodec = () => ({
parse: (raw) => JSON.parse(raw),
serialize: (value) => JSON.stringify(value),
});
export const createNumberCodec = () => ({
parse: (raw) => {
const value = Number(raw);
if (!Number.isFinite(value)) {
throw new Error('Stored value is not a finite number.');
}
return value;
},
serialize: (value) => `${value}`,
});
export const createBooleanCodec = () => ({
parse: (raw) => {
if (raw === 'true') {
return true;
}
if (raw === 'false') {
return false;
}
throw new Error('Stored value is not a boolean literal.');
},
serialize: (value) => `${value}`,
});
export const createStringUnionCodec = (values) => ({
parse: (raw) => {
if (values.includes(raw)) {
return raw;
}
throw new Error('Stored value is not one of the allowed string literals.');
},
serialize: (value) => value,
});
import { type Dispatch, type ReactNode, type RefCallback, type SetStateAction } from 'react';
import type { Keys } from 'react-hotkeys-hook';
import { type PersistentStateKey } from './persistent-state.js';
import { type RegisteredShortcut } from './shortcuts.js';
import type { ProjectStorage } from './storage.js';
import { type UrlStateHistoryMode, type UrlStateMode } from './url-state.js';
export type DebugFlagSource = 'default' | 'storage' | 'url';
export interface DebugFlagOptions {
defaultValue?: boolean;
description?: ReactNode;
hidden?: boolean;
hotkeys?: Keys;
label?: ReactNode;
sequence?: readonly string[];
storage?: ProjectStorage | null;
storageKey?: PersistentStateKey;
urlHistory?: UrlStateHistoryMode;
urlMode?: UrlStateMode;
urlParam?: string;
}
export interface DebugFlagState<T extends HTMLElement> {
clearStoredValue: () => void;
clearUrlOverride: () => void;
ref: RefCallback<T>;
shortcut: RegisteredShortcut | null;
setValue: Dispatch<SetStateAction<boolean>>;
source: DebugFlagSource;
toggle: () => void;
value: boolean;
}
export declare function useDebugFlag<T extends HTMLElement>(id: string, options?: DebugFlagOptions): DebugFlagState<T>;
//# sourceMappingURL=debug-flags.d.ts.map
{"version":3,"file":"debug-flags.d.ts","sourceRoot":"","sources":["../src/debug-flags.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,QAAQ,EAAE,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,OAAO,CAAC;AAC1G,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAG/C,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AAqBxB,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,SAAS,GAAG,KAAK,CAAC;AAE5D,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,IAAI,CAAC;IACf,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7B,OAAO,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IAChC,UAAU,CAAC,EAAE,kBAAkB,CAAC;IAChC,UAAU,CAAC,EAAE,mBAAmB,CAAC;IACjC,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,WAAW;IACnD,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;IACpB,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACpC,QAAQ,EAAE,QAAQ,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5C,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;CAChB;AAID,wBAAgB,YAAY,CAAC,CAAC,SAAS,WAAW,EAChD,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,gBAAqB,GAC7B,cAAc,CAAC,CAAC,CAAC,CAqEnB"}
import { useCallback } from 'react';
import { createBooleanCodec } from './codecs.js';
import { createProjectStorage } from './storage.js';
import { usePersistentState, } from './persistent-state.js';
import { useShortcutRegistry, } from './shortcuts.js';
import { useUrlState, } from './url-state.js';
const debugFlagUrlCodec = {
parse: (rawValue) => {
if (['1', 'true', 'yes', 'on'].includes(rawValue)) {
return true;
}
if (['0', 'false', 'no', 'off'].includes(rawValue)) {
return false;
}
throw new Error('Debug flag URL value is not a recognized boolean literal.');
},
serialize: (value) => value ? '1' : '0',
};
const isStateUpdater = (value) => (typeof value === 'function');
const fallbackDebugStorage = createProjectStorage('__tv-shared-debug-flags__', { storage: null });
export function useDebugFlag(id, options = {}) {
const defaultValue = options.defaultValue ?? false;
const storageKey = options.storageKey ?? ['debug', id];
const [storedValue, setStoredValue, storedControls] = usePersistentState(options.storage ?? fallbackDebugStorage, storageKey, {
codec: createBooleanCodec(),
defaultValue,
});
const [urlOverride, , urlControls] = useUrlState(options.urlParam ?? '__tv_shared_debug_flag_disabled__', {
codec: debugFlagUrlCodec,
defaultValue: null,
...(options.urlHistory === undefined ? {} : { history: options.urlHistory }),
...(options.urlMode === undefined ? {} : { mode: options.urlMode }),
});
const value = options.urlParam && urlOverride !== null ? urlOverride : storedValue;
const source = options.urlParam && urlControls.source === 'url'
? 'url'
: options.storage
? storedControls.source
: 'default';
const clearUrlOverride = useCallback(() => {
if (options.urlParam) {
urlControls.clear();
}
}, [options.urlParam, urlControls]);
const clearStoredValue = useCallback(() => {
storedControls.clear();
}, [storedControls]);
const setValue = useCallback((nextValue) => {
const resolvedValue = isStateUpdater(nextValue)
? nextValue(value)
: nextValue;
clearUrlOverride();
setStoredValue(resolvedValue);
}, [clearUrlOverride, setStoredValue, value]);
const toggle = useCallback(() => {
setValue((previousValue) => !previousValue);
}, [setValue]);
const shortcutRegistry = useShortcutRegistry(options.hotkeys || options.sequence
? [{
...(options.description === undefined ? {} : { description: options.description }),
...(options.hidden === undefined ? {} : { hidden: options.hidden }),
id,
...(options.hotkeys === undefined ? {} : { keys: options.hotkeys }),
label: options.label ?? id,
onTrigger: toggle,
...(options.sequence === undefined ? {} : { sequence: options.sequence }),
}]
: []);
return {
clearStoredValue,
clearUrlOverride,
ref: shortcutRegistry.ref,
shortcut: shortcutRegistry.shortcuts[0] ?? null,
setValue,
source,
toggle,
value,
};
}
import { type Dispatch, type SetStateAction } from 'react';
import { type ValueCodec } from './codecs.js';
import type { ProjectStorage, StorageKeyPart } from './storage.js';
export type PersistentStateKey = StorageKeyPart | readonly StorageKeyPart[];
export type PersistentStateSource = 'default' | 'storage';
export interface PersistentStateOptions<T> {
codec?: ValueCodec<T>;
defaultValue: T | (() => T);
}
export interface PersistentStateControls {
clear: () => void;
source: PersistentStateSource;
}
export declare function usePersistentState<T>(storage: ProjectStorage, key: PersistentStateKey, options: PersistentStateOptions<T>): [T, Dispatch<SetStateAction<T>>, PersistentStateControls];
//# sourceMappingURL=persistent-state.d.ts.map
{"version":3,"file":"persistent-state.d.ts","sourceRoot":"","sources":["../src/persistent-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6C,KAAK,QAAQ,EAAE,KAAK,cAAc,EAAE,MAAM,OAAO,CAAC;AACtG,OAAO,EAAqC,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AASnE,MAAM,MAAM,kBAAkB,GAAG,cAAc,GAAG,SAAS,cAAc,EAAE,CAAC;AAC5E,MAAM,MAAM,qBAAqB,GAAG,SAAS,GAAG,SAAS,CAAC;AAE1D,MAAM,WAAW,sBAAsB,CAAC,CAAC;IACvC,KAAK,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IACtB,YAAY,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,EAAE,qBAAqB,CAAC;CAC/B;AAgED,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,kBAAkB,EACvB,OAAO,EAAE,sBAAsB,CAAC,CAAC,CAAC,GACjC,CAAC,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,uBAAuB,CAAC,CA4I3D"}
import { useCallback, useEffect, useMemo, useState } from 'react';
import { createJsonCodec, valueCodecEquals } from './codecs.js';
const PERSISTENT_STATE_CHANGE_EVENT = 'tv-shared:persistent-state-change';
const isStateUpdater = (value) => (typeof value === 'function');
const resolveDefaultValue = (value) => (typeof value === 'function'
? value()
: value);
const normalizeKeyParts = (key) => {
if (typeof key === 'string' || typeof key === 'number') {
return [key];
}
return [...key];
};
const dispatchPersistentStateChange = (fullKey, rawValue) => {
if (typeof window === 'undefined') {
return;
}
window.dispatchEvent(new CustomEvent(PERSISTENT_STATE_CHANGE_EVENT, { detail: { fullKey, rawValue } }));
};
const readSnapshot = (storage, keyParts, codec, fallbackValue) => {
const rawValue = storage.readString(...keyParts);
if (rawValue === null) {
return {
source: 'default',
value: fallbackValue,
};
}
try {
return {
source: 'storage',
value: codec.parse(rawValue),
};
}
catch {
return {
source: 'default',
value: fallbackValue,
};
}
};
export function usePersistentState(storage, key, options) {
const codec = options.codec ?? createJsonCodec();
const keyParts = useMemo(() => normalizeKeyParts(key), [key]);
const fullKey = useMemo(() => storage.key(...keyParts), [keyParts, storage]);
const fallbackValue = resolveDefaultValue(options.defaultValue);
const [snapshot, setSnapshot] = useState(() => (readSnapshot(storage, keyParts, codec, fallbackValue)));
useEffect(() => {
const nextSnapshot = readSnapshot(storage, keyParts, codec, fallbackValue);
setSnapshot((previousSnapshot) => (previousSnapshot.source === nextSnapshot.source
&& valueCodecEquals(codec, previousSnapshot.value, nextSnapshot.value)
? previousSnapshot
: nextSnapshot));
}, [codec, fallbackValue, keyParts, storage]);
useEffect(() => {
if (typeof window === 'undefined') {
return undefined;
}
const handleStorage = (event) => {
if (event.key !== fullKey) {
return;
}
const nextSnapshot = readSnapshot(storage, keyParts, codec, fallbackValue);
setSnapshot((previousSnapshot) => (previousSnapshot.source === nextSnapshot.source
&& valueCodecEquals(codec, previousSnapshot.value, nextSnapshot.value)
? previousSnapshot
: nextSnapshot));
};
const handlePersistentStateChange = (event) => {
const detail = event.detail;
if (detail.fullKey !== fullKey) {
return;
}
if (detail.rawValue === null) {
setSnapshot((previousSnapshot) => (previousSnapshot.source === 'default'
&& valueCodecEquals(codec, previousSnapshot.value, fallbackValue)
? previousSnapshot
: {
source: 'default',
value: fallbackValue,
}));
return;
}
try {
const nextValue = codec.parse(detail.rawValue);
setSnapshot((previousSnapshot) => (previousSnapshot.source === 'storage'
&& valueCodecEquals(codec, previousSnapshot.value, nextValue)
? previousSnapshot
: {
source: 'storage',
value: nextValue,
}));
}
catch {
setSnapshot({
source: 'default',
value: fallbackValue,
});
}
};
window.addEventListener('storage', handleStorage);
window.addEventListener(PERSISTENT_STATE_CHANGE_EVENT, handlePersistentStateChange);
return () => {
window.removeEventListener('storage', handleStorage);
window.removeEventListener(PERSISTENT_STATE_CHANGE_EVENT, handlePersistentStateChange);
};
}, [codec, fallbackValue, fullKey, keyParts, storage]);
const setValue = useCallback((value) => {
setSnapshot((previousSnapshot) => {
const nextValue = isStateUpdater(value)
? value(previousSnapshot.value)
: value;
let rawValue;
try {
rawValue = codec.serialize(nextValue);
}
catch {
return previousSnapshot;
}
storage.writeString(rawValue, ...keyParts);
dispatchPersistentStateChange(fullKey, rawValue);
if (previousSnapshot.source === 'storage'
&& valueCodecEquals(codec, previousSnapshot.value, nextValue)) {
return previousSnapshot;
}
return {
source: 'storage',
value: nextValue,
};
});
}, [codec, fullKey, keyParts, storage]);
const clear = useCallback(() => {
storage.remove(...keyParts);
dispatchPersistentStateChange(fullKey, null);
setSnapshot((previousSnapshot) => (previousSnapshot.source === 'default'
&& valueCodecEquals(codec, previousSnapshot.value, fallbackValue)
? previousSnapshot
: {
source: 'default',
value: fallbackValue,
}));
}, [codec, fallbackValue, fullKey, keyParts, storage]);
return [
snapshot.value,
setValue,
{
clear,
source: snapshot.source,
},
];
}
export interface ShareContent {
text?: string;
title?: string;
url?: string;
}
export interface ClipboardTarget {
clipboard?: {
writeText: (text: string) => Promise<void>;
};
}
export interface ShareTarget extends ClipboardTarget {
share?: (data: ShareContent) => Promise<void>;
}
export type ShareResult = 'copied' | 'shared' | 'unavailable';
export declare const formatShareContent: ({ text, title, url }: ShareContent) => string;
export declare const writeClipboardText: (text: string, options?: {
navigator?: ClipboardTarget | null;
}) => Promise<boolean>;
export declare const shareContent: (content: ShareContent, options?: {
clipboardText?: string;
fallbackToClipboard?: boolean;
navigator?: ShareTarget | null;
}) => Promise<ShareResult>;
//# sourceMappingURL=share.d.ts.map
{"version":3,"file":"share.d.ts","sourceRoot":"","sources":["../src/share.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE;QACV,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KAC5C,CAAC;CACH;AAED,MAAM,WAAW,WAAY,SAAQ,eAAe;IAClD,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAED,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,aAAa,CAAC;AAgB9D,eAAO,MAAM,kBAAkB,GAAI,sBAAsB,YAAY,WAEpE,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,MAAM,MAAM,EACZ,UAAS;IACP,SAAS,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;CAC/B,qBAcP,CAAC;AAEF,eAAO,MAAM,YAAY,GACvB,SAAS,YAAY,EACrB,UAAS;IACP,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,SAAS,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CAC3B,KACL,OAAO,CAAC,WAAW,CAwBrB,CAAC"}
const resolveNavigator = (providedNavigator) => {
if (providedNavigator !== undefined) {
return providedNavigator;
}
if (typeof navigator === 'undefined') {
return null;
}
return navigator;
};
export const formatShareContent = ({ text, title, url }) => ([title, text, url].filter(Boolean).join('\n\n'));
export const writeClipboardText = async (text, options = {}) => {
const activeNavigator = resolveNavigator(options.navigator);
if (!activeNavigator?.clipboard?.writeText) {
return false;
}
try {
await activeNavigator.clipboard.writeText(text);
return true;
}
catch {
return false;
}
};
export const shareContent = async (content, options = {}) => {
const activeNavigator = resolveNavigator(options.navigator);
if (activeNavigator?.share) {
try {
await activeNavigator.share(content);
return 'shared';
}
catch {
// Fall through to clipboard when requested.
}
}
if (options.fallbackToClipboard !== false) {
const didCopy = await writeClipboardText(options.clipboardText ?? formatShareContent(content), { navigator: activeNavigator });
if (didCopy) {
return 'copied';
}
}
return 'unavailable';
};
import { type ComponentPropsWithoutRef, type ReactNode, type RefCallback } from 'react';
import type { Keys, Options as HotkeyOptions } from 'react-hotkeys-hook';
import { type KeySequenceOptions } from './hotkeys.js';
export interface ShortcutDefinition {
description?: ReactNode;
hidden?: boolean;
id: string;
keys?: Keys;
label: ReactNode;
onTrigger: (event: KeyboardEvent) => void;
sequence?: readonly string[];
}
export interface RegisteredShortcut {
description?: ReactNode;
hidden?: boolean;
id: string;
keys?: Keys;
label: ReactNode;
sequence?: readonly string[];
}
export interface ShortcutRegistryOptions {
hotkeys?: HotkeyOptions;
sequences?: KeySequenceOptions;
}
export interface ShortcutRegistryResult<T extends HTMLElement> {
ref: RefCallback<T>;
shortcuts: readonly RegisteredShortcut[];
visibleShortcuts: readonly RegisteredShortcut[];
}
export interface ShortcutPanelProps extends Omit<ComponentPropsWithoutRef<'section'>, 'children' | 'title'> {
emptyLabel?: ReactNode;
heading?: ReactNode;
shortcuts: readonly RegisteredShortcut[];
unstyled?: boolean;
}
export declare const formatShortcutGesture: (shortcut: Pick<RegisteredShortcut, "keys" | "sequence">) => any;
export declare function useShortcutRegistry<T extends HTMLElement>(shortcuts: readonly ShortcutDefinition[], options?: ShortcutRegistryOptions): ShortcutRegistryResult<T>;
export declare function ShortcutPanel({ className, emptyLabel, heading, shortcuts, style, unstyled, ...props }: ShortcutPanelProps): import("react/jsx-runtime").JSX.Element;
//# sourceMappingURL=shortcuts.d.ts.map
{"version":3,"file":"shortcuts.d.ts","sourceRoot":"","sources":["../src/shortcuts.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,wBAAwB,EAC7B,KAAK,SAAS,EACd,KAAK,WAAW,EAEjB,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,cAAc,CAAC;AAStB,MAAM,WAAW,kBAAkB;IACjC,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,SAAS,CAAC,EAAE,kBAAkB,CAAC;CAChC;AAED,MAAM,WAAW,sBAAsB,CAAC,CAAC,SAAS,WAAW;IAC3D,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;IACpB,SAAS,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACzC,gBAAgB,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACjD;AAED,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,wBAAwB,CAAC,SAAS,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC;IACzG,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,SAAS,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA6DD,eAAO,MAAM,qBAAqB,GAAI,UAAU,IAAI,CAAC,kBAAkB,EAAE,MAAM,GAAG,UAAU,CAAC,QAY5F,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,WAAW,EACvD,SAAS,EAAE,SAAS,kBAAkB,EAAE,EACxC,OAAO,GAAE,uBAA4B,GACpC,sBAAsB,CAAC,CAAC,CAAC,CA2C3B;AAED,wBAAgB,aAAa,CAAC,EAC5B,SAAS,EACT,UAAuC,EACvC,OAAqB,EACrB,SAAS,EACT,KAAK,EACL,QAAgB,EAChB,GAAG,KAAK,EACT,EAAE,kBAAkB,2CAwCpB"}
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useCallback, } from 'react';
import { useHotkeys, useKeySequence, } from './hotkeys.js';
const FALLBACK_DISABLED_HOTKEY = 'f24';
const FALLBACK_DISABLED_SEQUENCE = ['__tv_shared_disabled__'];
const DEFAULT_PANEL_STYLE = {
background: 'rgba(255, 255, 255, 0.85)',
border: '1px solid rgba(148, 163, 184, 0.3)',
borderRadius: '18px',
color: '#0f172a',
display: 'grid',
gap: '0.85rem',
padding: '1rem',
};
const DEFAULT_LIST_STYLE = {
display: 'grid',
gap: '0.65rem',
listStyle: 'none',
margin: 0,
padding: 0,
};
const DEFAULT_ITEM_STYLE = {
alignItems: 'center',
display: 'grid',
gap: '0.65rem',
gridTemplateColumns: 'minmax(0, 1fr) auto',
};
const DEFAULT_GESTURE_STYLE = {
background: 'rgba(15, 23, 42, 0.08)',
borderRadius: '999px',
fontSize: '0.8rem',
fontWeight: 700,
padding: '0.28rem 0.6rem',
whiteSpace: 'nowrap',
};
const noop = () => { };
const stripShortcut = ({ description, hidden, id, keys, label, sequence, }) => ({
...(description === undefined ? {} : { description }),
...(hidden === undefined ? {} : { hidden }),
id,
...(keys === undefined ? {} : { keys }),
label,
...(sequence === undefined ? {} : { sequence }),
});
const assignRef = (ref, value) => {
ref.current = value;
};
export const formatShortcutGesture = (shortcut) => {
if (shortcut.keys !== undefined) {
return Array.isArray(shortcut.keys)
? shortcut.keys.join(' / ')
: shortcut.keys;
}
if (shortcut.sequence !== undefined) {
return shortcut.sequence.join(' then ');
}
return '';
};
export function useShortcutRegistry(shortcuts, options = {}) {
const registeredShortcuts = shortcuts.map(stripShortcut);
const visibleShortcuts = registeredShortcuts.filter((shortcut) => !shortcut.hidden);
const hotkeyBindings = shortcuts
.filter((shortcut) => shortcut.keys !== undefined)
.map((shortcut) => ({
callback: shortcut.onTrigger,
keys: shortcut.keys,
}));
const sequenceBindings = shortcuts
.filter((shortcut) => shortcut.sequence !== undefined)
.map((shortcut) => ({
callback: shortcut.onTrigger,
sequence: shortcut.sequence,
}));
const hotkeyRef = useHotkeys(hotkeyBindings.length > 0
? hotkeyBindings
: [{ callback: noop, keys: FALLBACK_DISABLED_HOTKEY }], {
...options.hotkeys,
enabled: hotkeyBindings.length > 0 && options.hotkeys?.enabled !== false,
});
const sequenceRef = useKeySequence(sequenceBindings.length > 0
? sequenceBindings
: [{ callback: noop, sequence: FALLBACK_DISABLED_SEQUENCE }], {
...options.sequences,
enabled: sequenceBindings.length > 0 && options.sequences?.enabled !== false,
});
const ref = useCallback((node) => {
assignRef(hotkeyRef, node);
assignRef(sequenceRef, node);
}, [hotkeyRef, sequenceRef]);
return {
ref,
shortcuts: registeredShortcuts,
visibleShortcuts,
};
}
export function ShortcutPanel({ className, emptyLabel = 'No shortcuts registered.', heading = 'Shortcuts', shortcuts, style, unstyled = false, ...props }) {
if (shortcuts.length === 0) {
return (_jsxs("section", { className: className, style: unstyled ? style : { ...DEFAULT_PANEL_STYLE, ...style }, ...props, children: [_jsx("strong", { children: heading }), _jsx("p", { children: emptyLabel })] }));
}
return (_jsxs("section", { className: className, style: unstyled ? style : { ...DEFAULT_PANEL_STYLE, ...style }, ...props, children: [_jsx("strong", { children: heading }), _jsx("ul", { style: unstyled ? undefined : DEFAULT_LIST_STYLE, children: shortcuts.map((shortcut) => (_jsxs("li", { style: unstyled ? undefined : DEFAULT_ITEM_STYLE, children: [_jsxs("span", { children: [_jsx("span", { children: shortcut.label }), shortcut.description ? (_jsx("span", { style: { color: '#475569', display: 'block', marginTop: '0.2rem' }, children: shortcut.description })) : null] }), _jsx("kbd", { style: unstyled ? undefined : DEFAULT_GESTURE_STYLE, children: formatShortcutGesture(shortcut) })] }, shortcut.id))) })] }));
}
import { type ValueCodec } from './codecs.js';
import type { StorageKeyPart } from './storage.js';
declare const SNAPSHOT_FORMAT = "tv-shared.snapshot.v1";
export interface SnapshotEnvelope {
capturedAt: string;
format: typeof SNAPSHOT_FORMAT;
kind?: string;
payload: string;
version?: StorageKeyPart;
}
export interface SnapshotOptions<T> {
capturedAt?: Date | string;
codec?: ValueCodec<T>;
kind?: string;
version?: StorageKeyPart;
}
export interface ParsedSnapshot<T> {
capturedAt: string;
kind?: string;
value: T;
version?: StorageKeyPart;
}
export declare const createSnapshotEnvelope: <T>(value: T, options?: SnapshotOptions<T>) => SnapshotEnvelope;
export declare const serializeSnapshot: <T>(value: T, options?: SnapshotOptions<T>) => string;
export declare const parseSnapshot: <T>(rawValue: string, options?: Omit<SnapshotOptions<T>, "capturedAt">) => ParsedSnapshot<T>;
export declare const copySnapshotToClipboard: <T>(value: T, options?: SnapshotOptions<T> & {
navigator?: {
clipboard?: {
writeText: (text: string) => Promise<void>;
};
} | null;
}) => Promise<boolean>;
export {};
//# sourceMappingURL=snapshots.d.ts.map
{"version":3,"file":"snapshots.d.ts","sourceRoot":"","sources":["../src/snapshots.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAE/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,QAAA,MAAM,eAAe,0BAA0B,CAAC;AAEhD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,eAAe,CAAC;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,UAAU,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,CAAC,CAAC;IACT,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B;AAUD,eAAO,MAAM,sBAAsB,GAAI,CAAC,EACtC,OAAO,CAAC,EACR,UAAS,eAAe,CAAC,CAAC,CAAM,KAC/B,gBAUF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,CAAC,EACjC,OAAO,CAAC,EACR,UAAS,eAAe,CAAC,CAAC,CAAM,WACkC,CAAC;AAErE,eAAO,MAAM,aAAa,GAAI,CAAC,EAC7B,UAAU,MAAM,EAChB,UAAS,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,YAAY,CAAM,KACnD,cAAc,CAAC,CAAC,CA8BlB,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAU,CAAC,EAC7C,OAAO,CAAC,EACR,UAAS,eAAe,CAAC,CAAC,CAAC,GAAG;IAC5B,SAAS,CAAC,EAAE;QACV,SAAS,CAAC,EAAE;YACV,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;SAC5C,CAAC;KACH,GAAG,IAAI,CAAC;CACL,qBAMP,CAAC"}
import { createJsonCodec } from './codecs.js';
import { writeClipboardText } from './share.js';
const SNAPSHOT_FORMAT = 'tv-shared.snapshot.v1';
const resolveCapturedAt = (value) => {
if (value instanceof Date) {
return value.toISOString();
}
return value ?? new Date().toISOString();
};
export const createSnapshotEnvelope = (value, options = {}) => {
const codec = options.codec ?? createJsonCodec();
return {
capturedAt: resolveCapturedAt(options.capturedAt),
format: SNAPSHOT_FORMAT,
...(options.kind === undefined ? {} : { kind: options.kind }),
payload: codec.serialize(value),
...(options.version === undefined ? {} : { version: options.version }),
};
};
export const serializeSnapshot = (value, options = {}) => JSON.stringify(createSnapshotEnvelope(value, options), null, 2);
export const parseSnapshot = (rawValue, options = {}) => {
const codec = options.codec ?? createJsonCodec();
const parsedValue = JSON.parse(rawValue);
if (parsedValue.format !== SNAPSHOT_FORMAT) {
throw new Error('Snapshot format is not supported.');
}
if (typeof parsedValue.capturedAt !== 'string' || parsedValue.capturedAt.length === 0) {
throw new Error('Snapshot is missing a capturedAt timestamp.');
}
if (typeof parsedValue.payload !== 'string') {
throw new Error('Snapshot is missing a payload.');
}
if (options.kind !== undefined && parsedValue.kind !== options.kind) {
throw new Error(`Snapshot kind mismatch. Expected "${options.kind}".`);
}
if (options.version !== undefined && parsedValue.version !== options.version) {
throw new Error(`Snapshot version mismatch. Expected "${options.version}".`);
}
return {
capturedAt: parsedValue.capturedAt,
...(parsedValue.kind === undefined ? {} : { kind: parsedValue.kind }),
value: codec.parse(parsedValue.payload),
...(parsedValue.version === undefined ? {} : { version: parsedValue.version }),
};
};
export const copySnapshotToClipboard = async (value, options = {}) => writeClipboardText(serializeSnapshot(value, options), options.navigator === undefined
? {}
: { navigator: options.navigator });
import { type Dispatch, type SetStateAction } from 'react';
import { type PersistentStateControls, type PersistentStateKey } from './persistent-state.js';
import type { ProjectStorage } from './storage.js';
export type ThemePreference = 'dark' | 'light' | 'system';
export type ResolvedTheme = 'dark' | 'light';
export interface ThemePreferenceOptions {
applyToDocument?: boolean;
attributeName?: string;
defaultValue?: ThemePreference;
storageKey?: PersistentStateKey;
window?: Window | null;
}
export interface ThemePreferenceState extends PersistentStateControls {
resolvedTheme: ResolvedTheme;
setThemePreference: Dispatch<SetStateAction<ThemePreference>>;
systemTheme: ResolvedTheme;
themePreference: ThemePreference;
}
export declare const resolveThemePreference: (themePreference: ThemePreference, systemTheme: ResolvedTheme) => ResolvedTheme;
export declare const useSystemTheme: (options?: {
window?: Window | null;
}) => ResolvedTheme;
export declare const useThemePreference: (storage: ProjectStorage, options?: ThemePreferenceOptions) => ThemePreferenceState;
//# sourceMappingURL=theme.d.ts.map
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,QAAQ,EAAE,KAAK,cAAc,EAAE,MAAM,OAAO,CAAC;AAEhF,OAAO,EAEL,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACxB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAKnD,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAC1D,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,CAAC;AAE7C,MAAM,WAAW,sBAAsB;IACrC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,eAAe,CAAC;IAC/B,UAAU,CAAC,EAAE,kBAAkB,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,oBAAqB,SAAQ,uBAAuB;IACnE,aAAa,EAAE,aAAa,CAAC;IAC7B,kBAAkB,EAAE,QAAQ,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC,CAAC;IAC9D,WAAW,EAAE,aAAa,CAAC;IAC3B,eAAe,EAAE,eAAe,CAAC;CAClC;AAwBD,eAAO,MAAM,sBAAsB,GACjC,iBAAiB,eAAe,EAChC,aAAa,aAAa,KACzB,aAEF,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,UAAS;IACtC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnB,kBAwBL,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,SAAS,cAAc,EACvB,UAAS,sBAA2B,KACnC,oBAuCF,CAAC"}
import { useEffect, useState } from 'react';
import { createStringUnionCodec } from './codecs.js';
import { usePersistentState, } from './persistent-state.js';
const DARK_THEME_MEDIA_QUERY = '(prefers-color-scheme: dark)';
const themePreferenceCodec = createStringUnionCodec(['light', 'dark', 'system']);
const resolveWindow = (providedWindow) => {
if (providedWindow !== undefined) {
return providedWindow;
}
if (typeof window === 'undefined') {
return null;
}
return window;
};
const getSystemTheme = (providedWindow) => {
const activeWindow = resolveWindow(providedWindow);
if (!activeWindow?.matchMedia) {
return 'light';
}
return activeWindow.matchMedia(DARK_THEME_MEDIA_QUERY).matches ? 'dark' : 'light';
};
export const resolveThemePreference = (themePreference, systemTheme) => (themePreference === 'system' ? systemTheme : themePreference);
export const useSystemTheme = (options = {}) => {
const [systemTheme, setSystemTheme] = useState(() => getSystemTheme(options.window));
useEffect(() => {
const activeWindow = resolveWindow(options.window);
if (!activeWindow?.matchMedia) {
return undefined;
}
const mediaQuery = activeWindow.matchMedia(DARK_THEME_MEDIA_QUERY);
const handleChange = () => {
setSystemTheme(mediaQuery.matches ? 'dark' : 'light');
};
handleChange();
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}, [options.window, setSystemTheme]);
return systemTheme;
};
export const useThemePreference = (storage, options = {}) => {
const themeStorageKey = options.storageKey ?? 'theme-preference';
const [themePreference, setThemePreference, controls] = usePersistentState(storage, themeStorageKey, {
codec: themePreferenceCodec,
defaultValue: options.defaultValue ?? 'system',
});
const systemTheme = useSystemTheme(options.window === undefined
? undefined
: { window: options.window });
const resolvedTheme = resolveThemePreference(themePreference, systemTheme);
useEffect(() => {
if (!options.applyToDocument) {
return;
}
const activeWindow = resolveWindow(options.window);
const rootElement = activeWindow?.document?.documentElement;
if (!rootElement) {
return;
}
rootElement.setAttribute(options.attributeName ?? 'data-theme', resolvedTheme);
}, [options.applyToDocument, options.attributeName, options.window, resolvedTheme]);
return {
...controls,
resolvedTheme,
setThemePreference,
systemTheme,
themePreference,
};
};
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
export declare const tvProgramsWordmarkClassNames: {
readonly root: "tv-shared-tvprograms-wordmark";
readonly mark: "tv-shared-tvprograms-wordmark__mark";
readonly label: "tv-shared-tvprograms-wordmark__label";
};
export type TvProgramsWordmarkProps = Omit<ComponentPropsWithoutRef<'span'>, 'children'> & {
label?: ReactNode;
labelClassName?: string;
markClassName?: string;
unstyled?: boolean;
};
export declare function TvProgramsWordmark({ className, label, labelClassName, markClassName, style, unstyled, ...props }: TvProgramsWordmarkProps): import("react/jsx-runtime").JSX.Element;
//# sourceMappingURL=TvProgramsWordmark.d.ts.map
{"version":3,"file":"TvProgramsWordmark.d.ts","sourceRoot":"","sources":["../src/TvProgramsWordmark.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,wBAAwB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAoBhF,eAAO,MAAM,4BAA4B;;;;CAI/B,CAAC;AAEX,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,wBAAwB,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,GAAG;IACzF,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,EACjC,SAAS,EACT,KAAqB,EACrB,cAAc,EACd,aAAa,EACb,KAAK,EACL,QAAgB,EAChB,GAAG,KAAK,EACT,EAAE,uBAAuB,2CAmBzB"}
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { TvProgramsMark } from './TvProgramsMark.js';
const DEFAULT_ROOT_STYLE = {
alignItems: 'center',
color: 'inherit',
display: 'inline-flex',
fontFamily: 'inherit',
fontWeight: 700,
gap: '0.5rem',
lineHeight: 1,
};
const DEFAULT_MARK_STYLE = {
display: 'block',
flexShrink: 0,
height: '1.15em',
width: '1.15em',
};
export const tvProgramsWordmarkClassNames = {
root: 'tv-shared-tvprograms-wordmark',
mark: 'tv-shared-tvprograms-wordmark__mark',
label: 'tv-shared-tvprograms-wordmark__label',
};
export function TvProgramsWordmark({ className, label = 'TV Programs', labelClassName, markClassName, style, unstyled = false, ...props }) {
const rootClassName = [tvProgramsWordmarkClassNames.root, className].filter(Boolean).join(' ');
const nextMarkClassName = [tvProgramsWordmarkClassNames.mark, markClassName].filter(Boolean).join(' ');
const nextLabelClassName = [tvProgramsWordmarkClassNames.label, labelClassName].filter(Boolean).join(' ');
return (_jsxs("span", { className: rootClassName, style: unstyled ? style : { ...DEFAULT_ROOT_STYLE, ...style }, ...props, children: [_jsx(TvProgramsMark, { "aria-hidden": "true", className: nextMarkClassName, style: unstyled ? undefined : DEFAULT_MARK_STYLE }), _jsx("span", { className: nextLabelClassName, children: label })] }));
}
import { type Dispatch, type SetStateAction } from 'react';
import { type ValueCodec } from './codecs.js';
export type UrlStateHistoryMode = 'push' | 'replace';
export type UrlStateMode = 'hash' | 'query';
export type UrlStateSource = 'default' | 'url';
export interface UrlStateOptions<T> {
codec?: ValueCodec<T>;
defaultValue: T | (() => T);
history?: UrlStateHistoryMode;
mode?: UrlStateMode;
persistDefault?: boolean;
window?: Window | null;
}
export interface UrlStateControls {
clear: () => void;
source: UrlStateSource;
}
export declare function useUrlState<T>(key: string, options: UrlStateOptions<T>): [T, Dispatch<SetStateAction<T>>, UrlStateControls];
//# sourceMappingURL=url-state.d.ts.map
{"version":3,"file":"url-state.d.ts","sourceRoot":"","sources":["../src/url-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,KAAK,QAAQ,EAAE,KAAK,cAAc,EAAE,MAAM,OAAO,CAAC;AAC7F,OAAO,EAAuC,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AASnF,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,SAAS,CAAC;AACrD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,OAAO,CAAC;AAC5C,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,KAAK,CAAC;AAE/C,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,KAAK,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IACtB,YAAY,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5B,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,EAAE,cAAc,CAAC;CACxB;AAuJD,wBAAgB,WAAW,CAAC,CAAC,EAC3B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,GAC1B,CAAC,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAkHpD"}
import { useCallback, useEffect, useState } from 'react';
import { createStringCodec, valueCodecEquals } from './codecs.js';
const URL_STATE_CHANGE_EVENT = 'tv-shared:url-state-change';
const isStateUpdater = (value) => (typeof value === 'function');
const resolveDefaultValue = (value) => (typeof value === 'function'
? value()
: value);
const resolveWindow = (providedWindow) => {
if (providedWindow !== undefined) {
return providedWindow;
}
if (typeof window === 'undefined') {
return null;
}
return window;
};
const getParamsForMode = (url, mode) => (mode === 'query'
? new URLSearchParams(url.search)
: new URLSearchParams(url.hash.startsWith('#') ? url.hash.slice(1) : url.hash));
const setParamsForMode = (url, mode, params) => {
const serializedParams = params.toString();
if (mode === 'query') {
url.search = serializedParams.length > 0 ? `?${serializedParams}` : '';
return;
}
url.hash = serializedParams.length > 0 ? `#${serializedParams}` : '';
};
const getHistoryUrl = (url) => `${url.pathname}${url.search}${url.hash}`;
const dispatchUrlStateChange = (activeWindow, key, mode) => {
activeWindow.dispatchEvent(new CustomEvent(URL_STATE_CHANGE_EVENT, { detail: { key, mode } }));
};
const readSnapshot = (key, codec, fallbackValue, mode, activeWindow) => {
if (!activeWindow) {
return {
source: 'default',
value: fallbackValue,
};
}
const url = new URL(activeWindow.location.href);
const rawValue = getParamsForMode(url, mode).get(key);
if (rawValue === null) {
return {
source: 'default',
value: fallbackValue,
};
}
try {
return {
source: 'url',
value: codec.parse(rawValue),
};
}
catch {
return {
source: 'default',
value: fallbackValue,
};
}
};
const writeSnapshot = (activeWindow, key, value, codec, fallbackValue, mode, historyMode, persistDefault) => {
if (!activeWindow) {
return;
}
const url = new URL(activeWindow.location.href);
const params = getParamsForMode(url, mode);
if (!persistDefault && valueCodecEquals(codec, value, fallbackValue)) {
params.delete(key);
}
else {
params.set(key, codec.serialize(value));
}
setParamsForMode(url, mode, params);
activeWindow.history[historyMode === 'push' ? 'pushState' : 'replaceState'](activeWindow.history.state, '', getHistoryUrl(url));
dispatchUrlStateChange(activeWindow, key, mode);
};
const clearSnapshot = (activeWindow, key, mode, historyMode) => {
if (!activeWindow) {
return;
}
const url = new URL(activeWindow.location.href);
const params = getParamsForMode(url, mode);
params.delete(key);
setParamsForMode(url, mode, params);
activeWindow.history[historyMode === 'push' ? 'pushState' : 'replaceState'](activeWindow.history.state, '', getHistoryUrl(url));
dispatchUrlStateChange(activeWindow, key, mode);
};
export function useUrlState(key, options) {
const codec = options.codec ?? createStringCodec();
const fallbackValue = resolveDefaultValue(options.defaultValue);
const mode = options.mode ?? 'query';
const historyMode = options.history ?? 'replace';
const persistDefault = options.persistDefault ?? false;
const activeWindow = resolveWindow(options.window);
const [snapshot, setSnapshot] = useState(() => (readSnapshot(key, codec, fallbackValue, mode, activeWindow)));
useEffect(() => {
const nextSnapshot = readSnapshot(key, codec, fallbackValue, mode, activeWindow);
setSnapshot((previousSnapshot) => (previousSnapshot.source === nextSnapshot.source
&& valueCodecEquals(codec, previousSnapshot.value, nextSnapshot.value)
? previousSnapshot
: nextSnapshot));
}, [activeWindow, codec, fallbackValue, key, mode]);
useEffect(() => {
if (!activeWindow) {
return undefined;
}
const handleChange = () => {
const nextSnapshot = readSnapshot(key, codec, fallbackValue, mode, activeWindow);
setSnapshot((previousSnapshot) => (previousSnapshot.source === nextSnapshot.source
&& valueCodecEquals(codec, previousSnapshot.value, nextSnapshot.value)
? previousSnapshot
: nextSnapshot));
};
const handleUrlStateChange = (event) => {
const detail = event.detail;
if (detail.key !== key || detail.mode !== mode) {
return;
}
handleChange();
};
activeWindow.addEventListener('popstate', handleChange);
activeWindow.addEventListener('hashchange', handleChange);
activeWindow.addEventListener(URL_STATE_CHANGE_EVENT, handleUrlStateChange);
return () => {
activeWindow.removeEventListener('popstate', handleChange);
activeWindow.removeEventListener('hashchange', handleChange);
activeWindow.removeEventListener(URL_STATE_CHANGE_EVENT, handleUrlStateChange);
};
}, [activeWindow, codec, fallbackValue, key, mode]);
const setValue = useCallback((value) => {
setSnapshot((previousSnapshot) => {
const nextValue = isStateUpdater(value)
? value(previousSnapshot.value)
: value;
try {
writeSnapshot(activeWindow, key, nextValue, codec, fallbackValue, mode, historyMode, persistDefault);
}
catch {
return previousSnapshot;
}
if (previousSnapshot.source === 'url'
&& valueCodecEquals(codec, previousSnapshot.value, nextValue)) {
return previousSnapshot;
}
return {
source: 'url',
value: nextValue,
};
});
}, [activeWindow, codec, fallbackValue, historyMode, key, mode, persistDefault]);
const clear = useCallback(() => {
clearSnapshot(activeWindow, key, mode, historyMode);
setSnapshot((previousSnapshot) => (previousSnapshot.source === 'default'
&& valueCodecEquals(codec, previousSnapshot.value, fallbackValue)
? previousSnapshot
: {
source: 'default',
value: fallbackValue,
}));
}, [activeWindow, codec, fallbackValue, historyMode, key, mode]);
return [
snapshot.value,
setValue,
{
clear,
source: snapshot.source,
},
];
}
+1
-1

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

{"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,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvD,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,kBAAkB;IACjC,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;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IACzC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,MAAM,aAAa,GAAG,kBAAkB,CAAC;AAgC/C,QAAA,MAAM,oBAAoB,mFAWhB,CAAC;AAgIX,OAAO,EACL,oBAAoB,GACrB,CAAC;AAEF,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,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AACvB,wBAAgB,UAAU,CAAC,CAAC,SAAS,WAAW,EAC9C,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,OAAO,CAAC,EAAE,OAAO,EACjB,IAAI,CAAC,EAAE,cAAc,GACpB,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AAyDvB,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,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAErB;AAED,wBAAgB,cAAc,CAAC,CAAC,SAAS,WAAW,EAClD,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,EACxC,OAAO,CAAC,EAAE,kBAAkB,EAC5B,IAAI,CAAC,EAAE,cAAc,GACpB,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AACvB,wBAAgB,cAAc,CAAC,CAAC,SAAS,WAAW,EAClD,QAAQ,EAAE,SAAS,kBAAkB,EAAE,EACvC,OAAO,CAAC,EAAE,kBAAkB,EAC5B,IAAI,CAAC,EAAE,cAAc,GACpB,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC"}
{"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,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvD,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,kBAAkB;IACjC,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;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IACzC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,MAAM,aAAa,GAAG,kBAAkB,CAAC;AA2B/C,QAAA,MAAM,oBAAoB,mFAWhB,CAAC;AAgIX,OAAO,EACL,oBAAoB,GACrB,CAAC;AAEF,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,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AACvB,wBAAgB,UAAU,CAAC,CAAC,SAAS,WAAW,EAC9C,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,OAAO,CAAC,EAAE,OAAO,EACjB,IAAI,CAAC,EAAE,cAAc,GACpB,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AAyDvB,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,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAErB;AAED,wBAAgB,cAAc,CAAC,CAAC,SAAS,WAAW,EAClD,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,EACxC,OAAO,CAAC,EAAE,kBAAkB,EAC5B,IAAI,CAAC,EAAE,cAAc,GACpB,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AACvB,wBAAgB,cAAc,CAAC,CAAC,SAAS,WAAW,EAClD,QAAQ,EAAE,SAAS,kBAAkB,EAAE,EACvC,OAAO,CAAC,EAAE,kBAAkB,EAC5B,IAAI,CAAC,EAAE,cAAc,GACpB,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC"}

@@ -8,7 +8,2 @@ import { useCallback, useEffect, useRef } from 'react';

' ': 'space',
',': 'comma',
'-': 'slash',
'.': 'period',
'#': 'backslash',
'+': 'bracketright',
altleft: 'alt',

@@ -15,0 +10,0 @@ altright: 'alt',

export { BrandBadge, brandBadgeClassNames, type BrandBadgeProps, } from './BrandBadge.js';
export { TVPROGRAMS_DEFAULT_LABEL, TVPROGRAMS_HOSTNAME, TVPROGRAMS_URL, } from './constants.js';
export { TvProgramsMark, type TvProgramsMarkProps, } from './TvProgramsMark.js';
export { TvProgramsWordmark, tvProgramsWordmarkClassNames, type TvProgramsWordmarkProps, } from './TvProgramsWordmark.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, type KeySequenceBinding, type KeySequenceOptions, useKeySequence, } from './hotkeys.js';
export { createBooleanCodec, createJsonCodec, createNumberCodec, createStringCodec, createStringUnionCodec, valueCodecEquals, type ValueCodec, } from './codecs.js';
export { usePersistentState, type PersistentStateControls, type PersistentStateKey, type PersistentStateOptions, type PersistentStateSource, } from './persistent-state.js';
export { useUrlState, type UrlStateControls, type UrlStateHistoryMode, type UrlStateMode, type UrlStateOptions, type UrlStateSource, } from './url-state.js';
export { useDebugFlag, type DebugFlagOptions, type DebugFlagSource, type DebugFlagState, } from './debug-flags.js';
export { formatShortcutGesture, ShortcutPanel, useShortcutRegistry, type RegisteredShortcut, type ShortcutDefinition, type ShortcutPanelProps, type ShortcutRegistryOptions, type ShortcutRegistryResult, } from './shortcuts.js';
export { formatShareContent, shareContent, writeClipboardText, type ClipboardTarget, type ShareContent, type ShareResult, type ShareTarget, } from './share.js';
export { copySnapshotToClipboard, createSnapshotEnvelope, parseSnapshot, serializeSnapshot, type ParsedSnapshot, type SnapshotEnvelope, type SnapshotOptions, } from './snapshots.js';
export { resolveThemePreference, useSystemTheme, useThemePreference, type ResolvedTheme, type ThemePreference, type ThemePreferenceOptions, type ThemePreferenceState, } from './theme.js';
export { LiveAnnouncer, useLiveAnnouncer, usePrefersReducedMotion, type AnnouncementPriority, type LiveAnnouncerProps, type LiveAnnouncerValue, } from './a11y.js';
//# sourceMappingURL=index.d.ts.map

@@ -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;AACtB,OAAO,EACL,oBAAoB,EACpB,UAAU,EACV,SAAS,EACT,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,cAAc,GACf,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,kBAAkB,EAClB,4BAA4B,EAC5B,KAAK,uBAAuB,GAC7B,MAAM,yBAAyB,CAAC;AACjC,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,EAClB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,cAAc,GACf,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,sBAAsB,EACtB,gBAAgB,EAChB,KAAK,UAAU,GAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,kBAAkB,EAClB,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,GAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,YAAY,EACZ,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,cAAc,GACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,qBAAqB,EACrB,aAAa,EACb,mBAAmB,EACnB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,GAC5B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,kBAAkB,EAClB,YAAY,EACZ,kBAAkB,EAClB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,WAAW,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,aAAa,EACb,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,sBAAsB,EACtB,cAAc,EACd,kBAAkB,EAClB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,GAC1B,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,uBAAuB,EACvB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,GACxB,MAAM,WAAW,CAAC"}
export { BrandBadge, brandBadgeClassNames, } from './BrandBadge.js';
export { TVPROGRAMS_DEFAULT_LABEL, TVPROGRAMS_HOSTNAME, TVPROGRAMS_URL, } from './constants.js';
export { TvProgramsMark, } from './TvProgramsMark.js';
export { TvProgramsWordmark, tvProgramsWordmarkClassNames, } from './TvProgramsWordmark.js';
export { createProjectStorage, } from './storage.js';
export { KONAMI_CODE_SEQUENCE, useHotkeys, useKonami, useKeySequence, } from './hotkeys.js';
export { createBooleanCodec, createJsonCodec, createNumberCodec, createStringCodec, createStringUnionCodec, valueCodecEquals, } from './codecs.js';
export { usePersistentState, } from './persistent-state.js';
export { useUrlState, } from './url-state.js';
export { useDebugFlag, } from './debug-flags.js';
export { formatShortcutGesture, ShortcutPanel, useShortcutRegistry, } from './shortcuts.js';
export { formatShareContent, shareContent, writeClipboardText, } from './share.js';
export { copySnapshotToClipboard, createSnapshotEnvelope, parseSnapshot, serializeSnapshot, } from './snapshots.js';
export { resolveThemePreference, useSystemTheme, useThemePreference, } from './theme.js';
export { LiveAnnouncer, useLiveAnnouncer, usePrefersReducedMotion, } from './a11y.js';

@@ -8,2 +8,3 @@ import { type ComponentPropsWithoutRef, type ReactNode } from 'react';

export interface ProjectStorageNamespaceEntry {
keyParts: string[];
relativeKey: string;

@@ -10,0 +11,0 @@ rawValue: string;

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

{"version":3,"file":"storage-dev.d.ts","sourceRoot":"","sources":["../src/storage-dev.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,wBAAwB,EAAE,KAAK,SAAS,EAAuB,MAAM,OAAO,CAAC;AAC/G,OAAO,EAEL,KAAK,cAAc,EAEnB,KAAK,cAAc,EACnB,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,oCAAoC;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,cAAc,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,+BAA+B;IAC9C,OAAO,EAAE,4BAA4B,EAAE,CAAC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B;AAED,MAAM,WAAW,oCAAoC;IACnD,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,MAAM,4BAA4B,GAAG,IAAI,CAAC,wBAAwB,CAAC,SAAS,CAAC,EAAE,UAAU,CAAC,GAAG;IACjG,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC7B,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,QAAQ,CAAC,EAAE,SAAS,oCAAoC,EAAE,CAAC;CAC5D,CAAC;AAsMF,eAAO,MAAM,6BAA6B,GACxC,gBAAgB,cAAc,KAC7B,+BAOD,CAAC;AAEH,eAAO,MAAM,gCAAgC,GAC3C,gBAAgB,cAAc,WAC2C,CAAC;AAE5E,eAAO,MAAM,4BAA4B,GACvC,OAAO,MAAM,KACZ,+BAuCF,CAAC;AAEF,eAAO,MAAM,6BAA6B,GACxC,gBAAgB,cAAc,EAC9B,UAAU,+BAA+B,EACzC,UAAS,oCAAyC,WAoBnD,CAAC;AAEF,wBAAgB,uBAAuB,CAAC,EACtC,SAAS,EACT,kBAAuB,EACvB,YAA2C,EAC3C,UAAU,EACV,OAAO,EACP,KAAK,EACL,KAAmC,EACnC,QAAgB,EAChB,OAAO,EACP,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,4BAA4B,2CA+R9B"}
{"version":3,"file":"storage-dev.d.ts","sourceRoot":"","sources":["../src/storage-dev.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,wBAAwB,EAAE,KAAK,SAAS,EAAuB,MAAM,OAAO,CAAC;AAC/G,OAAO,EAEL,KAAK,cAAc,EAEnB,KAAK,cAAc,EACnB,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,oCAAoC;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,cAAc,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,+BAA+B;IAC9C,OAAO,EAAE,4BAA4B,EAAE,CAAC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B;AAED,MAAM,WAAW,oCAAoC;IACnD,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,MAAM,4BAA4B,GAAG,IAAI,CAAC,wBAAwB,CAAC,SAAS,CAAC,EAAE,UAAU,CAAC,GAAG;IACjG,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC7B,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,QAAQ,CAAC,EAAE,SAAS,oCAAoC,EAAE,CAAC;CAC5D,CAAC;AAyOF,eAAO,MAAM,6BAA6B,GACxC,gBAAgB,cAAc,KAC7B,+BAQD,CAAC;AAEH,eAAO,MAAM,gCAAgC,GAC3C,gBAAgB,cAAc,WAC2C,CAAC;AAE5E,eAAO,MAAM,4BAA4B,GACvC,OAAO,MAAM,KACZ,+BAmCF,CAAC;AAEF,eAAO,MAAM,6BAA6B,GACxC,gBAAgB,cAAc,EAC9B,UAAU,+BAA+B,EACzC,UAAS,oCAAyC,WAoBnD,CAAC;AAWF,wBAAgB,uBAAuB,CAAC,EACtC,SAAS,EACT,kBAAuB,EACvB,YAA2C,EAC3C,UAAU,EACV,OAAO,EACP,KAAK,EACL,KAAmC,EACnC,QAAgB,EAChB,OAAO,EACP,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,4BAA4B,2CA2T9B"}

@@ -125,4 +125,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";

};
const buildFullKey = (projectStoragePrefix, relativeKey) => (relativeKey.length === 0 ? projectStoragePrefix : `${projectStoragePrefix}:${relativeKey}`);
const formatEntryLabel = (entry) => (entry.relativeKey.length === 0 ? '(root key)' : entry.relativeKey);
const buildRelativeKey = (keyParts) => keyParts.join(':');
const formatEntryLabel = (entry) => (entry.keyParts.length === 0
? '(root key)'
: entry.keyParts.some((part) => part.includes(':') || part.length === 0)
? JSON.stringify(entry.keyParts)
: entry.relativeKey);
const relativeKeyToParts = (relativeKey) => {

@@ -145,15 +149,32 @@ if (relativeKey.length === 0) {

return (typeof entry.relativeKey === 'string'
&& typeof entry.rawValue === 'string');
&& typeof entry.rawValue === 'string'
&& (entry.keyParts === undefined
|| (Array.isArray(entry.keyParts)
&& entry.keyParts.every((part) => typeof part === 'string' && part.length > 0))));
};
const matchesNamespace = (projectStorage, snapshot) => {
if (snapshot.projectKey !== projectStorage.projectKey) {
return false;
const normalizeNamespaceEntry = (value) => {
if (!isNamespaceEntry(value)) {
throw new Error('Namespace JSON entries must be an array of { relativeKey, rawValue }.');
}
if (projectStorage.version === undefined) {
return snapshot.version === undefined;
const entry = value;
const keyParts = entry.keyParts ?? relativeKeyToParts(entry.relativeKey);
const relativeKey = buildRelativeKey(keyParts);
if (entry.keyParts && entry.relativeKey !== relativeKey) {
throw new Error('Namespace JSON relativeKey must match keyParts when keyParts is provided.');
}
return snapshot.version === projectStorage.version;
return {
keyParts,
relativeKey,
rawValue: entry.rawValue,
};
};
const matchesNamespace = (projectStorage, snapshot) => {
const snapshotProjectStorage = createProjectStorage(snapshot.projectKey, {
...(snapshot.version === undefined ? {} : { version: snapshot.version }),
});
return snapshotProjectStorage.key() === projectStorage.key();
};
export const exportProjectStorageNamespace = (projectStorage) => ({
entries: projectStorage.list().map(({ relativeKey, rawValue }) => ({
entries: projectStorage.list().map(({ keyParts, relativeKey, rawValue }) => ({
keyParts,
relativeKey,

@@ -187,10 +208,7 @@ rawValue,

}
if (!Array.isArray(entries) || !entries.every(isNamespaceEntry)) {
if (!Array.isArray(entries)) {
throw new Error('Namespace JSON entries must be an array of { relativeKey, rawValue }.');
}
entries.forEach((entry) => {
relativeKeyToParts(entry.relativeKey);
});
return {
entries,
entries: entries.map((entry) => normalizeNamespaceEntry(entry)),
projectKey,

@@ -209,6 +227,7 @@ ...(version === undefined ? {} : { version }),

for (const entry of snapshot.entries) {
projectStorage.writeString(entry.rawValue, ...relativeKeyToParts(entry.relativeKey));
projectStorage.writeString(entry.rawValue, ...entry.keyParts);
}
return snapshot.entries.length;
};
const resolveSelectedVersionOption = (value, versionOptions) => (versionOptions.find((option) => ((option.value === null ? '__none__' : `${option.value}`) === value))?.value ?? (value === '__none__' ? null : value));
export function ProjectStorageInspector({ className, defaultRelativeKey = '', emptyMessage = 'No keys in this namespace.', projectKey, storage, style, title = 'Project Storage Inspector', unstyled = false, version, versions, ...props }) {

@@ -218,4 +237,5 @@ const versionOptions = getVersionOptions(version, versions);

const [entries, setEntries] = useState([]);
const [selectedRelativeKey, setSelectedRelativeKey] = useState(defaultRelativeKey);
const [selectedFullKey, setSelectedFullKey] = useState(null);
const [draftRelativeKey, setDraftRelativeKey] = useState(defaultRelativeKey);
const [draftKeyParts, setDraftKeyParts] = useState(null);
const [editorValue, setEditorValue] = useState('');

@@ -229,33 +249,35 @@ const [status, setStatus] = useState(null);

const syncTransferValue = () => {
setTransferValue(stringifyProjectStorageNamespace(projectStorage));
const nextTransferValue = stringifyProjectStorageNamespace(projectStorage);
setTransferValue(nextTransferValue);
return nextTransferValue;
};
const refreshEntries = (nextSelectedKey = selectedRelativeKey) => {
const refreshEntries = ({ nextDraftRelativeKey = draftRelativeKey, selectedEntryFullKey = selectedFullKey, } = {}) => {
const nextEntries = projectStorage.list();
setEntries(nextEntries);
const matchingEntry = nextEntries.find((entry) => entry.relativeKey === nextSelectedKey);
const matchingEntry = selectedEntryFullKey
? nextEntries.find((entry) => entry.fullKey === selectedEntryFullKey)
: nextEntries.find((entry) => entry.relativeKey === nextDraftRelativeKey);
if (matchingEntry) {
setSelectedRelativeKey(matchingEntry.relativeKey);
setSelectedFullKey(matchingEntry.fullKey);
setDraftRelativeKey(matchingEntry.relativeKey);
setDraftKeyParts([...matchingEntry.keyParts]);
setEditorValue(matchingEntry.rawValue);
return;
}
if ((nextSelectedKey === null || nextSelectedKey.length === 0) && nextEntries[0]) {
setSelectedRelativeKey(nextEntries[0].relativeKey);
if ((selectedEntryFullKey === null || selectedEntryFullKey === undefined) && nextEntries[0]) {
setSelectedFullKey(nextEntries[0].fullKey);
setDraftRelativeKey(nextEntries[0].relativeKey);
setDraftKeyParts([...nextEntries[0].keyParts]);
setEditorValue(nextEntries[0].rawValue);
return;
}
setSelectedRelativeKey(nextSelectedKey);
setDraftRelativeKey(nextSelectedKey);
if (nextSelectedKey.length === 0) {
setSelectedFullKey(null);
setDraftRelativeKey(nextDraftRelativeKey);
setDraftKeyParts(null);
if (nextDraftRelativeKey.length === 0) {
setEditorValue('');
return;
}
const activeStorage = resolveInspectorStorage(storage);
if (!activeStorage) {
setEditorValue('');
return;
}
try {
setEditorValue(activeStorage.getItem(buildFullKey(projectStorage.key(), nextSelectedKey)) ?? '');
setEditorValue(projectStorage.readString(...relativeKeyToParts(nextDraftRelativeKey)) ?? '');
}

@@ -267,3 +289,3 @@ catch {

useEffect(() => {
refreshEntries(defaultRelativeKey);
refreshEntries({ nextDraftRelativeKey: defaultRelativeKey, selectedEntryFullKey: null });
syncTransferValue();

@@ -273,4 +295,5 @@ setStatus(null);

const handleSelectEntry = (entry) => {
setSelectedRelativeKey(entry.relativeKey);
setSelectedFullKey(entry.fullKey);
setDraftRelativeKey(entry.relativeKey);
setDraftKeyParts([...entry.keyParts]);
setEditorValue(entry.rawValue);

@@ -287,8 +310,12 @@ setStatus(null);

try {
activeStorage.setItem(buildFullKey(projectStorage.key(), nextRelativeKey), editorValue);
const nextFullKey = selectedFullKey && draftKeyParts
? selectedFullKey
: projectStorage.key(...(draftKeyParts ?? relativeKeyToParts(nextRelativeKey)));
activeStorage.setItem(nextFullKey, editorValue);
setStatus('Saved.');
refreshEntries(nextRelativeKey);
refreshEntries({ selectedEntryFullKey: nextFullKey });
syncTransferValue();
}
catch {
setStatus('Save failed.');
catch (error) {
setStatus(error instanceof Error ? error.message : 'Save failed.');
}

@@ -304,8 +331,12 @@ };

try {
activeStorage.removeItem(buildFullKey(projectStorage.key(), nextRelativeKey));
const nextFullKey = selectedFullKey && draftKeyParts
? selectedFullKey
: projectStorage.key(...(draftKeyParts ?? relativeKeyToParts(nextRelativeKey)));
activeStorage.removeItem(nextFullKey);
setStatus('Removed.');
refreshEntries('');
refreshEntries({ nextDraftRelativeKey: '', selectedEntryFullKey: null });
syncTransferValue();
}
catch {
setStatus('Remove failed.');
catch (error) {
setStatus(error instanceof Error ? error.message : 'Remove failed.');
}

@@ -316,3 +347,3 @@ };

setStatus('Namespace cleared.');
refreshEntries('');
refreshEntries({ nextDraftRelativeKey: '', selectedEntryFullKey: null });
syncTransferValue();

@@ -322,7 +353,10 @@ };

try {
const exportValue = stringifyProjectStorageNamespace(projectStorage);
if (typeof navigator === 'undefined' || !navigator.clipboard?.writeText) {
setTransferValue(exportValue);
setStatus('Clipboard copy is unavailable. Copy from the textarea instead.');
return;
}
await navigator.clipboard.writeText(transferValue);
await navigator.clipboard.writeText(exportValue);
setTransferValue(exportValue);
setStatus('Namespace JSON copied.');

@@ -338,3 +372,3 @@ }

const importedEntryCount = importProjectStorageNamespace(projectStorage, snapshot, { mode });
refreshEntries('');
refreshEntries({ nextDraftRelativeKey: '', selectedEntryFullKey: null });
syncTransferValue();

@@ -350,5 +384,5 @@ setStatus(`${mode === 'replace' ? 'Replaced' : 'Merged'} ${importedEntryCount} entries.`);

const nextValue = event.target.value;
setSelectedVersion(nextValue === '__none__' ? null : nextValue);
setSelectedVersion(resolveSelectedVersionOption(nextValue, versionOptions));
}, style: unstyled ? undefined : DEFAULT_INPUT_STYLE, value: selectedVersion === null ? '__none__' : `${selectedVersion}`, children: versionOptions.map((option) => (_jsx("option", { value: option.value === null ? '__none__' : `${option.value}`, children: option.label }, `${option.label}:${option.value ?? '__none__'}`))) })] })) : null, _jsx("button", { onClick: () => refreshEntries(), style: unstyled ? undefined : DEFAULT_BUTTON_STYLE, type: "button", children: "Refresh" }), _jsx("button", { onClick: handleClear, style: unstyled ? undefined : DEFAULT_BUTTON_STYLE, type: "button", children: "Clear Namespace" })] })] }), _jsxs("div", { style: unstyled ? undefined : DEFAULT_GRID_STYLE, children: [_jsxs("div", { style: unstyled ? undefined : DEFAULT_LIST_STYLE, children: [entries.length === 0 ? _jsx("div", { children: emptyMessage }) : null, entries.map((entry) => {
const isSelected = entry.relativeKey === selectedRelativeKey;
const isSelected = entry.fullKey === selectedFullKey;
return (_jsxs("button", { onClick: () => handleSelectEntry(entry), style: unstyled ? undefined : {

@@ -358,3 +392,7 @@ ...DEFAULT_KEY_BUTTON_STYLE,

}, type: "button", children: [_jsx("strong", { children: formatEntryLabel(entry) }), _jsxs("span", { style: unstyled ? undefined : DEFAULT_META_STYLE, children: [entry.rawValue.length, " chars"] })] }, entry.fullKey));
})] }), _jsxs("div", { style: unstyled ? undefined : DEFAULT_EDITOR_STYLE, children: [_jsxs("label", { children: [_jsx("div", { style: unstyled ? undefined : DEFAULT_META_STYLE, children: "Key suffix" }), _jsx("input", { "aria-label": "Key suffix", onChange: (event) => setDraftRelativeKey(event.target.value), style: unstyled ? undefined : DEFAULT_INPUT_STYLE, type: "text", value: draftRelativeKey })] }), _jsxs("label", { children: [_jsx("div", { style: unstyled ? undefined : DEFAULT_META_STYLE, children: "Raw value" }), _jsx("textarea", { "aria-label": "Raw value", onChange: (event) => setEditorValue(event.target.value), spellCheck: false, style: unstyled ? undefined : DEFAULT_TEXTAREA_STYLE, value: editorValue })] }), _jsxs("div", { style: unstyled ? undefined : DEFAULT_TOOLBAR_STYLE, children: [_jsx("button", { onClick: handleSave, style: unstyled ? undefined : DEFAULT_BUTTON_STYLE, type: "button", children: "Save Raw Value" }), _jsx("button", { onClick: handleRemove, style: unstyled ? undefined : DEFAULT_BUTTON_STYLE, type: "button", children: "Remove Key" })] }), _jsx("div", { style: unstyled ? undefined : DEFAULT_META_STYLE, children: status ?? 'Edits write raw strings directly to storage.' })] })] }), _jsxs("div", { style: unstyled ? undefined : DEFAULT_TRANSFER_STYLE, children: [_jsxs("label", { children: [_jsx("div", { style: unstyled ? undefined : DEFAULT_META_STYLE, children: "Namespace JSON" }), _jsx("textarea", { "aria-label": "Namespace JSON", onChange: (event) => setTransferValue(event.target.value), spellCheck: false, style: unstyled ? undefined : DEFAULT_TRANSFER_TEXTAREA_STYLE, value: transferValue })] }), _jsxs("div", { style: unstyled ? undefined : DEFAULT_TOOLBAR_STYLE, children: [_jsx("button", { onClick: syncTransferValue, style: unstyled ? undefined : DEFAULT_BUTTON_STYLE, type: "button", children: "Refresh Export JSON" }), _jsx("button", { onClick: () => void handleCopyNamespaceJson(), style: unstyled ? undefined : DEFAULT_BUTTON_STYLE, type: "button", children: "Copy Namespace JSON" }), _jsx("button", { onClick: () => handleImportNamespace('merge'), style: unstyled ? undefined : DEFAULT_BUTTON_STYLE, type: "button", children: "Import Merge" }), _jsx("button", { onClick: () => handleImportNamespace('replace'), style: unstyled ? undefined : DEFAULT_BUTTON_STYLE, type: "button", children: "Import Replace" })] }), _jsx("div", { style: unstyled ? undefined : DEFAULT_META_STYLE, children: "Import validates the selected project key and version before writing raw string values." })] })] }));
})] }), _jsxs("div", { style: unstyled ? undefined : DEFAULT_EDITOR_STYLE, children: [_jsxs("label", { children: [_jsx("div", { style: unstyled ? undefined : DEFAULT_META_STYLE, children: "Key suffix" }), _jsx("input", { "aria-label": "Key suffix", onChange: (event) => {
setDraftRelativeKey(event.target.value);
setDraftKeyParts(null);
setSelectedFullKey(null);
}, style: unstyled ? undefined : DEFAULT_INPUT_STYLE, type: "text", value: draftRelativeKey })] }), _jsxs("label", { children: [_jsx("div", { style: unstyled ? undefined : DEFAULT_META_STYLE, children: "Raw value" }), _jsx("textarea", { "aria-label": "Raw value", onChange: (event) => setEditorValue(event.target.value), spellCheck: false, style: unstyled ? undefined : DEFAULT_TEXTAREA_STYLE, value: editorValue })] }), _jsxs("div", { style: unstyled ? undefined : DEFAULT_TOOLBAR_STYLE, children: [_jsx("button", { onClick: handleSave, style: unstyled ? undefined : DEFAULT_BUTTON_STYLE, type: "button", children: "Save Raw Value" }), _jsx("button", { onClick: handleRemove, style: unstyled ? undefined : DEFAULT_BUTTON_STYLE, type: "button", children: "Remove Key" })] }), _jsx("div", { style: unstyled ? undefined : DEFAULT_META_STYLE, children: status ?? 'Edits write raw strings directly to storage. Key suffix uses ":" between key parts; use Namespace JSON keyParts for exact literal ":" segments.' })] })] }), _jsxs("div", { style: unstyled ? undefined : DEFAULT_TRANSFER_STYLE, children: [_jsxs("label", { children: [_jsx("div", { style: unstyled ? undefined : DEFAULT_META_STYLE, children: "Namespace JSON" }), _jsx("textarea", { "aria-label": "Namespace JSON", onChange: (event) => setTransferValue(event.target.value), spellCheck: false, style: unstyled ? undefined : DEFAULT_TRANSFER_TEXTAREA_STYLE, value: transferValue })] }), _jsxs("div", { style: unstyled ? undefined : DEFAULT_TOOLBAR_STYLE, children: [_jsx("button", { onClick: syncTransferValue, style: unstyled ? undefined : DEFAULT_BUTTON_STYLE, type: "button", children: "Refresh Export JSON" }), _jsx("button", { onClick: () => void handleCopyNamespaceJson(), style: unstyled ? undefined : DEFAULT_BUTTON_STYLE, type: "button", children: "Copy Namespace JSON" }), _jsx("button", { onClick: () => handleImportNamespace('merge'), style: unstyled ? undefined : DEFAULT_BUTTON_STYLE, type: "button", children: "Import Merge" }), _jsx("button", { onClick: () => handleImportNamespace('replace'), style: unstyled ? undefined : DEFAULT_BUTTON_STYLE, type: "button", children: "Import Replace" })] }), _jsx("div", { style: unstyled ? undefined : DEFAULT_META_STYLE, children: "Import validates the selected project key and version before writing raw string values, and keyParts keep literal \":\" segments lossless." })] })] }));
}

@@ -27,2 +27,3 @@ export type StorageKeyPart = string | number;

fullKey: string;
keyParts: string[];
relativeKey: string;

@@ -29,0 +30,0 @@ rawValue: string;

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

{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,CAAC;AAE7C,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACpC,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACnC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC;IAClC,GAAG,EAAE,CAAC,GAAG,KAAK,EAAE,cAAc,EAAE,KAAK,MAAM,CAAC;IAC5C,IAAI,EAAE,MAAM,mBAAmB,EAAE,CAAC;IAClC,UAAU,EAAE,CAAC,GAAG,KAAK,EAAE,cAAc,EAAE,KAAK,MAAM,GAAG,IAAI,CAAC;IAC1D,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,cAAc,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC;IACtD,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,cAAc,EAAE,KAAK,IAAI,CAAC;IACjE,SAAS,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,KAAK,EAAE,cAAc,EAAE,KAAK,IAAI,CAAC;IAChE,MAAM,EAAE,CAAC,GAAG,KAAK,EAAE,cAAc,EAAE,KAAK,IAAI,CAAC;IAC7C,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAoDD,eAAO,MAAM,oBAAoB,GAC/B,YAAY,MAAM,EAClB,UAAS,qBAA0B,KAClC,cA2IF,CAAC"}
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,CAAC;AAE7C,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACpC,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACnC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC;IAClC,GAAG,EAAE,CAAC,GAAG,KAAK,EAAE,cAAc,EAAE,KAAK,MAAM,CAAC;IAC5C,IAAI,EAAE,MAAM,mBAAmB,EAAE,CAAC;IAClC,UAAU,EAAE,CAAC,GAAG,KAAK,EAAE,cAAc,EAAE,KAAK,MAAM,GAAG,IAAI,CAAC;IAC1D,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,cAAc,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC;IACtD,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,cAAc,EAAE,KAAK,IAAI,CAAC;IACjE,SAAS,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,KAAK,EAAE,cAAc,EAAE,KAAK,IAAI,CAAC;IAChE,MAAM,EAAE,CAAC,GAAG,KAAK,EAAE,cAAc,EAAE,KAAK,IAAI,CAAC;IAC7C,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAiFD,eAAO,MAAM,oBAAoB,GAC/B,YAAY,MAAM,EAClB,UAAS,qBAA0B,KAClC,cA8IF,CAAC"}

@@ -9,2 +9,21 @@ const STORAGE_KEY_SEPARATOR = ':';

};
const encodeStorageKeyPart = (part, label) => (encodeURIComponent(normalizeStorageKeyPart(part, label)));
const decodeStorageKeyPart = (part) => {
try {
return decodeURIComponent(part);
}
catch {
return part;
}
};
const buildRelativeKey = (parts) => parts.join(STORAGE_KEY_SEPARATOR);
const parseStoredKeyParts = (fullKey, prefix, nestedPrefix) => {
if (fullKey === prefix) {
return [];
}
return fullKey
.slice(nestedPrefix.length)
.split(STORAGE_KEY_SEPARATOR)
.map((part) => decodeStorageKeyPart(part));
};
const resolveStorage = (storage) => {

@@ -29,7 +48,7 @@ if (storage !== undefined) {

const buildProjectStoragePrefix = (projectKey, version) => {
const normalizedProjectKey = normalizeStorageKeyPart(projectKey, 'projectKey');
const normalizedProjectKey = encodeStorageKeyPart(projectKey, 'projectKey');
if (version === undefined) {
return normalizedProjectKey;
}
return `${normalizedProjectKey}${STORAGE_KEY_SEPARATOR}v${normalizeStorageKeyPart(version, 'version')}`;
return `${normalizedProjectKey}${STORAGE_KEY_SEPARATOR}v${encodeStorageKeyPart(version, 'version')}`;
};

@@ -44,3 +63,3 @@ export const createProjectStorage = (projectKey, options = {}) => {

const suffix = parts
.map((part, index) => normalizeStorageKeyPart(part, `key part ${index + 1}`))
.map((part, index) => encodeStorageKeyPart(part, `key part ${index + 1}`))
.join(STORAGE_KEY_SEPARATOR);

@@ -65,5 +84,7 @@ return `${prefix}${STORAGE_KEY_SEPARATOR}${suffix}`;

}
const keyParts = parseStoredKeyParts(fullKey, prefix, nestedPrefix);
entries.push({
fullKey,
relativeKey: fullKey === prefix ? '' : fullKey.slice(nestedPrefix.length),
keyParts,
relativeKey: buildRelativeKey(keyParts),
rawValue,

@@ -70,0 +91,0 @@ });

{
"name": "@taylorvance/tv-shared-runtime",
"version": "0.6.0",
"version": "0.7.0",
"description": "Shared React runtime primitives for Taylor Vance portfolio projects.",

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

},
"./TvProgramsWordmark": {
"types": "./dist/TvProgramsWordmark.d.ts",
"import": "./dist/TvProgramsWordmark.js",
"default": "./dist/TvProgramsWordmark.js"
},
"./assets": {

@@ -27,2 +32,7 @@ "types": "./dist/assets.d.ts",

},
"./codecs": {
"types": "./dist/codecs.d.ts",
"import": "./dist/codecs.js",
"default": "./dist/codecs.js"
},
"./storage": {

@@ -33,2 +43,22 @@ "types": "./dist/storage.d.ts",

},
"./persistent-state": {
"types": "./dist/persistent-state.d.ts",
"import": "./dist/persistent-state.js",
"default": "./dist/persistent-state.js"
},
"./url-state": {
"types": "./dist/url-state.d.ts",
"import": "./dist/url-state.js",
"default": "./dist/url-state.js"
},
"./debug-flags": {
"types": "./dist/debug-flags.d.ts",
"import": "./dist/debug-flags.js",
"default": "./dist/debug-flags.js"
},
"./shortcuts": {
"types": "./dist/shortcuts.d.ts",
"import": "./dist/shortcuts.js",
"default": "./dist/shortcuts.js"
},
"./hotkeys": {

@@ -39,2 +69,22 @@ "types": "./dist/hotkeys.d.ts",

},
"./share": {
"types": "./dist/share.d.ts",
"import": "./dist/share.js",
"default": "./dist/share.js"
},
"./snapshots": {
"types": "./dist/snapshots.d.ts",
"import": "./dist/snapshots.js",
"default": "./dist/snapshots.js"
},
"./theme": {
"types": "./dist/theme.d.ts",
"import": "./dist/theme.js",
"default": "./dist/theme.js"
},
"./a11y": {
"types": "./dist/a11y.d.ts",
"import": "./dist/a11y.js",
"default": "./dist/a11y.js"
},
"./storage-dev": {

@@ -41,0 +91,0 @@ "types": "./dist/storage-dev.d.ts",

+169
-97

@@ -5,4 +5,6 @@ # `@taylorvance/tv-shared-runtime`

The canonical TV Programs logo files live in the repo-level `assets/` directory and are copied into this package during build so the package can continue to expose raw asset subpaths.
The package stays intentionally small. It is meant to hold stable cross-app building blocks, not app shells or business logic.
The canonical TV Programs logo files live in the repo-level `assets/` directory and are copied into this package during build so npm consumers can keep using raw asset subpaths.
## Public API

@@ -12,13 +14,11 @@

- `BrandBadge`
- `TvProgramsMark`
- `TVPROGRAMS_URL`
- `TVPROGRAMS_HOSTNAME`
- `TVPROGRAMS_DEFAULT_LABEL`
- `brandBadgeClassNames`
- `createProjectStorage`
- `useHotkeys`
- `useKeySequence`
- `useKonami`
- `KONAMI_CODE_SEQUENCE`
- brand primitives: `BrandBadge`, `TvProgramsMark`, `TvProgramsWordmark`
- shared branding constants: `TVPROGRAMS_URL`, `TVPROGRAMS_HOSTNAME`, `TVPROGRAMS_DEFAULT_LABEL`
- styling hooks: `brandBadgeClassNames`, `tvProgramsWordmarkClassNames`
- storage: `createProjectStorage`, `usePersistentState`
- URL and debug helpers: `useUrlState`, `useDebugFlag`
- hotkeys and shortcut helpers: `useHotkeys`, `useKeySequence`, `useKonami`, `useShortcutRegistry`, `ShortcutPanel`, `formatShortcutGesture`
- codecs: `createStringCodec`, `createJsonCodec`, `createNumberCodec`, `createBooleanCodec`, `createStringUnionCodec`
- share and snapshot helpers: `writeClipboardText`, `shareContent`, `formatShareContent`, `createSnapshotEnvelope`, `serializeSnapshot`, `parseSnapshot`, `copySnapshotToClipboard`
- theme and accessibility helpers: `useThemePreference`, `useSystemTheme`, `resolveThemePreference`, `usePrefersReducedMotion`, `LiveAnnouncer`, `useLiveAnnouncer`

@@ -28,5 +28,15 @@ Explicit subpaths:

- `@taylorvance/tv-shared-runtime/BrandBadge`
- `@taylorvance/tv-shared-runtime/TvProgramsWordmark`
- `@taylorvance/tv-shared-runtime/assets`
- `@taylorvance/tv-shared-runtime/codecs`
- `@taylorvance/tv-shared-runtime/storage`
- `@taylorvance/tv-shared-runtime/persistent-state`
- `@taylorvance/tv-shared-runtime/url-state`
- `@taylorvance/tv-shared-runtime/debug-flags`
- `@taylorvance/tv-shared-runtime/shortcuts`
- `@taylorvance/tv-shared-runtime/hotkeys`
- `@taylorvance/tv-shared-runtime/storage`
- `@taylorvance/tv-shared-runtime/share`
- `@taylorvance/tv-shared-runtime/snapshots`
- `@taylorvance/tv-shared-runtime/theme`
- `@taylorvance/tv-shared-runtime/a11y`
- `@taylorvance/tv-shared-runtime/storage-dev`

@@ -39,7 +49,8 @@

- Work in utility-class and plain-CSS apps.
- Prefer composition and slot hooks over opinionated app styling.
- Prefer composable hooks and helpers over broad abstractions.
- Let consumer apps keep ownership of layout and domain logic.
## `BrandBadge`
## Brand primitives
Quick default usage:
Default badge:

@@ -54,6 +65,10 @@ ```tsx

Explicit component-only entry:
Wordmark:
```tsx
import { BrandBadge } from '@taylorvance/tv-shared-runtime/BrandBadge';
import { TvProgramsWordmark } from '@taylorvance/tv-shared-runtime';
export function Header() {
return <TvProgramsWordmark />;
}
```

@@ -64,10 +79,10 @@

```tsx
import { BrandBadge } from '@taylorvance/tv-shared-runtime';
import { TvProgramsWordmark } from '@taylorvance/tv-shared-runtime';
export function Footer() {
export function Header() {
return (
<BrandBadge
className="brand-badge"
iconClassName="brand-badge-icon"
labelClassName="brand-badge-label"
<TvProgramsWordmark
className="brand-wordmark"
labelClassName="brand-wordmark-label"
markClassName="brand-wordmark-mark"
unstyled

@@ -79,62 +94,83 @@ />

## Logo assets
Raw asset subpaths remain available:
React component:
```tsx
import { TvProgramsMark } from '@taylorvance/tv-shared-runtime';
import tvMarkUrl from '@taylorvance/tv-shared-runtime/tv.svg';
```
URL exports:
## Storage and state
```tsx
import { TVPROGRAMS_MARK_SVG_URL } from '@taylorvance/tv-shared-runtime/assets';
```
Use `createProjectStorage()` for namespaced `localStorage` keys on shared origins such as localhost:
Raw asset subpaths:
```ts
import { createProjectStorage } from '@taylorvance/tv-shared-runtime/storage';
```tsx
import tvMarkUrl from '@taylorvance/tv-shared-runtime/tv.svg';
const storage = createProjectStorage('wordlink', { version: 1 });
```
## Project storage
Each key part is percent-encoded before it is joined into the stored key, so `storage.key('a:b')` and `storage.key('a', 'b')` do not collide.
Use `createProjectStorage` when a consumer needs browser `localStorage` keys that stay unique per project on shared origins such as localhost.
For React state backed by that namespace:
```ts
import { createProjectStorage } from '@taylorvance/tv-shared-runtime/storage';
```tsx
import {
createStringCodec,
usePersistentState,
} from '@taylorvance/tv-shared-runtime';
const storage = createProjectStorage('wordlink', { version: 1 });
const themePreference = storage.readString('theme-preference') ?? 'system';
export function NotesPanel() {
const [notes, setNotes, controls] = usePersistentState(storage, ['demo', 'notes'], {
codec: createStringCodec(),
defaultValue: '',
});
storage.writeString('dark', 'theme-preference');
storage.writeJson({ expanded: true }, 'panels', 'complexity');
const entries = storage.list();
return (
<>
<textarea value={notes} onChange={(event) => setNotes(event.target.value)} />
<button onClick={controls.clear}>Reset</button>
</>
);
}
```
When `version` is provided, keys follow the pattern `<projectKey>:v<version>:<key parts...>`, for example `wordlink:v1:theme-preference`.
For shareable URL state:
The helper is SSR-safe and treats storage-access failures as soft failures by returning `null` or doing nothing.
```tsx
import { createStringCodec, useUrlState } from '@taylorvance/tv-shared-runtime';
It also provides namespace-level maintenance helpers:
- `list()` returns the current keys and raw string values for the active project/version namespace.
- `clear()` removes only the current project/version namespace.
export function InspectorTabs() {
const [tab, setTab] = useUrlState('tab', {
codec: createStringCodec(),
defaultValue: 'overview',
});
## Storage dev tools
return <button onClick={() => setTab('history')}>{tab}</button>;
}
```
For dev-only inspection, manual edits, and namespace JSON import/export, use the explicit `storage-dev` entry:
`useUrlState()` supports both query params and hash-param mode.
## Debug flags and shortcuts
Use `useDebugFlag()` when a flag should be storage-backed, optionally overridable by a URL param, and optionally toggleable by a hotkey:
```tsx
import { ProjectStorageInspector } from '@taylorvance/tv-shared-runtime/storage-dev';
import { useDebugFlag } from '@taylorvance/tv-shared-runtime';
export function StorageDebugPanel() {
export function SessionDebug({ storage }: { storage: ProjectStorage }) {
const debugGrid = useDebugFlag<HTMLDivElement>('grid', {
hotkeys: 'g',
label: 'Toggle grid overlay',
storage,
urlParam: 'grid',
});
return (
<ProjectStorageInspector
projectKey="mcts-web"
versions={[
{ label: 'Version 1', value: 1 },
{ label: 'Version 2', value: 2 },
]}
/>
<section ref={debugGrid.ref} tabIndex={-1}>
<button onClick={debugGrid.toggle}>
{debugGrid.value ? 'Disable grid' : 'Enable grid'}
</button>
</section>
);

@@ -144,61 +180,73 @@ }

This inspector is meant for local tooling and debug screens, not default production UI.
For a reusable visible-shortcuts list, keep one shortcut definition source of truth and pass the visible subset into `ShortcutPanel`:
## 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';
import { ShortcutPanel, useShortcutRegistry } from '@taylorvance/tv-shared-runtime';
export function SessionPanel() {
const hotkeyRef = useHotkeys<HTMLDivElement>([
{ keys: 'r', callback: () => resetGame() },
{ keys: 'z', callback: () => undoMove() },
{ keys: 'x', callback: () => redoMove() },
export function ShortcutHelp() {
const shortcuts = useShortcutRegistry([
{ id: 'copy', keys: 'c', label: 'Copy snapshot', onTrigger: () => {} },
{ id: 'secret', hidden: true, sequence: ['d', 'e', 'm', 'o'], label: 'Secret', onTrigger: () => {} },
]);
return (
<section ref={hotkeyRef} tabIndex={-1}>
...
</section>
);
return <ShortcutPanel shortcuts={shortcuts.visibleShortcuts} />;
}
```
If you do not attach the returned ref, the hotkeys are global for the current document.
Hidden shortcuts stay active but are not shown by `ShortcutPanel`.
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`.
`useHotkeys()`, `useKeySequence()`, and `useKonami()` are still available directly for lower-level control.
For hidden multi-key sequences, `useKeySequence` supports either one sequence or multiple bindings with shared timeout and scope rules:
## Snapshots and share helpers
For deterministic app state, use the shared snapshot envelope instead of inventing a new JSON blob per app:
```tsx
import { useKeySequence } from '@taylorvance/tv-shared-runtime';
import {
copySnapshotToClipboard,
parseSnapshot,
serializeSnapshot,
} from '@taylorvance/tv-shared-runtime';
export function DebugPanel() {
useKeySequence([
{ sequence: ['d', 'e', 'b', 'u', 'g'], callback: () => setDebugMode(true) },
{ sequence: ['r', 'g', 'b'], callback: () => setRainbowMode(true) },
], { timeoutMs: 1_500 });
const snapshot = serializeSnapshot({ seed: '123', moves: ['A1-B2'] }, {
kind: 'session',
version: 1,
});
const parsed = parseSnapshot(snapshot, { kind: 'session', version: 1 });
return null;
}
await copySnapshotToClipboard(parsed.value, { kind: 'session', version: 1 });
```
`timeoutMs` defaults to `1000` and applies between each correct key press, not across the whole sequence.
For generic clipboard/share flows:
For an easy easter-egg path, `useKonami` exposes a built-in Konami listener with the same optional scoping model:
```tsx
import { shareContent } from '@taylorvance/tv-shared-runtime';
await shareContent({
title: 'wordlink',
text: 'seed=123',
});
```
The helper uses the Web Share API when available and falls back to the clipboard by default.
## Theme and accessibility helpers
Theme preference helper:
```tsx
import { useKonami } from '@taylorvance/tv-shared-runtime';
import {
createProjectStorage,
useThemePreference,
} from '@taylorvance/tv-shared-runtime';
export function SessionPanel() {
const hotkeyRef = useKonami<HTMLDivElement>(() => {
setDebugMode(true);
});
const storage = createProjectStorage('wordlink', { version: 1 });
export function ThemeToggle() {
const theme = useThemePreference(storage, { applyToDocument: true });
return (
<section ref={hotkeyRef} tabIndex={-1}>
...
</section>
<button onClick={() => theme.setThemePreference('dark')}>
{theme.themePreference} -> {theme.resolvedTheme}
</button>
);

@@ -208,2 +256,26 @@ }

The shared Konami sequence is also exported as `KONAMI_CODE_SEQUENCE`.
Reduced motion:
```tsx
import { usePrefersReducedMotion } from '@taylorvance/tv-shared-runtime';
```
Live announcements:
```tsx
import { LiveAnnouncer, useLiveAnnouncer } from '@taylorvance/tv-shared-runtime';
```
These helpers are intentionally light. They handle system-preference and announcement plumbing, while the consumer app still decides what to animate, theme, or announce.
## Storage dev tools
For dev-only inspection, manual edits, and namespace JSON import/export, use the explicit `storage-dev` entry:
```tsx
import { ProjectStorageInspector } from '@taylorvance/tv-shared-runtime/storage-dev';
```
This inspector is meant for local tooling and debug screens, not default production UI.
Namespace JSON exports include `keyParts` so keys containing literal separator characters round-trip exactly.