fix(ui): una cornice sola, e smette di spostarsi sotto le dita
Punto 14. La segnalazione dell'operatore del 28/07 non era stata circoscritta sul codice: «le dimensioni delle viste cambiano a seconda del menu». Sono quattro meccanismi distinti, tutti leggibili senza avere il tablet in mano. Ogni vista si dichiarava la propria larghezza: sette valori diversi su diciassette template, e nessuno coincideva con quello della navbar, che sta a max-w-7xl su tutte. Il contenuto era quindi disallineato dalla barra sopra di sé, e il disallineamento cambiava a ogni pagina. Il percorso dell'operatore faceva 1280 → 1024 → tutto schermo → 1280 in quattro passaggi. Il padding verticale mescolava py-8 e py-6 fra viste consecutive, così la prima card cambiava altezza a ogni schermata. La barra di scorrimento è classica da 8px e occupa spazio: una pagina lunga la mostrava, una corta no, e la schermata di misura la toglie sempre. A ogni navigazione tutto il contenuto centrato — navbar compresa — si spostava di 8px. La schermata di misura era alta 100vh, che su Android e iOS è l'altezza con la barra dell'indirizzo nascosta: su un tablet il piede, dove stanno «Fine ciclo misura» e il tastierino, finiva sotto il bordo. Ora: .tmf-page e .tmf-page-narrow in themes.css, con i valori esatti della navbar; scrollbar-gutter: stable; 100dvh dove serve. Due larghezze in tutto il prodotto al posto di sette, e il criterio è il tipo di pagina — liste, tabelle e tele stanno larghe, i moduli stanno stretti. Login e schermata di misura tengono la loro geometria, per i motivi scritti in docs/architecture/LAYOUT.md e ripetuti in EXEMPT dentro il test: la prima non ha navbar a cui allinearsi, la seconda non deve scorrere mentre si misura. Il test è statico e guarda i sorgenti: la vista scritta domani copierà la cornice dalla vicina, ed è lì che la deriva ricomincia. Resta da chiedere all'operatore su quale schermo l'ha vista: in verticale su un tablet quasi tutte quelle larghezze collassano e il difetto non si nota. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
|||||||
|
# Cornice delle pagine
|
||||||
|
|
||||||
|
Nasce dal punto 14 del documento del 28/07: *«le dimensioni delle viste cambiano a
|
||||||
|
seconda del menu; passando da una schermata all'altra la finestra non mantiene
|
||||||
|
proporzioni stabili»*. La segnalazione era di un operatore e non era ancora stata
|
||||||
|
circoscritta sul codice. Questo è l'esito della circoscrizione e la regola che ne
|
||||||
|
è uscita.
|
||||||
|
|
||||||
|
## Cosa succedeva
|
||||||
|
|
||||||
|
Tre meccanismi distinti, tutti verificabili sul codice senza avere il tablet in mano.
|
||||||
|
|
||||||
|
**1. Ogni vista si dichiarava la propria larghezza.** Sette valori diversi su
|
||||||
|
diciassette template, e nessuno coincideva con quello della navbar, che sta a
|
||||||
|
`max-w-7xl` su tutte le pagine. Il contenuto risultava quindi disallineato dalla
|
||||||
|
barra sopra di sé, e il disallineamento cambiava da pagina a pagina.
|
||||||
|
|
||||||
|
| vista | prima | dopo |
|
||||||
|
|---|---|---|
|
||||||
|
| `measure/select_recipe.html` | `max-w-7xl` `py-8` | `tmf-page` |
|
||||||
|
| `measure/task_list.html` | `max-w-5xl` `py-8` | `tmf-page` |
|
||||||
|
| `measure/task_complete.html` | `max-w-7xl` `py-6` | `tmf-page` |
|
||||||
|
| `maker/recipe_list.html` | `max-w-6xl` `py-8` | `tmf-page` |
|
||||||
|
| `maker/task_editor.html` | `max-w-5xl` `py-8` | `tmf-page` |
|
||||||
|
| `maker/task_drawing.html` | `max-w-5xl` `py-8` | `tmf-page` |
|
||||||
|
| `maker/recipe_preview.html` | `max-w-5xl` `py-8` | `tmf-page` |
|
||||||
|
| `admin/stations.html` | `max-w-7xl` `py-6` | `tmf-page` |
|
||||||
|
| `admin/users.html` | `max-w-7xl` `py-6` | `tmf-page` |
|
||||||
|
| `statistics/dashboard.html` | `max-w-7xl` `py-6` | `tmf-page` |
|
||||||
|
| `admin/settings.html` | `max-w-3xl` `py-6` | `tmf-page tmf-page-narrow` |
|
||||||
|
| `auth/profile.html` | `max-w-4xl` `py-8` | `tmf-page tmf-page-narrow` |
|
||||||
|
| `maker/recipe_editor.html` | `max-w-4xl` `py-8` | `tmf-page tmf-page-narrow` |
|
||||||
|
| `maker/version_history.html` | `max-w-4xl` `py-8` | `tmf-page tmf-page-narrow` |
|
||||||
|
| `errors/station_not_configured.html` | `max-w-2xl` `py-8` | `tmf-page tmf-page-narrow` |
|
||||||
|
|
||||||
|
Il percorso dell'operatore — quello da cui è arrivata la segnalazione — faceva
|
||||||
|
`1280px → 1024px → tutto schermo → 1280px` in quattro passaggi.
|
||||||
|
|
||||||
|
**2. Il padding verticale non era lo stesso.** `py-8` e `py-6` mescolati fra viste
|
||||||
|
consecutive: la prima card si trovava a un'altezza diversa a ogni cambio di
|
||||||
|
schermata.
|
||||||
|
|
||||||
|
**3. La barra di scorrimento appariva e spariva.** È una barra classica da 8px
|
||||||
|
(`themes.css`, `::-webkit-scrollbar`), quindi occupa spazio nel layout. Una pagina
|
||||||
|
lunga la mostrava, una corta no, e la schermata di misura la toglie sempre
|
||||||
|
(`body { overflow: hidden }`): a ogni navigazione tutto il contenuto centrato —
|
||||||
|
navbar compresa — si spostava di 8px in orizzontale.
|
||||||
|
|
||||||
|
## La regola
|
||||||
|
|
||||||
|
Due classi in `static/css/themes.css`, nessuna larghezza nei template.
|
||||||
|
|
||||||
|
- **`.tmf-page`** — la cornice: `max-width: 80rem`, padding `1rem / 1.5rem / 2rem`
|
||||||
|
ai tre breakpoint, `padding-block: 1.5rem`. Sono esattamente i valori della
|
||||||
|
navbar (`max-w-7xl px-4 sm:px-6 lg:px-8`), così il contenuto ci si allinea.
|
||||||
|
- **`.tmf-page-narrow`** — si aggiunge alla prima e stringe a `56rem`. È per i
|
||||||
|
moduli: un campo di testo largo 1280px non si compila meglio.
|
||||||
|
- **`html { scrollbar-gutter: stable }`** — lo spazio della barra è riservato
|
||||||
|
sempre, anche dove la pagina non scorre.
|
||||||
|
- **`body.h-screen { height: 100dvh }`** — la schermata di misura è alta quanto la
|
||||||
|
finestra vera. `100vh` su Android e iOS è l'altezza che la pagina avrebbe con la
|
||||||
|
barra dell'indirizzo nascosta: su un tablet il piede della schermata — dove
|
||||||
|
stanno «Fine ciclo misura» e il tastierino — finiva sotto il bordo, e ricompariva
|
||||||
|
quando la barra si ritraeva.
|
||||||
|
|
||||||
|
Due larghezze in tutto il prodotto al posto di sette, e il criterio è il tipo di
|
||||||
|
pagina: liste, tabelle e tele di disegno stanno larghe, i moduli stanno stretti.
|
||||||
|
|
||||||
|
## Le due eccezioni
|
||||||
|
|
||||||
|
Hanno geometria propria per un motivo, e sono elencate in
|
||||||
|
`tests/test_layout_shell.py` perché non sembrino dimenticate.
|
||||||
|
|
||||||
|
- **`auth/login.html`** — schermata piena senza navbar: non c'è niente a cui
|
||||||
|
allinearsi.
|
||||||
|
- **`measure/task_execute.html`** — la schermata di misura è un pannello a tutta
|
||||||
|
altezza che non deve scorrere mentre un operatore sta misurando
|
||||||
|
(`h-screen overflow-hidden`, footer nascosto). È l'unica vista in cui il cambio
|
||||||
|
di forma è voluto, ed è anche l'unica in cui l'operatore si ferma a lavorare:
|
||||||
|
entrarci e uscirne resta un salto, ma ora è l'unico.
|
||||||
|
|
||||||
|
## Cosa resta da verificare sul campo
|
||||||
|
|
||||||
|
La segnalazione non è mai stata riprodotta su un dispositivo: quanto sopra viene
|
||||||
|
dalla lettura del codice, non da una prova. Quello che è dimostrato è che il
|
||||||
|
layout *poteva* muoversi per quattro motivi distinti e che ora non può più per
|
||||||
|
nessuno dei quattro.
|
||||||
|
|
||||||
|
Se l'operatore vedesse ancora spostamenti, il sospetto successivo è la **tastiera
|
||||||
|
virtuale**: quando compare, il browser ridimensiona la finestra, e con `100dvh` la
|
||||||
|
schermata di misura si ridimensiona con lei invece di scorrere sotto. È il
|
||||||
|
comportamento giusto per il tastierino a schermo, ma va guardato con un calibro
|
||||||
|
USB collegato, dove la tastiera di sistema non dovrebbe comparire affatto.
|
||||||
|
|
||||||
|
Da chiedere all'operatore che ha aperto la segnalazione: **su quale schermo** l'ha
|
||||||
|
vista. Su un tablet in verticale la maggior parte delle larghezze qui sopra
|
||||||
|
collassa allo stesso valore e il difetto non si vede; su un monitor da scrivania
|
||||||
|
si vede tutto.
|
||||||
@@ -124,10 +124,66 @@ textarea:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Page shell — one frame for every view
|
||||||
|
|
||||||
|
Punto 14. Ogni schermata si disegnava la propria larghezza: sette valori
|
||||||
|
diversi fra le viste, nessuno allineato alla navbar, che sta a 80rem su
|
||||||
|
tutte. Passando da un menu all'altro il blocco di contenuto cambiava
|
||||||
|
larghezza e si spostava rispetto alla barra sopra di sé.
|
||||||
|
|
||||||
|
Qui la cornice è una sola e coincide con quella della navbar. Le pagine
|
||||||
|
che hanno bisogno di una colonna stretta - i moduli - la stringono al loro
|
||||||
|
interno: il bordo della pagina resta dov'è, cambia solo il contenuto.
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.tmf-page {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 80rem; /* == max-w-7xl, la stessa della navbar */
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: 1rem; /* == px-4 */
|
||||||
|
padding-block: 1.5rem; /* == py-6 */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.tmf-page { padding-inline: 1.5rem; } /* == sm:px-6 */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.tmf-page { padding-inline: 2rem; } /* == lg:px-8 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* I moduli restano su una colonna leggibile: un campo di testo largo 1280px non
|
||||||
|
si compila meglio, si compila peggio. Due larghezze in tutto il prodotto -
|
||||||
|
questa e quella sopra - al posto delle sette di prima. */
|
||||||
|
.tmf-page.tmf-page-narrow {
|
||||||
|
max-width: 56rem; /* == max-w-4xl */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Custom Scrollbar
|
Custom Scrollbar
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
|
/* Lo spazio della barra di scorrimento è riservato sempre, anche quando la
|
||||||
|
pagina non scorre. Senza, ogni passaggio fra una vista lunga e una corta -
|
||||||
|
e ogni ingresso nella schermata di misura, che blocca lo scorrimento -
|
||||||
|
spostava di 8px tutto il contenuto centrato, navbar compresa. */
|
||||||
|
html {
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* La schermata di misura è alta quanto la finestra vera, non quanto quella
|
||||||
|
teorica: su Android e iOS `100vh` è l'altezza che la pagina avrebbe con la
|
||||||
|
barra dell'indirizzo nascosta, quindi su un tablet il piede - dove stanno
|
||||||
|
«Fine ciclo misura» e il tastierino - finisce sotto il bordo dello schermo, e
|
||||||
|
ricompare quando la barra si ritrae. `100dvh` segue la finestra reale. */
|
||||||
|
@supports (height: 100dvh) {
|
||||||
|
body.h-screen {
|
||||||
|
height: 100dvh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Webkit (Chrome, Safari, Edge) */
|
/* Webkit (Chrome, Safari, Edge) */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% block title %}{{ _('Impostazioni') }} - TieMeasureFlow{% endblock %}
|
{% block title %}{{ _('Impostazioni') }} - TieMeasureFlow{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-6"
|
<div class="tmf-page tmf-page-narrow"
|
||||||
x-data="systemSettings()">
|
x-data="systemSettings()">
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
window.__allRecipes = {{ all_recipes|tojson }};
|
window.__allRecipes = {{ all_recipes|tojson }};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"
|
<div class="tmf-page"
|
||||||
x-data="stationManagement(window.__stations, window.__allRecipes)">
|
x-data="stationManagement(window.__stations, window.__allRecipes)">
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<script>window.__users = {{ users|tojson }};</script>
|
<script>window.__users = {{ users|tojson }};</script>
|
||||||
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"
|
<div class="tmf-page"
|
||||||
x-data="userManagement(window.__users)">
|
x-data="userManagement(window.__users)">
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% block title %}{{ _('Profilo') }} — TieMeasureFlow{% endblock %}
|
{% block title %}{{ _('Profilo') }} — TieMeasureFlow{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8 max-w-4xl">
|
<div class="tmf-page tmf-page-narrow">
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% block title %}{{ _('Stazione non configurata') }} — TieMeasureFlow{% endblock %}
|
{% block title %}{{ _('Stazione non configurata') }} — TieMeasureFlow{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container mx-auto px-4 py-8 max-w-2xl">
|
<div class="tmf-page tmf-page-narrow">
|
||||||
<div class="mt-20 p-8 bg-[var(--bg-card)] rounded-xl shadow-lg text-center border border-red-200 dark:border-red-800">
|
<div class="mt-20 p-8 bg-[var(--bg-card)] rounded-xl shadow-lg text-center border border-red-200 dark:border-red-800">
|
||||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full
|
||||||
bg-red-50 dark:bg-red-900/30 mb-6">
|
bg-red-50 dark:bg-red-900/30 mb-6">
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
{% set current_version = recipe.current_version if recipe and recipe.current_version else None %}
|
{% set current_version = recipe.current_version if recipe and recipe.current_version else None %}
|
||||||
{% set versions = recipe.versions if recipe and recipe.versions else [] %}
|
{% set versions = recipe.versions if recipe and recipe.versions else [] %}
|
||||||
|
|
||||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8 max-w-4xl"
|
<div class="tmf-page tmf-page-narrow"
|
||||||
x-data="recipeEditor()"
|
x-data="recipeEditor()"
|
||||||
x-cloak>
|
x-cloak>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<script>window.__recipeListData = {{ recipes|tojson }};</script>
|
<script>window.__recipeListData = {{ recipes|tojson }};</script>
|
||||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8 max-w-6xl"
|
<div class="tmf-page"
|
||||||
x-data="{
|
x-data="{
|
||||||
recipes: window.__recipeListData,
|
recipes: window.__recipeListData,
|
||||||
search: '{{ search or '' }}',
|
search: '{{ search or '' }}',
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</script>
|
</script>
|
||||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8 max-w-5xl"
|
<div class="tmf-page"
|
||||||
x-data="recipePreview()"
|
x-data="recipePreview()"
|
||||||
x-cloak>
|
x-cloak>
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
connectionError: {{ _("Errore di connessione al server")|tojson }}
|
connectionError: {{ _("Errore di connessione al server")|tojson }}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8 max-w-5xl"
|
<div class="tmf-page"
|
||||||
x-data="taskDrawing()"
|
x-data="taskDrawing()"
|
||||||
x-cloak>
|
x-cloak>
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8 max-w-5xl"
|
<div class="tmf-page"
|
||||||
x-data="taskEditor()"
|
x-data="taskEditor()"
|
||||||
x-cloak>
|
x-cloak>
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8 max-w-4xl"
|
<div class="tmf-page tmf-page-narrow"
|
||||||
x-data="versionHistory()"
|
x-data="versionHistory()"
|
||||||
x-init="loadMeasurementCounts()"
|
x-init="loadMeasurementCounts()"
|
||||||
x-cloak>
|
x-cloak>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<script>window.__selectRecipeData = {{ recipes|tojson }};</script>
|
<script>window.__selectRecipeData = {{ recipes|tojson }};</script>
|
||||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8 max-w-7xl"
|
<div class="tmf-page"
|
||||||
x-data="{
|
x-data="{
|
||||||
recipes: window.__selectRecipeData,
|
recipes: window.__selectRecipeData,
|
||||||
search: '{{ auto_recipe_code }}',
|
search: '{{ auto_recipe_code }}',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% block title %}{{ _('Riepilogo') }} - {{ recipe.name }}{% endblock %}
|
{% block title %}{{ _('Riepilogo') }} - {{ recipe.name }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
<div class="tmf-page">
|
||||||
{# The summary is where an operator lingers: the clock has to be here as well. #}
|
{# The summary is where an operator lingers: the clock has to be here as well. #}
|
||||||
{% include "components/production_clock.html" %}
|
{% include "components/production_clock.html" %}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% block title %}{{ recipe.name }} — {{ _('Task') }} — TieMeasureFlow{% endblock %}
|
{% block title %}{{ recipe.name }} — {{ _('Task') }} — TieMeasureFlow{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8 max-w-5xl">
|
<div class="tmf-page">
|
||||||
|
|
||||||
{# The clock follows the operator here too, and brings them back when it expires. #}
|
{# The clock follows the operator here too, and brings them back when it expires. #}
|
||||||
{% include "components/production_clock.html" %}
|
{% include "components/production_clock.html" %}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 py-6"
|
<div class="tmf-page"
|
||||||
x-data="spcDashboard()"
|
x-data="spcDashboard()"
|
||||||
x-init="init()">
|
x-init="init()">
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"""Point 14: the frame must not move when the operator changes view.
|
||||||
|
|
||||||
|
The report was «le dimensioni delle viste cambiano a seconda del menu». On the
|
||||||
|
code it was seven different page widths across seventeen templates, none of them
|
||||||
|
the width of the navbar sitting above them, plus a scrollbar that came and went.
|
||||||
|
|
||||||
|
This test guards the part that is easy to undo: the next page someone writes will
|
||||||
|
copy the shell from a neighbour, and if a neighbour has gone back to declaring its
|
||||||
|
own `container mx-auto ... max-w-5xl` the drift starts again.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
TEMPLATES = Path(__file__).resolve().parents[1] / "templates"
|
||||||
|
|
||||||
|
# Two views own their geometry on purpose, and say why here so that a reader does
|
||||||
|
# not have to guess whether they were forgotten:
|
||||||
|
# login — a full-viewport splash, no navbar to line up with;
|
||||||
|
# task_execute — the measurement screen, a fixed full-height instrument panel
|
||||||
|
# that must not scroll while an operator is measuring.
|
||||||
|
EXEMPT = {"auth/login.html", "measure/task_execute.html"}
|
||||||
|
|
||||||
|
# What a page must not go back to doing: setting its own width and padding.
|
||||||
|
FORBIDDEN = ("container mx-auto", "max-w-2xl mx-auto", "max-w-3xl mx-auto",
|
||||||
|
"max-w-4xl mx-auto", "max-w-5xl mx-auto", "max-w-6xl mx-auto",
|
||||||
|
"max-w-7xl mx-auto")
|
||||||
|
|
||||||
|
|
||||||
|
def _views():
|
||||||
|
for path in sorted(TEMPLATES.rglob("*.html")):
|
||||||
|
rel = path.relative_to(TEMPLATES).as_posix()
|
||||||
|
if rel.startswith("components/") or rel == "base.html":
|
||||||
|
continue
|
||||||
|
yield rel, path
|
||||||
|
|
||||||
|
|
||||||
|
def test_every_view_uses_the_shared_shell():
|
||||||
|
"""One frame, declared in one place, so it cannot drift per page."""
|
||||||
|
missing = [
|
||||||
|
rel for rel, path in _views()
|
||||||
|
if rel not in EXEMPT and "tmf-page" not in path.read_text(encoding="utf-8")
|
||||||
|
]
|
||||||
|
assert not missing, f"viste senza la cornice condivisa: {missing}"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("rel,path", list(_views()))
|
||||||
|
def test_no_view_declares_its_own_width(rel, path):
|
||||||
|
if rel in EXEMPT:
|
||||||
|
pytest.skip("geometria propria, documentata in EXEMPT")
|
||||||
|
html = path.read_text(encoding="utf-8")
|
||||||
|
found = [f for f in FORBIDDEN if f in html]
|
||||||
|
assert not found, (
|
||||||
|
f"{rel} torna a dichiarare la propria larghezza ({found}): "
|
||||||
|
"usare .tmf-page / .tmf-page-narrow"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_shell_matches_the_navbar():
|
||||||
|
"""The content lines up with the bar above it, or the page looks like it has
|
||||||
|
two widths at once - which is what the operator was seeing."""
|
||||||
|
navbar = (TEMPLATES / "components/navbar.html").read_text(encoding="utf-8")
|
||||||
|
assert "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8" in navbar, (
|
||||||
|
"la navbar ha cambiato cornice: allineare .tmf-page in themes.css"
|
||||||
|
)
|
||||||
|
css = (
|
||||||
|
TEMPLATES.parent / "static/css/themes.css"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
assert "max-width: 80rem;" in css # 80rem == max-w-7xl
|
||||||
|
assert "scrollbar-gutter: stable;" in css
|
||||||
Reference in New Issue
Block a user