fix(client): page sans overflow, listes scrollables, miniatures agrandies #6

Merged
kyuno merged 1 commit from fix/layout-scroll-and-thumbnails into main 2026-09-16 23:24:28 +02:00
5 changed files with 62 additions and 15 deletions
Showing only changes of commit d8dc263690 - Show all commits

View file

@ -1,10 +1,11 @@
.shell { .shell {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; height: 100vh;
margin: 0; margin: 0;
&__header { &__header {
flex: 0 0 auto;
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
background: var(--color-surface); background: var(--color-surface);
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
@ -17,13 +18,19 @@
} }
} }
// min-height: 0 est nécessaire ici (et à chaque niveau descendant
// jusqu'aux listes scrollables) : par défaut un enfant flex ne
// rétrécit jamais sous la taille de son contenu, ce qui empêcherait
// tout overflow interne et ferait déborder toute la page à la place.
&__main { &__main {
flex: 1; flex: 1 1 auto;
min-height: 0;
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
gap: 1.25rem; gap: 1.25rem;
padding: 1.5rem; padding: 1.5rem;
max-width: 960px; max-width: 1280px;
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
@ -31,16 +38,20 @@
@media (min-width: 800px) { @media (min-width: 800px) {
&__main { &__main {
grid-template-columns: 3fr 2fr; grid-template-columns: 2fr 1fr;
align-items: start; grid-template-rows: 1fr;
} }
} }
&__panel { &__panel {
min-width: 0; min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
background: var(--color-surface); background: var(--color-surface);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: 12px; border-radius: 12px;
padding: 1.25rem; padding: 1.25rem;
overflow: hidden;
} }
} }

View file

