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

@taylorvance/tv-shared-runtime

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

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

Comparing version
0.3.0
to
0.4.0
+35
dist/storage-dev.d.ts
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
import { type ProjectStorage, type StorageKeyPart, type StorageLike } from './storage.js';
export interface ProjectStorageInspectorVersionOption {
label: string;
value: StorageKeyPart | null;
}
export interface ProjectStorageNamespaceEntry {
relativeKey: string;
rawValue: string;
}
export interface ProjectStorageNamespaceSnapshot {
entries: ProjectStorageNamespaceEntry[];
projectKey: string;
version?: StorageKeyPart;
}
export interface ImportProjectStorageNamespaceOptions {
mode?: 'merge' | 'replace';
requireNamespaceMatch?: boolean;
}
export type ProjectStorageInspectorProps = Omit<ComponentPropsWithoutRef<'section'>, 'children'> & {
defaultRelativeKey?: string;
emptyMessage?: ReactNode;
projectKey: string;
storage?: StorageLike | null;
title?: ReactNode;
unstyled?: boolean;
version?: StorageKeyPart;
versions?: readonly ProjectStorageInspectorVersionOption[];
};
export declare const exportProjectStorageNamespace: (projectStorage: ProjectStorage) => ProjectStorageNamespaceSnapshot;
export declare const stringifyProjectStorageNamespace: (projectStorage: ProjectStorage) => string;
export declare const parseProjectStorageNamespace: (value: string) => ProjectStorageNamespaceSnapshot;
export declare const importProjectStorageNamespace: (projectStorage: ProjectStorage, snapshot: ProjectStorageNamespaceSnapshot, options?: ImportProjectStorageNamespaceOptions) => number;
export declare function ProjectStorageInspector({ className, defaultRelativeKey, emptyMessage, projectKey, storage, style, title, unstyled, version, versions, ...props }: ProjectStorageInspectorProps): import("react/jsx-runtime").JSX.Element;
//# sourceMappingURL=storage-dev.d.ts.map
{"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"}
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useEffect, useState } from 'react';
import { createProjectStorage, } from './storage.js';
const DEFAULT_ROOT_STYLE = {
backgroundColor: '#f8fafc',
border: '1px solid #cbd5e1',
borderRadius: '1rem',
color: '#0f172a',
display: 'grid',
gap: '1rem',
padding: '1rem',
};
const DEFAULT_HEADER_STYLE = {
alignItems: 'center',
display: 'flex',
flexWrap: 'wrap',
gap: '0.75rem',
justifyContent: 'space-between',
};
const DEFAULT_TOOLBAR_STYLE = {
alignItems: 'center',
display: 'flex',
flexWrap: 'wrap',
gap: '0.5rem',
};
const DEFAULT_GRID_STYLE = {
display: 'grid',
gap: '1rem',
gridTemplateColumns: 'minmax(14rem, 18rem) minmax(0, 1fr)',
};
const DEFAULT_LIST_STYLE = {
border: '1px solid #cbd5e1',
borderRadius: '0.75rem',
display: 'grid',
gap: '0.5rem',
maxHeight: '22rem',
overflow: 'auto',
padding: '0.75rem',
};
const DEFAULT_EDITOR_STYLE = {
display: 'grid',
gap: '0.75rem',
};
const DEFAULT_TRANSFER_STYLE = {
borderTop: '1px solid #cbd5e1',
display: 'grid',
gap: '0.75rem',
paddingTop: '1rem',
};
const DEFAULT_BUTTON_STYLE = {
backgroundColor: '#e2e8f0',
border: '1px solid #94a3b8',
borderRadius: '0.5rem',
color: 'inherit',
cursor: 'pointer',
font: 'inherit',
padding: '0.45rem 0.7rem',
};
const DEFAULT_INPUT_STYLE = {
backgroundColor: '#ffffff',
border: '1px solid #94a3b8',
borderRadius: '0.5rem',
color: 'inherit',
font: 'inherit',
padding: '0.5rem 0.65rem',
width: '100%',
};
const DEFAULT_TEXTAREA_STYLE = {
...DEFAULT_INPUT_STYLE,
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
minHeight: '14rem',
resize: 'vertical',
};
const DEFAULT_TRANSFER_TEXTAREA_STYLE = {
...DEFAULT_TEXTAREA_STYLE,
minHeight: '12rem',
};
const DEFAULT_KEY_BUTTON_STYLE = {
backgroundColor: '#ffffff',
border: '1px solid #cbd5e1',
borderRadius: '0.5rem',
cursor: 'pointer',
display: 'grid',
font: 'inherit',
gap: '0.2rem',
padding: '0.55rem 0.65rem',
textAlign: 'left',
width: '100%',
};
const DEFAULT_SELECTED_KEY_BUTTON_STYLE = {
borderColor: '#2563eb',
boxShadow: '0 0 0 1px #2563eb inset',
};
const DEFAULT_META_STYLE = {
color: '#475569',
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
fontSize: '0.8rem',
};
const resolveInspectorStorage = (storage) => {
if (storage !== undefined) {
return storage;
}
if (typeof window === 'undefined') {
return null;
}
try {
return window.localStorage;
}
catch {
return null;
}
};
const getVersionOptions = (version, versions) => {
if (versions && versions.length > 0) {
return [...versions];
}
if (version === undefined) {
return [];
}
return [{
label: `v${version}`,
value: version,
}];
};
const buildFullKey = (projectStoragePrefix, relativeKey) => (relativeKey.length === 0 ? projectStoragePrefix : `${projectStoragePrefix}:${relativeKey}`);
const formatEntryLabel = (entry) => (entry.relativeKey.length === 0 ? '(root key)' : entry.relativeKey);
const relativeKeyToParts = (relativeKey) => {
if (relativeKey.length === 0) {
return [];
}
const parts = relativeKey.split(':');
if (parts.some((part) => part.length === 0)) {
throw new Error('Imported relative keys must not contain empty segments.');
}
return parts;
};
const isStorageKeyPart = (value) => (typeof value === 'string' || typeof value === 'number');
const isNamespaceEntry = (value) => {
if (!value || typeof value !== 'object') {
return false;
}
const entry = value;
return (typeof entry.relativeKey === 'string'
&& typeof entry.rawValue === 'string');
};
const matchesNamespace = (projectStorage, snapshot) => {
if (snapshot.projectKey !== projectStorage.projectKey) {
return false;
}
if (projectStorage.version === undefined) {
return snapshot.version === undefined;
}
return snapshot.version === projectStorage.version;
};
export const exportProjectStorageNamespace = (projectStorage) => ({
entries: projectStorage.list().map(({ relativeKey, rawValue }) => ({
relativeKey,
rawValue,
})),
projectKey: projectStorage.projectKey,
...(projectStorage.version === undefined ? {} : { version: projectStorage.version }),
});
export const stringifyProjectStorageNamespace = (projectStorage) => JSON.stringify(exportProjectStorageNamespace(projectStorage), null, 2);
export const parseProjectStorageNamespace = (value) => {
let parsed;
try {
parsed = JSON.parse(value);
}
catch {
throw new Error('Namespace JSON must be valid JSON.');
}
if (!parsed || typeof parsed !== 'object') {
throw new Error('Namespace JSON must be an object.');
}
const parsedRecord = parsed;
const projectKey = parsedRecord.projectKey;
const version = parsedRecord.version;
const entries = parsedRecord.entries;
if (typeof projectKey !== 'string' || projectKey.length === 0) {
throw new Error('Namespace JSON must include a non-empty projectKey.');
}
if (version !== undefined && !isStorageKeyPart(version)) {
throw new Error('Namespace JSON version must be a string or number when present.');
}
if (!Array.isArray(entries) || !entries.every(isNamespaceEntry)) {
throw new Error('Namespace JSON entries must be an array of { relativeKey, rawValue }.');
}
entries.forEach((entry) => {
relativeKeyToParts(entry.relativeKey);
});
return {
entries,
projectKey,
...(version === undefined ? {} : { version }),
};
};
export const importProjectStorageNamespace = (projectStorage, snapshot, options = {}) => {
const { mode = 'merge', requireNamespaceMatch = true, } = options;
if (requireNamespaceMatch && !matchesNamespace(projectStorage, snapshot)) {
throw new Error('Imported namespace does not match the selected project key and version.');
}
if (mode === 'replace') {
projectStorage.clear();
}
for (const entry of snapshot.entries) {
projectStorage.writeString(entry.rawValue, ...relativeKeyToParts(entry.relativeKey));
}
return snapshot.entries.length;
};
export function ProjectStorageInspector({ className, defaultRelativeKey = '', emptyMessage = 'No keys in this namespace.', projectKey, storage, style, title = 'Project Storage Inspector', unstyled = false, version, versions, ...props }) {
const versionOptions = getVersionOptions(version, versions);
const [selectedVersion, setSelectedVersion] = useState(versionOptions[0]?.value ?? version ?? null);
const [entries, setEntries] = useState([]);
const [selectedRelativeKey, setSelectedRelativeKey] = useState(defaultRelativeKey);
const [draftRelativeKey, setDraftRelativeKey] = useState(defaultRelativeKey);
const [editorValue, setEditorValue] = useState('');
const [status, setStatus] = useState(null);
const [transferValue, setTransferValue] = useState('');
const projectStorage = createProjectStorage(projectKey, {
...(storage === undefined ? {} : { storage }),
...(selectedVersion === null ? {} : { version: selectedVersion }),
});
const syncTransferValue = () => {
setTransferValue(stringifyProjectStorageNamespace(projectStorage));
};
const refreshEntries = (nextSelectedKey = selectedRelativeKey) => {
const nextEntries = projectStorage.list();
setEntries(nextEntries);
const matchingEntry = nextEntries.find((entry) => entry.relativeKey === nextSelectedKey);
if (matchingEntry) {
setSelectedRelativeKey(matchingEntry.relativeKey);
setDraftRelativeKey(matchingEntry.relativeKey);
setEditorValue(matchingEntry.rawValue);
return;
}
if ((nextSelectedKey === null || nextSelectedKey.length === 0) && nextEntries[0]) {
setSelectedRelativeKey(nextEntries[0].relativeKey);
setDraftRelativeKey(nextEntries[0].relativeKey);
setEditorValue(nextEntries[0].rawValue);
return;
}
setSelectedRelativeKey(nextSelectedKey);
setDraftRelativeKey(nextSelectedKey);
if (nextSelectedKey.length === 0) {
setEditorValue('');
return;
}
const activeStorage = resolveInspectorStorage(storage);
if (!activeStorage) {
setEditorValue('');
return;
}
try {
setEditorValue(activeStorage.getItem(buildFullKey(projectStorage.key(), nextSelectedKey)) ?? '');
}
catch {
setEditorValue('');
}
};
useEffect(() => {
refreshEntries(defaultRelativeKey);
syncTransferValue();
setStatus(null);
}, [defaultRelativeKey, projectKey, selectedVersion, storage]);
const handleSelectEntry = (entry) => {
setSelectedRelativeKey(entry.relativeKey);
setDraftRelativeKey(entry.relativeKey);
setEditorValue(entry.rawValue);
setStatus(null);
};
const handleSave = () => {
const activeStorage = resolveInspectorStorage(storage);
const nextRelativeKey = draftRelativeKey.trim();
if (!activeStorage) {
setStatus('Storage is unavailable.');
return;
}
try {
activeStorage.setItem(buildFullKey(projectStorage.key(), nextRelativeKey), editorValue);
setStatus('Saved.');
refreshEntries(nextRelativeKey);
}
catch {
setStatus('Save failed.');
}
};
const handleRemove = () => {
const activeStorage = resolveInspectorStorage(storage);
const nextRelativeKey = draftRelativeKey.trim();
if (!activeStorage) {
setStatus('Storage is unavailable.');
return;
}
try {
activeStorage.removeItem(buildFullKey(projectStorage.key(), nextRelativeKey));
setStatus('Removed.');
refreshEntries('');
}
catch {
setStatus('Remove failed.');
}
};
const handleClear = () => {
projectStorage.clear();
setStatus('Namespace cleared.');
refreshEntries('');
syncTransferValue();
};
const handleCopyNamespaceJson = async () => {
try {
if (typeof navigator === 'undefined' || !navigator.clipboard?.writeText) {
setStatus('Clipboard copy is unavailable. Copy from the textarea instead.');
return;
}
await navigator.clipboard.writeText(transferValue);
setStatus('Namespace JSON copied.');
}
catch {
setStatus('Clipboard copy failed. Copy from the textarea instead.');
}
};
const handleImportNamespace = (mode) => {
try {
const snapshot = parseProjectStorageNamespace(transferValue);
const importedEntryCount = importProjectStorageNamespace(projectStorage, snapshot, { mode });
refreshEntries('');
syncTransferValue();
setStatus(`${mode === 'replace' ? 'Replaced' : 'Merged'} ${importedEntryCount} entries.`);
}
catch (error) {
setStatus(error instanceof Error ? error.message : 'Import failed.');
}
};
const mergedStyle = unstyled ? style : { ...DEFAULT_ROOT_STYLE, ...style };
return (_jsxs("section", { className: className, style: mergedStyle, ...props, children: [_jsxs("div", { style: unstyled ? undefined : DEFAULT_HEADER_STYLE, children: [_jsxs("div", { children: [_jsx("strong", { children: title }), _jsxs("div", { style: unstyled ? undefined : DEFAULT_META_STYLE, children: ["Namespace: ", _jsx("code", { children: projectStorage.key() })] })] }), _jsxs("div", { style: unstyled ? undefined : DEFAULT_TOOLBAR_STYLE, children: [versionOptions.length > 0 ? (_jsxs("label", { children: [_jsx("span", { style: unstyled ? undefined : DEFAULT_META_STYLE, children: "Version " }), _jsx("select", { "aria-label": "Storage version", onChange: (event) => {
const nextValue = event.target.value;
setSelectedVersion(nextValue === '__none__' ? null : nextValue);
}, 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;
return (_jsxs("button", { onClick: () => handleSelectEntry(entry), style: unstyled ? undefined : {
...DEFAULT_KEY_BUTTON_STYLE,
...(isSelected ? DEFAULT_SELECTED_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." })] })] }));
}
export type StorageKeyPart = string | number;
export interface StorageLike {
length?: number;
getItem(key: string): string | null;
key?(index: number): string | null;
setItem(key: string, value: string): void;
removeItem(key: string): void;
}
export interface ProjectStorageOptions {
storage?: StorageLike | null;
version?: StorageKeyPart;
}
export interface ProjectStorage {
readonly projectKey: string;
readonly version?: StorageKeyPart;
key: (...parts: StorageKeyPart[]) => string;
list: () => ProjectStorageEntry[];
readString: (...parts: StorageKeyPart[]) => string | null;
readJson: <T>(...parts: StorageKeyPart[]) => T | null;
writeString: (value: string, ...parts: StorageKeyPart[]) => void;
writeJson: (value: unknown, ...parts: StorageKeyPart[]) => void;
remove: (...parts: StorageKeyPart[]) => void;
clear: () => void;
}
export interface ProjectStorageEntry {
fullKey: string;
relativeKey: string;
rawValue: string;
}
export declare const createProjectStorage: (projectKey: string, options?: ProjectStorageOptions) => ProjectStorage;
//# sourceMappingURL=storage.d.ts.map
{"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"}
const STORAGE_KEY_SEPARATOR = ':';
const normalizeStorageKeyPart = (part, label) => {
const value = `${part}`;
if (value.length === 0) {
throw new Error(`${label} must not be empty.`);
}
return value;
};
const resolveStorage = (storage) => {
if (storage !== undefined) {
return storage;
}
if (typeof window === 'undefined') {
return null;
}
try {
return window.localStorage;
}
catch {
return null;
}
};
const isEnumerableStorage = (storage) => (typeof storage.key === 'function'
&& typeof storage.length === 'number'
&& Number.isInteger(storage.length)
&& storage.length >= 0);
const buildProjectStoragePrefix = (projectKey, version) => {
const normalizedProjectKey = normalizeStorageKeyPart(projectKey, 'projectKey');
if (version === undefined) {
return normalizedProjectKey;
}
return `${normalizedProjectKey}${STORAGE_KEY_SEPARATOR}v${normalizeStorageKeyPart(version, 'version')}`;
};
export const createProjectStorage = (projectKey, options = {}) => {
const prefix = buildProjectStoragePrefix(projectKey, options.version);
const nestedPrefix = `${prefix}${STORAGE_KEY_SEPARATOR}`;
const key = (...parts) => {
if (parts.length === 0) {
return prefix;
}
const suffix = parts
.map((part, index) => normalizeStorageKeyPart(part, `key part ${index + 1}`))
.join(STORAGE_KEY_SEPARATOR);
return `${prefix}${STORAGE_KEY_SEPARATOR}${suffix}`;
};
const list = () => {
const activeStorage = resolveStorage(options.storage);
if (!activeStorage || !isEnumerableStorage(activeStorage)) {
return [];
}
try {
const entries = [];
for (let index = 0; index < activeStorage.length; index++) {
const fullKey = activeStorage.key(index);
if (!fullKey || (fullKey !== prefix && !fullKey.startsWith(nestedPrefix))) {
continue;
}
const rawValue = activeStorage.getItem(fullKey);
if (rawValue === null) {
continue;
}
entries.push({
fullKey,
relativeKey: fullKey === prefix ? '' : fullKey.slice(nestedPrefix.length),
rawValue,
});
}
entries.sort((left, right) => left.fullKey.localeCompare(right.fullKey));
return entries;
}
catch {
return [];
}
};
const readString = (...parts) => {
const activeStorage = resolveStorage(options.storage);
if (!activeStorage) {
return null;
}
try {
return activeStorage.getItem(key(...parts));
}
catch {
return null;
}
};
const readJson = (...parts) => {
const value = readString(...parts);
if (value === null) {
return null;
}
try {
return JSON.parse(value);
}
catch {
return null;
}
};
const writeString = (value, ...parts) => {
const activeStorage = resolveStorage(options.storage);
if (!activeStorage) {
return;
}
try {
activeStorage.setItem(key(...parts), value);
}
catch {
// Ignore storage quota and privacy-mode failures.
}
};
const writeJson = (value, ...parts) => {
try {
writeString(JSON.stringify(value), ...parts);
}
catch {
// Ignore serialization failures for non-JSON-safe values.
}
};
const remove = (...parts) => {
const activeStorage = resolveStorage(options.storage);
if (!activeStorage) {
return;
}
try {
activeStorage.removeItem(key(...parts));
}
catch {
// Ignore storage-access failures.
}
};
const clear = () => {
const activeStorage = resolveStorage(options.storage);
if (!activeStorage) {
return;
}
for (const entry of list()) {
try {
activeStorage.removeItem(entry.fullKey);
}
catch {
// Ignore storage-access failures.
}
}
};
return {
projectKey,
...(options.version === undefined ? {} : { version: options.version }),
key,
list,
readString,
readJson,
writeString,
writeJson,
remove,
clear,
};
};
+1
-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 { createProjectStorage, type ProjectStorageEntry, type ProjectStorage, type ProjectStorageOptions, type StorageKeyPart, type StorageLike, } from './storage.js';
//# sourceMappingURL=index.d.ts.map
+1
-1

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

{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,oBAAoB,EACpB,KAAK,eAAe,GACrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,cAAc,GACf,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,qBAAqB,CAAC"}
{"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"}
export { BrandBadge, brandBadgeClassNames, } from './BrandBadge.js';
export { TVPROGRAMS_DEFAULT_LABEL, TVPROGRAMS_HOSTNAME, TVPROGRAMS_URL, } from './constants.js';
export { TvProgramsMark, } from './TvProgramsMark.js';
export { createProjectStorage, } from './storage.js';
{
"name": "@taylorvance/tv-shared-runtime",
"version": "0.3.0",
"version": "0.4.0",
"description": "Shared React runtime primitives for Taylor Vance portfolio projects.",

@@ -26,2 +26,12 @@ "type": "module",

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

@@ -28,0 +38,0 @@ "./tv.png": "./assets/tv.png"

@@ -17,2 +17,3 @@ # `@taylorvance/tv-shared-runtime`

- `brandBadgeClassNames`
- `createProjectStorage`

@@ -23,2 +24,4 @@ Explicit subpaths:

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

@@ -86,1 +89,47 @@ ## Design goals

```
## Project storage
Use `createProjectStorage` when a consumer needs browser `localStorage` keys that stay unique per project on shared origins such as localhost.
```ts
import { createProjectStorage } from '@taylorvance/tv-shared-runtime/storage';
const storage = createProjectStorage('wordlink', { version: 1 });
const themePreference = storage.readString('theme-preference') ?? 'system';
storage.writeString('dark', 'theme-preference');
storage.writeJson({ expanded: true }, 'panels', 'complexity');
const entries = storage.list();
```
When `version` is provided, keys follow the pattern `<projectKey>:v<version>:<key parts...>`, for example `wordlink:v1:theme-preference`.
The helper is SSR-safe and treats storage-access failures as soft failures by returning `null` or doing nothing.
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.
## 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';
export function StorageDebugPanel() {
return (
<ProjectStorageInspector
projectKey="mcts-web"
versions={[
{ label: 'Version 1', value: 1 },
{ label: 'Version 2', value: 2 },
]}
/>
);
}
```
This inspector is meant for local tooling and debug screens, not default production UI.