feat: release toolbox portal 0.1.0

This commit is contained in:
2026-07-20 18:40:51 +02:00
commit 5d2e466ad6
55 changed files with 11127 additions and 0 deletions

110
src/preferences.ts Normal file
View File

@@ -0,0 +1,110 @@
import type { ThemeMode } from './types';
export const PREFERENCES_KEY = '@add-ideas/toolbox-portal:v1:preferences';
export interface Preferences {
version: 1;
pinned: string[];
order: string[];
hidden: string[];
theme: ThemeMode;
}
export const defaultPreferences: Preferences = {
version: 1,
pinned: [],
order: [],
hidden: [],
theme: 'system',
};
function uniqueStrings(value: unknown, field: string): string[] {
if (
!Array.isArray(value) ||
value.some((item) => typeof item !== 'string' || item.length === 0)
)
throw new Error(`${field} must be an array of non-empty strings.`);
return [...new Set(value)];
}
export function parsePreferences(value: unknown): Preferences {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new Error('Preferences must be a JSON object.');
}
const candidate = value as Record<string, unknown>;
if (candidate.version !== 1)
throw new Error('Unsupported preferences version.');
const theme = candidate.theme;
if (!['light', 'dark', 'system'].includes(String(theme)))
throw new Error('Invalid theme preference.');
return {
version: 1,
pinned: uniqueStrings(candidate.pinned, 'pinned'),
order: uniqueStrings(candidate.order, 'order'),
hidden: uniqueStrings(candidate.hidden, 'hidden'),
theme: theme as ThemeMode,
};
}
export function readPreferences(storage: Storage = localStorage): Preferences {
const value = storage.getItem(PREFERENCES_KEY);
if (!value) return { ...defaultPreferences };
try {
return parsePreferences(JSON.parse(value));
} catch {
return { ...defaultPreferences };
}
}
export function writePreferences(
preferences: Preferences,
storage: Storage = localStorage
): void {
storage.setItem(PREFERENCES_KEY, JSON.stringify(preferences));
}
export function cleanPreferences(
preferences: Preferences,
appIds: string[]
): Preferences {
const known = new Set(appIds);
return {
...preferences,
pinned: preferences.pinned.filter((id) => known.has(id)),
hidden: preferences.hidden.filter((id) => known.has(id)),
order: [
...preferences.order.filter((id) => known.has(id)),
...appIds.filter((id) => !preferences.order.includes(id)),
],
};
}
export function sortAppIds(
appIds: string[],
preferences: Preferences
): string[] {
const order = new Map(preferences.order.map((id, index) => [id, index]));
const pinned = new Set(preferences.pinned);
return [...appIds].sort((left, right) => {
const pinDifference = Number(pinned.has(right)) - Number(pinned.has(left));
if (pinDifference) return pinDifference;
return (
(order.get(left) ?? Number.MAX_SAFE_INTEGER) -
(order.get(right) ?? Number.MAX_SAFE_INTEGER)
);
});
}
export function moveId(
order: string[],
id: string,
direction: -1 | 1
): string[] {
const current = order.indexOf(id);
if (current < 0) return order;
const target = current + direction;
if (target < 0 || target >= order.length) return order;
const next = [...order];
[next[current], next[target]] = [next[target]!, next[current]!];
return next;
}