+
@@ -432,33 +440,66 @@
border: 1px solid var(--color-border, #2f334d);
margin-top: 6px;
}
-.attachment-list {
- display: flex;
- flex-direction: column;
+/* kit .tabs caps itself at 900px and is a flex row — its .tabs-panels wrapper
+ shrink-wraps the content; the report layout spans the whole shell instead */
+:deep(.tabs) {
+ max-width: none;
+}
+:deep(.tabs-panels) {
+ width: 100%;
+}
+.attachment-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+ gap: 12px;
}
.attachment-item {
display: flex;
- align-items: center;
- gap: 10px;
- padding: 10px 4px;
- border-bottom: 1px solid var(--color-border, #2f334d);
+ flex-direction: column;
+ gap: 8px;
+ padding: 10px;
+ border: 1px solid var(--ltt-border, rgba(192, 202, 245, 0.24));
color: inherit;
text-decoration: none;
font-size: 13px;
-}
-.attachment-item:last-child {
- border-bottom: none;
+ transition: border-color 0.15s ease;
}
.attachment-item:hover {
- color: var(--color-accent, #7aa2f7);
+ border-color: var(--ltt-accent, #7aa2f7);
+}
+.attachment-thumb {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 150px;
+ overflow: hidden;
+ background: var(--ltt-surface-sunken, #10121c);
+}
+.attachment-thumb img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+}
+.attachment-thumb .ph {
+ font-size: 40px;
+ opacity: 0.4;
+}
+.attachment-info {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 10px;
+ min-width: 0;
}
.attachment-name {
- flex: 1;
- overflow-wrap: anywhere;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
.attachment-size {
opacity: 0.5;
font-size: 11px;
+ flex-shrink: 0;
}
.report-page-loading {
opacity: 0.6;
diff --git a/packages/web/verify-lightbox.mjs b/packages/web/verify-lightbox.mjs
new file mode 100644
index 0000000..3b8fb78
--- /dev/null
+++ b/packages/web/verify-lightbox.mjs
@@ -0,0 +1,129 @@
+/** Verify the report-page lightbox + attachments tab with a LARGE screenshot. */
+import { chromium } from "playwright-core";
+import { join, dirname } from "node:path";
+import { fileURLToPath } from "node:url";
+import { homedir } from "node:os";
+
+const root = dirname(fileURLToPath(import.meta.url));
+const SERVER = "http://localhost:8001";
+const WEB = "http://localhost:5173";
+const stamp = Date.now();
+const EMAIL = `lb-verify-${stamp}@example.com`;
+const PASSWORD = "lb-verify-pass";
+
+const executablePath = join(homedir(), ".cache/ms-playwright/chromium-1234/chrome-linux64/chrome");
+const browser = await chromium.launch({ executablePath, headless: true });
+
+async function api(path, options = {}) {
+ const r = await fetch(SERVER + path, options);
+ if (!r.ok) throw new Error(`API ${path} -> ${r.status}`);
+ return r.json();
+}
+
+const user = await api("/api/auth/register", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ nickname: `lb-verify-${stamp}`, email: EMAIL, password: PASSWORD }),
+});
+const login = await api("/api/auth/login", {
+ method: "POST",
+ headers: { "Content-Type": "application/json", "X-Client": "extension" },
+ body: JSON.stringify({ email: EMAIL, password: PASSWORD }),
+});
+const auth = { Authorization: `Bearer ${login.token}` };
+const project = await api("/api/projects", {
+ method: "POST",
+ headers: { "Content-Type": "application/json", ...auth },
+ body: JSON.stringify({ name: "Lightbox Verify" }),
+});
+
+// big screenshot: 1400x900 viewport @2x DPR → 2800x1800 natural
+const maker = await browser.newPage({ viewport: { width: 1400, height: 900 }, deviceScaleFactor: 2 });
+await maker.setContent(`
+
Big screenshot 2800×1800
`);
+const png = await maker.screenshot({ path: "/tmp/ltt-shots/big.png" });
+await maker.close();
+
+const form = new FormData();
+form.append("file", new Blob([png], { type: "image/png" }), "big.png");
+const up = await api("/api/uploads", { method: "POST", headers: auth, body: form });
+const note = await api("/api/reports", {
+ method: "POST",
+ headers: { "Content-Type": "application/json", ...auth },
+ body: JSON.stringify({
+ project_id: project.id,
+ type: "element_note",
+ title: "Lightbox verify",
+ environment: { user_agent: "verify", browser: "Chromium", os: "Linux", viewport: { w: 1400, h: 900 }, dpr: 2, language: "en" },
+ attachment_ids: [up.file_id],
+ }),
+});
+
+const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
+await page.goto(`${WEB}/r/${note.share_token}`);
+await page.waitForSelector(".screenshot-expand", { timeout: 15000 });
+await page.waitForTimeout(800);
+await page.screenshot({ path: "/tmp/ltt-shots/08-report.png" });
+
+// attachments tab preview (Правка 2)
+await page.click("text=Вложения");
+await page.waitForTimeout(800);
+console.log("widths:", await page.evaluate(() => {
+ const r = (el) => (el ? (({ x, width }) => `${Math.round(x)}+${Math.round(width)}`)(el.getBoundingClientRect()) : "none");
+ const tabs = document.querySelector(".tabs");
+ const panel = document.querySelector(".tab-panel-active");
+ const cs = panel ? getComputedStyle(panel) : null;
+ return {
+ tabs: r(tabs),
+ tabsMaxWidth: tabs ? getComputedStyle(tabs).maxWidth : null,
+ panel: r(panel),
+ panelWidth: cs?.width,
+ panelParent: r(panel?.parentElement),
+ panelParentTag: panel?.parentElement?.className,
+ card: r(document.querySelector(".tab-panel-active .card")),
+ };
+}));
+await page.screenshot({ path: "/tmp/ltt-shots/09-attachments.png" });
+await page.click("text=Детали");
+
+// open the lightbox (Правка 3)
+await page.click(".screenshot-expand");
+await page.waitForSelector(".lightbox", { timeout: 5000 });
+await page.waitForTimeout(1000);
+const zoom = page.locator(".lightbox-zoom");
+console.log("fit zoom (expect <100% for 2800px image):", (await zoom.textContent()).trim());
+await page.screenshot({ path: "/tmp/ltt-shots/10-lightbox-fit.png" });
+
+// wheel zoom toward the cursor
+await page.mouse.move(700, 400);
+for (let i = 0; i < 3; i++) await page.mouse.wheel(0, -120);
+await page.waitForTimeout(300);
+console.log("after wheel zoom-in:", (await zoom.textContent()).trim());
+
+// pan by dragging (drag ends over the backdrop — must NOT close)
+const before = await page.locator(".lightbox-img").getAttribute("style");
+await page.mouse.move(700, 450);
+await page.mouse.down();
+await page.mouse.move(400, 250, { steps: 8 });
+await page.mouse.up();
+const after = await page.locator(".lightbox-img").getAttribute("style");
+console.log("pan moved transform:", before !== after ? "YES" : "NO");
+console.log("lightbox still open after drag:", (await page.locator(".lightbox").count()) === 1);
+await page.screenshot({ path: "/tmp/ltt-shots/11-lightbox-panned.png" });
+
+// zoom cap: hammer zoom-in, must never exceed 100% (natural resolution)
+for (let i = 0; i < 12; i++) await page.click('.lightbox-btn[title="Zoom in"]');
+console.log("after 12 more zoom-ins (cap):", (await zoom.textContent()).trim());
+
+// toolbar zoom-out works
+await page.click('.lightbox-btn[title="Zoom out"]');
+await page.click('.lightbox-btn[title="Zoom out"]');
+console.log("after 2x zoom-out:", (await zoom.textContent()).trim());
+
+// Esc closes
+await page.keyboard.press("Escape");
+await page.waitForTimeout(300);
+console.log("lightbox closed on Esc:", (await page.locator(".lightbox").count()) === 0);
+
+await browser.close();
+console.log("VERIFY OK");
\ No newline at end of file