diff --git a/bye b/bye new file mode 100644 index 0000000..e69de29 diff --git a/lib/storage.mjs b/lib/storage.mjs index 709251e..578d5c4 100644 --- a/lib/storage.mjs +++ b/lib/storage.mjs @@ -169,6 +169,11 @@ export function delete_pdf(id) { return store.delete(`pdf:${id}`); } +// Returns all components that reference the given PDF id in their file_ids array. +export function find_pdf_references(pdf_id) { + return list_components().filter(c => c.file_ids?.includes(pdf_id)); +} + // --- Grid images --- export function list_grid_images() { diff --git a/public/app.mjs b/public/app.mjs index ff5f006..0ded26c 100644 --- a/public/app.mjs +++ b/public/app.mjs @@ -1479,32 +1479,49 @@ function render_file_picker_list() { row.appendChild(thumb); } + const name_wrap = document.createElement('div'); + name_wrap.className = 'fp-name-wrap'; const name_el = document.createElement('span'); name_el.className = 'fp-name'; name_el.textContent = pdf.display_name; + const filename_el = document.createElement('span'); + filename_el.className = 'fp-filename'; + filename_el.textContent = pdf.filename; + name_wrap.append(name_el, filename_el); const rename_btn = document.createElement('button'); rename_btn.type = 'button'; rename_btn.className = 'btn btn-secondary btn-sm'; rename_btn.textContent = 'Rename'; rename_btn.addEventListener('click', () => { - const input = document.createElement('input'); - input.type = 'text'; - input.className = 'fp-rename-input'; - input.value = pdf.display_name; - name_el.replaceWith(input); + const name_input = document.createElement('input'); + name_input.type = 'text'; + name_input.className = 'fp-rename-input'; + name_input.value = pdf.display_name; + name_input.placeholder = 'Display name'; + + const filename_input = document.createElement('input'); + filename_input.type = 'text'; + filename_input.className = 'fp-rename-input fp-rename-filename'; + filename_input.value = pdf.filename; + filename_input.placeholder = 'filename.pdf'; + filename_input.spellcheck = false; + + name_wrap.replaceChildren(name_input, filename_input); rename_btn.textContent = 'Save'; - input.focus(); - input.select(); + name_input.focus(); + name_input.select(); const do_rename = async () => { - const new_name = input.value.trim(); - if (!new_name || new_name === pdf.display_name) { + const new_display = name_input.value.trim(); + const new_filename = filename_input.value.trim(); + if (!new_display || !new_filename) { render_file_picker_list(); return; } + if (new_display === pdf.display_name && new_filename === pdf.filename) { render_file_picker_list(); return; } try { - const result = await api.rename_pdf(pdf.id, new_name); + const result = await api.rename_pdf(pdf.id, new_display, new_filename); const idx = all_pdfs.findIndex(p => p.id === pdf.id); if (idx !== -1) all_pdfs[idx] = result.pdf; all_pdfs.sort((a, b) => a.display_name.localeCompare(b.display_name)); @@ -1515,10 +1532,10 @@ function render_file_picker_list() { }; rename_btn.onclick = do_rename; - input.addEventListener('keydown', (e) => { + [name_input, filename_input].forEach(inp => inp.addEventListener('keydown', (e) => { if (e.key === 'Enter') { do_rename(); } if (e.key === 'Escape') { render_file_picker_list(); } - }); + })); }); const select_btn = document.createElement('button'); @@ -1542,7 +1559,7 @@ function render_file_picker_list() { render_file_picker_list(); }); - row.append(name_el, rename_btn, select_btn, del_btn); + row.append(name_wrap, rename_btn, select_btn, del_btn); return row; })); @@ -2062,22 +2079,32 @@ async function init() { const file = e.target.files[0]; if (!file) return; const name_input = document.getElementById('fp-upload-name'); + const filename_input = document.getElementById('fp-upload-filename'); if (!name_input.value.trim()) { name_input.value = file.name.replace(/\.pdf$/i, ''); } + if (!filename_input.value.trim()) { + filename_input.value = file.name; + } }); qs(document.getElementById('dialog-file-picker'), '#fp-upload-btn').addEventListener('click', async () => { const file_input = document.getElementById('fp-file-input'); const name_input = document.getElementById('fp-upload-name'); + const filename_input = document.getElementById('fp-upload-filename'); const file = file_input.files[0]; if (!file) return; + const display_name = name_input.value.trim(); + const filename = filename_input.value.trim(); + if (!display_name) { alert('Display name is required.'); return; } + if (!filename) { alert('Filename is required.'); return; } try { - const result = await api.upload_pdf(file, name_input.value.trim() || null); + const result = await api.upload_pdf(file, display_name, filename); all_pdfs.push(result.pdf); all_pdfs.sort((a, b) => a.display_name.localeCompare(b.display_name)); file_input.value = ''; name_input.value = ''; + filename_input.value = ''; render_file_picker_list(); } catch (err) { alert(err.message); diff --git a/public/lib/api.mjs b/public/lib/api.mjs index 936f699..e8998d2 100644 --- a/public/lib/api.mjs +++ b/public/lib/api.mjs @@ -46,13 +46,14 @@ export const delete_component_template = (id) => req('DELETE', `/api/component- // PDF files export const get_pdfs = () => req('GET', '/api/pdfs'); -export const rename_pdf = (id, display_name) => req('PUT', `/api/pdfs/${id}`, { display_name }); +export const rename_pdf = (id, display_name, filename) => req('PUT', `/api/pdfs/${id}`, { display_name, filename }); export const delete_pdf = (id) => req('DELETE', `/api/pdfs/${id}`); -export async function upload_pdf(file, display_name) { +export async function upload_pdf(file, display_name, filename) { const form = new FormData(); form.append('file', file); if (display_name) form.append('display_name', display_name); + if (filename) form.append('filename', filename); const res = await fetch('/api/pdfs', { method: 'POST', body: form }); const data = await res.json(); if (!data.ok) throw new Error(data.error ?? 'Upload failed'); diff --git a/public/style.css b/public/style.css index d326bf8..242f8bb 100644 --- a/public/style.css +++ b/public/style.css @@ -1701,22 +1701,44 @@ nav { background: var(--surface-raised); } -.fp-name { +.fp-name-wrap { flex: 1; + display: flex; + flex-direction: column; + gap: 0.1rem; + overflow: hidden; + min-width: 0; +} + +.fp-name { font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.fp-filename { + font-size: 0.75rem; + color: var(--text-dim); + font-family: var(--font-mono); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .fp-rename-input { - flex: 1; font-size: 0.9rem; background: var(--bg); color: var(--text); border: 1px solid var(--border-focus); border-radius: 4px; padding: 0.2rem 0.4rem; + width: 100%; +} + +.fp-rename-filename { + font-family: var(--font-mono); + font-size: 0.8rem; } .fp-upload-section { @@ -1729,7 +1751,14 @@ nav { display: flex; gap: 0.5rem; align-items: center; - flex-wrap: wrap; + margin-top: 0.4rem; +} + +.fp-field-label { + font-size: 0.85rem; + color: var(--text-dim); + white-space: nowrap; + min-width: 8rem; } .fp-upload-row input[type=text] { diff --git a/public/templates.html b/public/templates.html index a8ad97f..7779289 100644 --- a/public/templates.html +++ b/public/templates.html @@ -566,7 +566,16 @@