From 60c0125ae12be90d0bd279babfa4e40163553acd Mon Sep 17 00:00:00 2001 From: Bryce Kwon Date: Wed, 15 Jul 2026 09:20:08 -1000 Subject: Rebuild the page chrome as semantic HTML The masthead, tab bar, breadcrumb, content region and footer were HTML tables, which cannot reflow and forced the page to scroll sideways on small screens. Class and id names are kept so custom themes keep matching, the tabs gain title hints and the form controls accessible names. --- assets/cgit.css | 127 ++++++++++++++++++++++++------------ source/ui-shared.c | 150 ++++++++++++++++++++++++++----------------- tests/t0010-validate-html.sh | 11 ++++ tests/t0102-summary.sh | 4 +- 4 files changed, 189 insertions(+), 103 deletions(-) diff --git a/assets/cgit.css b/assets/cgit.css index d10a24d..e02d551 100644 --- a/assets/cgit.css +++ b/assets/cgit.css @@ -21,99 +21,142 @@ div#cgit table { border-collapse: collapse; } -div#cgit table#header { +div#cgit #header { + display: grid; + grid-template-columns: auto 1fr auto; + grid-template-areas: + "logo heading switch" + "logo desc owner"; + align-items: center; + column-gap: 10px; width: 100%; margin-bottom: 1em; } -div#cgit table#header td.logo { - width: 96px; - vertical-align: top; +div#cgit #header .logo { + grid-area: logo; + align-self: start; +} + +div#cgit #header .logo img { + display: block; + max-width: 96px; } -div#cgit table#header td.main { +div#cgit #header h1.main { + grid-area: heading; + margin: 0; font-size: 250%; - padding-left: 10px; + font-weight: normal; white-space: nowrap; } -div#cgit table#header td.main a { +div#cgit #header h1.main a { color: #000; } -div#cgit table#header td.form { - text-align: right; - vertical-align: bottom; - padding-right: 1em; - padding-bottom: 2px; - white-space: nowrap; -} - -div#cgit table#header td.form form, -div#cgit table#header td.form input, -div#cgit table#header td.form select { +div#cgit #header .branch-switch { + grid-area: switch; + justify-self: end; + align-self: end; font-size: 90%; } -div#cgit table#header td.sub { +div#cgit #header .sub { + grid-area: desc; + margin: 0; color: #777; border-top: solid 1px #ccc; - padding-left: 10px; } -div#cgit table.tabs { +div#cgit #header .owner { + grid-area: owner; + justify-self: end; + text-align: right; +} + +@media (max-width: 640px) { + div#cgit #header { + grid-template-columns: auto 1fr; + grid-template-areas: + "logo heading" + "desc desc" + "owner owner" + "switch switch"; + row-gap: 4px; + } + div#cgit #header .branch-switch, + div#cgit #header .owner { + justify-self: start; + text-align: left; + } + div#cgit #header h1.main { + white-space: normal; + } +} + +div#cgit .tabs { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + column-gap: 0.5em; border-bottom: solid 3px #ccc; - border-collapse: collapse; margin-top: 2em; - margin-bottom: 0px; - width: 100%; } -div#cgit table.tabs td { - padding: 0px 1em; - vertical-align: bottom; +div#cgit .tabs ul { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + flex: 1 1 auto; + list-style: none; + margin: 0; + padding: 0; } -div#cgit table.tabs td a { +div#cgit .tabs li { + margin: 0; + padding: 0 1em; +} + +div#cgit .tabs a { + display: inline-block; padding: 2px 0.75em; color: #777; font-size: 110%; } -div#cgit table.tabs td a.active { +div#cgit .tabs a.active { color: #000; background-color: #ccc; } -div#cgit table.tabs a[href^="http://"]:after, div#cgit table.tabs a[href^="https://"]:after { +div#cgit .tabs a.external:after { content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfgAhcJDQY+gm2TAAAAHWlUWHRDb21tZW50AAAAAABDcmVhdGVkIHdpdGggR0lNUGQuZQcAAABbSURBVAhbY2BABs4MU4CwhYHBh2Erww4wrGFQZHjI8B8IgUIscJWyDHcggltQhI4zGDCcRwhChPggHIggP1QoAVmQkSETrGoHsiAEsACtBYN0oDAMbgU6EBcAAL2eHUt4XUU4AAAAAElFTkSuQmCC); opacity: 0.5; margin: 0 0 0 5px; } -div#cgit table.tabs td.form { - text-align: right; -} - -div#cgit table.tabs td.form form { +div#cgit .tabs .search { padding-bottom: 2px; font-size: 90%; white-space: nowrap; } -div#cgit table.tabs td.form input, -div#cgit table.tabs td.form select { +div#cgit .tabs .search input, +div#cgit .tabs .search select { font-size: 90%; } -div#cgit div.path { +div#cgit .path { margin: 0px; padding: 5px 2em 2px 2em; color: #000; background-color: #eee; + overflow-wrap: anywhere; } -div#cgit div.content { +div#cgit .content { margin: 0px; padding: 2em; border-bottom: solid 3px #ccc; @@ -662,19 +705,19 @@ div#cgit span.deletions { color: #800; } -div#cgit div.footer { +div#cgit .footer { margin-top: 0.5em; text-align: center; font-size: 80%; color: #ccc; } -div#cgit div.footer a { +div#cgit .footer a { color: #ccc; text-decoration: none; } -div#cgit div.footer a:hover { +div#cgit .footer a:hover { text-decoration: underline; } diff --git a/source/ui-shared.c b/source/ui-shared.c index eb68ab7..69d2cdf 100644 --- a/source/ui-shared.c +++ b/source/ui-shared.c @@ -867,7 +867,7 @@ void cgit_print_docstart(void) void cgit_print_docend(void) { - html(" \n"); + html("\n"); if (ctx.cfg.embedded) { html(" \n"); if (ctx.cfg.footer) @@ -877,10 +877,10 @@ void cgit_print_docend(void) if (ctx.cfg.footer) html_include(ctx.cfg.footer); else { - htmlf("\n"); + html("\n"); } html(" \n"); html("\n\n"); @@ -1015,9 +1015,6 @@ static void print_header(void) { char *logo = NULL, *logo_link = NULL; - html("\n"); - html("\n"); - if (ctx.repo && ctx.repo->logo && *ctx.repo->logo) logo = ctx.repo->logo; else @@ -1026,44 +1023,47 @@ static void print_header(void) logo_link = ctx.repo->logo_link; else logo_link = ctx.cfg.logo_link; + + html("\n"); + html("' alt='cgit logo'/>\n"); } - html("\n"); + html("\n"); + + if (ctx.repo && ctx.env.authenticated) { + html("\n"); + cgit_add_hidden_formfields(0, 1, ctx.qry.page); + html(" "); + html(""); + html("\n"); + } - html("\n"); + html("\n"); } void cgit_print_pageheader(void) @@ -1084,39 +1086,63 @@ void cgit_print_pageheader(void) if (!ctx.env.authenticated || !ctx.cfg.noheader) print_header(); - html("
\n"); + if (ctx.env.authenticated) + html(""); - html("
\n"); cgit_add_hidden_formfields(1, 0, "log"); - html("\n"); html_option("grep", "log msg", ctx.qry.grep); html_option("author", "author", ctx.qry.grep); html_option("committer", "committer", ctx.qry.grep); html_option("range", "range", ctx.qry.grep); html("\n"); - html("\n"); html("\n"); html("
\n"); } else if (ctx.env.authenticated) { char *currenturl = cgit_currenturl(); - site_link(NULL, "index", NULL, hc("repolist"), NULL, NULL, 0, 1); - if (ctx.cfg.root_readme) - site_link("about", "about", NULL, hc("about"), + html("
    \n
  • "); + site_link(NULL, "index", "Repository index", hc("repolist"), NULL, NULL, 0, 1); + html("
  • \n"); + if (ctx.cfg.root_readme) { + html("
  • "); + site_link("about", "about", "About this site", hc("about"), NULL, NULL, 0, 1); - html("
