Add grid image system with multi-panel support and SPA routing

- Grid images: photograph component boxes in sub-sections, assemble
  into one logical grid via perspective warp (homography)
- Source image gallery: bulk upload photos separately from grid setup
- Grid drafts: persist partial work, resume across sessions
- Multi-panel grids: define rows/cols per photo, system computes panel
  layout; process partially configured grids, edit individual panels
- Pan/zoom canvas editor (HiDPI-aware, touch support) for corner alignment
- SPA routing with canonical URLs (history.pushState, server catch-all)
- Express error visibility: uncaughtException/unhandledRejection handlers
  and 4-arg error middleware
- Original filename stored on source image upload
- Various null-safety fixes and CSS [hidden] override fixes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-20 23:30:17 +00:00
parent ef2e53ea18
commit cf37759893
11 changed files with 2835 additions and 250 deletions

View File

@@ -80,3 +80,69 @@ export function set_inventory_entry(entry) {
export function delete_inventory_entry(id) {
return store.delete(`i:${id}`);
}
// --- Grid drafts ---
export function list_grid_drafts() {
const result = [];
for (const [key] of store.data.entries()) {
if (key.startsWith('d:')) result.push(store.get(key));
}
return result.sort((a, b) => b.updated_at - a.updated_at);
}
export function get_grid_draft(id) {
return store.get(`d:${id}`) ?? null;
}
export function set_grid_draft(draft) {
store.set(`d:${draft.id}`, draft);
}
export function delete_grid_draft(id) {
return store.delete(`d:${id}`);
}
// --- Source images ---
export function list_source_images() {
const result = [];
for (const [key] of store.data.entries()) {
if (key.startsWith('s:')) result.push(store.get(key));
}
return result.sort((a, b) => b.created_at - a.created_at);
}
export function get_source_image(id) {
return store.get(`s:${id}`) ?? null;
}
export function add_source_image(src) {
store.set(`s:${src.id}`, src);
}
export function delete_source_image(id) {
return store.delete(`s:${id}`);
}
// --- Grid images ---
export function list_grid_images() {
const result = [];
for (const [key] of store.data.entries()) {
if (key.startsWith('g:')) result.push(store.get(key));
}
return result.sort((a, b) => b.created_at - a.created_at);
}
export function get_grid_image(id) {
return store.get(`g:${id}`) ?? null;
}
export function set_grid_image(grid) {
store.set(`g:${grid.id}`, grid);
}
export function delete_grid_image(id) {
return store.delete(`g:${id}`);
}