#19 m6/submit-buttons
Patch
src/forge.rs+62 −2--- 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--- 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--- 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--
Comments (0)
No comments yet.
Reviews (1)
-
unknown · · commented
Closing as superseded. Current master already carries the issues/pulls submit buttons and linked empty states (
src/forge.rspage-head+btn-new; regressionsubmit_buttons_on_issues_pulls_indexes). Later UI work replaced the olderbody.push_str("<h1>…")shape, so this patch no longer applies cleanly and would regress the page-head layout.