"); - html("
\n"); - html("\n"); html("\n"); - html("
"); + html("\n"); free(currenturl); } - html("
\n"); + if (ctx.env.authenticated) + html("\n"); if (ctx.env.authenticated && ctx.repo && ctx.qry.vpath) { - html("
"); + html("
"); + html(""); } - html("
"); + html("
\n"); } void cgit_print_filemode(unsigned short mode) diff --git a/tests/t0010-validate-html.sh b/tests/t0010-validate-html.sh index ca08d69..91433f8 100755 --- a/tests/t0010-validate-html.sh +++ b/tests/t0010-validate-html.sh @@ -29,6 +29,17 @@ test -n "$tidy" || { exit } +# Releases of tidy that predate HTML5 reject the semantic elements (header, +# nav, main, footer) that cgit emits, reporting them as errors. Skip the +# validation unless tidy is new enough to understand them. +printf '\nt
x
\n' \ + | "$tidy" -q -e >/dev/null 2>&1 +test $? -ge 2 && { + skip_all='Skipping html validation tests: tidy is too old for HTML5' + test_done + exit +} + test_expect_success 'index page' 'test_url ""' test_expect_success 'foo' 'test_url "foo"' test_expect_success 'foo/log' 'test_url "foo/log"' diff --git a/tests/t0102-summary.sh b/tests/t0102-summary.sh index b8864cb..f1c1e78 100755 --- a/tests/t0102-summary.sh +++ b/tests/t0102-summary.sh @@ -7,7 +7,7 @@ test_expect_success 'generate foo summary' 'cgit_url "foo" >tmp' test_expect_success 'find commit 1' 'grep "commit 1" tmp' test_expect_success 'find commit 5' 'grep "commit 5" tmp' test_expect_success 'find branch master' 'grep "master" tmp' -test_expect_success 'no tags' '! grep "tags" tmp' +test_expect_success 'no tags' '! grep "Tag" tmp' test_expect_success 'clone-url expanded correctly' ' grep "git://example.org/foo.git" tmp ' @@ -17,7 +17,7 @@ test_expect_success 'no commit 45' '! grep "commit 45" tmp' test_expect_success 'find commit 46' 'grep "commit 46" tmp' test_expect_success 'find commit 50' 'grep "commit 50" tmp' test_expect_success 'find branch master' 'grep "master" tmp' -test_expect_success 'no tags' '! grep "tags" tmp' +test_expect_success 'no tags' '! grep "Tag" tmp' test_expect_success 'clone-url expanded correctly' ' grep "git://example.org/bar.git" tmp ' -- cgit v2.8.0