#19 m6/submit-buttons

closedopened
details
branches
master ← m6/submit-buttons
updated
id
0234b26d-75dc-4bd6-8ed6-38030290b89f

Patch

src/forge.rs+62 −2
diff --git a/src/forge.rs b/src/forge.rsindex 61a34a7..951136b 100644--- a/src/forge.rs+++ b/src/forge.rs@@ -948,8 +948,9 @@ fn render_issues_index(948948     description: Option<&str>,949949 ) -> Result<String, String> {950950     let mut body = String::from("<h1>Issues</h1>\n");951+    body.push_str("<p><a class=\"btn-new\" href=\"/new-issue.html\">+ New issue</a></p>\n");951952     if data.issues.is_empty() {952-        body.push_str("<p class=\"muted\">No issues yet.</p>\n");953+        body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");953954     } else {954955         body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");955956         for issue in &data.issues {@@ -994,8 +995,9 @@ fn render_issues_index(994995 995996 fn render_pulls_index(prs: &[Pr], site_name: &str, nav: &str, description: Option<&str>) -> String {996997     let mut body = String::from("<h1>Pulls</h1>\n");998+    body.push_str("<p><a class=\"btn-new\" href=\"/new-pr.html\">+ New PR prompt</a></p>\n");997999     if prs.is_empty() {998-        body.push_str("<p class=\"muted\">No pull requests yet.</p>\n");1000+        body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");9991001     } else {10001002         body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");10011003         for pr in prs {@@ -1391,6 +1393,64 @@ mod tests {13911393         }13921394     }13931395 1396+    /// f0rge #25: index pages carry prominent `+` submit buttons (correct1397+    /// hrefs, icon + words) and the empty states link to the forms.1398+    #[test]1399+    fn submit_buttons_on_issues_pulls_indexes() {1400+        // Empty forge: buttons + linked empty states.1401+        let repo = mktemp("submit-empty");1402+        let out = mktemp("submit-empty-out");1403+        render_loaded(&load(&repo), &out, "test", "<nav>", None).expect("render");1404+        let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");1405+        let pulls = std::fs::read_to_string(out.join("pulls/index.html")).expect("pulls");1406+        assert!(1407+            issues.contains("<a class=\"btn-new\" href=\"/new-issue.html\">+ New issue</a>"),1408+            "issues + button"1409+        );1410+        assert!(1411+            pulls.contains("<a class=\"btn-new\" href=\"/new-pr.html\">+ New PR prompt</a>"),1412+            "pulls + button"1413+        );1414+        assert!(1415+            issues.contains("No issues yet.")1416+                && issues.contains("href=\"/new-issue.html\">Suggest one"),1417+            "issues empty state links to form"1418+        );1419+        assert!(1420+            pulls.contains("No pull requests yet.")1421+                && pulls.contains("href=\"/new-pr.html\">Suggest one"),1422+            "pulls empty state links to form"1423+        );1424+        // Non-empty forge: buttons stay above the tables.1425+        write_forge(1426+            &repo,1427+            "issues/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/issue.md",1428+            "---\nid: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\ntitle: T\nstate: open\ncreated: 2026-09-10T03:00:00Z\n---\nbody\n",1429+        );1430+        write_forge(1431+            &repo,1432+            "prs/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/pr.md",1433+            "---\nid: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb\ntitle: P\nstate: open\nbase: main\nhead: fix\ncreated: 2026-09-10T03:00:00Z\n---\ndesc\n",1434+        );1435+        let out2 = mktemp("submit-full-out");1436+        render_loaded(&load(&repo), &out2, "test", "<nav>", None).expect("render");1437+        let issues2 = std::fs::read_to_string(out2.join("issues/index.html")).expect("issues");1438+        let pulls2 = std::fs::read_to_string(out2.join("pulls/index.html")).expect("pulls");1439+        assert!(1440+            issues2.contains("href=\"/new-issue.html\">+ New issue"),1441+            "issues + button with rows"1442+        );1443+        assert!(1444+            pulls2.contains("href=\"/new-pr.html\">+ New PR prompt"),1445+            "pulls + button with rows"1446+        );1447+        assert!(1448+            issues2.contains("table-scroll"),1449+            "issues table still renders"1450+        );1451+        assert!(pulls2.contains("table-scroll"), "pulls table still renders");1452+    }1453+13941454     #[test]13951455     fn verdict_badge_shapes() {13961456         assert!(verdict_badge("pass").contains("badge pass"));
src/inbox.rs+45 −3
diff --git a/src/inbox.rs b/src/inbox.rsindex 4998fb0..9806475 100644--- a/src/inbox.rs+++ b/src/inbox.rs@@ -593,7 +593,7 @@ pub fn store_submission(593593 // ---- forms (built into dist, zero JS) ----------------------------------594594 595595 fn license_block() -> String {596-    let mut out = String::from("<blockquote><p>");596+    let mut out = String::from("<blockquote id=\"license-full\"><p>");597597     let lines: Vec<&str> = LICENSE_TEXT.split('\n').collect();598598     for (i, line) in lines.iter().enumerate() {599599         if i > 0 {@@ -618,6 +618,9 @@ fn form_page(618618 ) -> String {619619     let body = format!(620620         "<h1>{heading}</h1>\n<p>{intro}</p>\n\621+         <p>What happens: your text goes to a private inbox only the maintainer reads — \622+         never published on this site. No account needed. The server answers a one-time \623+         ack id and shows nothing back.</p>\n\621624          <form method=\"post\" action=\"{action}\">\n\622625          <p><label for=\"f-title\">Title (required, up to 300 chars)</label><br>\n\623626          <input type=\"text\" id=\"f-title\" name=\"title\" maxlength=\"300\" required size=\"60\"></p>\n\@@ -631,6 +634,9 @@ fn form_page(631634          <p><label>Leave this empty: <input type=\"text\" name=\"website\" value=\"\" autocomplete=\"off\" tabindex=\"-1\"></label></p>\n\632635          </div>\n\633636          {license}\637+         <p>In plain words: you hand your text over completely — the project can keep, share, \638+         or change it for any purpose, and you keep no rights and are owed nothing. \639+         See the <a href=\"#license-full\">full grant text</a> above.</p>\n\634640          <p><label><input type=\"checkbox\" name=\"license\" value=\"agree\" required> \635641          I agree to the grant above (required — unchecked submissions are rejected).</label></p>\n\636642          <p><button type=\"submit\">Submit</button></p>\n\@@ -663,7 +669,7 @@ pub fn render_forms(663669         site_name,664670         nav,665671         "Suggest an issue",666-        "Detailed actionable text from your user. Free textbox on purpose — say what a maintainer can act on.",672+        "Spotted something worth fixing? Write it up here in plain words — say what a maintainer can act on.",667673         InboxType::Issue.route(),668674         "body",669675         "Body",@@ -673,7 +679,7 @@ pub fn render_forms(673679         site_name,674680         nav,675681         "Suggest a change",676-        "ONE prompt, plain text. No files, no diff, no branch. Code words inside stay text — never applied, never executed.",682+        "Have an idea for a fix? Send ONE prompt as plain text — no files, no diff, no branch. Code words inside stay text: never applied, never executed.",677683         InboxType::PrPrompt.route(),678684         "prompt",679685         "Prompt",@@ -1050,4 +1056,40 @@ mod tests {10501056         }10511057         std::fs::remove_dir_all(&dir).ok();10521058     }1059+1060+    /// f0rge #25: submission pages explain the deal in human words1061+    /// (private inbox, ack id, no account) plus a plain license summary1062+    /// linking to the full grant text — with the full checkbox kept.1063+    #[test]1064+    fn forms_explain_deal_and_summarize_license() {1065+        let dir = std::env::temp_dir().join(format!(1066+            "hu13-inbox-forms-deal-{}-{}",1067+            std::process::id(),1068+            std::time::SystemTime::now()1069+                .duration_since(std::time::UNIX_EPOCH)1070+                .map(|d| d.as_nanos())1071+                .unwrap_or(0)1072+        ));1073+        std::fs::create_dir_all(&dir).unwrap();1074+        render_forms(&dir, "test", "", None).unwrap();1075+        let issue = std::fs::read_to_string(dir.join("new-issue.html")).unwrap();1076+        let pr = std::fs::read_to_string(dir.join("new-pr.html")).unwrap();1077+        for html in [&issue, &pr] {1078+            assert!(html.contains("private inbox"), "private suggestion");1079+            assert!(html.contains("ack id"), "ack id explained");1080+            assert!(html.contains("No account needed"), "no account needed");1081+            assert!(html.contains("In plain words:"), "license summary");1082+            assert!(1083+                html.contains("href=\"#license-full\""),1084+                "summary links to full text"1085+            );1086+            assert!(html.contains("id=\"license-full\""), "full text anchor");1087+            assert!(html.contains("name=\"license\""), "checkbox kept");1088+            assert!(1089+                html.contains("irrevocably give it to this project"),1090+                "full grant text kept"1091+            );1092+        }1093+        std::fs::remove_dir_all(&dir).ok();1094+    }10531095 }
static/style.css+8 −1
diff --git a/static/style.css b/static/style.cssindex 32f05a8..11a2346 100644--- a/static/style.css+++ b/static/style.css@@ -59,6 +59,14 @@ button,input[type=submit]{display:inline-flex;align-items:center;justify-content5959   min-height:40px;min-width:40px;background:#7aa2f7;color:#16161e;border:1px solid #7aa2f7;6060   padding:.5rem 1rem;font:inherit;font-weight:700;cursor:pointer}6161 button:hover,input[type=submit]:hover{background:#7dcfff}62+/* Submit buttons (m6/submit-buttons, f0rge #25): link styled as a63+   button — Tokyo Night accent fill, >=40px touch target, icon + words. */64+.btn-new{display:inline-flex;align-items:center;justify-content:center;65+  min-height:40px;min-width:40px;background:#7aa2f7;color:#16161e;66+  border:1px solid #7aa2f7;padding:.5rem 1rem;font:inherit;font-weight:700;67+  text-decoration:none;cursor:pointer}68+.btn-new:visited{color:#16161e}69+.btn-new:hover,.btn-new:focus{background:#7dcfff;text-decoration:none}6270 .muted{color:#565f89}6371 .badge{display:inline-block;border:1px solid #292e42;padding:.1em .5em;font-size:.85em}6472 .badge.pass{color:#9ece6a}.badge.fail{color:#f7768e}65-- 2.55.0

Comments (0)

No comments yet.

Reviews (1)

all pulls