feat: redesign Toolbox navigation and personalization
This commit is contained in:
+40
-66
@@ -1,66 +1,31 @@
|
||||
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;
|
||||
}
|
||||
import {
|
||||
TOOLBOX_PREFERENCES_KEY,
|
||||
defaultToolboxPreferences,
|
||||
parseToolboxPreferences,
|
||||
readToolboxPreferences,
|
||||
writeToolboxPreferences,
|
||||
type ToolboxPreferences,
|
||||
} from '@add-ideas/toolbox-contract';
|
||||
|
||||
export const PREFERENCES_KEY = TOOLBOX_PREFERENCES_KEY;
|
||||
export type Preferences = ToolboxPreferences;
|
||||
export const defaultPreferences: Preferences = {
|
||||
version: 1,
|
||||
...defaultToolboxPreferences,
|
||||
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 const parsePreferences = parseToolboxPreferences;
|
||||
|
||||
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 };
|
||||
}
|
||||
return readToolboxPreferences(storage);
|
||||
}
|
||||
|
||||
export function writePreferences(
|
||||
preferences: Preferences,
|
||||
storage: Storage = localStorage
|
||||
): void {
|
||||
storage.setItem(PREFERENCES_KEY, JSON.stringify(preferences));
|
||||
writeToolboxPreferences(preferences, storage);
|
||||
}
|
||||
|
||||
export function cleanPreferences(
|
||||
@@ -84,27 +49,36 @@ export function sortAppIds(
|
||||
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 (
|
||||
return [...appIds].sort(
|
||||
(left, right) =>
|
||||
(order.get(left) ?? Number.MAX_SAFE_INTEGER) -
|
||||
(order.get(right) ?? Number.MAX_SAFE_INTEGER)
|
||||
);
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
export function moveId(
|
||||
export function reorderWithinIds(
|
||||
order: string[],
|
||||
id: string,
|
||||
direction: -1 | 1
|
||||
activeId: string,
|
||||
overId: string,
|
||||
peerIds: readonly string[]
|
||||
): 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;
|
||||
if (activeId === overId) return order;
|
||||
const peers = new Set(peerIds);
|
||||
if (!peers.has(activeId) || !peers.has(overId)) return order;
|
||||
|
||||
const orderedPeers = order.filter((id) => peers.has(id));
|
||||
for (const id of peerIds) {
|
||||
if (!orderedPeers.includes(id)) orderedPeers.push(id);
|
||||
}
|
||||
const from = orderedPeers.indexOf(activeId);
|
||||
const to = orderedPeers.indexOf(overId);
|
||||
if (from < 0 || to < 0) return order;
|
||||
const [moved] = orderedPeers.splice(from, 1);
|
||||
if (!moved) return order;
|
||||
orderedPeers.splice(to, 0, moved);
|
||||
|
||||
let peerIndex = 0;
|
||||
return order.map((id) =>
|
||||
peers.has(id) ? (orderedPeers[peerIndex++] ?? id) : id
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user