detached 39ca311

feat(web): expand directories in place in the Files view

commit
39ca311756f5cde2a82d8cd77938f885034a3d50
author
brice · · 2026-10-11 18:29 UTC
parent
bfab294
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
43 files · +534 -331
M cmd/ermite/testdata/web/blob-binary.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-code.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-empty.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-image.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-markdown.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-svg.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-text.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-too-large.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-trojan.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-big.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-binary.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-merge.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-modify-split.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-modify.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-rename.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-root.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-trojan.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commits-page2.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commits-path.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commits-root.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commits-small.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/empty.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="" data-sel="" data-nav="">22 <body data-ref="" data-sha="" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/home.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="" data-sel="" data-nav="">22 <body data-ref="" data-sha="" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/not-found-page.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="" data-sel="" data-nav="">22 <body data-ref="" data-sha="" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/not-found.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="" data-sel="" data-nav="">22 <body data-ref="" data-sha="" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/refs.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/summary.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/tree-nested.html +3 -7
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -106,7 +108,7 @@
106108 <th class="c-when">date</th>
107109 </tr>
108110 </thead>
109-<tbody id="tree-rows" data-rows>
111+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
110112 <tr data-cursor>
111113 <td class="cur" aria-hidden="true">
112114 </td>
@@ -341,12 +343,6 @@
341343 <span class="k">tag</span>
342344 </a>
343345 </li>
344-<li data-k="file src/lib/util.go">
345-<a href="/files/blob/main/src/lib/util.go" tabindex="-1">
346-<span class="lbl">src/lib/util.go</span>
347-<span class="k">file</span>
348-</a>
349-</li>
350346 </ul>
351347 <p class="panel-foot">
352348 <span>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
@@ -106,7 +108,7 @@
106 <th class="c-when">date</th>108 <th class="c-when">date</th>
107 </tr>109 </tr>
108 </thead>110 </thead>
109-<tbody id="tree-rows" data-rows>111+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
110 <tr data-cursor>112 <tr data-cursor>
111 <td class="cur" aria-hidden="true">113 <td class="cur" aria-hidden="true">
112 </td>114 </td>
@@ -341,12 +343,6 @@
341 <span class="k">tag</span>343 <span class="k">tag</span>
342 </a>344 </a>
343 </li>345 </li>
344-<li data-k="file src/lib/util.go">
345-<a href="/files/blob/main/src/lib/util.go" tabindex="-1">
346-<span class="lbl">src/lib/util.go</span>
347-<span class="k">file</span>
348-</a>
349-</li>
350 </ul>346 </ul>
351 <p class="panel-foot">347 <p class="panel-foot">
352 <span>348 <span>
M cmd/ermite/testdata/web/tree-root.html +7 -83
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -100,13 +102,13 @@
100102 <th class="c-when">date</th>
101103 </tr>
102104 </thead>
103-<tbody id="tree-rows" data-rows>
105+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
104106 <tr data-cursor>
105107 <td class="cur" aria-hidden="true">
106108 </td>
107-<td class="c-kind" aria-hidden="true">▾</td>
109+<td class="c-kind" aria-hidden="true">▸</td>
108110 <td class="c-name">
109-<a href="/files/tree/main/docs">docs/</a>
111+<a href="/files/tree/main/docs" data-expand="/files/tree/main/docs?rows" aria-expanded="false">docs/</a>
110112 </td>
111113 <td class="c-size num dim">-</td>
112114 <td class="c-sub dim">
@@ -120,9 +122,9 @@
120122 <tr>
121123 <td class="cur" aria-hidden="true">
122124 </td>
123-<td class="c-kind" aria-hidden="true">▾</td>
125+<td class="c-kind" aria-hidden="true">▸</td>
124126 <td class="c-name">
125-<a href="/files/tree/main/src">src/</a>
127+<a href="/files/tree/main/src" data-expand="/files/tree/main/src?rows" aria-expanded="false">src/</a>
126128 </td>
127129 <td class="c-size num dim">-</td>
128130 <td class="c-sub dim">
@@ -568,84 +570,6 @@
568570 <span class="k">tag</span>
569571 </a>
570572 </li>
571-<li data-k="file readme.md">
572-<a href="/files/blob/main/README.md" tabindex="-1">
573-<span class="lbl">README.md</span>
574-<span class="k">file</span>
575-</a>
576-</li>
577-<li data-k="file big.txt">
578-<a href="/files/blob/main/big.txt" tabindex="-1">
579-<span class="lbl">big.txt</span>
580-<span class="k">file</span>
581-</a>
582-</li>
583-<li data-k="file data.bin">
584-<a href="/files/blob/main/data.bin" tabindex="-1">
585-<span class="lbl">data.bin</span>
586-<span class="k">file</span>
587-</a>
588-</li>
589-<li data-k="file empty.txt">
590-<a href="/files/blob/main/empty.txt" tabindex="-1">
591-<span class="lbl">empty.txt</span>
592-<span class="k">file</span>
593-</a>
594-</li>
595-<li data-k="file evil.go">
596-<a href="/files/blob/main/evil.go" tabindex="-1">
597-<span class="lbl">evil.go</span>
598-<span class="k">file</span>
599-</a>
600-</li>
601-<li data-k="file fake.jpg">
602-<a href="/files/blob/main/fake.jpg" tabindex="-1">
603-<span class="lbl">fake.jpg</span>
604-<span class="k">file</span>
605-</a>
606-</li>
607-<li data-k="file fake.png">
608-<a href="/files/blob/main/fake.png" tabindex="-1">
609-<span class="lbl">fake.png</span>
610-<span class="k">file</span>
611-</a>
612-</li>
613-<li data-k="file lines.txt">
614-<a href="/files/blob/main/lines.txt" tabindex="-1">
615-<span class="lbl">lines.txt</span>
616-<span class="k">file</span>
617-</a>
618-</li>
619-<li data-k="file logo.svg">
620-<a href="/files/blob/main/logo.svg" tabindex="-1">
621-<span class="lbl">logo.svg</span>
622-<span class="k">file</span>
623-</a>
624-</li>
625-<li data-k="file main.go">
626-<a href="/files/blob/main/main.go" tabindex="-1">
627-<span class="lbl">main.go</span>
628-<span class="k">file</span>
629-</a>
630-</li>
631-<li data-k="file notes.txt">
632-<a href="/files/blob/main/notes.txt" tabindex="-1">
633-<span class="lbl">notes.txt</span>
634-<span class="k">file</span>
635-</a>
636-</li>
637-<li data-k="file page.html">
638-<a href="/files/blob/main/page.html" tabindex="-1">
639-<span class="lbl">page.html</span>
640-<span class="k">file</span>
641-</a>
642-</li>
643-<li data-k="file pic.png">
644-<a href="/files/blob/main/pic.png" tabindex="-1">
645-<span class="lbl">pic.png</span>
646-<span class="k">file</span>
647-</a>
648-</li>
649573 </ul>
650574 <p class="panel-foot">
651575 <span>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
@@ -100,13 +102,13 @@
100 <th class="c-when">date</th>102 <th class="c-when">date</th>
101 </tr>103 </tr>
102 </thead>104 </thead>
103-<tbody id="tree-rows" data-rows>105+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
104 <tr data-cursor>106 <tr data-cursor>
105 <td class="cur" aria-hidden="true">107 <td class="cur" aria-hidden="true">
106 </td>108 </td>
107-<td class="c-kind" aria-hidden="true">▾</td>109+<td class="c-kind" aria-hidden="true">▸</td>
108 <td class="c-name">110 <td class="c-name">
109-<a href="/files/tree/main/docs">docs/</a>111+<a href="/files/tree/main/docs" data-expand="/files/tree/main/docs?rows" aria-expanded="false">docs/</a>
110 </td>112 </td>
111 <td class="c-size num dim">-</td>113 <td class="c-size num dim">-</td>
112 <td class="c-sub dim">114 <td class="c-sub dim">
@@ -120,9 +122,9 @@
120 <tr>122 <tr>
121 <td class="cur" aria-hidden="true">123 <td class="cur" aria-hidden="true">
122 </td>124 </td>
123-<td class="c-kind" aria-hidden="true">▾</td>125+<td class="c-kind" aria-hidden="true">▸</td>
124 <td class="c-name">126 <td class="c-name">
125-<a href="/files/tree/main/src">src/</a>127+<a href="/files/tree/main/src" data-expand="/files/tree/main/src?rows" aria-expanded="false">src/</a>
126 </td>128 </td>
127 <td class="c-size num dim">-</td>129 <td class="c-size num dim">-</td>
128 <td class="c-sub dim">130 <td class="c-sub dim">
@@ -568,84 +570,6 @@
568 <span class="k">tag</span>570 <span class="k">tag</span>
569 </a>571 </a>
570 </li>572 </li>
571-<li data-k="file readme.md">
572-<a href="/files/blob/main/README.md" tabindex="-1">
573-<span class="lbl">README.md</span>
574-<span class="k">file</span>
575-</a>
576-</li>
577-<li data-k="file big.txt">
578-<a href="/files/blob/main/big.txt" tabindex="-1">
579-<span class="lbl">big.txt</span>
580-<span class="k">file</span>
581-</a>
582-</li>
583-<li data-k="file data.bin">
584-<a href="/files/blob/main/data.bin" tabindex="-1">
585-<span class="lbl">data.bin</span>
586-<span class="k">file</span>
587-</a>
588-</li>
589-<li data-k="file empty.txt">
590-<a href="/files/blob/main/empty.txt" tabindex="-1">
591-<span class="lbl">empty.txt</span>
592-<span class="k">file</span>
593-</a>
594-</li>
595-<li data-k="file evil.go">
596-<a href="/files/blob/main/evil.go" tabindex="-1">
597-<span class="lbl">evil.go</span>
598-<span class="k">file</span>
599-</a>
600-</li>
601-<li data-k="file fake.jpg">
602-<a href="/files/blob/main/fake.jpg" tabindex="-1">
603-<span class="lbl">fake.jpg</span>
604-<span class="k">file</span>
605-</a>
606-</li>
607-<li data-k="file fake.png">
608-<a href="/files/blob/main/fake.png" tabindex="-1">
609-<span class="lbl">fake.png</span>
610-<span class="k">file</span>
611-</a>
612-</li>
613-<li data-k="file lines.txt">
614-<a href="/files/blob/main/lines.txt" tabindex="-1">
615-<span class="lbl">lines.txt</span>
616-<span class="k">file</span>
617-</a>
618-</li>
619-<li data-k="file logo.svg">
620-<a href="/files/blob/main/logo.svg" tabindex="-1">
621-<span class="lbl">logo.svg</span>
622-<span class="k">file</span>
623-</a>
624-</li>
625-<li data-k="file main.go">
626-<a href="/files/blob/main/main.go" tabindex="-1">
627-<span class="lbl">main.go</span>
628-<span class="k">file</span>
629-</a>
630-</li>
631-<li data-k="file notes.txt">
632-<a href="/files/blob/main/notes.txt" tabindex="-1">
633-<span class="lbl">notes.txt</span>
634-<span class="k">file</span>
635-</a>
636-</li>
637-<li data-k="file page.html">
638-<a href="/files/blob/main/page.html" tabindex="-1">
639-<span class="lbl">page.html</span>
640-<span class="k">file</span>
641-</a>
642-</li>
643-<li data-k="file pic.png">
644-<a href="/files/blob/main/pic.png" tabindex="-1">
645-<span class="lbl">pic.png</span>
646-<span class="k">file</span>
647-</a>
648-</li>
649 </ul>573 </ul>
650 <p class="panel-foot">574 <p class="panel-foot">
651 <span>575 <span>
M cmd/ermite/testdata/web/tree-slash-ref.html +3 -7
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="feature/x" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -103,7 +105,7 @@
103105 <th class="c-when">date</th>
104106 </tr>
105107 </thead>
106-<tbody id="tree-rows" data-rows>
108+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
107109 <tr data-cursor>
108110 <td class="cur" aria-hidden="true">
109111 </td>
@@ -338,12 +340,6 @@
338340 <span class="k">tag</span>
339341 </a>
340342 </li>
341-<li data-k="file x/y.txt">
342-<a href="/files/blob/feature/x/x/y.txt" tabindex="-1">
343-<span class="lbl">x/y.txt</span>
344-<span class="k">file</span>
345-</a>
346-</li>
347343 </ul>
348344 <p class="panel-foot">
349345 <span>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="feature/x" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="feature/x" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
@@ -103,7 +105,7 @@
103 <th class="c-when">date</th>105 <th class="c-when">date</th>
104 </tr>106 </tr>
105 </thead>107 </thead>
106-<tbody id="tree-rows" data-rows>108+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
107 <tr data-cursor>109 <tr data-cursor>
108 <td class="cur" aria-hidden="true">110 <td class="cur" aria-hidden="true">
109 </td>111 </td>
@@ -338,12 +340,6 @@
338 <span class="k">tag</span>340 <span class="k">tag</span>
339 </a>341 </a>
340 </li>342 </li>
341-<li data-k="file x/y.txt">
342-<a href="/files/blob/feature/x/x/y.txt" tabindex="-1">
343-<span class="lbl">x/y.txt</span>
344-<span class="k">file</span>
345-</a>
346-</li>
347 </ul>343 </ul>
348 <p class="panel-foot">344 <p class="panel-foot">
349 <span>345 <span>
M cmd/ermite/testdata/web/tree-subdir.html +3 -13
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -103,7 +105,7 @@
103105 <th class="c-when">date</th>
104106 </tr>
105107 </thead>
106-<tbody id="tree-rows" data-rows>
108+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
107109 <tr data-cursor>
108110 <td class="cur" aria-hidden="true">
109111 </td>
@@ -363,18 +365,6 @@
363365 <span class="k">tag</span>
364366 </a>
365367 </li>
366-<li data-k="file docs/readme.md">
367-<a href="/files/blob/main/docs/README.md" tabindex="-1">
368-<span class="lbl">docs/README.md</span>
369-<span class="k">file</span>
370-</a>
371-</li>
372-<li data-k="file docs/guide.md">
373-<a href="/files/blob/main/docs/guide.md" tabindex="-1">
374-<span class="lbl">docs/guide.md</span>
375-<span class="k">file</span>
376-</a>
377-</li>
378368 </ul>
379369 <p class="panel-foot">
380370 <span>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
@@ -103,7 +105,7 @@
103 <th class="c-when">date</th>105 <th class="c-when">date</th>
104 </tr>106 </tr>
105 </thead>107 </thead>
106-<tbody id="tree-rows" data-rows>108+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
107 <tr data-cursor>109 <tr data-cursor>
108 <td class="cur" aria-hidden="true">110 <td class="cur" aria-hidden="true">
109 </td>111 </td>
@@ -363,18 +365,6 @@
363 <span class="k">tag</span>365 <span class="k">tag</span>
364 </a>366 </a>
365 </li>367 </li>
366-<li data-k="file docs/readme.md">
367-<a href="/files/blob/main/docs/README.md" tabindex="-1">
368-<span class="lbl">docs/README.md</span>
369-<span class="k">file</span>
370-</a>
371-</li>
372-<li data-k="file docs/guide.md">
373-<a href="/files/blob/main/docs/guide.md" tabindex="-1">
374-<span class="lbl">docs/guide.md</span>
375-<span class="k">file</span>
376-</a>
377-</li>
378 </ul>368 </ul>
379 <p class="panel-foot">369 <p class="panel-foot">
380 <span>370 <span>
M cmd/ermite/web_files_test.go +42 -0
@@ -100,6 +100,48 @@ func TestWebFilesGolden(t *testing.T) {
100100 }
101101 }
102102
103+// AC1, AC2, AC7: ?rows answers the entries of a directory as table rows only,
104+// for the Files view to insert under the directory; a private repository is
105+// the 404 of a missing one.
106+func TestWebTreeRows(t *testing.T) {
107+ h := newHarness(t, harnessOpts{})
108+ h.webFilesRepo("files")
109+ h.seed("secret-sauce", domain.VisibilityPrivate, "dir/a.txt")
110+
111+ // The page links its directories to their rows.
112+ _, _, page := h.get("/files/tree/main")
113+ assert.Contains(t, page, `href="/files/tree/main/docs" data-expand="/files/tree/main/docs?rows" aria-expanded="false"`)
114+ assert.Contains(t, page, `<script src="/-/static/js/files.`)
115+ assert.NotContains(t, page, "main.go</span>", "the palette lists no file")
116+
117+ status, hdr, body := h.get("/files/tree/main/docs?rows")
118+ require.Equal(t, 200, status)
119+ assert.Equal(t, "no-cache", hdr.Get("Cache-Control"))
120+ assert.Contains(t, hdr.Get("Content-Type"), "text/html")
121+ assert.NotContains(t, body, "<html")
122+ assert.NotContains(t, body, "<table")
123+ assert.Equal(t, 2, strings.Count(body, "<tr"), "one row per entry")
124+ assert.Contains(t, body, `<a href="/files/blob/main/docs/README.md">README.md</a>`)
125+ assert.Contains(t, body, `<a href="/files/blob/main/docs/guide.md">guide.md</a>`)
126+ assert.NotContains(t, body, "data-cursor", "the cursor stays on the page")
127+ assertNoInline(t, body)
128+
129+ // A subdirectory row can be opened in turn.
130+ _, _, body = h.get("/files/tree/main/src?rows")
131+ assert.Contains(t, body, `href="/files/tree/main/src/lib" data-expand="/files/tree/main/src/lib?rows" aria-expanded="false"`)
132+ assert.Contains(t, body, "▸")
133+ _, _, body = h.get("/files/tree/main/src/lib?rows")
134+ assert.Contains(t, body, `href="/files/blob/main/src/lib/util.go"`)
135+ assert.NotContains(t, body, "data-expand")
136+
137+ // Not found, and private as anonymous: the 404 page, nothing of the rows.
138+ for _, p := range []string{"/files/tree/main/nope?rows", "/secret-sauce/tree/main/dir?rows", "/secret-sauce/tree/main?rows"} {
139+ status, _, body = h.get(p)
140+ assert.Equal(t, 404, status, p)
141+ assert.NotContains(t, body, "a.txt", p)
142+ }
143+}
144+
103145 // Ref names with a slash are found by their longest prefix, and a commit id
104146 // works where a ref does.
105147 func TestWebFilesRefs(t *testing.T) {
@@ -100,6 +100,48 @@ func TestWebFilesGolden(t *testing.T) {
100 }100 }
101 }101 }
102 102
103+// AC1, AC2, AC7: ?rows answers the entries of a directory as table rows only,
104+// for the Files view to insert under the directory; a private repository is
105+// the 404 of a missing one.
106+func TestWebTreeRows(t *testing.T) {
107+ h := newHarness(t, harnessOpts{})
108+ h.webFilesRepo("files")
109+ h.seed("secret-sauce", domain.VisibilityPrivate, "dir/a.txt")
110+
111+ // The page links its directories to their rows.
112+ _, _, page := h.get("/files/tree/main")
113+ assert.Contains(t, page, `href="/files/tree/main/docs" data-expand="/files/tree/main/docs?rows" aria-expanded="false"`)
114+ assert.Contains(t, page, `<script src="/-/static/js/files.`)
115+ assert.NotContains(t, page, "main.go</span>", "the palette lists no file")
116+
117+ status, hdr, body := h.get("/files/tree/main/docs?rows")
118+ require.Equal(t, 200, status)
119+ assert.Equal(t, "no-cache", hdr.Get("Cache-Control"))
120+ assert.Contains(t, hdr.Get("Content-Type"), "text/html")
121+ assert.NotContains(t, body, "<html")
122+ assert.NotContains(t, body, "<table")
123+ assert.Equal(t, 2, strings.Count(body, "<tr"), "one row per entry")
124+ assert.Contains(t, body, `<a href="/files/blob/main/docs/README.md">README.md</a>`)
125+ assert.Contains(t, body, `<a href="/files/blob/main/docs/guide.md">guide.md</a>`)
126+ assert.NotContains(t, body, "data-cursor", "the cursor stays on the page")
127+ assertNoInline(t, body)
128+
129+ // A subdirectory row can be opened in turn.
130+ _, _, body = h.get("/files/tree/main/src?rows")
131+ assert.Contains(t, body, `href="/files/tree/main/src/lib" data-expand="/files/tree/main/src/lib?rows" aria-expanded="false"`)
132+ assert.Contains(t, body, "▸")
133+ _, _, body = h.get("/files/tree/main/src/lib?rows")
134+ assert.Contains(t, body, `href="/files/blob/main/src/lib/util.go"`)
135+ assert.NotContains(t, body, "data-expand")
136+
137+ // Not found, and private as anonymous: the 404 page, nothing of the rows.
138+ for _, p := range []string{"/files/tree/main/nope?rows", "/secret-sauce/tree/main/dir?rows", "/secret-sauce/tree/main?rows"} {
139+ status, _, body = h.get(p)
140+ assert.Equal(t, 404, status, p)
141+ assert.NotContains(t, body, "a.txt", p)
142+ }
143+}
144+
103 // Ref names with a slash are found by their longest prefix, and a commit id145 // Ref names with a slash are found by their longest prefix, and a commit id
104 // works where a ref does.146 // works where a ref does.
105 func TestWebFilesRefs(t *testing.T) {147 func TestWebFilesRefs(t *testing.T) {
M internal/adapters/web/ASSETS.md +1 -0
@@ -9,6 +9,7 @@ content-hashed names. Nothing is fetched at runtime.
99 | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` |
1010 | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` |
1111 | `static/js/ermite.js` | key sequences to `k-*` events | `mockups/d/js/ermite.js` | |
12+| `static/js/files.js` | the Files view: directories open in place (`window.ermiteTree`) | written here | |
1213 | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | |
1314 | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | |
1415 | `static/fonts/atkinson-next.woff2`, `atkinson-next-italic.woff2` | Atkinson Hyperlegible Next, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-next.txt`) | Google Fonts | `18b2a1a39a2fa298b0ba5390aca68462669826c90925656f1c1f6796e0e1bbaf`, `4a5037bfaf6680f40147407407ec09fa42925774bde809a579283d27f9f08106` |
@@ -9,6 +9,7 @@ content-hashed names. Nothing is fetched at runtime.
9 | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` |9 | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` |
10 | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` |10 | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` |
11 | `static/js/ermite.js` | key sequences to `k-*` events | `mockups/d/js/ermite.js` | |11 | `static/js/ermite.js` | key sequences to `k-*` events | `mockups/d/js/ermite.js` | |
12+| `static/js/files.js` | the Files view: directories open in place (`window.ermiteTree`) | written here | |
12 | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | |13 | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | |
13 | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | |14 | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | |
14 | `static/fonts/atkinson-next.woff2`, `atkinson-next-italic.woff2` | Atkinson Hyperlegible Next, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-next.txt`) | Google Fonts | `18b2a1a39a2fa298b0ba5390aca68462669826c90925656f1c1f6796e0e1bbaf`, `4a5037bfaf6680f40147407407ec09fa42925774bde809a579283d27f9f08106` |15 | `static/fonts/atkinson-next.woff2`, `atkinson-next-italic.woff2` | Atkinson Hyperlegible Next, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-next.txt`) | Google Fonts | `18b2a1a39a2fa298b0ba5390aca68462669826c90925656f1c1f6796e0e1bbaf`, `4a5037bfaf6680f40147407407ec09fa42925774bde809a579283d27f9f08106` |
M internal/adapters/web/assets.go +2 -1
@@ -23,7 +23,7 @@ const StaticPrefix = "/-/static/"
2323 // Assets are the URLs of the assets a page loads. Their names carry a hash of
2424 // the content, so they can be cached for ever.
2525 type Assets struct {
26- CSS, HTMX, Live, SSE, App, Icon string
26+ CSS, HTMX, Live, SSE, App, Files, Icon string
2727 // Font is the UI font, preloaded by every page.
2828 Font string
2929 // Rules is the speculation rules file named by the Speculation-Rules header.
@@ -52,6 +52,7 @@ var scripts = []struct {
5252 {"hx-live.min.js", func(a *Assets, u string) { a.Live = u }},
5353 {"hx-sse.min.js", func(a *Assets, u string) { a.SSE = u }},
5454 {"ermite.js", func(a *Assets, u string) { a.App = u }},
55+ {"files.js", func(a *Assets, u string) { a.Files = u }},
5556 }
5657
5758 // sheetAssets are the files the stylesheet references as url(<dir>/<file>):
@@ -23,7 +23,7 @@ const StaticPrefix = "/-/static/"
23 // Assets are the URLs of the assets a page loads. Their names carry a hash of23 // Assets are the URLs of the assets a page loads. Their names carry a hash of
24 // the content, so they can be cached for ever.24 // the content, so they can be cached for ever.
25 type Assets struct {25 type Assets struct {
26- CSS, HTMX, Live, SSE, App, Icon string26+ CSS, HTMX, Live, SSE, App, Files, Icon string
27 // Font is the UI font, preloaded by every page.27 // Font is the UI font, preloaded by every page.
28 Font string28 Font string
29 // Rules is the speculation rules file named by the Speculation-Rules header.29 // Rules is the speculation rules file named by the Speculation-Rules header.
@@ -52,6 +52,7 @@ var scripts = []struct {
52 {"hx-live.min.js", func(a *Assets, u string) { a.Live = u }},52 {"hx-live.min.js", func(a *Assets, u string) { a.Live = u }},
53 {"hx-sse.min.js", func(a *Assets, u string) { a.SSE = u }},53 {"hx-sse.min.js", func(a *Assets, u string) { a.SSE = u }},
54 {"ermite.js", func(a *Assets, u string) { a.App = u }},54 {"ermite.js", func(a *Assets, u string) { a.App = u }},
55+ {"files.js", func(a *Assets, u string) { a.Files = u }},
55 }56 }
56 57
57 // sheetAssets are the files the stylesheet references as url(<dir>/<file>):58 // sheetAssets are the files the stylesheet references as url(<dir>/<file>):
M internal/adapters/web/overlays.templ +1 -1
@@ -84,7 +84,7 @@ templ refItem(r RefItem) {
8484 }
8585
8686 // OverlayCmd is the ":" command line: one filterable list mixing sections,
87-// refs, files of this directory and repos.
87+// refs and repos.
8888 templ OverlayCmd(items []CmdItem) {
8989 <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
9090 <div class="panel-head">
@@ -84,7 +84,7 @@ templ refItem(r RefItem) {
84 }84 }
85 85
86 // OverlayCmd is the ":" command line: one filterable list mixing sections,86 // OverlayCmd is the ":" command line: one filterable list mixing sections,
87-// refs, files of this directory and repos.87+// refs and repos.
88 templ OverlayCmd(items []CmdItem) {88 templ OverlayCmd(items []CmdItem) {
89 <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">89 <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
90 <div class="panel-head">90 <div class="panel-head">
M internal/adapters/web/overlays_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/shell.templ +1 -0
@@ -20,6 +20,7 @@ templ Shell(l Layout, rail templ.Component) {
2020 <script src={ l.Assets.Live } defer></script>
2121 <script src={ l.Assets.SSE } defer></script>
2222 <script src={ l.Assets.App } defer></script>
23+ <script src={ l.Assets.Files } defer></script>
2324 </head>
2425 <body data-ref={ l.RefPath } data-sha={ l.SHA } data-sel="" data-nav="">
2526 <a class="skip" href="#main">skip to content</a>
@@ -20,6 +20,7 @@ templ Shell(l Layout, rail templ.Component) {
20 <script src={ l.Assets.Live } defer></script>20 <script src={ l.Assets.Live } defer></script>
21 <script src={ l.Assets.SSE } defer></script>21 <script src={ l.Assets.SSE } defer></script>
22 <script src={ l.Assets.App } defer></script>22 <script src={ l.Assets.App } defer></script>
23+ <script src={ l.Assets.Files } defer></script>
23 </head>24 </head>
24 <body data-ref={ l.RefPath } data-sha={ l.SHA } data-sel="" data-nav="">25 <body data-ref={ l.RefPath } data-sha={ l.SHA } data-sel="" data-nav="">
25 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/adapters/web/shell_templ.go +138 -125
@@ -138,33 +138,46 @@ func Shell(l Layout, rail templ.Component) templ.Component {
138138 if templ_7745c5c3_Err != nil {
139139 return templ_7745c5c3_Err
140140 }
141- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" defer></script></head><body data-ref=\"")
141+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" defer></script><script src=\"")
142142 if templ_7745c5c3_Err != nil {
143143 return templ_7745c5c3_Err
144144 }
145145 var templ_7745c5c3_Var10 string
146- templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath)
146+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Files)
147147 if templ_7745c5c3_Err != nil {
148- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 24, Col: 28}
148+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 23, Col: 31}
149149 }
150150 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
151151 if templ_7745c5c3_Err != nil {
152152 return templ_7745c5c3_Err
153153 }
154- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" data-sha=\"")
154+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" defer></script></head><body data-ref=\"")
155155 if templ_7745c5c3_Err != nil {
156156 return templ_7745c5c3_Err
157157 }
158158 var templ_7745c5c3_Var11 string
159- templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA)
159+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath)
160160 if templ_7745c5c3_Err != nil {
161- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 24, Col: 47}
161+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 25, Col: 28}
162162 }
163163 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
164164 if templ_7745c5c3_Err != nil {
165165 return templ_7745c5c3_Err
166166 }
167- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" data-sel=\"\" data-nav=\"\"><a class=\"skip\" href=\"#main\">skip to content</a>")
167+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" data-sha=\"")
168+ if templ_7745c5c3_Err != nil {
169+ return templ_7745c5c3_Err
170+ }
171+ var templ_7745c5c3_Var12 string
172+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA)
173+ if templ_7745c5c3_Err != nil {
174+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 25, Col: 47}
175+ }
176+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
177+ if templ_7745c5c3_Err != nil {
178+ return templ_7745c5c3_Err
179+ }
180+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" data-sel=\"\" data-nav=\"\"><a class=\"skip\" href=\"#main\">skip to content</a>")
168181 if templ_7745c5c3_Err != nil {
169182 return templ_7745c5c3_Err
170183 }
@@ -172,48 +185,48 @@ func Shell(l Layout, rail templ.Component) templ.Component {
172185 if templ_7745c5c3_Err != nil {
173186 return templ_7745c5c3_Err
174187 }
175- var templ_7745c5c3_Var12 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)}
176- templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
188+ var templ_7745c5c3_Var13 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)}
189+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var13...)
177190 if templ_7745c5c3_Err != nil {
178191 return templ_7745c5c3_Err
179192 }
180- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"")
193+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"")
181194 if templ_7745c5c3_Err != nil {
182195 return templ_7745c5c3_Err
183196 }
184- var templ_7745c5c3_Var13 string
185- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var12).String())
197+ var templ_7745c5c3_Var14 string
198+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var13).String())
186199 if templ_7745c5c3_Err != nil {
187200 return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 1, Col: 0}
188201 }
189- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
202+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
190203 if templ_7745c5c3_Err != nil {
191204 return templ_7745c5c3_Err
192205 }
193- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">")
206+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
194207 if templ_7745c5c3_Err != nil {
195208 return templ_7745c5c3_Err
196209 }
197210 if l.Lede != "" {
198- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">")
211+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">")
199212 if templ_7745c5c3_Err != nil {
200213 return templ_7745c5c3_Err
201214 }
202- var templ_7745c5c3_Var14 string
203- templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede)
215+ var templ_7745c5c3_Var15 string
216+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede)
204217 if templ_7745c5c3_Err != nil {
205- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 29, Col: 81}
218+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 30, Col: 81}
206219 }
207- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
220+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
208221 if templ_7745c5c3_Err != nil {
209222 return templ_7745c5c3_Err
210223 }
211- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</p></section>")
224+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</p></section>")
212225 if templ_7745c5c3_Err != nil {
213226 return templ_7745c5c3_Err
214227 }
215228 }
216- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<main id=\"main\" tabindex=\"-1\">")
229+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<main id=\"main\" tabindex=\"-1\">")
217230 if templ_7745c5c3_Err != nil {
218231 return templ_7745c5c3_Err
219232 }
@@ -221,12 +234,12 @@ func Shell(l Layout, rail templ.Component) templ.Component {
221234 if templ_7745c5c3_Err != nil {
222235 return templ_7745c5c3_Err
223236 }
224- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</main>")
237+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</main>")
225238 if templ_7745c5c3_Err != nil {
226239 return templ_7745c5c3_Err
227240 }
228241 if l.HasRail && rail != nil {
229- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<aside class=\"rail\" aria-label=\"Context\">")
242+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<aside class=\"rail\" aria-label=\"Context\">")
230243 if templ_7745c5c3_Err != nil {
231244 return templ_7745c5c3_Err
232245 }
@@ -234,12 +247,12 @@ func Shell(l Layout, rail templ.Component) templ.Component {
234247 if templ_7745c5c3_Err != nil {
235248 return templ_7745c5c3_Err
236249 }
237- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</aside>")
250+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</aside>")
238251 if templ_7745c5c3_Err != nil {
239252 return templ_7745c5c3_Err
240253 }
241254 }
242- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div>")
255+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div>")
243256 if templ_7745c5c3_Err != nil {
244257 return templ_7745c5c3_Err
245258 }
@@ -267,7 +280,7 @@ func Shell(l Layout, rail templ.Component) templ.Component {
267280 if templ_7745c5c3_Err != nil {
268281 return templ_7745c5c3_Err
269282 }
270- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</body></html>")
283+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</body></html>")
271284 if templ_7745c5c3_Err != nil {
272285 return templ_7745c5c3_Err
273286 }
@@ -293,260 +306,260 @@ func Header(l Layout) templ.Component {
293306 }()
294307 }
295308 ctx = templ.InitializeContext(ctx)
296- templ_7745c5c3_Var15 := templ.GetChildren(ctx)
297- if templ_7745c5c3_Var15 == nil {
298- templ_7745c5c3_Var15 = templ.NopComponent
309+ templ_7745c5c3_Var16 := templ.GetChildren(ctx)
310+ if templ_7745c5c3_Var16 == nil {
311+ templ_7745c5c3_Var16 = templ.NopComponent
299312 }
300313 ctx = templ.ClearChildren(ctx)
301- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span> <span class=\"brand-name\">")
314+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span> <span class=\"brand-name\">")
302315 if templ_7745c5c3_Err != nil {
303316 return templ_7745c5c3_Err
304317 }
305- var templ_7745c5c3_Var16 string
306- templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(l.Instance)
318+ var templ_7745c5c3_Var17 string
319+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(l.Instance)
307320 if templ_7745c5c3_Err != nil {
308- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 57, Col: 41}
321+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 58, Col: 41}
309322 }
310- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
323+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
311324 if templ_7745c5c3_Err != nil {
312325 return templ_7745c5c3_Err
313326 }
314- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</span></a> ")
327+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</span></a> ")
315328 if templ_7745c5c3_Err != nil {
316329 return templ_7745c5c3_Err
317330 }
318331 if len(l.Crumbs) > 0 {
319- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")
332+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")
320333 if templ_7745c5c3_Err != nil {
321334 return templ_7745c5c3_Err
322335 }
323336 for _, c := range l.Crumbs {
324- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<li>")
337+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<li>")
325338 if templ_7745c5c3_Err != nil {
326339 return templ_7745c5c3_Err
327340 }
328341 if c.Href != "" {
329- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<a href=\"")
342+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<a href=\"")
330343 if templ_7745c5c3_Err != nil {
331344 return templ_7745c5c3_Err
332345 }
333- var templ_7745c5c3_Var17 templ.SafeURL
334- templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
346+ var templ_7745c5c3_Var18 templ.SafeURL
347+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
335348 if templ_7745c5c3_Err != nil {
336- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 66, Col: 38}
349+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 67, Col: 38}
337350 }
338- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
351+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
339352 if templ_7745c5c3_Err != nil {
340353 return templ_7745c5c3_Err
341354 }
342- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\"")
355+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\"")
343356 if templ_7745c5c3_Err != nil {
344357 return templ_7745c5c3_Err
345358 }
346359 if c.Current {
347- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " aria-current=\"page\"")
360+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " aria-current=\"page\"")
348361 if templ_7745c5c3_Err != nil {
349362 return templ_7745c5c3_Err
350363 }
351364 }
352- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, ">")
365+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, ">")
353366 if templ_7745c5c3_Err != nil {
354367 return templ_7745c5c3_Err
355368 }
356- var templ_7745c5c3_Var18 string
357- templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
369+ var templ_7745c5c3_Var19 string
370+ templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
358371 if templ_7745c5c3_Err != nil {
359- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 70, Col: 19}
372+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 71, Col: 19}
360373 }
361- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
374+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
362375 if templ_7745c5c3_Err != nil {
363376 return templ_7745c5c3_Err
364377 }
365- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</a>")
378+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</a>")
366379 if templ_7745c5c3_Err != nil {
367380 return templ_7745c5c3_Err
368381 }
369382 } else {
370- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<span")
383+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<span")
371384 if templ_7745c5c3_Err != nil {
372385 return templ_7745c5c3_Err
373386 }
374387 if c.Current {
375- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " aria-current=\"page\"")
388+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " aria-current=\"page\"")
376389 if templ_7745c5c3_Err != nil {
377390 return templ_7745c5c3_Err
378391 }
379392 }
380- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, ">")
393+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, ">")
381394 if templ_7745c5c3_Err != nil {
382395 return templ_7745c5c3_Err
383396 }
384- var templ_7745c5c3_Var19 string
385- templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
397+ var templ_7745c5c3_Var20 string
398+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
386399 if templ_7745c5c3_Err != nil {
387- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 76, Col: 19}
400+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 77, Col: 19}
388401 }
389- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
402+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
390403 if templ_7745c5c3_Err != nil {
391404 return templ_7745c5c3_Err
392405 }
393- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</span>")
406+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</span>")
394407 if templ_7745c5c3_Err != nil {
395408 return templ_7745c5c3_Err
396409 }
397410 }
398- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</li>")
411+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</li>")
399412 if templ_7745c5c3_Err != nil {
400413 return templ_7745c5c3_Err
401414 }
402415 }
403- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</ol></nav>")
416+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</ol></nav>")
404417 if templ_7745c5c3_Err != nil {
405418 return templ_7745c5c3_Err
406419 }
407420 }
408- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<div class=\"hdr-acts\">")
421+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<div class=\"hdr-acts\">")
409422 if templ_7745c5c3_Err != nil {
410423 return templ_7745c5c3_Err
411424 }
412425 if l.Ref != "" {
413- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<button type=\"button\" class=\"btn ref\" aria-haspopup=\"dialog\" aria-controls=\"panel-refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-refs'))\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")
426+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<button type=\"button\" class=\"btn ref\" aria-haspopup=\"dialog\" aria-controls=\"panel-refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-refs'))\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")
414427 if templ_7745c5c3_Err != nil {
415428 return templ_7745c5c3_Err
416429 }
417- var templ_7745c5c3_Var20 string
418- templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)
430+ var templ_7745c5c3_Var21 string
431+ templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)
419432 if templ_7745c5c3_Err != nil {
420- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 95, Col: 36}
433+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 96, Col: 36}
421434 }
422- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
435+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
423436 if templ_7745c5c3_Err != nil {
424437 return templ_7745c5c3_Err
425438 }
426- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</span> <span class=\"sha\">")
439+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</span> <span class=\"sha\">")
427440 if templ_7745c5c3_Err != nil {
428441 return templ_7745c5c3_Err
429442 }
430- var templ_7745c5c3_Var21 string
431- templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)
443+ var templ_7745c5c3_Var22 string
444+ templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)
432445 if templ_7745c5c3_Err != nil {
433- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 96, Col: 33}
446+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 97, Col: 33}
434447 }
435- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
448+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
436449 if templ_7745c5c3_Err != nil {
437450 return templ_7745c5c3_Err
438451 }
439- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</span></button> ")
452+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</span></button> ")
440453 if templ_7745c5c3_Err != nil {
441454 return templ_7745c5c3_Err
442455 }
443456 }
444- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<button type=\"button\" class=\"btn search\" aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-cmd'))\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> <button type=\"button\" class=\"btn icon\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\" aria-label=\"Keyboard shortcuts\" title=\"keyboard shortcuts (?)\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-help'))\">?</button> ")
457+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<button type=\"button\" class=\"btn search\" aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-cmd'))\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> <button type=\"button\" class=\"btn icon\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\" aria-label=\"Keyboard shortcuts\" title=\"keyboard shortcuts (?)\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-help'))\">?</button> ")
445458 if templ_7745c5c3_Err != nil {
446459 return templ_7745c5c3_Err
447460 }
448461 if SignedIn(ctx) {
449- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\">owner</span> <button type=\"submit\" class=\"btn\" title=\"sign out of this browser\">Sign out</button></form>")
462+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\">owner</span> <button type=\"submit\" class=\"btn\" title=\"sign out of this browser\">Sign out</button></form>")
450463 if templ_7745c5c3_Err != nil {
451464 return templ_7745c5c3_Err
452465 }
453466 }
454- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</div></div>")
467+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</div></div>")
455468 if templ_7745c5c3_Err != nil {
456469 return templ_7745c5c3_Err
457470 }
458471 if len(l.Sections) > 0 {
459- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")
472+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")
460473 if templ_7745c5c3_Err != nil {
461474 return templ_7745c5c3_Err
462475 }
463476 for _, s := range l.Sections {
464- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<li><a href=\"")
477+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<li><a href=\"")
465478 if templ_7745c5c3_Err != nil {
466479 return templ_7745c5c3_Err
467480 }
468- var templ_7745c5c3_Var22 templ.SafeURL
469- templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
481+ var templ_7745c5c3_Var23 templ.SafeURL
482+ templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
470483 if templ_7745c5c3_Err != nil {
471- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 127, Col: 36}
484+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 128, Col: 36}
472485 }
473- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
486+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
474487 if templ_7745c5c3_Err != nil {
475488 return templ_7745c5c3_Err
476489 }
477- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" data-key=\"")
490+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" data-key=\"")
478491 if templ_7745c5c3_Err != nil {
479492 return templ_7745c5c3_Err
480493 }
481- var templ_7745c5c3_Var23 string
482- templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
494+ var templ_7745c5c3_Var24 string
495+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
483496 if templ_7745c5c3_Err != nil {
484- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 128, Col: 24}
497+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 129, Col: 24}
485498 }
486- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
499+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
487500 if templ_7745c5c3_Err != nil {
488501 return templ_7745c5c3_Err
489502 }
490- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" aria-keyshortcuts=\"")
503+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\" aria-keyshortcuts=\"")
491504 if templ_7745c5c3_Err != nil {
492505 return templ_7745c5c3_Err
493506 }
494- var templ_7745c5c3_Var24 string
495- templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
507+ var templ_7745c5c3_Var25 string
508+ templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
496509 if templ_7745c5c3_Err != nil {
497- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 129, Col: 33}
510+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 130, Col: 33}
498511 }
499- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
512+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
500513 if templ_7745c5c3_Err != nil {
501514 return templ_7745c5c3_Err
502515 }
503- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\"")
516+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\"")
504517 if templ_7745c5c3_Err != nil {
505518 return templ_7745c5c3_Err
506519 }
507520 if s.Current {
508- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, " aria-current=\"page\"")
521+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, " aria-current=\"page\"")
509522 if templ_7745c5c3_Err != nil {
510523 return templ_7745c5c3_Err
511524 }
512525 }
513- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, ">")
526+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, ">")
514527 if templ_7745c5c3_Err != nil {
515528 return templ_7745c5c3_Err
516529 }
517- var templ_7745c5c3_Var25 string
518- templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
530+ var templ_7745c5c3_Var26 string
531+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
519532 if templ_7745c5c3_Err != nil {
520- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 133, Col: 17}
533+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 134, Col: 17}
521534 }
522- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
535+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
523536 if templ_7745c5c3_Err != nil {
524537 return templ_7745c5c3_Err
525538 }
526- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<kbd aria-hidden=\"true\">")
539+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<kbd aria-hidden=\"true\">")
527540 if templ_7745c5c3_Err != nil {
528541 return templ_7745c5c3_Err
529542 }
530- var templ_7745c5c3_Var26 string
531- templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)
543+ var templ_7745c5c3_Var27 string
544+ templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)
532545 if templ_7745c5c3_Err != nil {
533- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 133, Col: 50}
546+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 134, Col: 50}
534547 }
535- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
548+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
536549 if templ_7745c5c3_Err != nil {
537550 return templ_7745c5c3_Err
538551 }
539- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</kbd></a></li>")
552+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</kbd></a></li>")
540553 if templ_7745c5c3_Err != nil {
541554 return templ_7745c5c3_Err
542555 }
543556 }
544- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</ul></nav>")
557+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</ul></nav>")
545558 if templ_7745c5c3_Err != nil {
546559 return templ_7745c5c3_Err
547560 }
548561 }
549- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</header>")
562+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</header>")
550563 if templ_7745c5c3_Err != nil {
551564 return templ_7745c5c3_Err
552565 }
@@ -572,38 +585,38 @@ func Footer(l Layout) templ.Component {
572585 }()
573586 }
574587 ctx = templ.InitializeContext(ctx)
575- templ_7745c5c3_Var27 := templ.GetChildren(ctx)
576- if templ_7745c5c3_Var27 == nil {
577- templ_7745c5c3_Var27 = templ.NopComponent
588+ templ_7745c5c3_Var28 := templ.GetChildren(ctx)
589+ if templ_7745c5c3_Var28 == nil {
590+ templ_7745c5c3_Var28 = templ.NopComponent
578591 }
579592 ctx = templ.ClearChildren(ctx)
580- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<footer class=\"foot\"><span>ermite ")
593+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<footer class=\"foot\"><span>ermite ")
581594 if templ_7745c5c3_Err != nil {
582595 return templ_7745c5c3_Err
583596 }
584- var templ_7745c5c3_Var28 string
585- templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
597+ var templ_7745c5c3_Var29 string
598+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
586599 if templ_7745c5c3_Err != nil {
587- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 146, Col: 26}
600+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 147, Col: 26}
588601 }
589- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
602+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
590603 if templ_7745c5c3_Err != nil {
591604 return templ_7745c5c3_Err
592605 }
593- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</span> <a href=\"")
606+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</span> <a href=\"")
594607 if templ_7745c5c3_Err != nil {
595608 return templ_7745c5c3_Err
596609 }
597- var templ_7745c5c3_Var29 templ.SafeURL
598- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
610+ var templ_7745c5c3_Var30 templ.SafeURL
611+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
599612 if templ_7745c5c3_Err != nil {
600- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 147, Col: 38}
613+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 148, Col: 38}
601614 }
602- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
615+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
603616 if templ_7745c5c3_Err != nil {
604617 return templ_7745c5c3_Err
605618 }
606- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <span class=\"foot-keys\"><kbd>?</kbd> keyboard shortcuts</span></footer>")
619+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <span class=\"foot-keys\"><kbd>?</kbd> keyboard shortcuts</span></footer>")
607620 if templ_7745c5c3_Err != nil {
608621 return templ_7745c5c3_Err
609622 }
@@ -631,12 +644,12 @@ func Hint(l Layout) templ.Component {
631644 }()
632645 }
633646 ctx = templ.InitializeContext(ctx)
634- templ_7745c5c3_Var30 := templ.GetChildren(ctx)
635- if templ_7745c5c3_Var30 == nil {
636- templ_7745c5c3_Var30 = templ.NopComponent
647+ templ_7745c5c3_Var31 := templ.GetChildren(ctx)
648+ if templ_7745c5c3_Var31 == nil {
649+ templ_7745c5c3_Var31 = templ.NopComponent
637650 }
638651 ctx = templ.ClearChildren(ctx)
639- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")
652+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")
640653 if templ_7745c5c3_Err != nil {
641654 return templ_7745c5c3_Err
642655 }
@@ -644,7 +657,7 @@ func Hint(l Layout) templ.Component {
644657 if templ_7745c5c3_Err != nil {
645658 return templ_7745c5c3_Err
646659 }
647- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</div>")
660+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</div>")
648661 if templ_7745c5c3_Err != nil {
649662 return templ_7745c5c3_Err
650663 }
@@ -138,33 +138,46 @@ func Shell(l Layout, rail templ.Component) templ.Component {
138 if templ_7745c5c3_Err != nil {138 if templ_7745c5c3_Err != nil {
139 return templ_7745c5c3_Err139 return templ_7745c5c3_Err
140 }140 }
141- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" defer></script></head><body data-ref=\"")141+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" defer></script><script src=\"")
142 if templ_7745c5c3_Err != nil {142 if templ_7745c5c3_Err != nil {
143 return templ_7745c5c3_Err143 return templ_7745c5c3_Err
144 }144 }
145 var templ_7745c5c3_Var10 string145 var templ_7745c5c3_Var10 string
146- templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath)146+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Files)
147 if templ_7745c5c3_Err != nil {147 if templ_7745c5c3_Err != nil {
148- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 24, Col: 28}148+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 23, Col: 31}
149 }149 }
150 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)150 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
151 if templ_7745c5c3_Err != nil {151 if templ_7745c5c3_Err != nil {
152 return templ_7745c5c3_Err152 return templ_7745c5c3_Err
153 }153 }
154- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" data-sha=\"")154+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" defer></script></head><body data-ref=\"")
155 if templ_7745c5c3_Err != nil {155 if templ_7745c5c3_Err != nil {
156 return templ_7745c5c3_Err156 return templ_7745c5c3_Err
157 }157 }
158 var templ_7745c5c3_Var11 string158 var templ_7745c5c3_Var11 string
159- templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA)159+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath)
160 if templ_7745c5c3_Err != nil {160 if templ_7745c5c3_Err != nil {
161- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 24, Col: 47}161+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 25, Col: 28}
162 }162 }
163 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)163 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
164 if templ_7745c5c3_Err != nil {164 if templ_7745c5c3_Err != nil {
165 return templ_7745c5c3_Err165 return templ_7745c5c3_Err
166 }166 }
167- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" data-sel=\"\" data-nav=\"\"><a class=\"skip\" href=\"#main\">skip to content</a>")167+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" data-sha=\"")
168+ if templ_7745c5c3_Err != nil {
169+ return templ_7745c5c3_Err
170+ }
171+ var templ_7745c5c3_Var12 string
172+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA)
173+ if templ_7745c5c3_Err != nil {
174+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 25, Col: 47}
175+ }
176+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
177+ if templ_7745c5c3_Err != nil {
178+ return templ_7745c5c3_Err
179+ }
180+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" data-sel=\"\" data-nav=\"\"><a class=\"skip\" href=\"#main\">skip to content</a>")
168 if templ_7745c5c3_Err != nil {181 if templ_7745c5c3_Err != nil {
169 return templ_7745c5c3_Err182 return templ_7745c5c3_Err
170 }183 }
@@ -172,48 +185,48 @@ func Shell(l Layout, rail templ.Component) templ.Component {
172 if templ_7745c5c3_Err != nil {185 if templ_7745c5c3_Err != nil {
173 return templ_7745c5c3_Err186 return templ_7745c5c3_Err
174 }187 }
175- var templ_7745c5c3_Var12 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)}188+ var templ_7745c5c3_Var13 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)}
176- templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)189+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var13...)
177 if templ_7745c5c3_Err != nil {190 if templ_7745c5c3_Err != nil {
178 return templ_7745c5c3_Err191 return templ_7745c5c3_Err
179 }192 }
180- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"")193+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"")
181 if templ_7745c5c3_Err != nil {194 if templ_7745c5c3_Err != nil {
182 return templ_7745c5c3_Err195 return templ_7745c5c3_Err
183 }196 }
184- var templ_7745c5c3_Var13 string197+ var templ_7745c5c3_Var14 string
185- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var12).String())198+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var13).String())
186 if templ_7745c5c3_Err != nil {199 if templ_7745c5c3_Err != nil {
187 return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 1, Col: 0}200 return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 1, Col: 0}
188 }201 }
189- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)202+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
190 if templ_7745c5c3_Err != nil {203 if templ_7745c5c3_Err != nil {
191 return templ_7745c5c3_Err204 return templ_7745c5c3_Err
192 }205 }
193- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">")206+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
194 if templ_7745c5c3_Err != nil {207 if templ_7745c5c3_Err != nil {
195 return templ_7745c5c3_Err208 return templ_7745c5c3_Err
196 }209 }
197 if l.Lede != "" {210 if l.Lede != "" {
198- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">")211+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">")
199 if templ_7745c5c3_Err != nil {212 if templ_7745c5c3_Err != nil {
200 return templ_7745c5c3_Err213 return templ_7745c5c3_Err
201 }214 }
202- var templ_7745c5c3_Var14 string215+ var templ_7745c5c3_Var15 string
203- templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede)216+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede)
204 if templ_7745c5c3_Err != nil {217 if templ_7745c5c3_Err != nil {
205- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 29, Col: 81}218+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 30, Col: 81}
206 }219 }
207- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))220+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
208 if templ_7745c5c3_Err != nil {221 if templ_7745c5c3_Err != nil {
209 return templ_7745c5c3_Err222 return templ_7745c5c3_Err
210 }223 }
211- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</p></section>")224+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</p></section>")
212 if templ_7745c5c3_Err != nil {225 if templ_7745c5c3_Err != nil {
213 return templ_7745c5c3_Err226 return templ_7745c5c3_Err
214 }227 }
215 }228 }
216- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<main id=\"main\" tabindex=\"-1\">")229+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<main id=\"main\" tabindex=\"-1\">")
217 if templ_7745c5c3_Err != nil {230 if templ_7745c5c3_Err != nil {
218 return templ_7745c5c3_Err231 return templ_7745c5c3_Err
219 }232 }
@@ -221,12 +234,12 @@ func Shell(l Layout, rail templ.Component) templ.Component {
221 if templ_7745c5c3_Err != nil {234 if templ_7745c5c3_Err != nil {
222 return templ_7745c5c3_Err235 return templ_7745c5c3_Err
223 }236 }
224- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</main>")237+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</main>")
225 if templ_7745c5c3_Err != nil {238 if templ_7745c5c3_Err != nil {
226 return templ_7745c5c3_Err239 return templ_7745c5c3_Err
227 }240 }
228 if l.HasRail && rail != nil {241 if l.HasRail && rail != nil {
229- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<aside class=\"rail\" aria-label=\"Context\">")242+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<aside class=\"rail\" aria-label=\"Context\">")
230 if templ_7745c5c3_Err != nil {243 if templ_7745c5c3_Err != nil {
231 return templ_7745c5c3_Err244 return templ_7745c5c3_Err
232 }245 }
@@ -234,12 +247,12 @@ func Shell(l Layout, rail templ.Component) templ.Component {
234 if templ_7745c5c3_Err != nil {247 if templ_7745c5c3_Err != nil {
235 return templ_7745c5c3_Err248 return templ_7745c5c3_Err
236 }249 }
237- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</aside>")250+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</aside>")
238 if templ_7745c5c3_Err != nil {251 if templ_7745c5c3_Err != nil {
239 return templ_7745c5c3_Err252 return templ_7745c5c3_Err
240 }253 }
241 }254 }
242- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div>")255+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div>")
243 if templ_7745c5c3_Err != nil {256 if templ_7745c5c3_Err != nil {
244 return templ_7745c5c3_Err257 return templ_7745c5c3_Err
245 }258 }
@@ -267,7 +280,7 @@ func Shell(l Layout, rail templ.Component) templ.Component {
267 if templ_7745c5c3_Err != nil {280 if templ_7745c5c3_Err != nil {
268 return templ_7745c5c3_Err281 return templ_7745c5c3_Err
269 }282 }
270- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</body></html>")283+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</body></html>")
271 if templ_7745c5c3_Err != nil {284 if templ_7745c5c3_Err != nil {
272 return templ_7745c5c3_Err285 return templ_7745c5c3_Err
273 }286 }
@@ -293,260 +306,260 @@ func Header(l Layout) templ.Component {
293 }()306 }()
294 }307 }
295 ctx = templ.InitializeContext(ctx)308 ctx = templ.InitializeContext(ctx)
296- templ_7745c5c3_Var15 := templ.GetChildren(ctx)309+ templ_7745c5c3_Var16 := templ.GetChildren(ctx)
297- if templ_7745c5c3_Var15 == nil {310+ if templ_7745c5c3_Var16 == nil {
298- templ_7745c5c3_Var15 = templ.NopComponent311+ templ_7745c5c3_Var16 = templ.NopComponent
299 }312 }
300 ctx = templ.ClearChildren(ctx)313 ctx = templ.ClearChildren(ctx)
301- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span> <span class=\"brand-name\">")314+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span> <span class=\"brand-name\">")
302 if templ_7745c5c3_Err != nil {315 if templ_7745c5c3_Err != nil {
303 return templ_7745c5c3_Err316 return templ_7745c5c3_Err
304 }317 }
305- var templ_7745c5c3_Var16 string318+ var templ_7745c5c3_Var17 string
306- templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(l.Instance)319+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(l.Instance)
307 if templ_7745c5c3_Err != nil {320 if templ_7745c5c3_Err != nil {
308- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 57, Col: 41}321+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 58, Col: 41}
309 }322 }
310- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))323+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
311 if templ_7745c5c3_Err != nil {324 if templ_7745c5c3_Err != nil {
312 return templ_7745c5c3_Err325 return templ_7745c5c3_Err
313 }326 }
314- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</span></a> ")327+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</span></a> ")
315 if templ_7745c5c3_Err != nil {328 if templ_7745c5c3_Err != nil {
316 return templ_7745c5c3_Err329 return templ_7745c5c3_Err
317 }330 }
318 if len(l.Crumbs) > 0 {331 if len(l.Crumbs) > 0 {
319- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")332+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")
320 if templ_7745c5c3_Err != nil {333 if templ_7745c5c3_Err != nil {
321 return templ_7745c5c3_Err334 return templ_7745c5c3_Err
322 }335 }
323 for _, c := range l.Crumbs {336 for _, c := range l.Crumbs {
324- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<li>")337+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<li>")
325 if templ_7745c5c3_Err != nil {338 if templ_7745c5c3_Err != nil {
326 return templ_7745c5c3_Err339 return templ_7745c5c3_Err
327 }340 }
328 if c.Href != "" {341 if c.Href != "" {
329- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<a href=\"")342+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<a href=\"")
330 if templ_7745c5c3_Err != nil {343 if templ_7745c5c3_Err != nil {
331 return templ_7745c5c3_Err344 return templ_7745c5c3_Err
332 }345 }
333- var templ_7745c5c3_Var17 templ.SafeURL346+ var templ_7745c5c3_Var18 templ.SafeURL
334- templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))347+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
335 if templ_7745c5c3_Err != nil {348 if templ_7745c5c3_Err != nil {
336- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 66, Col: 38}349+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 67, Col: 38}
337 }350 }
338- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))351+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
339 if templ_7745c5c3_Err != nil {352 if templ_7745c5c3_Err != nil {
340 return templ_7745c5c3_Err353 return templ_7745c5c3_Err
341 }354 }
342- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\"")355+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\"")
343 if templ_7745c5c3_Err != nil {356 if templ_7745c5c3_Err != nil {
344 return templ_7745c5c3_Err357 return templ_7745c5c3_Err
345 }358 }
346 if c.Current {359 if c.Current {
347- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " aria-current=\"page\"")360+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " aria-current=\"page\"")
348 if templ_7745c5c3_Err != nil {361 if templ_7745c5c3_Err != nil {
349 return templ_7745c5c3_Err362 return templ_7745c5c3_Err
350 }363 }
351 }364 }
352- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, ">")365+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, ">")
353 if templ_7745c5c3_Err != nil {366 if templ_7745c5c3_Err != nil {
354 return templ_7745c5c3_Err367 return templ_7745c5c3_Err
355 }368 }
356- var templ_7745c5c3_Var18 string369+ var templ_7745c5c3_Var19 string
357- templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)370+ templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
358 if templ_7745c5c3_Err != nil {371 if templ_7745c5c3_Err != nil {
359- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 70, Col: 19}372+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 71, Col: 19}
360 }373 }
361- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))374+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
362 if templ_7745c5c3_Err != nil {375 if templ_7745c5c3_Err != nil {
363 return templ_7745c5c3_Err376 return templ_7745c5c3_Err
364 }377 }
365- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</a>")378+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</a>")
366 if templ_7745c5c3_Err != nil {379 if templ_7745c5c3_Err != nil {
367 return templ_7745c5c3_Err380 return templ_7745c5c3_Err
368 }381 }
369 } else {382 } else {
370- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<span")383+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<span")
371 if templ_7745c5c3_Err != nil {384 if templ_7745c5c3_Err != nil {
372 return templ_7745c5c3_Err385 return templ_7745c5c3_Err
373 }386 }
374 if c.Current {387 if c.Current {
375- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " aria-current=\"page\"")388+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " aria-current=\"page\"")
376 if templ_7745c5c3_Err != nil {389 if templ_7745c5c3_Err != nil {
377 return templ_7745c5c3_Err390 return templ_7745c5c3_Err
378 }391 }
379 }392 }
380- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, ">")393+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, ">")
381 if templ_7745c5c3_Err != nil {394 if templ_7745c5c3_Err != nil {
382 return templ_7745c5c3_Err395 return templ_7745c5c3_Err
383 }396 }
384- var templ_7745c5c3_Var19 string397+ var templ_7745c5c3_Var20 string
385- templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)398+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
386 if templ_7745c5c3_Err != nil {399 if templ_7745c5c3_Err != nil {
387- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 76, Col: 19}400+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 77, Col: 19}
388 }401 }
389- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))402+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
390 if templ_7745c5c3_Err != nil {403 if templ_7745c5c3_Err != nil {
391 return templ_7745c5c3_Err404 return templ_7745c5c3_Err
392 }405 }
393- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</span>")406+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</span>")
394 if templ_7745c5c3_Err != nil {407 if templ_7745c5c3_Err != nil {
395 return templ_7745c5c3_Err408 return templ_7745c5c3_Err
396 }409 }
397 }410 }
398- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</li>")411+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</li>")
399 if templ_7745c5c3_Err != nil {412 if templ_7745c5c3_Err != nil {
400 return templ_7745c5c3_Err413 return templ_7745c5c3_Err
401 }414 }
402 }415 }
403- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</ol></nav>")416+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</ol></nav>")
404 if templ_7745c5c3_Err != nil {417 if templ_7745c5c3_Err != nil {
405 return templ_7745c5c3_Err418 return templ_7745c5c3_Err
406 }419 }
407 }420 }
408- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<div class=\"hdr-acts\">")421+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<div class=\"hdr-acts\">")
409 if templ_7745c5c3_Err != nil {422 if templ_7745c5c3_Err != nil {
410 return templ_7745c5c3_Err423 return templ_7745c5c3_Err
411 }424 }
412 if l.Ref != "" {425 if l.Ref != "" {
413- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<button type=\"button\" class=\"btn ref\" aria-haspopup=\"dialog\" aria-controls=\"panel-refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-refs'))\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")426+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<button type=\"button\" class=\"btn ref\" aria-haspopup=\"dialog\" aria-controls=\"panel-refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-refs'))\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")
414 if templ_7745c5c3_Err != nil {427 if templ_7745c5c3_Err != nil {
415 return templ_7745c5c3_Err428 return templ_7745c5c3_Err
416 }429 }
417- var templ_7745c5c3_Var20 string430+ var templ_7745c5c3_Var21 string
418- templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)431+ templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)
419 if templ_7745c5c3_Err != nil {432 if templ_7745c5c3_Err != nil {
420- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 95, Col: 36}433+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 96, Col: 36}
421 }434 }
422- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))435+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
423 if templ_7745c5c3_Err != nil {436 if templ_7745c5c3_Err != nil {
424 return templ_7745c5c3_Err437 return templ_7745c5c3_Err
425 }438 }
426- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</span> <span class=\"sha\">")439+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</span> <span class=\"sha\">")
427 if templ_7745c5c3_Err != nil {440 if templ_7745c5c3_Err != nil {
428 return templ_7745c5c3_Err441 return templ_7745c5c3_Err
429 }442 }
430- var templ_7745c5c3_Var21 string443+ var templ_7745c5c3_Var22 string
431- templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)444+ templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)
432 if templ_7745c5c3_Err != nil {445 if templ_7745c5c3_Err != nil {
433- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 96, Col: 33}446+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 97, Col: 33}
434 }447 }
435- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))448+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
436 if templ_7745c5c3_Err != nil {449 if templ_7745c5c3_Err != nil {
437 return templ_7745c5c3_Err450 return templ_7745c5c3_Err
438 }451 }
439- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</span></button> ")452+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</span></button> ")
440 if templ_7745c5c3_Err != nil {453 if templ_7745c5c3_Err != nil {
441 return templ_7745c5c3_Err454 return templ_7745c5c3_Err
442 }455 }
443 }456 }
444- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<button type=\"button\" class=\"btn search\" aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-cmd'))\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> <button type=\"button\" class=\"btn icon\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\" aria-label=\"Keyboard shortcuts\" title=\"keyboard shortcuts (?)\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-help'))\">?</button> ")457+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<button type=\"button\" class=\"btn search\" aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-cmd'))\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> <button type=\"button\" class=\"btn icon\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\" aria-label=\"Keyboard shortcuts\" title=\"keyboard shortcuts (?)\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-help'))\">?</button> ")
445 if templ_7745c5c3_Err != nil {458 if templ_7745c5c3_Err != nil {
446 return templ_7745c5c3_Err459 return templ_7745c5c3_Err
447 }460 }
448 if SignedIn(ctx) {461 if SignedIn(ctx) {
449- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\">owner</span> <button type=\"submit\" class=\"btn\" title=\"sign out of this browser\">Sign out</button></form>")462+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\">owner</span> <button type=\"submit\" class=\"btn\" title=\"sign out of this browser\">Sign out</button></form>")
450 if templ_7745c5c3_Err != nil {463 if templ_7745c5c3_Err != nil {
451 return templ_7745c5c3_Err464 return templ_7745c5c3_Err
452 }465 }
453 }466 }
454- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</div></div>")467+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</div></div>")
455 if templ_7745c5c3_Err != nil {468 if templ_7745c5c3_Err != nil {
456 return templ_7745c5c3_Err469 return templ_7745c5c3_Err
457 }470 }
458 if len(l.Sections) > 0 {471 if len(l.Sections) > 0 {
459- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")472+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")
460 if templ_7745c5c3_Err != nil {473 if templ_7745c5c3_Err != nil {
461 return templ_7745c5c3_Err474 return templ_7745c5c3_Err
462 }475 }
463 for _, s := range l.Sections {476 for _, s := range l.Sections {
464- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<li><a href=\"")477+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<li><a href=\"")
465 if templ_7745c5c3_Err != nil {478 if templ_7745c5c3_Err != nil {
466 return templ_7745c5c3_Err479 return templ_7745c5c3_Err
467 }480 }
468- var templ_7745c5c3_Var22 templ.SafeURL481+ var templ_7745c5c3_Var23 templ.SafeURL
469- templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))482+ templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
470 if templ_7745c5c3_Err != nil {483 if templ_7745c5c3_Err != nil {
471- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 127, Col: 36}484+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 128, Col: 36}
472 }485 }
473- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))486+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
474 if templ_7745c5c3_Err != nil {487 if templ_7745c5c3_Err != nil {
475 return templ_7745c5c3_Err488 return templ_7745c5c3_Err
476 }489 }
477- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" data-key=\"")490+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" data-key=\"")
478 if templ_7745c5c3_Err != nil {491 if templ_7745c5c3_Err != nil {
479 return templ_7745c5c3_Err492 return templ_7745c5c3_Err
480 }493 }
481- var templ_7745c5c3_Var23 string494+ var templ_7745c5c3_Var24 string
482- templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)495+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
483 if templ_7745c5c3_Err != nil {496 if templ_7745c5c3_Err != nil {
484- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 128, Col: 24}497+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 129, Col: 24}
485 }498 }
486- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)499+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
487 if templ_7745c5c3_Err != nil {500 if templ_7745c5c3_Err != nil {
488 return templ_7745c5c3_Err501 return templ_7745c5c3_Err
489 }502 }
490- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" aria-keyshortcuts=\"")503+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\" aria-keyshortcuts=\"")
491 if templ_7745c5c3_Err != nil {504 if templ_7745c5c3_Err != nil {
492 return templ_7745c5c3_Err505 return templ_7745c5c3_Err
493 }506 }
494- var templ_7745c5c3_Var24 string507+ var templ_7745c5c3_Var25 string
495- templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)508+ templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
496 if templ_7745c5c3_Err != nil {509 if templ_7745c5c3_Err != nil {
497- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 129, Col: 33}510+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 130, Col: 33}
498 }511 }
499- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)512+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
500 if templ_7745c5c3_Err != nil {513 if templ_7745c5c3_Err != nil {
501 return templ_7745c5c3_Err514 return templ_7745c5c3_Err
502 }515 }
503- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\"")516+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\"")
504 if templ_7745c5c3_Err != nil {517 if templ_7745c5c3_Err != nil {
505 return templ_7745c5c3_Err518 return templ_7745c5c3_Err
506 }519 }
507 if s.Current {520 if s.Current {
508- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, " aria-current=\"page\"")521+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, " aria-current=\"page\"")
509 if templ_7745c5c3_Err != nil {522 if templ_7745c5c3_Err != nil {
510 return templ_7745c5c3_Err523 return templ_7745c5c3_Err
511 }524 }
512 }525 }
513- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, ">")526+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, ">")
514 if templ_7745c5c3_Err != nil {527 if templ_7745c5c3_Err != nil {
515 return templ_7745c5c3_Err528 return templ_7745c5c3_Err
516 }529 }
517- var templ_7745c5c3_Var25 string530+ var templ_7745c5c3_Var26 string
518- templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)531+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
519 if templ_7745c5c3_Err != nil {532 if templ_7745c5c3_Err != nil {
520- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 133, Col: 17}533+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 134, Col: 17}
521 }534 }
522- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))535+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
523 if templ_7745c5c3_Err != nil {536 if templ_7745c5c3_Err != nil {
524 return templ_7745c5c3_Err537 return templ_7745c5c3_Err
525 }538 }
526- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<kbd aria-hidden=\"true\">")539+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<kbd aria-hidden=\"true\">")
527 if templ_7745c5c3_Err != nil {540 if templ_7745c5c3_Err != nil {
528 return templ_7745c5c3_Err541 return templ_7745c5c3_Err
529 }542 }
530- var templ_7745c5c3_Var26 string543+ var templ_7745c5c3_Var27 string
531- templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)544+ templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)
532 if templ_7745c5c3_Err != nil {545 if templ_7745c5c3_Err != nil {
533- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 133, Col: 50}546+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 134, Col: 50}
534 }547 }
535- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))548+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
536 if templ_7745c5c3_Err != nil {549 if templ_7745c5c3_Err != nil {
537 return templ_7745c5c3_Err550 return templ_7745c5c3_Err
538 }551 }
539- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</kbd></a></li>")552+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</kbd></a></li>")
540 if templ_7745c5c3_Err != nil {553 if templ_7745c5c3_Err != nil {
541 return templ_7745c5c3_Err554 return templ_7745c5c3_Err
542 }555 }
543 }556 }
544- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</ul></nav>")557+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</ul></nav>")
545 if templ_7745c5c3_Err != nil {558 if templ_7745c5c3_Err != nil {
546 return templ_7745c5c3_Err559 return templ_7745c5c3_Err
547 }560 }
548 }561 }
549- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</header>")562+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</header>")
550 if templ_7745c5c3_Err != nil {563 if templ_7745c5c3_Err != nil {
551 return templ_7745c5c3_Err564 return templ_7745c5c3_Err
552 }565 }
@@ -572,38 +585,38 @@ func Footer(l Layout) templ.Component {
572 }()585 }()
573 }586 }
574 ctx = templ.InitializeContext(ctx)587 ctx = templ.InitializeContext(ctx)
575- templ_7745c5c3_Var27 := templ.GetChildren(ctx)588+ templ_7745c5c3_Var28 := templ.GetChildren(ctx)
576- if templ_7745c5c3_Var27 == nil {589+ if templ_7745c5c3_Var28 == nil {
577- templ_7745c5c3_Var27 = templ.NopComponent590+ templ_7745c5c3_Var28 = templ.NopComponent
578 }591 }
579 ctx = templ.ClearChildren(ctx)592 ctx = templ.ClearChildren(ctx)
580- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<footer class=\"foot\"><span>ermite ")593+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<footer class=\"foot\"><span>ermite ")
581 if templ_7745c5c3_Err != nil {594 if templ_7745c5c3_Err != nil {
582 return templ_7745c5c3_Err595 return templ_7745c5c3_Err
583 }596 }
584- var templ_7745c5c3_Var28 string597+ var templ_7745c5c3_Var29 string
585- templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)598+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
586 if templ_7745c5c3_Err != nil {599 if templ_7745c5c3_Err != nil {
587- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 146, Col: 26}600+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 147, Col: 26}
588 }601 }
589- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))602+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
590 if templ_7745c5c3_Err != nil {603 if templ_7745c5c3_Err != nil {
591 return templ_7745c5c3_Err604 return templ_7745c5c3_Err
592 }605 }
593- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</span> <a href=\"")606+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</span> <a href=\"")
594 if templ_7745c5c3_Err != nil {607 if templ_7745c5c3_Err != nil {
595 return templ_7745c5c3_Err608 return templ_7745c5c3_Err
596 }609 }
597- var templ_7745c5c3_Var29 templ.SafeURL610+ var templ_7745c5c3_Var30 templ.SafeURL
598- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))611+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
599 if templ_7745c5c3_Err != nil {612 if templ_7745c5c3_Err != nil {
600- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 147, Col: 38}613+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 148, Col: 38}
601 }614 }
602- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))615+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
603 if templ_7745c5c3_Err != nil {616 if templ_7745c5c3_Err != nil {
604 return templ_7745c5c3_Err617 return templ_7745c5c3_Err
605 }618 }
606- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <span class=\"foot-keys\"><kbd>?</kbd> keyboard shortcuts</span></footer>")619+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <span class=\"foot-keys\"><kbd>?</kbd> keyboard shortcuts</span></footer>")
607 if templ_7745c5c3_Err != nil {620 if templ_7745c5c3_Err != nil {
608 return templ_7745c5c3_Err621 return templ_7745c5c3_Err
609 }622 }
@@ -631,12 +644,12 @@ func Hint(l Layout) templ.Component {
631 }()644 }()
632 }645 }
633 ctx = templ.InitializeContext(ctx)646 ctx = templ.InitializeContext(ctx)
634- templ_7745c5c3_Var30 := templ.GetChildren(ctx)647+ templ_7745c5c3_Var31 := templ.GetChildren(ctx)
635- if templ_7745c5c3_Var30 == nil {648+ if templ_7745c5c3_Var31 == nil {
636- templ_7745c5c3_Var30 = templ.NopComponent649+ templ_7745c5c3_Var31 = templ.NopComponent
637 }650 }
638 ctx = templ.ClearChildren(ctx)651 ctx = templ.ClearChildren(ctx)
639- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")652+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")
640 if templ_7745c5c3_Err != nil {653 if templ_7745c5c3_Err != nil {
641 return templ_7745c5c3_Err654 return templ_7745c5c3_Err
642 }655 }
@@ -644,7 +657,7 @@ func Hint(l Layout) templ.Component {
644 if templ_7745c5c3_Err != nil {657 if templ_7745c5c3_Err != nil {
645 return templ_7745c5c3_Err658 return templ_7745c5c3_Err
646 }659 }
647- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</div>")660+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</div>")
648 if templ_7745c5c3_Err != nil {661 if templ_7745c5c3_Err != nil {
649 return templ_7745c5c3_Err662 return templ_7745c5c3_Err
650 }663 }
M internal/adapters/web/static/css/30-list.css +4 -1
@@ -38,7 +38,10 @@
3838 }
3939 .list.tree {
4040 & .c-kind { width: 36px; color: var(--ink-mute); padding-right: 0; text-align: center; }
41- & .c-name { width: 32%; font-family: var(--mono); font-size: var(--fs-s); }
41+ /* --depth is set by files.js on the rows opened in place */
42+ & .c-name { width: 32%; font-family: var(--mono); font-size: var(--fs-s); padding-inline-start: calc(var(--s3) + var(--depth, 0) * 1.25ch); }
43+ & tr[aria-busy=true] .c-kind { opacity: .4; }
44+ & .tree-more td { padding-inline-start: calc(var(--s3) + 36px + var(--depth, 0) * 1.25ch); color: var(--ink-dim); font-size: var(--fs-s); }
4245 & .c-size { width: 90px; text-align: right; }
4346 & .c-when { width: 100px; }
4447 & .c-sub { width: auto; }
@@ -38,7 +38,10 @@
38 }38 }
39 .list.tree {39 .list.tree {
40 & .c-kind { width: 36px; color: var(--ink-mute); padding-right: 0; text-align: center; }40 & .c-kind { width: 36px; color: var(--ink-mute); padding-right: 0; text-align: center; }
41- & .c-name { width: 32%; font-family: var(--mono); font-size: var(--fs-s); }41+ /* --depth is set by files.js on the rows opened in place */
42+ & .c-name { width: 32%; font-family: var(--mono); font-size: var(--fs-s); padding-inline-start: calc(var(--s3) + var(--depth, 0) * 1.25ch); }
43+ & tr[aria-busy=true] .c-kind { opacity: .4; }
44+ & .tree-more td { padding-inline-start: calc(var(--s3) + 36px + var(--depth, 0) * 1.25ch); color: var(--ink-dim); font-size: var(--fs-s); }
42 & .c-size { width: 90px; text-align: right; }45 & .c-size { width: 90px; text-align: right; }
43 & .c-when { width: 100px; }46 & .c-when { width: 100px; }
44 & .c-sub { width: auto; }47 & .c-sub { width: auto; }
A internal/adapters/web/static/js/files.js +81 -0
@@ -0,0 +1,81 @@
1+// files.js: the Files view. A directory opens in place: its entries are
2+// fetched once as table rows (the tree page with ?rows), inserted under it
3+// and kept in the DOM, so closing and opening it again needs no request.
4+//
5+// Rows opened in place carry data-depth and the --depth custom property
6+// (set through the CSSOM: the CSP forbids style attributes). Rows of the
7+// page itself have depth 0. Nothing here touches the document at load; the
8+// handlers are declared in the templates (hx-on:click on #tree-rows).
9+window.ermiteTree = (() => {
10+ const depthOf = (tr) => Number(tr.dataset.depth || 0);
11+ const linkOf = (tr) => tr.querySelector('a[data-expand]');
12+ const isOpen = (a) => a.getAttribute('aria-expanded') === 'true';
13+
14+ function setOpen(tr, a, open) {
15+ a.setAttribute('aria-expanded', open ? 'true' : 'false');
16+ const kind = tr.querySelector('.c-kind');
17+ if (kind) kind.textContent = open ? '▾' : '▸';
18+ }
19+
20+ // collapse hides every row under tr.
21+ function collapse(tr, a) {
22+ setOpen(tr, a, false);
23+ const d = depthOf(tr);
24+ for (let r = tr.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) r.hidden = true;
25+ }
26+
27+ // expand shows the rows under tr that no collapsed directory between hides.
28+ function expand(tr, a) {
29+ setOpen(tr, a, true);
30+ const d = depthOf(tr);
31+ let closedAt = Infinity; // depth of the collapsed directory whose rows are skipped
32+ for (let r = tr.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) {
33+ const rd = depthOf(r);
34+ if (rd <= closedAt) closedAt = Infinity;
35+ r.hidden = rd > closedAt;
36+ const la = linkOf(r);
37+ if (closedAt === Infinity && la && !isOpen(la)) closedAt = rd;
38+ }
39+ }
40+
41+ async function load(tr, a) {
42+ tr.setAttribute('aria-busy', 'true');
43+ try {
44+ const res = await fetch(a.dataset.expand, { headers: { Accept: 'text/html' } });
45+ if (!res.ok) throw new Error('HTTP ' + res.status);
46+ const t = document.createElement('template');
47+ t.innerHTML = await res.text();
48+ const d = depthOf(tr) + 1;
49+ const rows = Array.from(t.content.children);
50+ for (const r of rows) {
51+ r.dataset.depth = String(d);
52+ r.style.setProperty('--depth', String(d));
53+ }
54+ tr.after(...rows);
55+ tr.dataset.loaded = '1';
56+ tr.removeAttribute('aria-busy');
57+ expand(tr, a);
58+ } catch (e) {
59+ location.href = a.href;
60+ }
61+ }
62+
63+ function toggle(tr) {
64+ const a = linkOf(tr);
65+ if (!a || tr.getAttribute('aria-busy') === 'true') return;
66+ if (isOpen(a)) collapse(tr, a);
67+ else if (tr.dataset.loaded) expand(tr, a);
68+ else load(tr, a);
69+ }
70+
71+ return {
72+ // click opens or closes the directory under a plain left click; any
73+ // modified or other click keeps the link's own navigation.
74+ click(event) {
75+ const a = event.target.closest('a[data-expand]');
76+ if (!a || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
77+ event.preventDefault();
78+ toggle(a.closest('tr'));
79+ },
80+ };
81+})();
@@ -0,0 +1,81 @@
1+// files.js: the Files view. A directory opens in place: its entries are
2+// fetched once as table rows (the tree page with ?rows), inserted under it
3+// and kept in the DOM, so closing and opening it again needs no request.
4+//
5+// Rows opened in place carry data-depth and the --depth custom property
6+// (set through the CSSOM: the CSP forbids style attributes). Rows of the
7+// page itself have depth 0. Nothing here touches the document at load; the
8+// handlers are declared in the templates (hx-on:click on #tree-rows).
9+window.ermiteTree = (() => {
10+ const depthOf = (tr) => Number(tr.dataset.depth || 0);
11+ const linkOf = (tr) => tr.querySelector('a[data-expand]');
12+ const isOpen = (a) => a.getAttribute('aria-expanded') === 'true';
13+
14+ function setOpen(tr, a, open) {
15+ a.setAttribute('aria-expanded', open ? 'true' : 'false');
16+ const kind = tr.querySelector('.c-kind');
17+ if (kind) kind.textContent = open ? '▾' : '▸';
18+ }
19+
20+ // collapse hides every row under tr.
21+ function collapse(tr, a) {
22+ setOpen(tr, a, false);
23+ const d = depthOf(tr);
24+ for (let r = tr.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) r.hidden = true;
25+ }
26+
27+ // expand shows the rows under tr that no collapsed directory between hides.
28+ function expand(tr, a) {
29+ setOpen(tr, a, true);
30+ const d = depthOf(tr);
31+ let closedAt = Infinity; // depth of the collapsed directory whose rows are skipped
32+ for (let r = tr.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) {
33+ const rd = depthOf(r);
34+ if (rd <= closedAt) closedAt = Infinity;
35+ r.hidden = rd > closedAt;
36+ const la = linkOf(r);
37+ if (closedAt === Infinity && la && !isOpen(la)) closedAt = rd;
38+ }
39+ }
40+
41+ async function load(tr, a) {
42+ tr.setAttribute('aria-busy', 'true');
43+ try {
44+ const res = await fetch(a.dataset.expand, { headers: { Accept: 'text/html' } });
45+ if (!res.ok) throw new Error('HTTP ' + res.status);
46+ const t = document.createElement('template');
47+ t.innerHTML = await res.text();
48+ const d = depthOf(tr) + 1;
49+ const rows = Array.from(t.content.children);
50+ for (const r of rows) {
51+ r.dataset.depth = String(d);
52+ r.style.setProperty('--depth', String(d));
53+ }
54+ tr.after(...rows);
55+ tr.dataset.loaded = '1';
56+ tr.removeAttribute('aria-busy');
57+ expand(tr, a);
58+ } catch (e) {
59+ location.href = a.href;
60+ }
61+ }
62+
63+ function toggle(tr) {
64+ const a = linkOf(tr);
65+ if (!a || tr.getAttribute('aria-busy') === 'true') return;
66+ if (isOpen(a)) collapse(tr, a);
67+ else if (tr.dataset.loaded) expand(tr, a);
68+ else load(tr, a);
69+ }
70+
71+ return {
72+ // click opens or closes the directory under a plain left click; any
73+ // modified or other click keeps the link's own navigation.
74+ click(event) {
75+ const a = event.target.closest('a[data-expand]');
76+ if (!a || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
77+ event.preventDefault();
78+ toggle(a.closest('tr'));
79+ },
80+ };
81+})();
M internal/adapters/web/tree.go +21 -12
@@ -12,14 +12,14 @@ import (
1212 pb "bnema.dev/ermite-api/gen/ermite"
1313 )
1414
15-// maxFileTargets bounds the files of a directory offered in the ":" list.
16-const maxFileTargets = 200
17-
1815 // TreeRow is one entry of a directory.
1916 type TreeRow struct {
20- Glyph string // ▾ directory, @ symbolic link, + submodule, · file
21- Name string
22- Href string // "" for a submodule
17+ Glyph string // ▸ directory, @ symbolic link, + submodule, · file
18+ Name string
19+ Href string // "" for a submodule
20+ // Expand is the URL of the rows of a directory (Href?rows); "" for any
21+ // other entry.
22+ Expand string
2323 Size string
2424 Subject string
2525 CHref string
@@ -35,6 +35,9 @@ type TreePage struct {
3535 Readme *Readme
3636 // Notes say what the listing leaves out.
3737 Notes []string
38+ // More, for the rows of a directory expanded in place, is the page of the
39+ // directory when it has more entries than are listed; "" otherwise.
40+ More string
3841 }
3942
4043 // tree lists a directory of a ref, with the last commit of each entry, and
@@ -57,14 +60,15 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
5760 entries := res.Msg.GetEntries()
5861 now := s.cfg.Now()
5962
63+ rowsOnly := r.URL.Query().Has("rows")
6064 p := TreePage{}
61- l := s.repoLayout(ctx, d, "tree")
6265 for i, e := range entries {
63- row := TreeRow{Glyph: "·", Name: visibleText(e.GetName()), Size: humanSize(e.GetSize()), Cursor: i == 0}
66+ row := TreeRow{Glyph: "·", Name: visibleText(e.GetName()), Size: humanSize(e.GetSize()), Cursor: i == 0 && !rowsOnly}
6467 switch e.GetKind() {
6568 case pb.EntryKind_ENTRY_KIND_DIRECTORY:
66- row.Glyph, row.Name, row.Size = "▾", row.Name+"/", "-"
69+ row.Glyph, row.Name, row.Size = "▸", row.Name+"/", "-"
6770 row.Href = repoHref(d.name(), "tree", t.name, e.GetPath())
71+ row.Expand = row.Href + "?rows"
6872 case pb.EntryKind_ENTRY_KIND_SYMLINK:
6973 row.Glyph = "@"
7074 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())
@@ -72,9 +76,6 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
7276 row.Glyph, row.Size = "+", "-"
7377 default:
7478 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())
75- if len(l.Cmd) < maxFileTargets {
76- l.Cmd = append(l.Cmd, CmdItem{Kind: "file", Label: visibleText(e.GetPath()), Href: row.Href})
77- }
7879 }
7980 if c := e.GetLastCommit(); c != nil {
8081 last := commitRow(d.name(), c, now)
@@ -82,6 +83,13 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
8283 }
8384 p.Rows = append(p.Rows, row)
8485 }
86+ if rowsOnly {
87+ // The rows alone, for a directory opened in place (files.js).
88+ if res.Msg.GetTruncated() {
89+ p.More = repoHref(d.name(), "tree", t.name, t.path)
90+ }
91+ return TreeRows(p), http.StatusOK, nil
92+ }
8593 if res.Msg.GetTruncated() {
8694 p.Notes = append(p.Notes, "this directory has more entries than are listed here: the first "+itoa(len(entries))+" are shown.")
8795 }
@@ -96,6 +104,7 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
96104 p.Readme = rd.readme
97105 }
98106
107+ l := s.repoLayout(ctx, d, "tree")
99108 pinRef(&l, t)
100109 l.Crumbs = pathCrumbs(d.name(), "tree", t)
101110 if t.path == "" {
@@ -12,14 +12,14 @@ import (
12 pb "bnema.dev/ermite-api/gen/ermite"12 pb "bnema.dev/ermite-api/gen/ermite"
13 )13 )
14 14
15-// maxFileTargets bounds the files of a directory offered in the ":" list.
16-const maxFileTargets = 200
17-
18 // TreeRow is one entry of a directory.15 // TreeRow is one entry of a directory.
19 type TreeRow struct {16 type TreeRow struct {
20- Glyph string // ▾ directory, @ symbolic link, + submodule, · file17+ Glyph string // ▸ directory, @ symbolic link, + submodule, · file
21- Name string18+ Name string
22- Href string // "" for a submodule19+ Href string // "" for a submodule
20+ // Expand is the URL of the rows of a directory (Href?rows); "" for any
21+ // other entry.
22+ Expand string
23 Size string23 Size string
24 Subject string24 Subject string
25 CHref string25 CHref string
@@ -35,6 +35,9 @@ type TreePage struct {
35 Readme *Readme35 Readme *Readme
36 // Notes say what the listing leaves out.36 // Notes say what the listing leaves out.
37 Notes []string37 Notes []string
38+ // More, for the rows of a directory expanded in place, is the page of the
39+ // directory when it has more entries than are listed; "" otherwise.
40+ More string
38 }41 }
39 42
40 // tree lists a directory of a ref, with the last commit of each entry, and43 // tree lists a directory of a ref, with the last commit of each entry, and
@@ -57,14 +60,15 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
57 entries := res.Msg.GetEntries()60 entries := res.Msg.GetEntries()
58 now := s.cfg.Now()61 now := s.cfg.Now()
59 62
63+ rowsOnly := r.URL.Query().Has("rows")
60 p := TreePage{}64 p := TreePage{}
61- l := s.repoLayout(ctx, d, "tree")
62 for i, e := range entries {65 for i, e := range entries {
63- row := TreeRow{Glyph: "·", Name: visibleText(e.GetName()), Size: humanSize(e.GetSize()), Cursor: i == 0}66+ row := TreeRow{Glyph: "·", Name: visibleText(e.GetName()), Size: humanSize(e.GetSize()), Cursor: i == 0 && !rowsOnly}
64 switch e.GetKind() {67 switch e.GetKind() {
65 case pb.EntryKind_ENTRY_KIND_DIRECTORY:68 case pb.EntryKind_ENTRY_KIND_DIRECTORY:
66- row.Glyph, row.Name, row.Size = "▾", row.Name+"/", "-"69+ row.Glyph, row.Name, row.Size = "▸", row.Name+"/", "-"
67 row.Href = repoHref(d.name(), "tree", t.name, e.GetPath())70 row.Href = repoHref(d.name(), "tree", t.name, e.GetPath())
71+ row.Expand = row.Href + "?rows"
68 case pb.EntryKind_ENTRY_KIND_SYMLINK:72 case pb.EntryKind_ENTRY_KIND_SYMLINK:
69 row.Glyph = "@"73 row.Glyph = "@"
70 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())74 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())
@@ -72,9 +76,6 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
72 row.Glyph, row.Size = "+", "-"76 row.Glyph, row.Size = "+", "-"
73 default:77 default:
74 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())78 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())
75- if len(l.Cmd) < maxFileTargets {
76- l.Cmd = append(l.Cmd, CmdItem{Kind: "file", Label: visibleText(e.GetPath()), Href: row.Href})
77- }
78 }79 }
79 if c := e.GetLastCommit(); c != nil {80 if c := e.GetLastCommit(); c != nil {
80 last := commitRow(d.name(), c, now)81 last := commitRow(d.name(), c, now)
@@ -82,6 +83,13 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
82 }83 }
83 p.Rows = append(p.Rows, row)84 p.Rows = append(p.Rows, row)
84 }85 }
86+ if rowsOnly {
87+ // The rows alone, for a directory opened in place (files.js).
88+ if res.Msg.GetTruncated() {
89+ p.More = repoHref(d.name(), "tree", t.name, t.path)
90+ }
91+ return TreeRows(p), http.StatusOK, nil
92+ }
85 if res.Msg.GetTruncated() {93 if res.Msg.GetTruncated() {
86 p.Notes = append(p.Notes, "this directory has more entries than are listed here: the first "+itoa(len(entries))+" are shown.")94 p.Notes = append(p.Notes, "this directory has more entries than are listed here: the first "+itoa(len(entries))+" are shown.")
87 }95 }
@@ -96,6 +104,7 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
96 p.Readme = rd.readme104 p.Readme = rd.readme
97 }105 }
98 106
107+ l := s.repoLayout(ctx, d, "tree")
99 pinRef(&l, t)108 pinRef(&l, t)
100 l.Crumbs = pathCrumbs(d.name(), "tree", t)109 l.Crumbs = pathCrumbs(d.name(), "tree", t)
101 if t.path == "" {110 if t.path == "" {
M internal/adapters/web/tree.templ +24 -7
@@ -21,10 +21,8 @@ templ Tree(l Layout, p TreePage) {
2121 <thead>
2222 <tr><th class="cur"></th><th class="c-kind"></th><th class="c-name">name</th><th class="c-size">size</th><th class="c-sub">last commit</th><th class="c-who">author</th><th class="c-when">date</th></tr>
2323 </thead>
24- <tbody id="tree-rows" data-rows>
25- for _, r := range p.Rows {
26- @TreeRowView(r)
27- }
24+ <tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
25+ @TreeRows(p)
2826 </tbody>
2927 </table>
3028 if len(p.Rows) == 0 {
@@ -39,15 +37,34 @@ templ Tree(l Layout, p TreePage) {
3937 }
4038 }
4139
42-// TreeRowView is one entry of a directory. Glyph: ▾ directory, @ symbolic
43-// link, + submodule, · file.
40+// TreeRows is the rows of a directory alone: what files.js inserts under a
41+// directory it opens in place. When the directory has more entries than are
42+// listed, the last row links to its page.
43+templ TreeRows(p TreePage) {
44+ for _, r := range p.Rows {
45+ @TreeRowView(r)
46+ }
47+ if p.More != "" {
48+ <tr class="tree-more"><td colspan="7"><a href={ templ.SafeURL(p.More) }>more entries: open the directory</a></td></tr>
49+ }
50+}
51+
52+// TreeRowView is one entry of a directory. Glyph: ▸ directory (▾ once opened
53+// in place), @ symbolic link, + submodule, · file. A directory link carries
54+// data-expand, the URL of its rows.
4455 templ TreeRowView(r TreeRow) {
4556 <tr data-cursor?={ r.Cursor }>
4657 <td class="cur" aria-hidden="true"></td>
4758 <td class="c-kind" aria-hidden="true">{ r.Glyph }</td>
4859 <td class="c-name">
4960 if r.Href != "" {
50- <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a>
61+ <a
62+ href={ templ.SafeURL(r.Href) }
63+ if r.Expand != "" {
64+ data-expand={ r.Expand }
65+ aria-expanded="false"
66+ }
67+ >{ r.Name }</a>
5168 } else {
5269 { r.Name }
5370 }
@@ -21,10 +21,8 @@ templ Tree(l Layout, p TreePage) {
21 <thead>21 <thead>
22 <tr><th class="cur"></th><th class="c-kind"></th><th class="c-name">name</th><th class="c-size">size</th><th class="c-sub">last commit</th><th class="c-who">author</th><th class="c-when">date</th></tr>22 <tr><th class="cur"></th><th class="c-kind"></th><th class="c-name">name</th><th class="c-size">size</th><th class="c-sub">last commit</th><th class="c-who">author</th><th class="c-when">date</th></tr>
23 </thead>23 </thead>
24- <tbody id="tree-rows" data-rows>24+ <tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
25- for _, r := range p.Rows {25+ @TreeRows(p)
26- @TreeRowView(r)
27- }
28 </tbody>26 </tbody>
29 </table>27 </table>
30 if len(p.Rows) == 0 {28 if len(p.Rows) == 0 {
@@ -39,15 +37,34 @@ templ Tree(l Layout, p TreePage) {
39 }37 }
40 }38 }
41 39
42-// TreeRowView is one entry of a directory. Glyph: ▾ directory, @ symbolic40+// TreeRows is the rows of a directory alone: what files.js inserts under a
43-// link, + submodule, · file.41+// directory it opens in place. When the directory has more entries than are
42+// listed, the last row links to its page.
43+templ TreeRows(p TreePage) {
44+ for _, r := range p.Rows {
45+ @TreeRowView(r)
46+ }
47+ if p.More != "" {
48+ <tr class="tree-more"><td colspan="7"><a href={ templ.SafeURL(p.More) }>more entries: open the directory</a></td></tr>
49+ }
50+}
51+
52+// TreeRowView is one entry of a directory. Glyph: ▸ directory (▾ once opened
53+// in place), @ symbolic link, + submodule, · file. A directory link carries
54+// data-expand, the URL of its rows.
44 templ TreeRowView(r TreeRow) {55 templ TreeRowView(r TreeRow) {
45 <tr data-cursor?={ r.Cursor }>56 <tr data-cursor?={ r.Cursor }>
46 <td class="cur" aria-hidden="true"></td>57 <td class="cur" aria-hidden="true"></td>
47 <td class="c-kind" aria-hidden="true">{ r.Glyph }</td>58 <td class="c-kind" aria-hidden="true">{ r.Glyph }</td>
48 <td class="c-name">59 <td class="c-name">
49 if r.Href != "" {60 if r.Href != "" {
50- <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a>61+ <a
62+ href={ templ.SafeURL(r.Href) }
63+ if r.Expand != "" {
64+ data-expand={ r.Expand }
65+ aria-expanded="false"
66+ }
67+ >{ r.Name }</a>
51 } else {68 } else {
52 { r.Name }69 { r.Name }
53 }70 }
M internal/adapters/web/tree_templ.go +148 -73 generated file: collapsed

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 44

↑↓ moveenter openesc close