managarten/packages/shared-stores/src/view.svelte.ts
Till JS 45790ffbb8 refactor(mana): rename inventar → inventory across the codebase
The workbench-registry app id 'inventar' did not match its
@mana/shared-branding MANA_APPS counterpart 'inventory', so the tier-
gating join in apps/web/src/lib/app-registry/registry.ts silently
failed for the inventory module — it fell into the "no MANA_APPS
entry, default visible" fallback and was effectively un-gated. The
codebase had also voted overwhelmingly for 'inventar' (53 files) vs
'inventory' (3 files in shared-branding), so the long-standing
mismatch was just bookkeeping debt waiting to bite.

Pre-release, no live data, so the cleanest fix is to align everything
on the English 'inventory':

- Workbench-registry id, module.config.ts appId, module folder, route
  folder and i18n locale folder all renamed via git mv
- Standalone apps/inventar/ workspace package renamed
- All imports, store identifiers (InventarEvents → InventoryEvents,
  INVENTAR_GUEST_SEED, inventarModuleConfig), i18n keys and href/goto
  paths follow the rename
- The German display label "Inventar" is preserved everywhere it is a
  user-visible string (page titles, i18n values, toast labels)
- Dexie table prefixes (invCollections, invItems, …) are unchanged
- Drive-by fix: ListView.svelte was querying non-existent
  inventarCollections/inventarItems tables — corrected to the actual
  invCollections/invItems names from module.config
- The "inventar ↔ inventory id mismatch" workaround comment in
  registry.ts is removed since the mismatch no longer exists

module-registry.ts also picks up the user's parallel newsModuleConfig
addition because both edits land in the same import block — keeping
them split would have left the build in an inconsistent state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:50:24 +02:00

176 lines
4.3 KiB
TypeScript

/**
* View Store Factory
*
* Creates a type-safe view/filter/sort store with localStorage persistence.
* Replaces ~110 LOC boilerplate per module.
*
* @example
* ```typescript
* import { createViewStore } from '@mana/shared-stores';
*
* type MyViewMode = 'list' | 'grid' | 'kanban';
*
* interface MyFilters {
* search?: string;
* status?: string[];
* tagIds?: string[];
* }
*
* export const viewStore = createViewStore<MyViewMode, MyFilters>({
* storagePrefix: 'inventory',
* defaultViewMode: 'list',
* defaultSort: { field: 'name', direction: 'asc' },
* hasActiveFilters: (f) => !!(f.search || f.status?.length || f.tagIds?.length),
* });
* ```
*/
import { browser } from '$app/environment';
export interface SortOption {
field: string;
direction: 'asc' | 'desc';
}
export interface SavedFilter<F> {
id: string;
name: string;
criteria: F;
createdAt: string;
}
export interface ViewStoreConfig<V extends string, F extends object> {
/** Prefix for localStorage keys (e.g. 'inventory' → 'inventory_view_mode') */
storagePrefix: string;
/** Default view mode */
defaultViewMode: V;
/** Default sort option */
defaultSort: SortOption;
/** Returns true if any filters are active (used for UI indicators) */
hasActiveFilters?: (filters: F) => boolean;
}
export interface ViewStore<V extends string, F extends object> {
readonly viewMode: V;
readonly sort: SortOption;
readonly activeFilters: F;
readonly savedFilters: SavedFilter<F>[];
readonly hasActiveFilters: boolean;
initialize(): void;
setViewMode(mode: V): void;
setSort(newSort: SortOption): void;
setFilters(filters: F): void;
updateFilter<K extends keyof F>(key: K, value: F[K]): void;
clearFilters(): void;
saveFilter(name: string): void;
loadFilter(id: string): void;
deleteSavedFilter(id: string): void;
}
function load<T>(key: string, fallback: T): T {
if (!browser) return fallback;
try {
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : fallback;
} catch {
return fallback;
}
}
function save(key: string, value: unknown) {
if (!browser) return;
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {
// Silently fail (quota exceeded, private mode, etc.)
}
}
export function createViewStore<V extends string, F extends object>(
config: ViewStoreConfig<V, F>
): ViewStore<V, F> {
const VIEW_KEY = `${config.storagePrefix}_view_mode`;
const SORT_KEY = `${config.storagePrefix}_sort`;
const FILTERS_KEY = `${config.storagePrefix}_saved_filters`;
let viewMode = $state<V>(config.defaultViewMode);
let sort = $state<SortOption>(config.defaultSort);
let activeFilters = $state<F>({} as F);
let savedFilters = $state<SavedFilter<F>[]>([]);
let initialized = $state(false);
return {
get viewMode() {
return viewMode;
},
get sort() {
return sort;
},
get activeFilters() {
return activeFilters;
},
get savedFilters() {
return savedFilters;
},
get hasActiveFilters() {
if (config.hasActiveFilters) return config.hasActiveFilters(activeFilters);
return Object.values(activeFilters).some(
(v) => v !== undefined && v !== null && v !== '' && (!Array.isArray(v) || v.length > 0)
);
},
initialize() {
if (initialized) return;
viewMode = load<V>(VIEW_KEY, config.defaultViewMode);
sort = load<SortOption>(SORT_KEY, config.defaultSort);
savedFilters = load<SavedFilter<F>[]>(FILTERS_KEY, []);
initialized = true;
},
setViewMode(mode: V) {
viewMode = mode;
save(VIEW_KEY, mode);
},
setSort(newSort: SortOption) {
sort = newSort;
save(SORT_KEY, newSort);
},
setFilters(filters: F) {
activeFilters = filters;
},
updateFilter<K extends keyof F>(key: K, value: F[K]) {
activeFilters = { ...activeFilters, [key]: value };
},
clearFilters() {
activeFilters = {} as F;
},
saveFilter(name: string) {
const filter: SavedFilter<F> = {
id: crypto.randomUUID(),
name,
criteria: { ...activeFilters },
createdAt: new Date().toISOString(),
};
savedFilters = [...savedFilters, filter];
save(FILTERS_KEY, savedFilters);
},
loadFilter(id: string) {
const filter = savedFilters.find((f) => f.id === id);
if (filter) {
activeFilters = { ...filter.criteria };
}
},
deleteSavedFilter(id: string) {
savedFilters = savedFilters.filter((f) => f.id !== id);
save(FILTERS_KEY, savedFilters);
},
};
}