feat(attachments): attach external notes, screenshots, photos, and image URLs to sparks

This commit is contained in:
airent106 committed 2026-10-29 11:55:02 -06:00
1 parent 8a25771839
commit 4c5560a5a1
6 files changed
+888 -5

No files matched your search

+414
View File
@@ -39,6 +39,8 @@ const state = {
nbGarmentColorName: "Vintage Cream",
nbEditParentMockup: null,
uploadedOverlayImage: null,
stagedDrawerAttachmentDataUrl: null,
stagedClipAttachmentDataUrl: null,
settings: {
hasApiKey: false,
apiKeySource: "none",
@@ -122,6 +124,7 @@ async function initApp() {
bindGlobalEvents();
await Promise.all([loadSettings(), loadWhackCatalog(), loadIdeas()]);
renderCompositorCanvas();
checkWebShareTargetQueryParams();
}
async function loadSettings() {
@@ -263,8 +266,10 @@ function renderIdeaCardHtml(idea) {
${checklistTotal > 0 ? `<span> • ☑️ ${checklistDone}/${checklistTotal}</span>` : ""}
${whackCount > 0 ? `<span title="Whack Pack reflections"> • 🃏${whackCount}</span>` : ""}
${idea.mockups.length > 0 ? `<span title="Visual Mockups"> • 🍌${idea.mockups.length}</span>` : ""}
${(idea.attachments || []).length > 0 ? `<span title="External Attachments"> • 📎${idea.attachments.length}</span>` : ""}
</div>
<div class="card-quick-actions">
<button type="button" class="icon-btn btn-card-clip" data-idea-id="${idea.id}" title="Attach External Note or Image">📎</button>
<button type="button" class="icon-btn btn-card-mockup" data-idea-id="${idea.id}" title="Open in Nano Banana Mockup Studio">🍌</button>
<button type="button" class="icon-btn btn-card-whack" data-idea-id="${idea.id}" title="Draw a Whack Card for this Idea">🃏</button>
${
@@ -292,6 +297,13 @@ function bindIdeaCardEvents(container) {
});
});
container.querySelectorAll(".btn-card-clip").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
openExternalClipModal({ ideaId: Number(btn.dataset.ideaId) });
});
});
container.querySelectorAll(".btn-card-mockup").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
@@ -910,10 +922,131 @@ function renderDrawerContent() {
// Mockup Gallery
renderDrawerMockupGallery(idea);
// External Attachments
renderDrawerAttachments(idea);
// Whack History
renderDrawerWhackHistory(idea);
}
function renderDrawerAttachments(idea) {
const countEl = document.getElementById("drawer-attachments-count");
const listEl = document.getElementById("drawer-attachments-list");
const attachments = idea.attachments || [];
if (countEl) countEl.textContent = attachments.length;
if (!listEl) return;
if (attachments.length === 0) {
listEl.innerHTML = `<p class="muted-label">No external notes or reference images attached yet. Drop a file, paste a screenshot (Ctrl+V), or enter an external URL above.</p>`;
return;
}
listEl.innerHTML = attachments
.map((att) => {
const isCover = att.imageUrl && idea.cover_mockup_url === att.imageUrl;
return `
<div class="attachment-card ${isCover ? "is-cover" : ""}">
<div style="display:flex;justify-content:space-between;align-items:center;gap:0.4rem;">
<strong style="font-size:0.84rem;">${att.type === "image" ? "🖼️" : "📝"} ${escapeHtml(att.title)}</strong>
<span class="muted-label">${new Date(att.createdAt).toLocaleDateString()}</span>
</div>
${
att.imageUrl
? `<img src="${escapeHtml(att.imageUrl)}" alt="${escapeHtml(att.title)}" class="attachment-thumb" data-zoom-url="${escapeHtml(att.imageUrl)}" />`
: ""
}
${
att.textContent
? `<p style="font-size:0.82rem;color:var(--text-secondary);white-space:pre-wrap;">${escapeHtml(att.textContent)}</p>`
: ""
}
${
att.sourceUrl
? `<a href="${escapeHtml(att.sourceUrl)}" target="_blank" rel="noopener noreferrer" style="font-size:0.76rem;color:var(--accent-primary);word-break:break-all;">🔗 ${escapeHtml(att.sourceUrl)}</a>`
: ""
}
<div class="mockup-gallery-actions">
${
att.imageUrl
? `
<button type="button" class="btn btn-primary btn-xs btn-att-to-mockup" data-att-id="${escapeHtml(att.id)}">
🎨 Send to Mockup Studio
</button>
<button type="button" class="btn btn-ghost btn-xs btn-att-to-canvas" data-att-url="${escapeHtml(att.imageUrl)}">
👕 Tee Canvas
</button>
${
!isCover
? `<button type="button" class="btn btn-ghost btn-xs btn-att-cover" data-att-id="${escapeHtml(att.id)}">⭐ Cover</button>`
: `<span class="tag-chip" style="color:var(--accent-primary);">★ Cover</span>`
}
`
: ""
}
<button type="button" class="btn btn-danger-ghost btn-xs btn-att-delete" data-att-id="${escapeHtml(att.id)}">🗑️</button>
</div>
</div>
`;
})
.join("");
listEl.querySelectorAll("img[data-zoom-url]").forEach((img) => {
img.addEventListener("click", () => openImageLightbox(img.dataset.zoomUrl, img.alt));
});
listEl.querySelectorAll(".btn-att-to-mockup").forEach((btn) => {
btn.addEventListener("click", async () => {
const res = await apiFetch(`/api/ideas/${idea.id}/attachments/${btn.dataset.attId}/promote-mockup`, {
method: "POST"
});
state.activeIdea = res.idea;
await loadIdeas();
switchDrawerTab("mockup");
const promoted = state.activeIdea.mockups[0];
if (promoted) {
state.nbEditParentMockup = promoted;
document.getElementById("nb-edit-mode-box").classList.remove("hidden");
document.getElementById("nb-edit-target-label").textContent = promoted.title;
}
showToast("Promoted reference image to Mockup Studio for AI refinement!");
});
});
listEl.querySelectorAll(".btn-att-to-canvas").forEach((btn) => {
btn.addEventListener("click", () => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
state.uploadedOverlayImage = img;
switchDrawerTab("mockup");
renderCompositorCanvas();
showToast("Placed attached image onto the Interactive T-Shirt Canvas!");
};
img.src = btn.dataset.attUrl;
});
});
listEl.querySelectorAll(".btn-att-cover").forEach((btn) => {
btn.addEventListener("click", async () => {
await apiFetch(`/api/ideas/${idea.id}/attachments/${btn.dataset.attId}/cover`, {
method: "PATCH"
});
showToast("Set attached image as Spark cover!");
await loadIdeas();
});
});
listEl.querySelectorAll(".btn-att-delete").forEach((btn) => {
btn.addEventListener("click", async () => {
await apiFetch(`/api/ideas/${idea.id}/attachments/${btn.dataset.attId}`, {
method: "DELETE"
});
showToast("Removed attachment");
await loadIdeas();
});
});
}
function renderBulletList(elementId, items, fieldName) {
const ul = document.getElementById(elementId);
if (!ul) return;
@@ -1420,6 +1553,95 @@ function openImageLightbox(url, title = "Mockup Preview") {
document.getElementById("lightbox-meta").innerHTML = "";
}
function readFileAsDataUrl(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
function stageDrawerImage(dataUrl) {
state.stagedDrawerAttachmentDataUrl = dataUrl;
const row = document.getElementById("drawer-attach-preview-row");
const img = document.getElementById("drawer-attach-preview-img");
if (dataUrl) {
if (img) img.src = dataUrl;
row?.classList.remove("hidden");
} else {
if (img) img.src = "";
row?.classList.add("hidden");
}
}
function stageClipModalImage(dataUrl) {
state.stagedClipAttachmentDataUrl = dataUrl;
const row = document.getElementById("clip-preview-row");
const img = document.getElementById("clip-preview-img");
if (dataUrl) {
if (img) img.src = dataUrl;
row?.classList.remove("hidden");
} else {
if (img) img.src = "";
row?.classList.add("hidden");
}
}
function openExternalClipModal({ ideaId = null, prefillTitle = "", prefillText = "", prefillUrl = "" } = {}) {
const modal = document.getElementById("external-clip-modal");
const targetSel = document.getElementById("clip-target-idea");
const newIdeaGroup = document.getElementById("clip-new-idea-group");
if (!modal || !targetSel) return;
const activeIdeas = state.ideas.filter((i) => i.status !== "archived");
targetSel.innerHTML =
activeIdeas
.map((i) => `<option value="${i.id}">[${i.status.toUpperCase()}] ${escapeHtml(i.title)}</option>`)
.join("") + `<option value="new">+ ✨ Create New Spark from Clip</option>`;
if (ideaId && activeIdeas.some((i) => i.id === Number(ideaId))) {
targetSel.value = String(ideaId);
newIdeaGroup?.classList.add("hidden");
} else if (state.activeIdeaId && activeIdeas.some((i) => i.id === Number(state.activeIdeaId))) {
targetSel.value = String(state.activeIdeaId);
newIdeaGroup?.classList.add("hidden");
} else if (prefillTitle || activeIdeas.length === 0) {
targetSel.value = "new";
newIdeaGroup?.classList.remove("hidden");
} else {
targetSel.value = String(activeIdeas[0].id);
newIdeaGroup?.classList.add("hidden");
}
document.getElementById("clip-new-idea-title").value = prefillTitle || "";
document.getElementById("clip-title-input").value = prefillTitle || "";
document.getElementById("clip-text-input").value = prefillText || "";
document.getElementById("clip-source-url").value = prefillUrl || "";
document.getElementById("clip-external-img-url").value = "";
stageClipModalImage(null);
modal.classList.remove("hidden");
}
function checkWebShareTargetQueryParams() {
const params = new URLSearchParams(window.location.search);
const clipTitle = params.get("title") || params.get("clipTitle") || "";
const clipText = params.get("text") || params.get("clipText") || "";
const clipUrl = params.get("url") || params.get("clipUrl") || "";
const ideaId = params.get("ideaId") ? Number(params.get("ideaId")) : null;
if (clipTitle || clipText || clipUrl) {
openExternalClipModal({
ideaId,
prefillTitle: clipTitle,
prefillText: clipText,
prefillUrl: clipUrl
});
window.history.replaceState({}, document.title, window.location.pathname);
}
}
function switchWhackSubmode(mode) {
state.whackSubmode = mode;
document.querySelectorAll("[data-whack-mode]").forEach((btn) => {
@@ -1937,6 +2159,198 @@ function bindGlobalEvents() {
document.getElementById("btn-close-lightbox")?.addEventListener("click", () => {
document.getElementById("lightbox-modal")?.classList.add("hidden");
});
// --- EXTERNAL ATTACHMENT & CLIPBOARD PASTE / DROP EVENTS ---
document.getElementById("btn-open-external-clip")?.addEventListener("click", () => {
openExternalClipModal();
});
document.getElementById("btn-close-clip-modal")?.addEventListener("click", () => {
document.getElementById("external-clip-modal")?.classList.add("hidden");
});
document.getElementById("btn-cancel-clip-modal")?.addEventListener("click", () => {
document.getElementById("external-clip-modal")?.classList.add("hidden");
});
document.getElementById("clip-target-idea")?.addEventListener("change", (e) => {
document.getElementById("clip-new-idea-group")?.classList.toggle("hidden", e.target.value !== "new");
});
// File input & Drag-and-Drop in Drawer
document.getElementById("drawer-attach-file-input")?.addEventListener("change", async (e) => {
const file = e.target.files?.[0];
if (!file) return;
const dataUrl = await readFileAsDataUrl(file);
stageDrawerImage(dataUrl);
const titleEl = document.getElementById("drawer-attach-title");
if (titleEl && !titleEl.value) titleEl.value = file.name.replace(/\.[^.]+$/, "");
});
document.getElementById("btn-drawer-clear-staged-img")?.addEventListener("click", () => {
stageDrawerImage(null);
});
const drawerDropzone = document.getElementById("drawer-attachment-dropzone");
if (drawerDropzone) {
drawerDropzone.addEventListener("dragover", (e) => {
e.preventDefault();
drawerDropzone.classList.add("drag-over");
});
drawerDropzone.addEventListener("dragleave", () => drawerDropzone.classList.remove("drag-over"));
drawerDropzone.addEventListener("drop", async (e) => {
e.preventDefault();
drawerDropzone.classList.remove("drag-over");
const file = e.dataTransfer?.files?.[0];
if (file && file.type.startsWith("image/")) {
const dataUrl = await readFileAsDataUrl(file);
stageDrawerImage(dataUrl);
const titleEl = document.getElementById("drawer-attach-title");
if (titleEl && !titleEl.value) titleEl.value = file.name.replace(/\.[^.]+$/, "");
}
});
}
document.getElementById("btn-drawer-save-attachment")?.addEventListener("click", async () => {
if (!state.activeIdeaId) return;
const title = document.getElementById("drawer-attach-title")?.value.trim() || "";
const urlInput = document.getElementById("drawer-attach-url")?.value.trim() || "";
const textContent = document.getElementById("drawer-attach-text")?.value.trim() || "";
const appendToNotes = document.getElementById("drawer-attach-append-notes")?.checked || false;
const setAsCover = document.getElementById("drawer-attach-set-cover")?.checked || false;
const isImageExt = /\.(png|jpe?g|webp|gif|svg)(\?.*)?$/i.test(urlInput);
const payload = {
title,
textContent,
appendToNotes,
setAsCover,
imageDataUrl: state.stagedDrawerAttachmentDataUrl || "",
externalImageUrl: !state.stagedDrawerAttachmentDataUrl && isImageExt ? urlInput : "",
sourceUrl: urlInput
};
try {
const res = await apiFetch(`/api/ideas/${state.activeIdeaId}/attachments`, {
method: "POST",
body: JSON.stringify(payload)
});
state.activeIdea = res.idea;
document.getElementById("drawer-attach-title").value = "";
document.getElementById("drawer-attach-url").value = "";
document.getElementById("drawer-attach-text").value = "";
stageDrawerImage(null);
showToast("📎 Attached to Spark!");
await loadIdeas();
} catch (err) {
showToast(err.message);
}
});
// File input & Drag-and-Drop in External Clip Modal
document.getElementById("clip-file-input")?.addEventListener("change", async (e) => {
const file = e.target.files?.[0];
if (!file) return;
const dataUrl = await readFileAsDataUrl(file);
stageClipModalImage(dataUrl);
const titleEl = document.getElementById("clip-title-input");
if (titleEl && !titleEl.value) titleEl.value = file.name.replace(/\.[^.]+$/, "");
});
document.getElementById("btn-clip-clear-img")?.addEventListener("click", () => {
stageClipModalImage(null);
});
const clipDropzone = document.getElementById("clip-dropzone");
if (clipDropzone) {
clipDropzone.addEventListener("dragover", (e) => {
e.preventDefault();
clipDropzone.classList.add("drag-over");
});
clipDropzone.addEventListener("dragleave", () => clipDropzone.classList.remove("drag-over"));
clipDropzone.addEventListener("drop", async (e) => {
e.preventDefault();
clipDropzone.classList.remove("drag-over");
const file = e.dataTransfer?.files?.[0];
if (file && file.type.startsWith("image/")) {
const dataUrl = await readFileAsDataUrl(file);
stageClipModalImage(dataUrl);
}
});
}
document.getElementById("external-clip-form")?.addEventListener("submit", async (e) => {
e.preventDefault();
const targetVal = document.getElementById("clip-target-idea").value;
const newIdeaTitle = document.getElementById("clip-new-idea-title").value.trim();
const title = document.getElementById("clip-title-input").value.trim();
const externalImageUrl = document.getElementById("clip-external-img-url").value.trim();
const textContent = document.getElementById("clip-text-input").value.trim();
const sourceUrl = document.getElementById("clip-source-url").value.trim();
const appendToNotes = document.getElementById("clip-append-notes").checked;
const setAsCover = document.getElementById("clip-set-cover").checked;
try {
let targetIdeaId = targetVal === "new" ? null : Number(targetVal);
if (!targetIdeaId) {
const created = await apiFetch("/api/ideas", {
method: "POST",
body: JSON.stringify({
title: newIdeaTitle || title || "Clipped External Spark",
category: "spark"
})
});
targetIdeaId = created.idea.id;
}
await apiFetch(`/api/ideas/${targetIdeaId}/attachments`, {
method: "POST",
body: JSON.stringify({
title: title || "External Clip",
imageDataUrl: state.stagedClipAttachmentDataUrl || "",
externalImageUrl,
textContent,
sourceUrl,
appendToNotes,
setAsCover
})
});
document.getElementById("external-clip-modal")?.classList.add("hidden");
stageClipModalImage(null);
showToast("📎 Attached external note/image to Spark!");
await loadIdeas();
} catch (err) {
showToast(err.message);
}
});
// Global Clipboard Paste (Ctrl+V) for screenshots & copied images
window.addEventListener("paste", async (e) => {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (!file) continue;
e.preventDefault();
const dataUrl = await readFileAsDataUrl(file);
const clipModalOpen = !document.getElementById("external-clip-modal")?.classList.contains("hidden");
const drawerOpen = !document.getElementById("idea-drawer")?.classList.contains("hidden");
if (clipModalOpen) {
stageClipModalImage(dataUrl);
showToast("📋 Pasted screenshot into Clip dialog!");
} else if (drawerOpen) {
stageDrawerImage(dataUrl);
switchDrawerTab("details");
showToast("📋 Pasted screenshot into Spark Workbench — click 'Attach to Spark' to save!");
} else {
openExternalClipModal();
stageClipModalImage(dataUrl);
showToast("📋 Pasted screenshot — pick a Spark to attach it to!");
}
break;
}
}
});
}
initApp();
+133
View File
@@ -37,6 +37,14 @@
<option value="research">🔬 Research</option>
</select>
<button type="submit" class="btn btn-primary btn-sm">Capture</button>
<button
type="button"
id="btn-open-external-clip"
class="btn btn-secondary btn-sm"
title="Attach external notes, clipboard screenshots, photos, or image URLs to a spark"
>
📎 Clip / Attach
</button>
<button
type="button"
id="btn-quick-whack-prompt"
@@ -466,6 +474,56 @@
<button type="submit" class="btn btn-ghost btn-sm">+ Add Link</button>
</form>
</div>
<!-- External Attachments: Clipped Notes, Screenshots & Reference Images -->
<div class="field-group">
<div class="field-header-row">
<label>📎 Attached External Notes & Reference Images (<span id="drawer-attachments-count">0</span>)</label>
<span class="muted-label">Paste (Ctrl+V), drop files, or clip from URL</span>
</div>
<div id="drawer-attachment-dropzone" class="attachment-dropzone">
<div class="dropzone-header">
<span>📥 <strong>Drop image here</strong>, press <kbd>Ctrl+V</kbd> to paste a screenshot, or attach a note/URL below:</span>
<label class="btn btn-secondary btn-xs" style="cursor:pointer;">
📷 Choose Image / Camera
<input type="file" id="drawer-attach-file-input" accept="image/*" hidden />
</label>
</div>
<div id="drawer-attach-preview-row" class="attach-preview-row hidden">
<img id="drawer-attach-preview-img" src="" alt="Staged attachment" />
<div style="flex:1;">
<strong style="font-size:0.82rem;">Staged Image Ready to Attach</strong>
<p class="muted-label">Add an optional caption or note below and click Attach.</p>
</div>
<button type="button" id="btn-drawer-clear-staged-img" class="btn btn-ghost btn-xs">✕ Remove</button>
</div>
<div class="form-row-2">
<input type="text" id="drawer-attach-title" class="text-input input-sm" placeholder="Title / Caption (e.g. Vintage Ski Patch Ref)" />
<input type="url" id="drawer-attach-url" class="text-input input-sm" placeholder="External Image URL or Source Link (https://...)" />
</div>
<textarea id="drawer-attach-text" rows="2" class="textarea-input" placeholder="Paste external note, quote, spec snippet, or commentary..."></textarea>
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:0.5rem;">
<div style="display:flex;gap:0.85rem;flex-wrap:wrap;">
<label class="filter-checkbox">
<input type="checkbox" id="drawer-attach-append-notes" />
<span>Also append text to Markdown Notes</span>
</label>
<label class="filter-checkbox">
<input type="checkbox" id="drawer-attach-set-cover" />
<span>Set image as Spark Cover</span>
</label>
</div>
<button type="button" id="btn-drawer-save-attachment" class="btn btn-primary btn-sm">
📎 Attach to Spark
</button>
</div>
</div>
<div id="drawer-attachments-list" class="attachments-grid"></div>
</div>
</div>
<!-- TAB 2: NANO BANANA 2.1 VISUAL MOCKUP STUDIO -->
@@ -700,6 +758,81 @@
</div>
</div>
<!-- MODAL: EXTERNAL CLIPPER / ATTACH TO SPARK -->
<div id="external-clip-modal" class="modal-backdrop hidden">
<div class="modal-card" style="max-width:600px;">
<div class="modal-header">
<h3>📎 Attach External Note or Image to Spark</h3>
<button type="button" id="btn-close-clip-modal" class="btn btn-ghost btn-sm">✕</button>
</div>
<form id="external-clip-form" class="modal-body">
<p class="muted-label">
Clip a screenshot (<kbd>Ctrl+V</kbd> anywhere in this dialog), upload a photo from your phone, fetch an external image URL, or attach notes/links to an existing Spark (or create a new one).
</p>
<div class="field-group">
<label for="clip-target-idea">Target Spark / Idea</label>
<select id="clip-target-idea" class="filter-select full-width"></select>
</div>
<div id="clip-new-idea-group" class="field-group hidden">
<label for="clip-new-idea-title">New Spark Title</label>
<input type="text" id="clip-new-idea-title" class="text-input" placeholder="e.g. Vintage Campfire Enamel Mug #merch @creative" />
</div>
<div class="field-group">
<label for="clip-title-input">Attachment Title / Caption</label>
<input type="text" id="clip-title-input" class="text-input" placeholder="e.g. Field Notes Photo / Competitor Spec / Article Excerpt" />
</div>
<div class="field-group">
<label>Image Source (File / Camera, Clipboard Paste, or Direct Image URL)</label>
<div id="clip-dropzone" class="attachment-dropzone">
<div style="display:flex;justify-content:space-between;align-items:center;gap:0.5rem;flex-wrap:wrap;">
<span class="muted-label">📋 Paste image (<kbd>Ctrl+V</kbd>) or drop file here</span>
<label class="btn btn-secondary btn-xs" style="cursor:pointer;">
📷 Select Photo / File
<input type="file" id="clip-file-input" accept="image/*" hidden />
</label>
</div>
<div id="clip-preview-row" class="attach-preview-row hidden">
<img id="clip-preview-img" src="" alt="Clip preview" />
<span class="muted-label" style="flex:1;">Image staged for attachment</span>
<button type="button" id="btn-clip-clear-img" class="btn btn-ghost btn-xs">✕ Clear</button>
</div>
<input type="url" id="clip-external-img-url" class="text-input input-sm" placeholder="Or paste an external image URL (https://example.com/photo.jpg)..." />
</div>
</div>
<div class="field-group">
<label for="clip-text-input">External Note / Excerpt / Markdown</label>
<textarea id="clip-text-input" rows="3" class="textarea-input" placeholder="Paste notes, thoughts, article quotes, or garment/project details..."></textarea>
</div>
<div class="field-group">
<label for="clip-source-url">Source Webpage URL (Optional)</label>
<input type="url" id="clip-source-url" class="text-input input-sm" placeholder="https://..." />
</div>
<div style="display:flex;gap:1rem;flex-wrap:wrap;">
<label class="filter-checkbox">
<input type="checkbox" id="clip-append-notes" checked />
<span>Append note text to Spark's Markdown Notes</span>
</label>
<label class="filter-checkbox">
<input type="checkbox" id="clip-set-cover" checked />
<span>Set image as Spark cover</span>
</label>
</div>
<div class="modal-footer">
<button type="button" id="btn-cancel-clip-modal" class="btn btn-ghost">Cancel</button>
<button type="submit" class="btn btn-primary">📎 Save Attachment to Spark</button>
</div>
</form>
</div>
</div>
<!-- MODAL: CARD / MOCKUP LIGHTBOX INSPECTOR -->
<div id="lightbox-modal" class="modal-backdrop hidden">
<div class="lightbox-card">
+85
View File
@@ -1678,3 +1678,88 @@ body {
}
}
/* EXTERNAL ATTACHMENTS & CLIPPER */
.attachment-dropzone {
background: var(--bg-elevated);
border: 1.5px dashed var(--border-strong);
border-radius: var(--radius-md);
padding: 0.85rem;
display: flex;
flex-direction: column;
gap: 0.65rem;
transition: border-color 0.15s ease, background 0.15s ease;
}
.attachment-dropzone.drag-over {
border-color: var(--accent-primary);
background: rgba(245, 158, 11, 0.08);
}
.dropzone-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
font-size: 0.82rem;
color: var(--text-secondary);
}
.attach-preview-row {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem;
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
}
.attach-preview-row img {
width: 64px;
height: 64px;
object-fit: cover;
border-radius: var(--radius-sm);
border: 1px solid var(--border-strong);
}
.attachments-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 0.75rem;
margin-top: 0.35rem;
}
.attachment-card {
background: var(--bg-elevated);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.attachment-card.is-cover {
border-color: var(--accent-primary);
}
.attachment-thumb {
width: 100%;
height: 150px;
object-fit: cover;
border-radius: var(--radius-sm);
cursor: zoom-in;
background: #10131b;
}
kbd {
background: var(--bg-surface);
border: 1px solid var(--border-strong);
border-radius: 4px;
padding: 0.08rem 0.35rem;
font-size: 0.74rem;
font-family: monospace;
}