Version static URLs by mtime; flatter tabs

The stylesheet and bundle were served with Last-Modified only, so a browser
could pair new JavaScript with a stale app.css and render the tab strip and
thumbnails unstyled. Their URLs now carry the file's mtime. Tabs are flat on
the chrome with the open one underlined; the close button shows on hover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Olive Vaughn 2026-09-29 13:08:38 -04:00
parent f590ff19cf
commit 270c5a4369
3 changed files with 48 additions and 15 deletions

View file

@ -20,12 +20,12 @@ and PUTs with ordinary CSRF protection — no endpoint in this app is exempt.
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>arthur</title>
<link rel="stylesheet" href="{% static 'arthur/app.css' %}">
<link rel="stylesheet" href="{% static 'arthur/app.css' %}?v={{ css_version }}">
</head>
<body>
{% csrf_token %}
<div id="app"></div>
<script src="{% static 'mediapipe/vision_bundle.js' %}"></script>
<script src="{% static 'arthur/js/main.js' %}"></script>
<script src="{% static 'arthur/js/main.js' %}?v={{ js_version }}"></script>
</body>
</html>

View file

@ -119,10 +119,27 @@ def _crop_blob(chunks):
# the page
def _asset_version(relative):
"""A static file's modification time, for its URL.
The stylesheet and the bundle are served with `Last-Modified` and nothing
else, so a browser is free to keep a stale copy on heuristic freshness — and
new JavaScript over an old stylesheet renders a pane the stylesheet has never
heard of as bare elements. A version in the URL makes each edit a new URL."""
for root in settings.STATICFILES_DIRS:
path = Path(root) / relative
if path.exists():
return str(int(path.stat().st_mtime))
return "0"
def page(request):
"""The host page. This replaced `frontend/public/index.html` at step 9, and
`:dev-http` in shadow-cljs.edn went away with it."""
return render(request, "clips/index.html")
return render(request, "clips/index.html", {
"css_version": _asset_version("arthur/app.css"),
"js_version": _asset_version("arthur/js/main.js"),
})
# ---------------------------------------------------------------------------

View file

@ -336,31 +336,47 @@ input[type="range"] { width: 100%; accent-color: var(--sel); }
overflow: hidden;
}
/* One tab per open symbol. Flat, like the pane heads, with the open one lifted
to the palette bar's colour so it reads as the thing the bar below belongs to. */
/* One tab per open symbol: flat, on the chrome, with the open one marked by the
selection colour underneath rather than by a raised shape. */
.tabs {
display: flex;
gap: 1px;
padding: 3px 6px 0;
background: var(--line);
align-items: stretch;
height: 24px;
flex: 0 0 24px;
padding: 0 4px;
background: var(--chrome);
border-bottom: 1px solid var(--line);
overflow-x: auto;
}
.tab {
display: flex;
align-items: center;
gap: 6px;
padding: 2px 8px;
background: var(--sunk);
gap: 4px;
padding: 0 6px 0 10px;
color: var(--dim);
border-radius: 3px 3px 0 0;
border-right: 1px solid var(--hair);
cursor: pointer;
white-space: nowrap;
}
.tab.on { background: var(--chrome); color: var(--fg); }
.tab .close { padding: 0 3px; border: 0; background: none; color: var(--dim); line-height: 1; }
.tab .close:hover { color: var(--fg); }
.tab:hover { color: var(--fg); background: var(--sunk); }
.tab.on { color: var(--fg); background: var(--pane); box-shadow: inset 0 -2px 0 var(--sel); }
.tab .close {
width: 16px;
height: 16px;
padding: 0;
border: 0;
border-radius: 2px;
background: none;
color: var(--dim);
line-height: 16px;
visibility: hidden;
}
.tab:hover .close, .tab.on .close { visibility: visible; }
.tab .close:hover { background: var(--hair); color: var(--fg); }
.palette-bar {
display: flex;