web based infinite canvas
at main 93 lines 3.2 kB view raw
1import type { BoardMeta, DocRepo } from "../persistence/repo"; 2 3export type FileBrowserActions = { 4 open(boardId: string): Promise<void>; 5 create(name: string): Promise<string>; 6 rename(boardId: string, name: string): Promise<void>; 7 delete(boardId: string): Promise<void>; 8}; 9 10export type FileBrowserViewModel = { 11 /** All known boards pulled from the DocRepo */ 12 boards: BoardMeta[]; 13 /** Current search query */ 14 query: string; 15 /** Boards that match the query (preserves incoming order) */ 16 filteredBoards: BoardMeta[]; 17 /** Selected board identifier, or null if nothing is selected */ 18 selectedId: string | null; 19 /** Bound repository actions */ 20 actions: FileBrowserActions; 21}; 22 23export type FileBrowserOptions = { repo: DocRepo; boards?: BoardMeta[]; query?: string; selectedId?: string | null }; 24 25export const FileBrowserVM = { 26 create(options: FileBrowserOptions): FileBrowserViewModel { 27 const boards = [...(options.boards ?? [])]; 28 const query = normalizeQuery(options.query); 29 const filteredBoards = filterBoards(boards, query); 30 const selectedId = resolveSelection(options.selectedId ?? null, filteredBoards); 31 const actions = createActions(options.repo); 32 return { boards, query, filteredBoards, selectedId, actions }; 33 }, 34 35 setBoards(vm: FileBrowserViewModel, boards: BoardMeta[]): FileBrowserViewModel { 36 const cloned = [...boards]; 37 const filteredBoards = filterBoards(cloned, vm.query); 38 const selectedId = resolveSelection(vm.selectedId, filteredBoards); 39 return { ...vm, boards: cloned, filteredBoards, selectedId }; 40 }, 41 42 setQuery(vm: FileBrowserViewModel, query: string): FileBrowserViewModel { 43 const normalized = normalizeQuery(query); 44 const filteredBoards = filterBoards(vm.boards, normalized); 45 const selectedId = resolveSelection(vm.selectedId, filteredBoards); 46 return { ...vm, query: normalized, filteredBoards, selectedId }; 47 }, 48 49 select(vm: FileBrowserViewModel, boardId: string | null): FileBrowserViewModel { 50 const selectedId = resolveSelection(boardId, vm.filteredBoards); 51 return { ...vm, selectedId }; 52 }, 53}; 54 55function normalizeQuery(query?: string | null): string { 56 return query?.trim() ?? ""; 57} 58 59function filterBoards(boards: BoardMeta[], query: string): BoardMeta[] { 60 if (!query) { 61 return [...boards]; 62 } 63 const needle = query.toLowerCase(); 64 return boards.filter((board) => { 65 const nameMatch = board.name.toLowerCase().includes(needle); 66 const idMatch = board.id.toLowerCase().includes(needle); 67 return nameMatch || idMatch; 68 }); 69} 70 71function resolveSelection(requested: string | null, boards: BoardMeta[]): string | null { 72 if (requested && boards.some((board) => board.id === requested)) { 73 return requested; 74 } 75 return boards[0]?.id ?? null; 76} 77 78function createActions(repo: DocRepo): FileBrowserActions { 79 return { 80 async open(boardId: string) { 81 await repo.openBoard(boardId); 82 }, 83 async create(name: string) { 84 return repo.createBoard(name); 85 }, 86 async rename(boardId: string, name: string) { 87 await repo.renameBoard(boardId, name); 88 }, 89 async delete(boardId: string) { 90 await repo.deleteBoard(boardId); 91 }, 92 }; 93}