[FIX] event header, proposals: unclipped warning, wider proposal column
The header warning sat in a zone that could not grow past the header content, so a five-line warning lost its last line at 1366 x 700. The zone now takes part in the layout: the header grows with it up to 7.25rem, then the zone scrolls; under a 70rem header it drops below at full width. The proposal cell gets a 14rem minimum, so the kept row no longer wraps word by word. Checked: node 4012, browser 868. --- FR --- [FIX] en-tête, propositions : avis jamais rogné, colonne plus large L'avis de l'en-tête vivait dans une zone qui ne dépassait pas le contenu de l'en-tête : un avis de cinq lignes perdait sa dernière à 1366 x 700. La zone prend part à la mise en page : l'en-tête grandit avec elle jusqu'à 7,25rem, puis elle défile ; sous un en-tête de 70rem, elle passe dessous en pleine largeur. La cellule de proposition prend 14rem au moins, la ligne retenue ne se replie plus mot à mot. Vérifié : node 4012, navigateur 868. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
27f652c732
commit
6f7cd3511b
7 changed files with 162 additions and 42 deletions
|
|
@ -12,13 +12,13 @@
|
|||
//
|
||||
// Chacun est un message d'avis : le fond, le filet et l'encre de l'avis,
|
||||
// le triangle de sa marque devant le texte. Le remède se pose en haut à
|
||||
// droite, sur la première ligne du texte, et non sous lui : dans la zone
|
||||
// des annonces, qui défile à la hauteur de la tête, l'action d'un
|
||||
// avertissement se voit dès que son début se voit, et n'est jamais
|
||||
// rognée par le bas de la zone. Ce qu'il nomme — propositions écartées,
|
||||
// placements en dérive — vient dessous, sur toute la largeur du message :
|
||||
// l'action n'étroitise que la première phrase. Une zone trop étroite pour
|
||||
// le texte et l'action côte à côte passe l'action sous le texte.
|
||||
// droite, sur la première ligne du texte ; une liste trop étroite pour le
|
||||
// texte et l'action côte à côte passe l'action sous le texte, qui garde
|
||||
// alors toute la largeur du message. Ce qu'il nomme — propositions
|
||||
// écartées, placements en dérive — vient dessous, sur toute la largeur du
|
||||
// message : l'action n'étroitise que la première phrase. La zone des
|
||||
// annonces qui les porte prend la hauteur d'un avertissement ordinaire,
|
||||
// action comprise, et défile au-delà (CadreMode.svelte).
|
||||
import { getContext } from 'svelte';
|
||||
import Commande from '../primitives/Commande.svelte';
|
||||
import Icone from '../primitives/Icone.svelte';
|
||||
|
|
@ -91,10 +91,9 @@
|
|||
{/if}
|
||||
|
||||
<style>
|
||||
/* Des messages serrés : dans la zone des annonces, à la hauteur de la
|
||||
tête, les deux avertissements d'un journal abîmé, de deux lignes
|
||||
chacun, se voient l'un et l'autre. La liste est un conteneur de
|
||||
largeur, que lit la disposition étroite ci-dessous. */
|
||||
/* Des messages serrés : la zone des annonces en montre davantage sous
|
||||
son plafond. La liste est un conteneur de largeur, que lit la
|
||||
disposition étroite ci-dessous. */
|
||||
.avertissements {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -144,8 +143,10 @@
|
|||
}
|
||||
|
||||
/* Une liste trop étroite pour le texte et l'action côte à côte — moins
|
||||
de 26rem — passe l'action sous le texte. */
|
||||
@container (max-width: 26rem) {
|
||||
de 30rem, comme la place des annonces à côté de la tête d'une fenêtre
|
||||
de 1 366 px — passe l'action sous le texte : le texte y garde toute la
|
||||
largeur du message, et tient en moins de lignes. */
|
||||
@container (max-width: 30rem) {
|
||||
p {
|
||||
grid-column: 2 / 4;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,11 +31,15 @@
|
|||
// devant un support rompu, dont les issues restent en vue.
|
||||
//
|
||||
// Sous une tête, et hors d'un support rompu, les annonces se posent à
|
||||
// côté d'elle, dans la place que sa navigation laisse à droite : leur zone
|
||||
// prend la hauteur de la tête et défile dedans, si bien qu'un
|
||||
// avertissement ne retire aucune hauteur à la surface, ni au plan qu'elle
|
||||
// porte. Une fenêtre trop étroite pour les deux les passe à la ligne, et
|
||||
// la zone garde alors sa hauteur minimale.
|
||||
// côté d'elle, dans la place que sa navigation laisse à droite. Leur zone
|
||||
// prend la hauteur de ce qu'elle porte, jusqu'à un plafond, et la tête
|
||||
// grandit avec elle : un avertissement s'y lit en entier, son action
|
||||
// comprise, jamais rogné par le bas de la zone ; au-delà du plafond, la
|
||||
// zone défile. Un avertissement qui tient dans la hauteur de la tête ne
|
||||
// retire donc aucune hauteur à la surface, ni au plan qu'elle porte, et
|
||||
// des avertissements nombreux ne lui retirent que ce qui sépare le
|
||||
// plafond de la hauteur de la tête. Une tête trop étroite pour les deux
|
||||
// passe la place à la ligne, sous son contenu, sur toute sa largeur.
|
||||
//
|
||||
// La tête se rend en un seul point du DOM, quelle que soit la place des
|
||||
// annonces : seules les annonces passent de côté à dessous quand une
|
||||
|
|
@ -341,18 +345,19 @@
|
|||
}
|
||||
|
||||
/* À côté de la tête : la tête passe en ligne, son contenu à gauche, la
|
||||
place des annonces à droite. La place ne donne à la ligne que sa
|
||||
hauteur minimale ; la zone des annonces la remplit, posée en absolu,
|
||||
et ne l'agrandit jamais : la ligne a la hauteur du contenu de la tête.
|
||||
La place prend une part fixe de la largeur, et le contenu le reste :
|
||||
place des annonces à droite. La ligne a la hauteur du plus haut des
|
||||
deux : le contenu de la tête, ou la zone des annonces. La place prend
|
||||
une part fixe de la largeur, et le contenu le reste :
|
||||
la zone ne bouge pas d'une section à l'autre, quand le libellé de la
|
||||
section ouverte, en gras, change la largeur de la navigation. Sa part,
|
||||
36 %, tient en deux lignes de Noto Sans chacun des deux avertissements
|
||||
d'un journal tronqué dans une fenêtre de 1 366 px : le second paraît
|
||||
dans la hauteur de la tête. Sous 30rem de contenu, la place passe à la
|
||||
ligne. */
|
||||
36 %, laisse au contenu les huit onglets sur une ligne dans une fenêtre
|
||||
de 1 366 px. Une tête de moins de 70rem — celle d'une fenêtre de
|
||||
1 024 px — passe la place à la ligne, sous le contenu, sur toute sa
|
||||
largeur, où un avertissement d'une phrase tient en deux lignes, son
|
||||
action à côté. La tête est pour cela un conteneur de largeur. */
|
||||
.tete.cote {
|
||||
flex-flow: row wrap;
|
||||
container: tete / inline-size;
|
||||
align-items: stretch;
|
||||
gap: 0 var(--espace-3);
|
||||
padding-right: 0;
|
||||
|
|
@ -372,19 +377,31 @@
|
|||
}
|
||||
|
||||
.place {
|
||||
position: relative;
|
||||
flex: 0 0 36%;
|
||||
min-width: 16rem;
|
||||
min-height: 3rem;
|
||||
border-left: 1px solid var(--couleur-carte-filet);
|
||||
}
|
||||
|
||||
/* À côté de la tête, la zone a un retrait vertical réduit : les deux
|
||||
avertissements d'un journal abîmé paraissent dans sa hauteur. */
|
||||
/* Passée à la ligne, la place prend toute la largeur de la tête, séparée
|
||||
du contenu par un filet au-dessus d'elle. */
|
||||
@container tete (max-width: 70rem) {
|
||||
.place {
|
||||
flex-basis: 100%;
|
||||
border-top: 1px solid var(--couleur-carte-filet);
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dans la place, la zone a un retrait vertical de 2 px et un plafond,
|
||||
retrait compris : 7,25rem tiennent entier, dans la place de 36 % d'une
|
||||
fenêtre de 1 366 px, un avertissement de trois lignes de texte et son
|
||||
action dessous (Avertissements.svelte). Au-delà, la zone défile, et la
|
||||
tête, qui ne grandit pas plus, laisse le reste du cadre à la surface. */
|
||||
.place > .annonces {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding-block: var(--espace-1);
|
||||
box-sizing: border-box;
|
||||
max-height: 7.25rem;
|
||||
padding-block: 0.125rem;
|
||||
}
|
||||
|
||||
/* Seules — sans tête, en plein écran, ou devant un support rompu —, les
|
||||
|
|
|
|||
|
|
@ -433,6 +433,41 @@ describe('les avertissements, en messages d’avis', () => {
|
|||
});
|
||||
}
|
||||
});
|
||||
// Le texte de l'avertissement et son action ne sont jamais rognés : ni
|
||||
// le texte, ni le message, ni aucune boîte qui les porte jusqu'au cadre
|
||||
// ne déborde de sa hauteur, et chacun tient dans la boîte visible de
|
||||
// chaque ascendant qui coupe ce qui le dépasse.
|
||||
for (const largeur of [1366, 1024]) {
|
||||
test(`à ${largeur} × 700, le texte de PLACES_MANQUANTES et « Ouvrir les tables » se lisent en entier : aucune boîte ne les rogne jusqu’au cadre`, async () => {
|
||||
await fixerCadre(largeur, 700);
|
||||
const m = await neuf();
|
||||
await enManque(m);
|
||||
const message = await attendre(() => {
|
||||
const element = dans(m, '[data-annonces] [data-avertissement="PLACES_MANQUANTES"]');
|
||||
expect(element).not.toBeNull();
|
||||
return element;
|
||||
});
|
||||
const texte = message.querySelector('p');
|
||||
const action = message.querySelector('[data-commande]');
|
||||
expect(texte.textContent).toBe(t('PLACES_MANQUANTES', { placesManquantes: 1 }));
|
||||
expect(action).not.toBeNull();
|
||||
const borne = cadre(m);
|
||||
for (let element = texte; element !== borne.parentElement; element = element.parentElement) {
|
||||
const nom = `${element.tagName.toLowerCase()}${element.hasAttribute('data-annonces') ? '[data-annonces]' : ''}`;
|
||||
expect(element.scrollHeight, `${nom} : hauteur du contenu`).toBeLessThanOrEqual(element.clientHeight + 1);
|
||||
if (element === texte || style(element).overflowY === 'visible') continue;
|
||||
const b = boite(element);
|
||||
const bas = b.top + element.clientTop + element.clientHeight;
|
||||
for (const [lu, porte] of [['texte', texte], ['action', action]]) {
|
||||
expect(boite(porte).top, `${lu} dans ${nom}`).toBeGreaterThanOrEqual(b.top + element.clientTop - 0.5);
|
||||
expect(boite(porte).bottom, `${lu} dans ${nom}`).toBeLessThanOrEqual(bas + 0.5);
|
||||
}
|
||||
}
|
||||
expect(visible(action)).toBe(true);
|
||||
expect(document.documentElement.scrollHeight).toBeLessThanOrEqual(innerHeight);
|
||||
expect(document.documentElement.scrollWidth).toBeLessThanOrEqual(innerWidth);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('le support rompu, en message de refus (§ 8.6)', () => {
|
||||
|
|
|
|||
|
|
@ -756,7 +756,7 @@ describe('l’écran tient dans la fenêtre, avertissements compris (§ 8.4)', (
|
|||
}
|
||||
});
|
||||
|
||||
test('à 1 366 × 700, les avertissements de dérive se posent à côté de la tête : le plan garde toute sa hauteur, et ses chaises au zoom ajusté', async () => {
|
||||
test('à 1 366 × 700, les avertissements de dérive se posent à côté de la tête : le plan ne cède que ce dont leur zone dépasse la tête, et garde ses chaises au zoom ajusté', async () => {
|
||||
const m = await monter();
|
||||
await m.seance.creer({ nom: 'Bal des Grisailles', date: null, siegesParDefaut: 8, tours: 3 });
|
||||
await m.seance.passerEnEcriture();
|
||||
|
|
@ -775,6 +775,10 @@ describe('l’écran tient dans la fenêtre, avertissements compris (§ 8.4)', (
|
|||
expect(dans(m, '[data-annonces]')).toBeNull();
|
||||
const hauteurSeule = dans(m, '[data-plan]').getBoundingClientRect().height;
|
||||
expect(hauteurSeule).toBeGreaterThan(0);
|
||||
// Le haut de la surface qui porte la section : la tête qui grandit le
|
||||
// pousse d'autant.
|
||||
const surface = () => dans(m, '[data-section]').parentElement.getBoundingClientRect();
|
||||
const hautSeul = surface().top;
|
||||
// Trois gestes admis sur le retenu, chacun une dérive annoncée.
|
||||
await m.seance.executer('ajouterParticipant', { champs: { nom: 'Rouvraie' } });
|
||||
await m.seance.executer('creerTable', { forme: 'ronde' });
|
||||
|
|
@ -790,7 +794,13 @@ describe('l’écran tient dans la fenêtre, avertissements compris (§ 8.4)', (
|
|||
expect(premier.top).toBeGreaterThanOrEqual(boiteZone.top);
|
||||
expect(premier.top).toBeLessThan(boiteZone.bottom - 16);
|
||||
expect(document.documentElement.scrollHeight).toBeLessThanOrEqual(innerHeight);
|
||||
expect(dans(m, '[data-plan]').getBoundingClientRect().height).toBeGreaterThanOrEqual(hauteurSeule - 1);
|
||||
// La zone, à son plafond, défile ; la tête ne grandit que de ce dont la
|
||||
// zone la dépasse, et le plan ne cède que cela.
|
||||
expect(zone.scrollHeight).toBeGreaterThan(zone.clientHeight);
|
||||
const pousse = surface().top - hautSeul;
|
||||
expect(pousse).toBeGreaterThanOrEqual(0);
|
||||
expect(pousse).toBeLessThan(boiteZone.height);
|
||||
expect(dans(m, '[data-plan]').getBoundingClientRect().height).toBeGreaterThanOrEqual(hauteurSeule - pousse - 1);
|
||||
const tables = tous(m, '[data-plan] [data-table]');
|
||||
expect(tables.length).toBe(4);
|
||||
expect(tous(m, '[data-plan] [data-siege]').length).toBe(4 * 8);
|
||||
|
|
|
|||
|
|
@ -36,10 +36,11 @@
|
|||
// écran, et à lui seul.
|
||||
//
|
||||
// Le support rompu et les avertissements se posent dans la zone des
|
||||
// annonces du cadre, qui défile seule, et non au-dessus de la section : la
|
||||
// section garde la hauteur de la surface, plan compris, quel que soit le
|
||||
// nombre des avertissements, et le support rompu, en tête de la zone,
|
||||
// garde ses issues en vue (CadreMode.svelte).
|
||||
// annonces du cadre, qui défile seule au-delà de son plafond, et non
|
||||
// au-dessus de la section : la section ne cède à la zone que ce dont elle
|
||||
// dépasse la tête, quel que soit le nombre des avertissements, et le
|
||||
// support rompu, en tête de la zone, garde ses issues en vue
|
||||
// (CadreMode.svelte).
|
||||
import { getContext } from 'svelte';
|
||||
import SectionBadges from '../badges/SectionBadges.svelte';
|
||||
import Diagnostic from '../diagnostic/Diagnostic.svelte';
|
||||
|
|
@ -170,8 +171,7 @@
|
|||
|
||||
/* Les phases côte à côte, séparées d'un blanc : les huit onglets tiennent
|
||||
sur une ligne à côté de la place des annonces dans une fenêtre de
|
||||
1 366 px, et un avertissement ne retire donc aucune hauteur au plan
|
||||
(CadreMode.svelte). */
|
||||
1 366 px (CadreMode.svelte). */
|
||||
nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
|||
|
|
@ -402,6 +402,56 @@ describe('retenir, voir sur le plan', () => {
|
|||
expect(ids(m)).toEqual(vue.lignes.map(({ id }) => id));
|
||||
});
|
||||
|
||||
// Les lignes du texte d'un élément : les boîtes de ses fragments de
|
||||
// texte, rangées de haut en bas, dont chacune ouvre une ligne quand elle
|
||||
// commence sous le milieu de la ligne courante.
|
||||
function lignesDuTexte(element) {
|
||||
const boites = [];
|
||||
const parcours = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
||||
for (let noeud = parcours.nextNode(); noeud !== null; noeud = parcours.nextNode()) {
|
||||
const plage = document.createRange();
|
||||
plage.selectNodeContents(noeud);
|
||||
boites.push(...[...plage.getClientRects()].filter((r) => r.width > 0 && r.height > 0));
|
||||
}
|
||||
boites.sort((a, b) => a.top - b.top);
|
||||
let lignes = 0;
|
||||
let milieu = -Infinity;
|
||||
for (const r of boites) {
|
||||
if (r.top < milieu) continue;
|
||||
lignes += 1;
|
||||
milieu = (r.top + r.bottom) / 2;
|
||||
}
|
||||
return lignes;
|
||||
}
|
||||
|
||||
test('à 1 366 px, la cellule de la ligne du retenu tient son numéro et « Tel que la proposition n° 3. » en trois lignes au plus', async () => {
|
||||
const m = await ouvrir(executer(avecPlans(chargeDemo('petite-conflit'), PLANS_CONFLIT), 'retenirProposition', { id: 3 }));
|
||||
const cellule = await attendre(() => {
|
||||
const trouvee = ligneRetenu(m)?.querySelector('td[data-colonne="proposition"]');
|
||||
expect(trouvee ?? null).not.toBeNull();
|
||||
return trouvee;
|
||||
});
|
||||
const vue = tableau(evaluation(charge(m)).table);
|
||||
const libelle = t(vue.retenu.libelle.cle, vue.retenu.libelle.details);
|
||||
expect(libelle).toBe('Tel que la proposition n° 3.');
|
||||
expect(texteDe(cellule)).toBe(`${format.nombre(3)} ${libelle}`);
|
||||
expect(lignesDuTexte(cellule)).toBeLessThanOrEqual(3);
|
||||
});
|
||||
|
||||
test('à 1 366 px, la cellule du retenu ajusté tient son numéro et « Ajusté à la main depuis la proposition n° 2. » en trois lignes au plus', async () => {
|
||||
const m = await ouvrir(chargeSix());
|
||||
commande(ligne(m, 2), 'retenir').click();
|
||||
await attendre(() => expect(charge(m).retenu?.proposition).toBe(2));
|
||||
await m.seance.executer('deplacerDansRetenu', { tour: 1, participant: 1, table: 2, siege: null });
|
||||
const libelle = t('propositions.retenu.ajuste', { proposition: 2 });
|
||||
const cellule = await attendre(() => {
|
||||
const trouvee = ligneRetenu(m).querySelector('td[data-colonne="proposition"]');
|
||||
expect(texteDe(trouvee)).toBe(`${format.nombre(2)} ${libelle}`);
|
||||
return trouvee;
|
||||
});
|
||||
expect(lignesDuTexte(cellule)).toBeLessThanOrEqual(3);
|
||||
});
|
||||
|
||||
test('« Voir » de la ligne du retenu montre le retenu sur le plan, après une proposition demandée, sans écriture', async () => {
|
||||
const m = await ouvrir(executer(avecPlans(chargeDemo('petite-conflit'), PLANS_CONFLIT), 'retenirProposition', { id: 3 }));
|
||||
m.ui.montrerProposition(1);
|
||||
|
|
|
|||
|
|
@ -323,8 +323,15 @@
|
|||
border-left: 3px solid var(--couleur-etat-retenu-marque);
|
||||
}
|
||||
|
||||
td[data-colonne='proposition'] {
|
||||
min-width: 7rem;
|
||||
/* Les colonnes de phrases ont la largeur d'une phrase courte : la
|
||||
cellule des propositions tient son numéro et « Tel que la proposition
|
||||
n° 3. » en deux lignes, « Ajusté à la main depuis… » en trois, et celle
|
||||
des retours imposés chacune de ses deux phrases en deux lignes au plus.
|
||||
Plus étroites, leurs phrases passeraient à la ligne mot à mot et
|
||||
allongeraient la ligne entière ; le tableau défile déjà en largeur. */
|
||||
td[data-colonne='proposition'],
|
||||
td[data-colonne='imposes'] {
|
||||
min-width: 14rem;
|
||||
}
|
||||
|
||||
.identifiant {
|
||||
|
|
|
|||
Loading…
Reference in a new issue