From edbefcc738eb731a895032478dc6c388c5540d24 Mon Sep 17 00:00:00 2001 From: suyx Date: Wed, 5 Aug 2026 19:19:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(POSTV1-10):=20=E5=A2=9E=E5=8A=A0=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E4=BA=A4=E4=BA=92=E5=8F=8D=E9=A6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/interaction-feedback.css | 152 ++++++++++++++++++++++++++ apps/web/src/main.tsx | 2 + tests/e2e/projects-workspace.spec.ts | 14 ++- tests/e2e/wp4-03-text-editor.spec.ts | 6 +- 4 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/interaction-feedback.css diff --git a/apps/web/src/interaction-feedback.css b/apps/web/src/interaction-feedback.css new file mode 100644 index 0000000..4a1135f --- /dev/null +++ b/apps/web/src/interaction-feedback.css @@ -0,0 +1,152 @@ +:root { + --dada-interaction-duration: 120ms; + --dada-interaction-easing: cubic-bezier(0.2, 0.8, 0.2, 1); +} + +:where(button:not(:disabled), a[href], label:has(input:not(:disabled))) { + cursor: pointer; +} + +:where(button:not(:disabled), a[href]) { + transition: + transform var(--dada-interaction-duration) var(--dada-interaction-easing), + box-shadow var(--dada-interaction-duration) var(--dada-interaction-easing), + border-color var(--dada-interaction-duration) ease, + background-color var(--dada-interaction-duration) ease, + color var(--dada-interaction-duration) ease, + opacity var(--dada-interaction-duration) ease; +} + +:where(input:not(:disabled), select:not(:disabled), textarea:not(:disabled)) { + transition: + border-color var(--dada-interaction-duration) ease, + box-shadow var(--dada-interaction-duration) ease, + background-color var(--dada-interaction-duration) ease; +} + +:where(.product-page, .product-loading) :focus-visible { + outline: 2px solid #005fcc; + outline-offset: 3px; +} + +:where(input:not(:disabled), select:not(:disabled), textarea:not(:disabled)):focus-visible { + border-color: #005fcc; + box-shadow: 0 0 0 3px rgb(0 95 204 / 16%); +} + +.project-card, +.project-preview img, +.ratio-control span, +.reference-input, +.editor-sticker-preview, +.editor-template-mark, +.editor-color-card-preview, +.editor-dynamic-preview, +.editor-source-preview-canvas, +.editor-thumb { + transition: + transform var(--dada-interaction-duration) var(--dada-interaction-easing), + box-shadow var(--dada-interaction-duration) var(--dada-interaction-easing), + border-color var(--dada-interaction-duration) ease, + background-color var(--dada-interaction-duration) ease; +} + +@media (hover: hover) and (pointer: fine) { + :where(button:not(:disabled)):hover { + border-color: #111111; + box-shadow: 0 2px 0 rgb(17 17 17 / 35%); + } + + :where(a[href]):hover { + color: #005fcc; + opacity: 0.78; + text-decoration-thickness: 2px; + text-underline-offset: 3px; + } + + .product-header nav a:hover { + color: #111111; + background: #e9e9e5; + box-shadow: inset 0 -3px #111111; + opacity: 1; + } + + .product-header nav a[aria-current="page"]:hover { + background: #f2f500; + } + + .ratio-control label:hover span, + .reference-input:hover { + border-color: #111111; + background: #ffffd6; + box-shadow: inset 0 -3px #111111; + } + + .project-card:hover { + border-color: #111111; + box-shadow: 0 3px 0 rgb(17 17 17 / 22%); + } + + .editor-asset-tabs button:not(:disabled):hover, + .editor-source:hover, + .editor-sticker-grid button:not(:disabled):hover, + .editor-template-categories button:not(:disabled):hover, + .editor-template-grid button:not(:disabled):hover, + .editor-provider-grid button:not(:disabled):hover, + .editor-candidates button:not(:disabled):hover { + border-color: #111111; + background: #ffffd6; + } + + :where(input:not(:disabled), select:not(:disabled), textarea:not(:disabled)):hover { + border-color: #111111; + } +} + +@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) { + :where(button:not(:disabled), a[href]):hover { + transform: translateY(-1px); + } + + .project-card:hover .project-preview img, + .editor-sticker-grid button:not(:disabled):hover .editor-sticker-preview { + transform: scale(1.02); + } + + .editor-template-grid button:not(:disabled):hover .editor-template-mark, + .editor-provider-grid button:not(:disabled):hover > :first-child { + transform: scale(1.03); + } +} + +@media (prefers-reduced-motion: no-preference) { + :where(button:not(:disabled), a[href]):active { + transform: translateY(1px); + transition-duration: 45ms; + } +} + +:where(button:not(:disabled)):active { + box-shadow: inset 0 2px 0 rgb(17 17 17 / 24%); +} + +:where(a[href]):active { + opacity: 0.58; +} + +@media (prefers-reduced-motion: reduce) { + :where(button, a[href], input, select, textarea), + .project-card, + .project-preview img, + .ratio-control span, + .reference-input, + .editor-sticker-preview, + .editor-template-mark, + .editor-color-card-preview, + .editor-dynamic-preview, + .editor-source-preview-canvas, + .editor-thumb { + animation-duration: 0s !important; + transition-duration: 0s !important; + } +} diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 150799f..a578ed1 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -16,6 +16,8 @@ import { ProjectDetailPage, ProjectsPage, WorkspacePage } from "./project-pages. import { EditorPage } from "./editor-page.js"; import { AdminOverviewPage, AdminPlaceholderPage, AdminProtectedRoute } from "./admin-shell.js"; +import "./interaction-feedback.css"; + const root = document.getElementById("root"); if (!root) { diff --git a/tests/e2e/projects-workspace.spec.ts b/tests/e2e/projects-workspace.spec.ts index 59d13f2..8f04603 100644 --- a/tests/e2e/projects-workspace.spec.ts +++ b/tests/e2e/projects-workspace.spec.ts @@ -105,9 +105,21 @@ test("TDD-WP2-PROJ-005 limits failed-empty selection on the project list", async await expect(page.getByLabel("选择失败草稿:失败草稿")).toBeVisible(); await expect(page.getByLabel("选择失败草稿:城市工作室")).toHaveCount(0); await page.getByLabel("选择失败草稿:失败草稿").check(); - await page.getByRole("button", { name: "批量移入回收站" }).click(); + const batchTrashButton = page.getByRole("button", { name: "批量移入回收站" }); + await batchTrashButton.hover(); + await expect(batchTrashButton).toHaveCSS("transform", "matrix(1, 0, 0, 1, 0, -1)"); + const buttonBounds = await batchTrashButton.boundingBox(); + if (!buttonBounds) throw new Error("Batch trash button geometry is unavailable."); + await page.mouse.move(buttonBounds.x + buttonBounds.width / 2, buttonBounds.y + buttonBounds.height / 2); + await page.mouse.down(); + await expect(batchTrashButton).toHaveCSS("transform", "matrix(1, 0, 0, 1, 0, 1)"); + await page.mouse.move(0, 0); + await page.mouse.up(); + await batchTrashButton.click(); expect(batchPayload).toEqual({ project_ids: [failedId] }); await expect(page.getByText("失败草稿已移入回收站")).toBeVisible(); + await page.emulateMedia({ reducedMotion: "reduce" }); + await expect(page.getByRole("link", { name: "打开项目:城市工作室" })).toHaveCSS("transition-duration", "0s"); expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(375); await captureEvidence(page, "TDD-WP2-PROJ-005-failed-draft-retry", "projects-mobile.png"); }); diff --git a/tests/e2e/wp4-03-text-editor.spec.ts b/tests/e2e/wp4-03-text-editor.spec.ts index 5372f0a..535ce92 100644 --- a/tests/e2e/wp4-03-text-editor.spec.ts +++ b/tests/e2e/wp4-03-text-editor.spec.ts @@ -98,7 +98,11 @@ test("TDD-WP4-TXT-003 exposes the frozen catalog, display-name search and accoun await expect(page.locator(".editor-template-grid button")).toHaveCount(0); await search.fill(""); expect(page.getByText("添加普通文字", { exact: true })).toHaveCount(0); - await page.getByRole("button", { name: /FLOWER001 春日计划/ }).click(); + const flowerTemplate = page.getByRole("button", { name: /FLOWER001 春日计划/ }); + await flowerTemplate.hover(); + await expect(flowerTemplate).toHaveCSS("background-color", "rgb(255, 255, 214)"); + await expect(flowerTemplate.locator(".editor-template-mark")).toHaveCSS("transform", "matrix(1.03, 0, 0, 1.03, 0, 0)"); + await flowerTemplate.click(); await expect(page.getByText("对象 1 / 50")).toBeVisible(); await expect.poll(() => backend.recent).toEqual([{ asset_id: "FLOWER001", asset_kind: "text_template", resource_version: "p0a-complex-v1" }]); await page.reload();