Many UX and correctness improvements

- Components URL reflects selected component (/components/:id), survives refresh
- Word-split search: "0603 res" matches "Resistor 0603"
- Left pane resizable with localStorage persistence
- Field values rendered via central render_field_value() (units, URLs, extensible)
- Fields sorted alphabetically in both detail view and edit dialog
- Edit component dialog widened; field rows use shared grid columns (table-like)
- No space between value and unit (supports prefix suffixes like k, M, µ)
- Grid viewer highlights and scrolls to cell when navigating from component detail
- Cell inventory overlay items are <a> tags — middle-click opens in new tab
- PDF files stored with sanitized human-readable filename, not random ID
- PDF rename also renames file on disk
- Atomic rename via renameat2(RENAME_NOREPLACE) through tools/mv-sync
- Fix .cell-thumb-link → .cell-thumb-preview (div, not anchor), cursor: zoom-in
- Fix field name overflow in detail view (auto column width, overflow-wrap)
- Fix link color: use --accent instead of browser default dark blue

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 02:49:11 +00:00
parent 7ef5bb5381
commit 58c93f2bd0
7 changed files with 233 additions and 51 deletions

View File

@@ -63,6 +63,31 @@ function remove_image_file(img_id) {
try { unlinkSync(join('./data/images', img_id)); } catch {}
}
const MV_SYNC = new URL('../tools/mv-sync', import.meta.url).pathname;
// Atomically rename src -> dst, failing if dst already exists.
// Uses renameat2(RENAME_NOREPLACE) via tools/mv-sync.
// Throws on unexpected errors; returns false if dst exists.
function rename_no_replace(src, dst) {
try {
execFileSync(MV_SYNC, [src, dst]);
return true;
} catch (e) {
if (e.status === 1 && e.stderr?.toString().includes('File exists')) return false;
throw e;
}
}
function sanitize_pdf_filename(display_name) {
const base = display_name
.replace(/\.pdf$/i, '')
.replace(/[^a-zA-Z0-9._\- ]/g, '')
.trim()
.replace(/\s+/g, '_')
|| 'document';
return base + '.pdf';
}
function generate_pdf_thumb(pdf_path, thumb_prefix) {
// Returns thumb filename (id-thumb.png) on success, null if pdftoppm unavailable.
try {
@@ -507,11 +532,15 @@ app.post('/api/pdfs', pdf_upload.single('file'), (req, res) => {
return fail(res, 'a file with that name already exists');
}
const id = generate_id();
const filename = sanitize_pdf_filename(display_name);
const temp_path = join('./data/pdfs', req.file.filename);
const final_path = join('./data/pdfs', filename);
if (!rename_no_replace(temp_path, final_path)) return fail(res, 'a file with that name already exists on disk');
const thumb_prefix = join('./data/pdfs', id + '-thumb');
const thumb_file = generate_pdf_thumb(join('./data/pdfs', req.file.filename), thumb_prefix);
const thumb_file = generate_pdf_thumb(final_path, thumb_prefix);
const pdf = {
id,
filename: req.file.filename,
filename,
display_name,
original_name: req.file.originalname,
size: req.file.size,
@@ -530,7 +559,12 @@ app.put('/api/pdfs/:id', (req, res) => {
if (list_pdfs().some(p => p.display_name === display_name && p.id !== pdf.id)) {
return fail(res, 'a file with that name already exists');
}
const updated = { ...pdf, display_name };
const new_filename = sanitize_pdf_filename(display_name);
if (new_filename !== pdf.filename) {
const new_path = join('./data/pdfs', new_filename);
if (!rename_no_replace(join('./data/pdfs', pdf.filename), new_path)) return fail(res, 'a file with that name already exists on disk');
}
const updated = { ...pdf, display_name, filename: new_filename };
set_pdf(updated);
ok(res, { pdf: updated });
});