| 321 |
321 |
|
|
| 322 |
322 |
|
Use `hx-trigger="revealed"` with a loading placeholder only when the data is expensive to fetch AND hidden behind a `<details>` element (e.g., 2FA status, passkey list). Never use loading placeholders for content that's visible on initial tab load.
|
| 323 |
323 |
|
|
| 324 |
|
- |
### Inline Scripts
|
|
324 |
+ |
### Static JavaScript
|
| 325 |
325 |
|
|
| 326 |
|
- |
Keep inline `<script>` blocks under 20 lines. Extract larger scripts to static JS files in `server/static/`. Static files are cached by the browser; inline scripts are re-parsed on every HTMX fragment swap.
|
|
326 |
+ |
JavaScript lives in `server/static/`, one file per feature area:
|
|
327 |
+ |
|
|
328 |
+ |
```
|
|
329 |
+ |
static/
|
|
330 |
+ |
mnw.js — core utilities (CSRF, toasts, tabs, shortcuts) — loaded globally
|
|
331 |
+ |
upload.js — S3 upload (S3Uploader, initDropzone) — loaded globally
|
|
332 |
+ |
passkey.js — WebAuthn registration/login
|
|
333 |
+ |
insertions.js — clip management
|
|
334 |
+ |
wizard.js — wizard navigation
|
|
335 |
+ |
docs-search.js — doc search index
|
|
336 |
+ |
item-details.js — bundle, section, tag management
|
|
337 |
+ |
item-upload.js — audio + version upload flows
|
|
338 |
+ |
blog-editor.js — blog save/autosave/publish
|
|
339 |
+ |
style.css — main stylesheet
|
|
340 |
+ |
wizard.css — wizard-specific styles
|
|
341 |
+ |
```
|
|
342 |
+ |
|
|
343 |
+ |
Only `mnw.js`, `upload.js`, and `htmx.min.js` are loaded globally (in `base.html` / `_head_assets.html`). All other JS files are loaded via `{% block scripts %}` in the page that needs them.
|
|
344 |
+ |
|
|
345 |
+ |
**Passing server data to static JS:** Use `data-*` attributes on the feature's container element. The JS file reads them on init.
|
|
346 |
+ |
|
|
347 |
+ |
```html
|
|
348 |
+ |
<!-- In template -->
|
|
349 |
+ |
<div id="audio-upload" data-item-id="{{ item.id }}">
|
|
350 |
+ |
<!-- upload UI -->
|
|
351 |
+ |
</div>
|
|
352 |
+ |
|
|
353 |
+ |
<!-- In {% block scripts %} -->
|
|
354 |
+ |
<script src="/static/item-upload.js"></script>
|
|
355 |
+ |
```
|
|
356 |
+ |
|
|
357 |
+ |
```javascript
|
|
358 |
+ |
// static/item-upload.js
|
|
359 |
+ |
(function() {
|
|
360 |
+ |
var el = document.getElementById('audio-upload');
|
|
361 |
+ |
if (!el) return;
|
|
362 |
+ |
var itemId = el.dataset.itemId;
|
|
363 |
+ |
// ...
|
|
364 |
+ |
})();
|
|
365 |
+ |
```
|
|
366 |
+ |
|
|
367 |
+ |
For complex structured data (JSON arrays/objects that can't fit in an attribute), use a minimal inline script:
|
|
368 |
+ |
|
|
369 |
+ |
```html
|
|
370 |
+ |
<script>window.MNW = window.MNW || {}; window.MNW.pageData = { segments: {{ segments_json|safe }} };</script>
|
|
371 |
+ |
<script src="/static/audio-player.js"></script>
|
|
372 |
+ |
```
|
|
373 |
+ |
|
|
374 |
+ |
**HTMX partial re-initialization:** Static JS loaded in `{% block scripts %}` runs once on page load. For HTMX partials (tab content swapped dynamically), use `htmx:afterSwap` to re-initialize:
|
|
375 |
+ |
|
|
376 |
+ |
```javascript
|
|
377 |
+ |
document.body.addEventListener('htmx:afterSwap', function(e) {
|
|
378 |
+ |
if (e.detail.target.id === 'tab-content') {
|
|
379 |
+ |
initMyFeature();
|
|
380 |
+ |
}
|
|
381 |
+ |
});
|
|
382 |
+ |
```
|
|
383 |
+ |
|
|
384 |
+ |
**When inline is OK:** Under 20 lines, no template variables that could be data attributes, and tightly coupled to a single template's DOM structure (e.g., tab overflow close handler, single-use form validation).
|
|
385 |
+ |
|
|
386 |
+ |
### Inline CSS
|
|
387 |
+ |
|
|
388 |
+ |
Page-specific `<style>` blocks in `{% block head %}` are acceptable when the styles are truly unique to that page. Shared patterns (form layouts, tables, status badges, cards) belong in `style.css`.
|
| 327 |
389 |
|
|
| 328 |
390 |
|
### Information Density
|
| 329 |
391 |
|
|