website

#astro#js#html#css

git clone https://git.pyrossh.dev/website

木 Personal website of pyrossh. Built with astrojs, shiki, vite.


986738a — pyrossh 2026-11-06T15:29:20+05:30
Show loading feedback for repo page navigation
Files changed (2) hide show
  1. assets/css/repos.css +45 -0
  2. tests/pages.test.js +30 -0
assets/css/repos.css CHANGED
@@ -247,6 +247,14 @@
247
247
  border-radius: 6px;
248
248
  font-size: 0.9rem;
249
249
 
250
+ /* The tree is fetched separately (see treeSidebar in file-tree.njk). htmx
251
+ puts .htmx-request on this element only for that fetch, so a failed
252
+ request doesn't leave this showing forever. */
253
+ &.htmx-request::before {
254
+ content: "Loading files…";
255
+ opacity: 0.6;
256
+ }
257
+
250
258
  ul {
251
259
  list-style: none;
252
260
  margin: 0 0 0 1.1rem;
@@ -475,3 +483,40 @@ html:root {
475
483
  .d2h-wrapper {
476
484
  color: var(--color-text);
477
485
  }
486
+
487
+ /* Top progress bar for htmx navigation (repo tabs, the file tree, opening a
488
+ file), driven by the .htmx-request class htmx puts on whatever is fetching.
489
+ Showing is delayed so fast or cached responses don't flicker. Hiding is
490
+ delayed too, to bridge the gap between the Code tab's page request and the
491
+ tree request it triggers, which would otherwise blink the bar. */
492
+ body::before {
493
+ content: "";
494
+ position: fixed;
495
+ top: 0;
496
+ left: 0;
497
+ z-index: 100;
498
+ width: 40%;
499
+ height: 3px;
500
+ pointer-events: none;
501
+ background: var(--color-accent);
502
+ opacity: 0;
503
+ transition: opacity 100ms linear 100ms;
504
+ animation: htmx-progress 1.1s ease-in-out infinite;
505
+ }
506
+
507
+ body:has(.htmx-request)::before {
508
+ opacity: 1;
509
+ transition-delay: 150ms;
510
+ }
511
+
512
+ @keyframes htmx-progress {
513
+ from { transform: translateX(-100%); }
514
+ to { transform: translateX(250%); }
515
+ }
516
+
517
+ @media (prefers-reduced-motion: reduce) {
518
+ body::before {
519
+ width: 100%;
520
+ animation: none;
521
+ }
522
+ }
tests/pages.test.js CHANGED
@@ -207,6 +207,36 @@ describe("repo pages", () => {
207
207
  }
208
208
  });
209
209
 
210
+ test("the Code tab shows loading feedback until the tree arrives", async () => {
211
+ const treeUrl = `**/repos/${repoId}/tree/`;
212
+ let releaseTree;
213
+ const treeHeld = new Promise((resolve) => (releaseTree = resolve));
214
+ await page.route(treeUrl, async (route) => {
215
+ await treeHeld;
216
+ await route.continue();
217
+ });
218
+ const barOpacity = () => getComputedStyle(document.body, "::before").opacity;
219
+ try {
220
+ await goto(`/repos/${repoId}/`);
221
+ await page.waitForFunction(() => htmx);
222
+ await page.locator(".nav a", { hasText: "Code" }).click();
223
+
224
+ await page.locator(".file-explorer-tree.htmx-request").waitFor();
225
+ const placeholder = await page
226
+ .locator(".file-explorer-tree")
227
+ .evaluate((el) => getComputedStyle(el, "::before").content);
228
+ assert.equal(placeholder, '"Loading files…"');
229
+ await page.waitForFunction(`(${barOpacity})() === "1"`);
230
+
231
+ releaseTree();
232
+ await page.locator(".file-explorer-tree li a").first().waitFor({ state: "attached" });
233
+ await page.waitForFunction(`(${barOpacity})() === "0"`);
234
+ } finally {
235
+ releaseTree();
236
+ await page.unroute(treeUrl);
237
+ }
238
+ });
239
+
210
240
  test("repo files index renders the tree and a default file", async () => {
211
241
  const response = await goto(`/repos/${repoId}/files/`);
212
242
  assert.equal(response.status(), 200);