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/);