diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d9133c..6d02326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,8 @@ Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le pr ### Fixed +- **Le panneau de `ui-datepicker` débordait de l'écran sur un viewport étroit**. Trois causes cumulées, corrigées ensemble : le panneau était plafonné à `100vw - units-lg` mais la colonne de mois gardait une largeur fixe de 360px sans pouvoir rétrécir, donc la dernière colonne de jours et le chevron « suivant » passaient sous la bordure ; l'overlay CDK n'avait ni marge de viewport ni repoussage (`push`), donc un champ ancré n'importe où ailleurs qu'à gauche sortait le panneau hors de l'écran ; et en `numberOfMonths > 1` la rangée de mois faisait deux fois 360px, dont le second mois entièrement invisible. La colonne de mois et les grilles mois / année rétrécissent maintenant avec le panneau, la pastille d'un jour suit la largeur de sa colonne quand celle-ci passe sous `size/components/default` (la bande d'une plage la suit aussi), les mois s'empilent verticalement sous 600px avec un filet horizontal en séparateur, et l'overlay garde 12px de gouttière avec les bords du viewport. Le panneau défile désormais au-delà de `100dvh - units-3xl` plutôt que de déborder en hauteur (paysage sur téléphone), et un panneau `inline` ne dépasse plus la largeur de son conteneur. + - **Les `@media` générées depuis `responsive.json` sortaient dans l'ordre du JSON, pas par largeur croissante** — donc le desktop héritait des valeurs tablette. Les modes sont lus dans l'ordre où Figma les exporte (`modeDesktop`, `modeTablet`, `modeMobile`), et le bloc `@media (min-width: 1440px)` était émis **avant** `@media (min-width: 1024px)`. Sur un écran de 1440px les deux règles s'appliquent : à spécificité égale, c'est la dernière écrite qui gagne, et tout le desktop repartait sur l'échelle **tablette** (`--spacing-default: var(--units-md)` au lieu de `var(--units-default)`, `--grid-columns: 8` au lieu de `12`, toute la série `--size-components-*`, `--screen-width`/`--screen-height`). `scripts/tokens.build.mjs` trie maintenant les groupes `@media` par `min-width` croissante, puis par `max-width` décroissante — les deux conventions (mobile-first, desktop-first) placent ainsi le bloc le plus étroit en dernier, quel que soit l'ordre des modes dans le JSON source. Le tri comprend `min-width`/`max-width`, la syntaxe de plage (`width >= 900px`, `900px <= width`) et les unités `px`/`rem`/`em` ; une media query sans contrainte de largeur garde son ordre d'apparition. - **En thème sombre, les jours situés _dans_ une plage `ui-datepicker` étaient illisibles** (FSHSP-166). Le chiffre prenait `--form-high-content-default`, qui bascule au quasi-blanc en sombre, alors que le fond de la bande est le même lavande clair dans les deux thèmes : blanc sur lavande, soit un contraste d'environ 1,3:1. Il prend maintenant le token de contenu apparié à ce fond (`--informative-highlightlow-content-default`, identique en clair et en sombre), ce qui remonte le contraste à ~4,7:1 (AA). Le survol et le focus d'un jour de la plage basculent sur la pastille pleine `informative.highlightHigh`, elle aussi stable d'un thème à l'autre — l'ancien fond neutre virait au gris foncé en sombre, sous un chiffre désormais foncé. diff --git a/figma/README.md b/figma/README.md index a6cde2e..f9e3419 100644 --- a/figma/README.md +++ b/figma/README.md @@ -38,7 +38,7 @@ rather than adding them to `semantics` or `metrics`. variables are `remote`, hence read-only (see `CLAUDE.md`). - Keep the aliases as aliases: that is what preserves brand, light/dark and responsive. - Literal values are in `px` (converted from `rem`, base 16), like the token files. -- `$extensions.com.4sh.ui-kit.skipped` lists 52 entries with nothing to create: `calc()` +- `$extensions.com.4sh.ui-kit.skipped` lists 53 entries with nothing to create: `calc()` expressions, multi-value CSS shorthands, relative units (`em`, `ch`, `%`), duration / easing / cursor / z-index, and internal plumbing. diff --git a/figma/component-vars.json b/figma/component-vars.json index f1eceb0..1fc9744 100644 --- a/figma/component-vars.json +++ b/figma/component-vars.json @@ -13,7 +13,7 @@ "tokens": 646, "alias": 521, "literal": 125, - "skipped": 52, + "skipped": 53, "derivedDescriptions": 206 }, "aliasTargets": [ @@ -54,6 +54,11 @@ "component": "ui-config", "reason": "sans équivalent variable Figma (durée, easing, curseur, plan d’empilement, ratio)." }, + { + "cssVar": "--ui-datepicker-panel-max-height", + "component": "ui-datepicker", + "reason": "expression calc() : une variable Figma ne porte pas de calcul." + }, { "cssVar": "--ui-datepicker-panel-max-width", "component": "ui-datepicker", diff --git a/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.html b/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.html index 6389d4f..9b016a6 100644 --- a/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.html +++ b/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.html @@ -395,6 +395,8 @@ [cdkConnectedOverlayOrigin]="overlayOrigin()!" [cdkConnectedOverlayOpen]="panelOpen()" [cdkConnectedOverlayPositions]="overlayPositions()" + [cdkConnectedOverlayViewportMargin]="overlayViewportMargin" + [cdkConnectedOverlayPush]="true" [cdkConnectedOverlayHasBackdrop]="true" cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop" (backdropClick)="close(false)" diff --git a/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.scss b/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.scss index c54bea9..3d3b645 100644 --- a/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.scss +++ b/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.scss @@ -14,6 +14,7 @@ $panel-padding: var(--ui-datepicker-panel-padding, #{utils.$overlay-panel-paddin $panel-gap: var(--ui-datepicker-panel-gap, var(--units-sm)); /// Espace entre sections (header / grille / heure) $panel-shadow: var(--ui-datepicker-panel-shadow, #{utils.$overlay-panel-shadow}); /// Ombre du panneau en overlay (retirée en inline) $panel-max-width: var(--ui-datepicker-panel-max-width, calc(100vw - var(--units-lg))); /// Largeur maximale du panneau, gouttière laissée au viewport. +$panel-max-height: var(--ui-datepicker-panel-max-height, calc(100dvh - var(--units-3xl))); /// Hauteur maximale du panneau en overlay, au-delà de laquelle il défile. $day-size: var(--ui-datepicker-day-size, var(--size-components-default)); /// Diamètre d'une cellule jour $day-radius: var(--ui-datepicker-day-radius, var(--radius-full)); /// Rayon des cellules jour (cercle) @@ -36,6 +37,8 @@ $title-padding-y: var(--ui-datepicker-title-padding-y, var(--units-2xs)); $title-padding-x: var(--ui-datepicker-title-padding-x, var(--units-sm)); /// Inset horizontal du titre cliquable. $title-radius: var(--ui-datepicker-title-radius, var(--radius-sm)); /// Rayon du titre cliquable. +$months-stack-below: 37.5rem; // = breakpoint tabletPortrait : sous ce seuil deux mois ne tiennent plus côte à côte. + $months-gap: var(--ui-datepicker-months-gap, var(--units-lg)); /// Espacement entre les mois affichés côte à côte. $month-gap: var(--ui-datepicker-month-gap, var(--units-sm)); /// Espacement entre les sections d'une colonne de mois. $month-divider-padding-left: var(--ui-datepicker-month-divider-padding-left, var(--units-lg)); /// Retrait après le filet séparant deux mois. @@ -96,7 +99,15 @@ $buttonbar-padding-top: var(--ui-datepicker-buttonbar-padding-top, var(--units-s box-shadow: $panel-shadow; font-family: var(--fontfamily-base); - &._inline { box-shadow: none; } + &:not(._inline) { + max-height: $panel-max-height; + overflow: auto; + overscroll-behavior: contain; + } + &._inline { + box-shadow: none; + max-width: min(100%, #{$panel-max-width}); + } } // --- Header (month navigation) ------------------------------------- @@ -134,6 +145,7 @@ $buttonbar-padding-top: var(--ui-datepicker-buttonbar-padding-top, var(--units-s &-title { flex: 1 1 auto; + min-width: 0; text-align: center; font-size: $title-font-size; font-weight: var(--weight-bold); @@ -164,8 +176,10 @@ $buttonbar-padding-top: var(--ui-datepicker-buttonbar-padding-top, var(--units-s display: flex; flex-direction: column; gap: $month-gap; - flex: 0 0 auto; + flex: 0 1 auto; width: $panel-width; + min-width: 0; + max-width: 100%; &:not(:first-child) { border-left: $stroke-width solid var(--form-high-stroke-default); @@ -173,6 +187,16 @@ $buttonbar-padding-top: var(--ui-datepicker-buttonbar-padding-top, var(--units-s } } + @media (width < #{$months-stack-below}) { + &-months { flex-direction: column; } + &-month:not(:first-child) { + border-left: none; + padding-left: 0; + border-top: $stroke-width solid var(--form-high-stroke-default); + padding-top: $month-divider-padding-left; + } + } + // Per-panel header: [prev|spacer] title [next|spacer] — arrows at the outer // edges, title centred over its column and aligned with the chevrons. &-subheader { @@ -187,6 +211,7 @@ $buttonbar-padding-top: var(--ui-datepicker-buttonbar-padding-top, var(--units-s display: grid; gap: $picker-gap; width: $panel-width; + max-width: 100%; // The `role="row"` wrappers exist for ARIA only: the cells stay direct grid items. &-row { display: contents; } @@ -247,7 +272,7 @@ $buttonbar-padding-top: var(--ui-datepicker-buttonbar-padding-top, var(--units-s // --- Day cell ------------------------------------------------------ // Gutter left by a chip inside its (wider) grid column — where the range band's caps land. - $cap-inset: calc((100% - #{$day-size}) / 2); + $cap-inset: calc(max(0px, (100% - #{$day-size}) / 2)); &-day { position: relative; @@ -257,7 +282,8 @@ $buttonbar-padding-top: var(--ui-datepicker-buttonbar-padding-top, var(--units-s justify-content: center; box-sizing: border-box; width: 100%; - height: $day-size; + aspect-ratio: 1; + max-height: $day-size; padding: 0; border: none; background: transparent; @@ -274,8 +300,8 @@ $buttonbar-padding-top: var(--ui-datepicker-buttonbar-padding-top, var(--units-s left: 50%; transform: translate(-50%, -50%); box-sizing: border-box; - width: $day-size; - height: $day-size; + width: min(100%, #{$day-size}); + height: 100%; border: $stroke-width solid transparent; border-radius: $day-radius; z-index: -1; @@ -321,7 +347,7 @@ $buttonbar-padding-top: var(--ui-datepicker-buttonbar-padding-top, var(--units-s position: absolute; top: 50%; inset-inline: 0; - height: $day-size; + height: 100%; transform: translateY(-50%); background-color: $range-background; z-index: -2; @@ -373,6 +399,7 @@ $buttonbar-padding-top: var(--ui-datepicker-buttonbar-padding-top, var(--units-s display: flex; align-items: center; justify-content: center; + flex-wrap: wrap; gap: $time-gap; padding-top: $time-padding-top; border-top: $stroke-width solid var(--form-high-stroke-default); @@ -449,6 +476,7 @@ $buttonbar-padding-top: var(--ui-datepicker-buttonbar-padding-top, var(--units-s display: flex; align-items: center; justify-content: space-between; + flex-wrap: wrap; gap: $buttonbar-gap; padding-top: $buttonbar-padding-top; border-top: $stroke-width solid var(--form-high-stroke-default); diff --git a/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.ts b/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.ts index 8ccffbd..759608e 100644 --- a/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.ts +++ b/projects/ui-kit/forms/ui-datepicker/src/lib/ui-datepicker.ts @@ -375,6 +375,9 @@ export class UiDatepicker extends BaseFormField { /** @ignore Below the trigger, flipping above when `autoFlip` and space is lacking. */ protected readonly overlayPositions = computed(() => dropdownOverlayPositions(this.autoFlip())); + /** @ignore Gutter kept with the viewport edges, matching the one `$panel-max-width` + * reserves; with `cdkConnectedOverlayPush` it keeps the panel fully on screen. */ + protected readonly overlayViewportMargin = 12; /** @ignore Bound (stable) callbacks exposed to the `#buttonbar` template. */ protected readonly todayCallback = (event?: Event): void => { diff --git a/projects/ui-kit/forms/ui-datepicker/ui-datepicker.mdx b/projects/ui-kit/forms/ui-datepicker/ui-datepicker.mdx index 9156e92..78f8a8e 100644 --- a/projects/ui-kit/forms/ui-datepicker/ui-datepicker.mdx +++ b/projects/ui-kit/forms/ui-datepicker/ui-datepicker.mdx @@ -73,7 +73,8 @@ un mois / une année. `numberOfMonths` affiche N panneaux mois côte à côte, séparés par un filet, avec les titres de mois alignés en face des flèches ← → (aux extrémités). Le changement de vue à tiroir reste -réservé au mode mono-mois. +réservé au mode mono-mois. Sous 600px de viewport, les mois s'empilent verticalement et le +filet passe à l'horizontale. @@ -82,6 +83,11 @@ réservé au mode mono-mois. Le panneau s'ouvre sous le champ et **se retourne automatiquement au-dessus** quand la place manque en bas (CDK Overlay). `autoFlip="false"` verrouille l'ouverture vers le bas. +Sur un viewport étroit, le panneau reste entièrement à l'écran : il garde 12px de gouttière +avec les bords (l'overlay est repoussé quand l'ancrage seul n'y suffit pas), sa largeur se +plafonne à celle du viewport, et la grille des jours rétrécit avec lui plutôt que de déborder. +Au-delà de `--ui-datepicker-panel-max-height`, il défile. + ## Date & heure diff --git a/projects/ui-kit/styles/component-vars.scss b/projects/ui-kit/styles/component-vars.scss index ed858d2..ec04a25 100644 --- a/projects/ui-kit/styles/component-vars.scss +++ b/projects/ui-kit/styles/component-vars.scss @@ -11,7 +11,7 @@ // design token instead — it follows brand, light/dark and viewport. // // Generated by `npm run docs:config`, values read from the compiled CSS — do not -// edit here. 678 variables; 20 more exist whose value depends on the +// edit here. 679 variables; 20 more exist whose value depends on the // rendered variant, listed in each component’s “Theming” section in Storybook. // ===================================================================== @@ -146,6 +146,7 @@ --ui-datepicker-cell-min-height: var(--size-components-sm); --ui-datepicker-day-size: var(--size-components-default); --ui-datepicker-nav-size: var(--size-components-default); + --ui-datepicker-panel-max-height: calc(100dvh - var(--units-3xl)); --ui-datepicker-panel-max-width: calc(100vw - var(--units-lg)); --ui-datepicker-panel-width: 22.5rem; --ui-datepicker-time-btn-height: var(--size-components-2xs);