diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 8c66b0a..c0f58d6 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -3145,7 +3145,12 @@ function openDashboardProjectSwitcher() { const reviewCount = getProjectReviews(project.id).length; const isActive = project.id === selectedProject?.id; return ` -
+
+ `; }).join(""); openActionModal({ @@ -3187,6 +3192,18 @@ function openDashboardProjectSwitcher() { }, { name: "projectId", label: "当前项目", type: "select", value: getSelectedProject()?.id || "", options } ], + onOpen: ({ fields }) => { + const select = fields.querySelector('[data-action-field="projectId"]'); + fields.querySelectorAll("[data-project-choice]").forEach((button) => { + button.addEventListener("click", () => { + if (select) { + select.value = button.dataset.projectChoice || ""; + } + fields.querySelectorAll("[data-project-choice]").forEach((item) => item.classList.remove("active")); + button.classList.add("active"); + }); + }); + }, onSubmit: async (payload) => { appState.selectedProjectId = payload.projectId || ""; setBusy(true, "正在切换项目视图..."); @@ -7164,6 +7181,9 @@ async function createProject() { { name: "name", label: "项目名称", placeholder: "例如:创业 IP 增长实验室" }, { name: "description", label: "项目说明", type: "textarea", rows: 4, placeholder: "写一句这个项目主要解决什么问题、接下来准备推进什么" } ], + onOpen: ({ fields }) => { + fields.querySelector('[data-action-field="name"]')?.focus(); + }, onSubmit: async (values) => { const name = String(values.name || "").trim(); if (!name) throw new Error("请填写项目名称"); diff --git a/web/storyforge-web-v4/assets/styles.css b/web/storyforge-web-v4/assets/styles.css index c3c6edb..0c8b871 100644 --- a/web/storyforge-web-v4/assets/styles.css +++ b/web/storyforge-web-v4/assets/styles.css @@ -1468,6 +1468,20 @@ select { padding: 14px; } +.project-choice-card { + width: 100%; + display: grid; + gap: 8px; + text-align: left; + cursor: pointer; + appearance: none; +} + +.project-choice-card:hover { + border-color: rgba(79, 143, 238, 0.24); + box-shadow: inset 0 0 0 1px rgba(79, 143, 238, 0.08); +} + .detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -2212,6 +2226,21 @@ tbody tr:hover { padding-inline: 14px; } + .field-stack input, + .field-stack textarea, + .field-stack select { + min-height: 46px; + font-size: 16px; + } + + .field-stack textarea { + min-height: 108px; + } + + .project-choice-card { + min-height: 96px; + } + .page-detail-tabs { flex-wrap: nowrap; overflow-x: auto; diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index 4f92fe8..6c1febe 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -114,6 +114,17 @@ test("project creation and switching use in-app sheets instead of browser prompt assert.match(projectSwitcher, /切换到这个项目/); }); +test("mobile project sheets support direct project picking and zoom-safe form controls", () => { + const projectSwitcher = extractBetween(APP, "function openDashboardProjectSwitcher()", "function openDashboardActionReasonAction("); + const createProject = extractBetween(APP, "async function createProject()", "function openPreferredModelAction()"); + assert.match(projectSwitcher, /data-project-choice=/); + assert.match(projectSwitcher, /onOpen:\s*\(/); + assert.match(projectSwitcher, /select\.value = button\.dataset\.projectChoice/); + assert.match(createProject, /onOpen:\s*\(/); + assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.field-stack input,\s*[\s\S]*\.field-stack textarea,\s*[\s\S]*\.field-stack select\s*\{[\s\S]*min-height:\s*46px/); + assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.field-stack input,\s*[\s\S]*\.field-stack textarea,\s*[\s\S]*\.field-stack select\s*\{[\s\S]*font-size:\s*16px/); +}); + test("mobile touch targets raise tappable buttons, tabs, and action tags closer to native sizes", () => { assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.btn,\s*[\s\S]*\.tab,\s*[\s\S]*\.tag\.clickable-tag\s*\{[\s\S]*min-height:\s*44px/); assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.btn,\s*[\s\S]*\.tab,\s*[\s\S]*\.tag\.clickable-tag\s*\{[\s\S]*display:\s*inline-flex/);