@taylorvance/tv-shared-runtime
Advanced tools
| 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"} |
+72
| 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 +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"} |
+0
-5
@@ -8,7 +8,2 @@ import { useCallback, useEffect, useRef } from 'react'; | ||
| ' ': 'space', | ||
| ',': 'comma', | ||
| '-': 'slash', | ||
| '.': 'period', | ||
| '#': 'backslash', | ||
| '+': 'bracketright', | ||
| altleft: 'alt', | ||
@@ -15,0 +10,0 @@ altright: 'alt', |
+10
-0
| 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"} |
+10
-0
| 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"} |
+86
-48
@@ -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"} |
+25
-4
@@ -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 @@ }); |
+51
-1
| { | ||
| "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. |
120160
108.41%58
107.14%2040
116.1%273
35.82%