@ -22,6 +22,16 @@
box-sizing: border-box; box-sizing: border-box;
} }
// La page elle-même ne défile jamais : seules les listes internes (file
// d'attente, résultats de recherche) le font, voir Shell.scss et les
// vues concernées (chacune doit propager height:100%/min-height:0
// jusqu'à son propre élément scrollable pour que ça fonctionne).
html,
body {
height: 100%;
overflow: hidden;
}
body { body {
margin: 0; margin: 0;
background: var(--color-bg); background: var(--color-bg);

View file

@ -2,6 +2,16 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.85rem; gap: 0.85rem;
height: 100%;
min-height: 0;
// Par défaut, aucun enfant direct ne grandit/rétrécit (taille fixée à
// son contenu) - seul __results (règle plus bas, même spécificité
// mais déclarée après donc prioritaire) doit remplir l'espace restant
// et défiler en interne.
> * {
flex: 0 0 auto;
}
h2 { h2 {
margin: 0; margin: 0;
@ -83,26 +93,28 @@
padding: 0; padding: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 0.6rem;
max-height: 65vh; flex: 1 1 0;
min-height: 0;
overflow-y: auto; overflow-y: auto;
} }
&__result { &__result {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.85rem;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius); border-radius: var(--radius);
padding: 0.5rem; padding: 0.6rem;
} }
&__result-thumb { &__result-thumb {
width: 96px; width: 144px;
height: 54px; height: 81px;
object-fit: cover; object-fit: cover;
border-radius: 4px; border-radius: 6px;
flex-shrink: 0; flex-shrink: 0;
background: var(--color-bg);
} }
&__result-info { &__result-info {
@ -112,10 +124,11 @@
&__result-title { &__result-title {
margin: 0; margin: 0;
font-size: 0.9rem; font-size: 0.92rem;
white-space: nowrap; display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
} }
&__result-meta { &__result-meta {

View file

@ -2,6 +2,15 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.75rem; gap: 0.75rem;
height: 100%;
min-height: 0;
// Par défaut, aucun enfant direct ne grandit/rétrécit - seul __list
// (règle plus bas, même spécificité mais déclarée après donc
// prioritaire) doit remplir l'espace restant et défiler en interne.
> * {
flex: 0 0 auto;
}
h2 { h2 {
margin: 0; margin: 0;
@ -33,6 +42,9 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 0.5rem;
flex: 1 1 0;
min-height: 0;
overflow-y: auto;
} }
&__item { &__item {

View file

@ -38,6 +38,7 @@ Pas de `docker-compose.yml` pour l'instant (une seule image, pas encore de dépe
- `src/views/downloadForm/` : formulaire d'ajout, deux entrées possibles (URL directe, ou recherche de chaîne via `GET /api/channel` puis ajout par résultat) partageant un même sélecteur de mode (vidéo/audio/miniature + options). Le mode courant est lu depuis le DOM à l'ajout, pas stocké en état séparé. - `src/views/downloadForm/` : formulaire d'ajout, deux entrées possibles (URL directe, ou recherche de chaîne via `GET /api/channel` puis ajout par résultat) partageant un même sélecteur de mode (vidéo/audio/miniature + options). Le mode courant est lu depuis le DOM à l'ajout, pas stocké en état séparé.
- `src/views/queuePanel/` : sonde `GET /api/queue` toutes les 1,5s (pas de push temps réel pour l'instant, voir Statut) et affiche chaque élément (titre ou URL à défaut, statut, barre de progression, erreur). Un même bouton retire l'élément quel que soit son état - libellé "Annuler" s'il est en attente/en cours, "Supprimer" une fois terminé (`DELETE /api/queue/:id` gère les deux cas côté serveur). Lien "Télécharger" natif (`<a download>`) uniquement si un fichier est encore disponible ; bouton "Tout télécharger" dans l'en-tête qui déclenche, avec un léger décalage entre chacun (évite le blocage navigateur des téléchargements multiples simultanés), tous les téléchargements prêts. Expose `refresh()` pour un rafraîchissement immédiat après un ajout. - `src/views/queuePanel/` : sonde `GET /api/queue` toutes les 1,5s (pas de push temps réel pour l'instant, voir Statut) et affiche chaque élément (titre ou URL à défaut, statut, barre de progression, erreur). Un même bouton retire l'élément quel que soit son état - libellé "Annuler" s'il est en attente/en cours, "Supprimer" une fois terminé (`DELETE /api/queue/:id` gère les deux cas côté serveur). Lien "Télécharger" natif (`<a download>`) uniquement si un fichier est encore disponible ; bouton "Tout télécharger" dans l'en-tête qui déclenche, avec un léger décalage entre chacun (évite le blocage navigateur des téléchargements multiples simultanés), tous les téléchargements prêts. Expose `refresh()` pour un rafraîchissement immédiat après un ajout.
- Piège CSS relevé en testant dans un vrai navigateur (pas seulement les tests) : un attribut `hidden` combiné à une règle `display: flex` sur le même sélecteur se fait écraser par cette règle (même spécificité, la feuille de l'auteur gagne sur celle du user-agent) - toujours ajouter `&[hidden] { display: none; }` quand un conteneur togglé porte aussi un `display` explicite. - Piège CSS relevé en testant dans un vrai navigateur (pas seulement les tests) : un attribut `hidden` combiné à une règle `display: flex` sur le même sélecteur se fait écraser par cette règle (même spécificité, la feuille de l'auteur gagne sur celle du user-agent) - toujours ajouter `&[hidden] { display: none; }` quand un conteneur togglé porte aussi un `display` explicite.
- Layout en hauteur pleine page (`html`/`body`/`.shell` à `height:100%`/`100vh`, `overflow:hidden` sur `body`) : la page elle-même ne défile jamais, seules `download-form__results` et `queue-panel__list` le font (chacune `flex:1 1 0; min-height:0; overflow-y:auto` dans un parent qui propage `height:100%`/`min-height:0` depuis `.shell__panel`). `min-height: 0` est nécessaire à *chaque* niveau flex descendant - un enfant flex ne rétrécit jamais sous la taille de son contenu par défaut, ce qui ferait déborder la page entière au lieu de faire défiler juste la liste. Chaque vue neutralise le grow/shrink de ses enfants directs non-scrollables via `> * { flex: 0 0 auto; }`, puis la règle plus spécifique de l'élément scrollable (même spécificité, déclarée après donc prioritaire) le fait remplir l'espace restant.
- Tests : Vitest (`client/test/`), uniquement sur les fonctions pures (`format.ts`) pour l'instant - pas de configuration jsdom, le câblage DOM (formulaires, rendu de liste) est vérifié manuellement dans le navigateur plutôt que testé automatiquement. - Tests : Vitest (`client/test/`), uniquement sur les fonctions pures (`format.ts`) pour l'instant - pas de configuration jsdom, le câblage DOM (formulaires, rendu de liste) est vérifié manuellement dans le navigateur plutôt que testé automatiquement.
- `src/styles/base.scss` : palette et styles de base partagés (custom properties CSS sur `:root`, pas de `$variables` Sass - une variable Sass ne traverserait pas les fichiers, chaque vue étant compilée/importée séparément ; une custom property CSS si, puisqu'elle se résout à l'exécution dans le navigateur une fois tout le CSS regroupé par esbuild). Importé une fois depuis `app.ts`. - `src/styles/base.scss` : palette et styles de base partagés (custom properties CSS sur `:root`, pas de `$variables` Sass - une variable Sass ne traverserait pas les fichiers, chaque vue étant compilée/importée séparément ; une custom property CSS si, puisqu'elle se résout à l'exécution dans le navigateur une fois tout le CSS regroupé par esbuild). Importé une fois depuis `app.ts`.