@hgdc/ui 0.63.0 → 0.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/account-menu.d.ts +4 -4
- package/dist/account-menu.js +26 -8
- package/dist/app-shell.d.ts +13 -14
- package/dist/app-shell.js +23 -15
- package/dist/email.js +1 -1
- package/dist/family-svg.js +2 -2
- package/dist/module-bar-store.d.ts +9 -0
- package/dist/module-bar-store.js +56 -0
- package/dist/module-bar.d.ts +16 -0
- package/dist/module-bar.js +91 -0
- package/dist/modules.d.ts +31 -0
- package/dist/modules.js +47 -0
- package/dist/org-placeholder.d.ts +7 -0
- package/dist/org-placeholder.js +10 -0
- package/dist/org-switcher.d.ts +2 -4
- package/dist/org-switcher.js +3 -15
- package/dist/product-name.d.ts +3 -0
- package/dist/product-name.js +7 -0
- package/dist/products.d.ts +5 -5
- package/dist/products.js +10 -10
- package/dist/shell-frame.d.ts +6 -9
- package/dist/shell-frame.js +11 -15
- package/dist/shell-links.d.ts +4 -1
- package/dist/shell-links.js +12 -5
- package/dist/shell-memory.d.ts +0 -8
- package/dist/shell-memory.js +9 -36
- package/dist/shell-placement.d.ts +17 -11
- package/dist/shell-placement.js +9 -9
- package/dist/sidebar-head.d.ts +9 -1
- package/dist/sidebar-head.js +9 -1
- package/dist/sidebar-nav.d.ts +5 -1
- package/dist/sidebar-nav.js +5 -3
- package/dist/use-product-leave.js +0 -1
- package/email-icons/host.png +0 -0
- package/family/app/host/apple-touch-icon.png +0 -0
- package/family/app/host/favicon.ico +0 -0
- package/family/app/host/favicon.svg +1 -1
- package/family/app/host/icon-192.png +0 -0
- package/family/app/host/icon-512.png +0 -0
- package/family/app/host/icon-maskable-512.png +0 -0
- package/family/host-dark.svg +1 -1
- package/family/host.svg +1 -1
- package/family/tile/host.svg +1 -1
- package/package.json +1 -1
- package/styles.css +12 -56
- package/dist/product-rail.d.ts +0 -41
- package/dist/product-rail.js +0 -183
package/styles.css
CHANGED
|
@@ -19,11 +19,8 @@
|
|
|
19
19
|
|
|
20
20
|
@custom-variant dark (&:is(.dark *));
|
|
21
21
|
/* Barre latérale réduite en rail : `data-sidebar="rail"` sur <html>, posé avant la première
|
|
22
|
-
peinture par `sidebarInitScript` (`@hgdc/ui/app-shell`)
|
|
23
|
-
|
|
24
|
-
@custom-variant rail (&:where([data-sidebar="rail"] *, [data-rail-peek] *));
|
|
25
|
-
/* Pliée par la personne seulement (pas le temps du survol de la barre des produits). */
|
|
26
|
-
@custom-variant folded (&:where([data-sidebar="rail"] *));
|
|
22
|
+
peinture par `sidebarInitScript` (`@hgdc/ui/app-shell`). */
|
|
23
|
+
@custom-variant rail (&:where([data-sidebar="rail"] *));
|
|
27
24
|
/* Écran tactile, quelle que soit la largeur : `coarse:min-h-11` rend ses 44 px à une entrée que
|
|
28
25
|
`lg:` avait resserrée pour la souris. Déclarée après les points de rupture, elle les emporte. */
|
|
29
26
|
@custom-variant coarse (@media (pointer: coarse));
|
|
@@ -772,10 +769,9 @@
|
|
|
772
769
|
|
|
773
770
|
/* Scène du chargement (ADR 0015) : la zone de contenu de la coque, calculée depuis l'écran, pour
|
|
774
771
|
que le H soit au même pixel dans la coque, avant elle (rendu serveur, page d'entrée) et d'un
|
|
775
|
-
produit à l'autre. Téléphone : sous l'en-tête de 56 px. Ordinateur : barre
|
|
776
|
-
|
|
772
|
+
produit à l'autre. Téléphone : sous l'en-tête de 56 px. Ordinateur : barre latérale
|
|
773
|
+
(256 px, 64 en rail), marge de 8 px, en-tête de 56 px. */
|
|
777
774
|
:root {
|
|
778
|
-
--hgdc-rail-w: 0px;
|
|
779
775
|
--hgdc-content-left: 0px;
|
|
780
776
|
--hgdc-stage-top: calc(env(safe-area-inset-top, 0px) + 3.5rem);
|
|
781
777
|
--hgdc-stage-right: 0px;
|
|
@@ -784,9 +780,8 @@
|
|
|
784
780
|
}
|
|
785
781
|
@media (width >= 64rem) {
|
|
786
782
|
:root {
|
|
787
|
-
--hgdc-rail-w: 3.5rem;
|
|
788
783
|
--hgdc-sidebar-w: 16rem;
|
|
789
|
-
--hgdc-content-left:
|
|
784
|
+
--hgdc-content-left: var(--hgdc-sidebar-w);
|
|
790
785
|
--hgdc-stage-top: 4rem;
|
|
791
786
|
--hgdc-stage-right: 0.5rem;
|
|
792
787
|
--hgdc-stage-bottom: 0.5rem;
|
|
@@ -795,9 +790,6 @@
|
|
|
795
790
|
:root[data-sidebar="rail"] {
|
|
796
791
|
--hgdc-sidebar-w: 4rem;
|
|
797
792
|
}
|
|
798
|
-
:root:has([data-slot="app-shell"]:not([data-product-rail])) {
|
|
799
|
-
--hgdc-rail-w: 0px;
|
|
800
|
-
}
|
|
801
793
|
}
|
|
802
794
|
|
|
803
795
|
/* Le chargeur du document (`FamilyLoaderRoot`) : un seul H par page, en position fixe sur la scène.
|
|
@@ -844,21 +836,17 @@
|
|
|
844
836
|
:root:not(:has([data-slot="app-shell"])) [data-slot="family-loader-root"] > [data-frame] {
|
|
845
837
|
display: block;
|
|
846
838
|
}
|
|
847
|
-
/* Sur ordinateur, le cadre est la coque elle-même (`ShellFrame`) :
|
|
848
|
-
|
|
839
|
+
/* Sur ordinateur, le cadre est la coque elle-même (`ShellFrame`) : organisation retenue,
|
|
840
|
+
nom du module, navigation en squelette, carte du contenu (ADR 0020). */
|
|
849
841
|
@media (width >= 64rem) {
|
|
850
842
|
[data-slot="family-loader-root"] > [data-frame] {
|
|
851
843
|
background: var(--shell);
|
|
852
844
|
}
|
|
853
845
|
}
|
|
854
|
-
/*
|
|
855
|
-
produit courant marqué dans la barre, comme `aria-current` dans la coque. */
|
|
846
|
+
/* Le nom du module de l'adresse (`<html data-hgdc-product>`, `sidebarInitScript`), seul. */
|
|
856
847
|
[data-slot="shell-frame"] [data-product] {
|
|
857
848
|
display: none;
|
|
858
849
|
}
|
|
859
|
-
[data-slot="shell-frame-rail"] [data-product] {
|
|
860
|
-
display: flex;
|
|
861
|
-
}
|
|
862
850
|
:root[data-hgdc-product="account"] [data-slot="shell-frame"] [data-product="account"],
|
|
863
851
|
:root[data-hgdc-product="host"] [data-slot="shell-frame"] [data-product="host"],
|
|
864
852
|
:root[data-hgdc-product="mail"] [data-slot="shell-frame"] [data-product="mail"],
|
|
@@ -871,20 +859,6 @@
|
|
|
871
859
|
:root[data-hgdc-product="manager"] [data-slot="shell-frame"] [data-product="manager"] {
|
|
872
860
|
display: contents;
|
|
873
861
|
}
|
|
874
|
-
:root[data-hgdc-product="account"] [data-slot="shell-frame-rail"] [data-product="account"],
|
|
875
|
-
:root[data-hgdc-product="host"] [data-slot="shell-frame-rail"] [data-product="host"],
|
|
876
|
-
:root[data-hgdc-product="mail"] [data-slot="shell-frame-rail"] [data-product="mail"],
|
|
877
|
-
:root[data-hgdc-product="portal"] [data-slot="shell-frame-rail"] [data-product="portal"],
|
|
878
|
-
:root[data-hgdc-product="status"] [data-slot="shell-frame-rail"] [data-product="status"],
|
|
879
|
-
:root[data-hgdc-product="analytics"] [data-slot="shell-frame-rail"] [data-product="analytics"],
|
|
880
|
-
:root[data-hgdc-product="docs"] [data-slot="shell-frame-rail"] [data-product="docs"],
|
|
881
|
-
:root[data-hgdc-product="iassistant"] [data-slot="shell-frame-rail"] [data-product="iassistant"],
|
|
882
|
-
:root[data-hgdc-product="id"] [data-slot="shell-frame-rail"] [data-product="id"],
|
|
883
|
-
:root[data-hgdc-product="manager"] [data-slot="shell-frame-rail"] [data-product="manager"] {
|
|
884
|
-
display: flex;
|
|
885
|
-
background: var(--shell-active);
|
|
886
|
-
color: var(--shell-foreground);
|
|
887
|
-
}
|
|
888
862
|
/* Navigation en squelette, à la hauteur retenue pour ce produit (`--hgdc-nav-height`). */
|
|
889
863
|
[data-slot="shell-frame-nav"] {
|
|
890
864
|
max-height: var(--hgdc-nav-height, 7.5rem);
|
|
@@ -922,7 +896,7 @@
|
|
|
922
896
|
}
|
|
923
897
|
|
|
924
898
|
/* Passage d'un produit à l'autre sous la même origine (`app.hgdc.ch/<produit>`, ADR 0020) : une
|
|
925
|
-
transition de vue entre documents. La barre
|
|
899
|
+
transition de vue entre documents. La barre latérale et la barre du haut
|
|
926
900
|
gardent leur place (même nom des deux côtés : elles ne bougent pas, leur contenu passe en
|
|
927
901
|
fondu) ; le reste de la page passe en fondu court. Le navigateur garde l'ancienne page à l'écran
|
|
928
902
|
jusqu'à ce que la nouvelle soit prête : pas de page blanche. Chrome, Edge et Safari 18.2+ ;
|
|
@@ -934,9 +908,6 @@
|
|
|
934
908
|
navigation: auto;
|
|
935
909
|
}
|
|
936
910
|
}
|
|
937
|
-
[data-slot="product-rail"] {
|
|
938
|
-
view-transition-name: hgdc-product-rail;
|
|
939
|
-
}
|
|
940
911
|
[data-slot="app-shell-sidebar"] {
|
|
941
912
|
view-transition-name: hgdc-sidebar;
|
|
942
913
|
}
|
|
@@ -946,9 +917,6 @@
|
|
|
946
917
|
/* Le cadre du chargeur (hors de la coque) et le H lui-même portent les mêmes noms, seulement quand
|
|
947
918
|
ils sont à l'écran : mêmes conditions que l'affichage du chargeur, plus haut. */
|
|
948
919
|
:root:has([data-slot="page-pending"]):not(:has([data-slot="app-shell"])) {
|
|
949
|
-
& [data-slot="shell-frame-rail"] {
|
|
950
|
-
view-transition-name: hgdc-product-rail;
|
|
951
|
-
}
|
|
952
920
|
& [data-slot="shell-frame-sidebar"] {
|
|
953
921
|
view-transition-name: hgdc-sidebar;
|
|
954
922
|
}
|
|
@@ -963,12 +931,6 @@
|
|
|
963
931
|
view-transition-name: hgdc-loader;
|
|
964
932
|
}
|
|
965
933
|
}
|
|
966
|
-
/* Un clic dans la barre des produits : la barre se replie et la barre latérale revient d'un coup,
|
|
967
|
-
pour que l'image de départ soit déjà celle de l'arrivée (`ProductRail`). */
|
|
968
|
-
:root[data-hgdc-switching] [data-slot="product-rail"],
|
|
969
|
-
:root[data-hgdc-switching] [data-slot="app-shell-sidebar"] {
|
|
970
|
-
transition: none;
|
|
971
|
-
}
|
|
972
934
|
::view-transition-old(root),
|
|
973
935
|
::view-transition-new(root) {
|
|
974
936
|
animation-duration: var(--duration-fast);
|
|
@@ -985,27 +947,21 @@
|
|
|
985
947
|
animation-duration: var(--duration-base);
|
|
986
948
|
animation-timing-function: var(--ease-ios);
|
|
987
949
|
}
|
|
988
|
-
/*
|
|
989
|
-
est
|
|
990
|
-
|
|
991
|
-
une page prête sans H), ils apparaissent ou s'effacent en fondu court. */
|
|
992
|
-
::view-transition-group(hgdc-product-rail),
|
|
950
|
+
/* Le H : identique des deux côtés, à la même phase, calé sur l'horloge. Ni fondu ni mouvement :
|
|
951
|
+
l'ancienne image est masquée, la nouvelle, vivante, s'affiche telle quelle. Présent d'un seul côté
|
|
952
|
+
(une page prête sans H), il apparaît ou s'efface en fondu court. */
|
|
993
953
|
::view-transition-group(hgdc-loader),
|
|
994
|
-
::view-transition-new(hgdc-product-rail),
|
|
995
954
|
::view-transition-new(hgdc-loader) {
|
|
996
955
|
animation: none;
|
|
997
956
|
}
|
|
998
|
-
::view-transition-old(hgdc-product-rail),
|
|
999
957
|
::view-transition-old(hgdc-loader) {
|
|
1000
958
|
animation: none;
|
|
1001
959
|
opacity: 0;
|
|
1002
960
|
}
|
|
1003
|
-
::view-transition-old(hgdc-product-rail):only-child,
|
|
1004
961
|
::view-transition-old(hgdc-loader):only-child {
|
|
1005
962
|
opacity: 1;
|
|
1006
963
|
animation: hgdc-view-out var(--duration-fast) var(--ease-out) both;
|
|
1007
964
|
}
|
|
1008
|
-
::view-transition-new(hgdc-product-rail):only-child,
|
|
1009
965
|
::view-transition-new(hgdc-loader):only-child {
|
|
1010
966
|
animation: hgdc-view-in var(--duration-fast) var(--ease-out) both;
|
|
1011
967
|
}
|
package/dist/product-rail.d.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { type ReactNode } from "react";
|
|
2
|
-
import { type HgdcCatalogProduct } from "./products.ts";
|
|
3
|
-
/** Délai d'intention avant d'élargir la barre au survol : un passage de souris ne l'ouvre pas. */
|
|
4
|
-
export declare const PRODUCT_RAIL_OPEN_DELAY = 150;
|
|
5
|
-
/**
|
|
6
|
-
* La place de l'organisation tant que le produit ne l'a pas (ses données arrivent) : la marque de
|
|
7
|
-
* la dernière organisation affichée sous cette origine, au pixel près de celle d'`OrgSwitcher`
|
|
8
|
-
* (initiales ou logo, posés sur `<html>` par `sidebarInitScript` avant la première peinture),
|
|
9
|
-
* sinon un carré de squelette de la même taille. Rien ne bouge quand la vraie arrive.
|
|
10
|
-
*/
|
|
11
|
-
export declare function OrgPlaceholder(): React.JSX.Element;
|
|
12
|
-
/**
|
|
13
|
-
* La barre des produits du cadre de chargement (`FamilyLoaderRoot`, hors de la coque) : la même
|
|
14
|
-
* colonne que `ProductRail` repliée, `OrgPlaceholder` en haut, les mêmes pastilles, sans liens. Le
|
|
15
|
-
* produit courant est marqué par le CSS (`<html data-hgdc-product>`, posé par
|
|
16
|
-
* `sidebarInitScript`) : au rendu serveur, le kit ne connaît pas l'adresse.
|
|
17
|
-
*/
|
|
18
|
-
export declare function ProductRailFrame(): React.JSX.Element;
|
|
19
|
-
/**
|
|
20
|
-
* La barre des produits de la famille, tout à gauche de la coque sur ordinateur : organisation en
|
|
21
|
-
* haut, Account, puis les pastilles des produits. 56 px de large ; au survol (après un court délai
|
|
22
|
-
* d'intention) ou au focus clavier, elle s'élargit et écrit le nom de chaque produit. Elle ne
|
|
23
|
-
* recouvre jamais la page : `AppShell` plie la barre latérale du produit le temps du survol, la
|
|
24
|
-
* barre prend exactement la place libérée (248 px) ; si la barre latérale est déjà pliée, elle
|
|
25
|
-
* pousse la page (220 px). Un menu ouvert depuis la barre (sélecteur d'organisation) la garde
|
|
26
|
-
* élargie ; tout revient quand la souris est sortie et que le menu est fermé.
|
|
27
|
-
*/
|
|
28
|
-
export declare function ProductRail({ current, organization, accountHref, catalog, onExpandedChange, }: {
|
|
29
|
-
/** Le mot du produit affiché (« host ») : marqué en teinte. */
|
|
30
|
-
current?: string | undefined;
|
|
31
|
-
/**
|
|
32
|
-
* `OrgSwitcher`, rendu compact. Absent (`undefined` : les données du produit arrivent), sa
|
|
33
|
-
* place est tenue par `OrgPlaceholder`. `null` : pas d'organisation, rien en haut.
|
|
34
|
-
*/
|
|
35
|
-
organization?: ReactNode;
|
|
36
|
-
/** Adresse d'Account (un Account local en développement). */
|
|
37
|
-
accountHref?: string | undefined;
|
|
38
|
-
catalog?: ReadonlyArray<HgdcCatalogProduct> | undefined;
|
|
39
|
-
/** Prévenu à chaque ouverture et fermeture : `AppShell` y plie sa barre latérale. */
|
|
40
|
-
onExpandedChange?: ((expanded: boolean) => void) | undefined;
|
|
41
|
-
}): React.JSX.Element;
|
package/dist/product-rail.js
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
-
import { ProductBadge } from "./product-badge.js";
|
|
4
|
-
import { PRODUCT_PRERENDER_ATTRIBUTE } from "./product-switch.js";
|
|
5
|
-
import { productRailEntries } from "./products.js";
|
|
6
|
-
import { markProductSwitch, takeProductSwitch } from "./shell-memory.js";
|
|
7
|
-
import { ShellPlacementContext, useLeaveOnClick } from "./shell-placement.js";
|
|
8
|
-
import { cn } from "./cn.js";
|
|
9
|
-
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
10
|
-
const noSubscription = () => () => { };
|
|
11
|
-
/** `location.origin` après l'hydratation, `undefined` au rendu serveur (liens absolus d'abord). */
|
|
12
|
-
function usePageOrigin() {
|
|
13
|
-
return useSyncExternalStore(noSubscription, () => window.location.origin, () => undefined);
|
|
14
|
-
}
|
|
15
|
-
/** Délai d'intention avant d'élargir la barre au survol : un passage de souris ne l'ouvre pas. */
|
|
16
|
-
export const PRODUCT_RAIL_OPEN_DELAY = 150;
|
|
17
|
-
/** Boîte d'une entrée de la barre : la même pour un lien et pour sa place dans le cadre de chargement. */
|
|
18
|
-
const PRODUCT_ITEM = "flex h-10 items-center gap-3 rounded-xl px-2 text-shell-muted coarse:h-11";
|
|
19
|
-
/** Colonne de la barre (56 px) et son contenu : les mêmes pour la barre et pour le cadre de chargement. */
|
|
20
|
-
const RAIL_COLUMN = "hidden w-14 shrink-0 overflow-hidden lg:block";
|
|
21
|
-
const RAIL_INNER = "group/rail flex h-full w-full flex-col gap-2 overflow-hidden bg-shell-rail px-2 py-3 text-shell-foreground";
|
|
22
|
-
const RAIL_LIST = "grid min-h-0 gap-1 overflow-x-hidden overflow-y-auto";
|
|
23
|
-
function ProductLabel({ entry }) {
|
|
24
|
-
return (_jsxs(_Fragment, { children: [_jsx(ProductBadge, { product: entry.badge, className: "size-6" }), _jsxs("span", { className: "min-w-0 flex-1 truncate opacity-0 transition-opacity duration-(--duration-fast) ease-out group-data-expanded/rail:opacity-100 motion-reduce:transition-none", children: [_jsx("span", { className: "font-heading text-sm font-medium", children: entry.name }), entry.hint ? _jsx("span", { className: "sr-only", children: ", " }) : null, entry.hint ? _jsx("span", { className: "ml-1.5 text-xs text-shell-muted", children: entry.hint }) : null] })] }));
|
|
25
|
-
}
|
|
26
|
-
function ProductLink({ entry }) {
|
|
27
|
-
// Chaque produit s'ouvre en document complet : la coque montre le H dès le clic.
|
|
28
|
-
const leaveOnClick = useLeaveOnClick();
|
|
29
|
-
return (_jsx("li", { children: _jsx("a", { href: entry.href, onClick: leaveOnClick, ...(entry.sameOrigin ? { [PRODUCT_PRERENDER_ATTRIBUTE]: "" } : {}), "aria-current": entry.current ? "page" : undefined, className: cn(PRODUCT_ITEM, "outline-none transition-colors duration-(--duration-fast) ease-out", "hover:bg-shell-hover hover:text-shell-foreground focus-visible:ring-2 focus-visible:ring-ring/50", "aria-[current=page]:bg-shell-active aria-[current=page]:text-shell-foreground"), children: _jsx(ProductLabel, { entry: entry }) }) }));
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* La place de l'organisation tant que le produit ne l'a pas (ses données arrivent) : la marque de
|
|
33
|
-
* la dernière organisation affichée sous cette origine, au pixel près de celle d'`OrgSwitcher`
|
|
34
|
-
* (initiales ou logo, posés sur `<html>` par `sidebarInitScript` avant la première peinture),
|
|
35
|
-
* sinon un carré de squelette de la même taille. Rien ne bouge quand la vraie arrive.
|
|
36
|
-
*/
|
|
37
|
-
export function OrgPlaceholder() {
|
|
38
|
-
return (_jsx("div", { "data-slot": "org-placeholder", "aria-hidden": true, className: "flex h-10 w-full shrink-0 items-center px-1 coarse:h-11", children: _jsx("span", { "data-slot": "org-placeholder-mark", className: "grid size-8 shrink-0 place-items-center rounded-lg text-xs font-semibold uppercase" }) }));
|
|
39
|
-
}
|
|
40
|
-
function RailDivider() {
|
|
41
|
-
return _jsx("div", { "aria-hidden": true, className: "mx-2 h-px shrink-0 bg-foreground/8" });
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* La barre des produits du cadre de chargement (`FamilyLoaderRoot`, hors de la coque) : la même
|
|
45
|
-
* colonne que `ProductRail` repliée, `OrgPlaceholder` en haut, les mêmes pastilles, sans liens. Le
|
|
46
|
-
* produit courant est marqué par le CSS (`<html data-hgdc-product>`, posé par
|
|
47
|
-
* `sidebarInitScript`) : au rendu serveur, le kit ne connaît pas l'adresse.
|
|
48
|
-
*/
|
|
49
|
-
export function ProductRailFrame() {
|
|
50
|
-
const { products, account } = productRailEntries();
|
|
51
|
-
return (_jsx("div", { "data-slot": "shell-frame-rail", className: RAIL_COLUMN, children: _jsxs("div", { className: RAIL_INNER, children: [_jsx(OrgPlaceholder, {}), _jsx(RailDivider, {}), _jsx("ul", { className: RAIL_LIST, children: [account, ...products].map((entry) => (_jsx("li", { children: _jsx("span", { "data-product": entry.badge, className: PRODUCT_ITEM, children: _jsx(ProductLabel, { entry: entry }) }) }, entry.badge))) })] }) }));
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* La barre des produits de la famille, tout à gauche de la coque sur ordinateur : organisation en
|
|
55
|
-
* haut, Account, puis les pastilles des produits. 56 px de large ; au survol (après un court délai
|
|
56
|
-
* d'intention) ou au focus clavier, elle s'élargit et écrit le nom de chaque produit. Elle ne
|
|
57
|
-
* recouvre jamais la page : `AppShell` plie la barre latérale du produit le temps du survol, la
|
|
58
|
-
* barre prend exactement la place libérée (248 px) ; si la barre latérale est déjà pliée, elle
|
|
59
|
-
* pousse la page (220 px). Un menu ouvert depuis la barre (sélecteur d'organisation) la garde
|
|
60
|
-
* élargie ; tout revient quand la souris est sortie et que le menu est fermé.
|
|
61
|
-
*/
|
|
62
|
-
export function ProductRail({ current, organization, accountHref, catalog, onExpandedChange, }) {
|
|
63
|
-
// Trois raisons de rester élargie : le survol (après le délai d'intention), le focus clavier, et
|
|
64
|
-
// un menu ouvert depuis la barre (sélecteur d'organisation ou tout autre) : la souris qui quitte
|
|
65
|
-
// la barre pour aller dans le menu ne doit pas la replier sous elle.
|
|
66
|
-
const [hovered, setHovered] = useState(false);
|
|
67
|
-
const [focused, setFocused] = useState(false);
|
|
68
|
-
const [menuOpen, setMenuOpen] = useState(false);
|
|
69
|
-
const expanded = hovered || focused || menuOpen;
|
|
70
|
-
// Avant la peinture : la barre latérale se plie dans la même image que la barre s'élargit, les
|
|
71
|
-
// deux transitions partent ensemble et la page ne bouge pas.
|
|
72
|
-
useIsomorphicLayoutEffect(() => {
|
|
73
|
-
onExpandedChange?.(expanded);
|
|
74
|
-
}, [expanded, onExpandedChange]);
|
|
75
|
-
const ref = useRef(null);
|
|
76
|
-
const pageOrigin = usePageOrigin();
|
|
77
|
-
const { products, account } = productRailEntries({ current, accountHref, catalog, pageOrigin });
|
|
78
|
-
// Écouteurs natifs : la barre est un repère `nav`, pas un contrôle ; c'est son survol, le focus
|
|
79
|
-
// de ses liens et les menus qu'elle ouvre qui l'élargissent.
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
const rail = ref.current;
|
|
82
|
-
if (!rail)
|
|
83
|
-
return;
|
|
84
|
-
let timer;
|
|
85
|
-
// Arrivée depuis la barre d'un autre produit, la souris restée dessus : la barre ne s'élargit
|
|
86
|
-
// pas sous elle, il faut d'abord en sortir (ADR 0020).
|
|
87
|
-
let arrivedUnderPointer = takeProductSwitch();
|
|
88
|
-
const cancel = () => {
|
|
89
|
-
window.clearTimeout(timer);
|
|
90
|
-
timer = undefined;
|
|
91
|
-
};
|
|
92
|
-
const onEnter = (event) => {
|
|
93
|
-
if (event.pointerType !== "mouse" || arrivedUnderPointer)
|
|
94
|
-
return;
|
|
95
|
-
cancel();
|
|
96
|
-
timer = window.setTimeout(() => setHovered(true), PRODUCT_RAIL_OPEN_DELAY);
|
|
97
|
-
};
|
|
98
|
-
const onLeave = () => {
|
|
99
|
-
arrivedUnderPointer = false;
|
|
100
|
-
cancel();
|
|
101
|
-
setHovered(false);
|
|
102
|
-
};
|
|
103
|
-
// Au clavier seulement : un clic de souris ne laisse pas la barre ouverte, ni le focus rendu au
|
|
104
|
-
// déclencheur quand un menu ouvert à la souris se ferme. La dernière interaction fait foi.
|
|
105
|
-
let keyboard = false;
|
|
106
|
-
const onDocKey = () => {
|
|
107
|
-
keyboard = true;
|
|
108
|
-
};
|
|
109
|
-
const onDocPointer = () => {
|
|
110
|
-
keyboard = false;
|
|
111
|
-
};
|
|
112
|
-
const onFocusIn = (event) => {
|
|
113
|
-
if (keyboard && event.target && focusVisible(event.target))
|
|
114
|
-
setFocused(true);
|
|
115
|
-
};
|
|
116
|
-
const onFocusOut = (event) => {
|
|
117
|
-
if (!(event.relatedTarget instanceof Node) || !rail.contains(event.relatedTarget))
|
|
118
|
-
setFocused(false);
|
|
119
|
-
};
|
|
120
|
-
// Un produit choisi : la barre se replie comme un menu qui se ferme, pendant que l'autre produit
|
|
121
|
-
// s'ouvre. La page d'arrivée la montre repliée : rien ne bouge au passage (ADR 0020).
|
|
122
|
-
const onClick = (event) => {
|
|
123
|
-
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
|
|
124
|
-
return;
|
|
125
|
-
if (!(event.target instanceof Element) || !event.target.closest("a[href]"))
|
|
126
|
-
return;
|
|
127
|
-
// Repliée d'un coup, sans transition (`data-hgdc-switching`) : l'image de départ de la
|
|
128
|
-
// transition de vue montre déjà la barre telle que la page d'arrivée la peint.
|
|
129
|
-
document.documentElement.dataset.hgdcSwitching = "";
|
|
130
|
-
markProductSwitch();
|
|
131
|
-
cancel();
|
|
132
|
-
setHovered(false);
|
|
133
|
-
setFocused(false);
|
|
134
|
-
};
|
|
135
|
-
const onKey = (event) => {
|
|
136
|
-
if (event.key !== "Escape")
|
|
137
|
-
return;
|
|
138
|
-
cancel();
|
|
139
|
-
setHovered(false);
|
|
140
|
-
setFocused(false);
|
|
141
|
-
};
|
|
142
|
-
// Un menu ouvert depuis la barre : son déclencheur porte `aria-expanded="true"` (Base UI, Radix,
|
|
143
|
-
// natif). Le menu lui-même vit dans un portail, hors de la barre : on suit donc le déclencheur.
|
|
144
|
-
const syncMenu = () => setMenuOpen(rail.querySelector('[aria-expanded="true"]') !== null);
|
|
145
|
-
const observer = new MutationObserver(syncMenu);
|
|
146
|
-
observer.observe(rail, { subtree: true, attributes: true, attributeFilter: ["aria-expanded"], childList: true });
|
|
147
|
-
syncMenu();
|
|
148
|
-
document.addEventListener("keydown", onDocKey, true);
|
|
149
|
-
document.addEventListener("pointerdown", onDocPointer, true);
|
|
150
|
-
rail.addEventListener("pointerenter", onEnter);
|
|
151
|
-
rail.addEventListener("pointerleave", onLeave);
|
|
152
|
-
rail.addEventListener("focusin", onFocusIn);
|
|
153
|
-
rail.addEventListener("focusout", onFocusOut);
|
|
154
|
-
rail.addEventListener("keydown", onKey);
|
|
155
|
-
rail.addEventListener("click", onClick);
|
|
156
|
-
return () => {
|
|
157
|
-
cancel();
|
|
158
|
-
observer.disconnect();
|
|
159
|
-
document.removeEventListener("keydown", onDocKey, true);
|
|
160
|
-
document.removeEventListener("pointerdown", onDocPointer, true);
|
|
161
|
-
rail.removeEventListener("pointerenter", onEnter);
|
|
162
|
-
rail.removeEventListener("pointerleave", onLeave);
|
|
163
|
-
rail.removeEventListener("focusin", onFocusIn);
|
|
164
|
-
rail.removeEventListener("focusout", onFocusOut);
|
|
165
|
-
rail.removeEventListener("keydown", onKey);
|
|
166
|
-
rail.removeEventListener("click", onClick);
|
|
167
|
-
};
|
|
168
|
-
}, []);
|
|
169
|
-
return (_jsx("div", { "data-slot": "product-rail", "data-expanded": expanded ? "" : undefined, className: cn(RAIL_COLUMN,
|
|
170
|
-
// Même durée et même courbe que la barre latérale qui se plie : la somme des deux largeurs
|
|
171
|
-
// reste constante (56 + 256 = 248 + 64), la page ne bouge pas.
|
|
172
|
-
"transition-[width] duration-(--duration-base) ease-ios motion-reduce:transition-none", "data-expanded:w-62 folded:data-expanded:w-55"), children: _jsxs("nav", { ref: ref, "aria-label": "Produits", "data-expanded": expanded ? "" : undefined, className: RAIL_INNER, children: [organization === undefined ? (_jsxs(_Fragment, { children: [_jsx(OrgPlaceholder, {}), _jsx(RailDivider, {})] })) : organization ? (_jsxs(_Fragment, { children: [_jsx(ShellPlacementContext, { value: "product-rail", children: organization }), _jsx(RailDivider, {})] })) : null, _jsxs("ul", { className: RAIL_LIST, children: [_jsx(ProductLink, { entry: account }), products.map((entry) => (_jsx(ProductLink, { entry: entry }, entry.badge)))] })] }) }));
|
|
173
|
-
}
|
|
174
|
-
function focusVisible(target) {
|
|
175
|
-
if (!(target instanceof Element))
|
|
176
|
-
return false;
|
|
177
|
-
try {
|
|
178
|
-
return target.matches(":focus-visible");
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
return true;
|
|
182
|
-
}
|
|
183
|
-
}
|