@hgdc/ui 0.36.0 → 0.38.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 +11 -0
- package/dist/app-shell.d.ts +6 -1
- package/dist/app-shell.js +29 -7
- package/dist/product-rail.js +23 -3
- package/dist/product-switch.d.ts +25 -6
- package/dist/product-switch.js +68 -17
- package/dist/products.d.ts +16 -1
- package/dist/products.js +38 -9
- package/package.json +1 -1
- package/styles.css +40 -0
package/README.md
CHANGED
|
@@ -63,6 +63,17 @@ Primitives déjà là : `avatar` `badge` `button` `dialog` `drawer` `dropdown-me
|
|
|
63
63
|
|
|
64
64
|
Le menu « Produits » et la palette (`badge` d'une commande) montrent la pastille de chaque produit.
|
|
65
65
|
|
|
66
|
+
### D'un produit à l'autre sous `app.hgdc.ch` (ADR 0020)
|
|
67
|
+
|
|
68
|
+
Entre deux produits servis sous la même origine, la coque fait comme une seule app : transition de vue entre documents (la barre des produits, la barre latérale et la barre du haut restent en place, le contenu passe en fondu de 150 ms, rien en mouvement réduit), lien relatif marqué `data-hgdc-prerender` et prérendu au survol (`PRODUCT_SPECULATION_RULES` de `@hgdc/ui/product-switch`). Le H couvre le contenu 100 ms après le clic (`PRODUCT_LEAVE_DELAY_MS`) : jamais pour une page prérendue. Chrome et Edge : tout ; Safari 18.2+ : la transition, sans prérendu ; Firefox : navigation ordinaire.
|
|
69
|
+
|
|
70
|
+
Ce que chaque produit fait :
|
|
71
|
+
|
|
72
|
+
- Monter `@hgdc/ui` et `@hgdc/auth` : `styles.css` déclare `@view-transition`, `AppShell` pose les noms et les règles. Rien d'autre à écrire.
|
|
73
|
+
- CSP : aucun produit ne pose `script-src` aujourd'hui. Celui qui en pose un ajoute `'inline-speculation-rules'` (règles en ligne insérées par la coque) ; sans cela, Chrome les ignore et rien ne casse.
|
|
74
|
+
- Une page qui compte une visite ou lance un effet au chargement attend l'affichage : `document.prerendering` faux, sinon `prerenderingchange`. La relecture des organisations le fait déjà.
|
|
75
|
+
- `localStorage` est commun à tous les produits de l'origine : `theme` et `sidebar` sont communs, c'est voulu ; les groupes repliables sont propres au produit (`sidebar-group:<produit>:<titre>`). Une clé du produit commence par son mot (`mail:last-org`).
|
|
76
|
+
|
|
66
77
|
### Mise en page (ADR 0012)
|
|
67
78
|
|
|
68
79
|
Une seule règle dans les sept produits, posée par la coque :
|
package/dist/app-shell.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export declare function SidebarNavGroup(props: {
|
|
|
77
77
|
active?: boolean;
|
|
78
78
|
/** Ouvert tant que la personne n'a rien choisi. Fermé par défaut. */
|
|
79
79
|
defaultOpen?: boolean;
|
|
80
|
-
/** Clé de mémorisation, par défaut le titre : à donner quand deux groupes portent le même. */
|
|
80
|
+
/** Clé de mémorisation, par défaut le titre, propre au produit de la coque : à donner quand deux groupes portent le même. */
|
|
81
81
|
storageKey?: string;
|
|
82
82
|
}): React.JSX.Element;
|
|
83
83
|
/**
|
|
@@ -118,6 +118,11 @@ export type AppShellAssistant = {
|
|
|
118
118
|
/** Lettre du raccourci `Ctrl` (`⌘`) + lettre qui ouvre et ferme l'assistant (`"j"`). Aucun par défaut. */
|
|
119
119
|
shortcut?: string;
|
|
120
120
|
};
|
|
121
|
+
/**
|
|
122
|
+
* Délai entre le clic vers un autre produit et le H : une page prérendue s'affiche avant (une ou
|
|
123
|
+
* deux images), le H n'apparaît alors jamais ; une page à charger le montre aussitôt après.
|
|
124
|
+
*/
|
|
125
|
+
export declare const PRODUCT_LEAVE_DELAY_MS = 100;
|
|
121
126
|
export declare function AppShell({ product, organization, productRail, accountHref, nav, links, account, sidebarFooter, header, onSearch, searchLabel, assistant, children, contentKey, navigating, mainClassName, statusPage, }: {
|
|
122
127
|
/** Le produit affiché (`"host"`) : sa marque en haut de la barre, « vous êtes ici » dans la barre des produits. */
|
|
123
128
|
product: FamilyProduct;
|
package/dist/app-shell.js
CHANGED
|
@@ -77,7 +77,9 @@ function useToggleShortcut() {
|
|
|
77
77
|
}, []);
|
|
78
78
|
}
|
|
79
79
|
// ── Navigation ──────────────────────────────────────────────────────────────────────
|
|
80
|
-
const ShellContext = createContext({
|
|
80
|
+
const ShellContext = createContext({
|
|
81
|
+
closeMobileNav: () => { },
|
|
82
|
+
});
|
|
81
83
|
/** Pour fermer le tiroir mobile depuis un élément de la barre (un sélecteur, un bouton). */
|
|
82
84
|
export function useAppShell() {
|
|
83
85
|
return useContext(ShellContext);
|
|
@@ -189,7 +191,11 @@ export function SidebarNavGroup(props) {
|
|
|
189
191
|
return _jsx(TitledNavGroup, { label: props.label, children: props.children });
|
|
190
192
|
}
|
|
191
193
|
function CollapsibleNavGroup({ label, children, icon, active, defaultOpen = false, storageKey, }) {
|
|
192
|
-
|
|
194
|
+
// Propre au produit : sous une origine partagée (`app.hgdc.ch/<produit>`), `localStorage` est
|
|
195
|
+
// commun à tous. Le thème et la barre réduite le sont volontairement (`theme`, `sidebar`), un
|
|
196
|
+
// groupe « Réglages » de Mail et de Host ne l'est pas (ADR 0020).
|
|
197
|
+
const { product } = useContext(ShellContext);
|
|
198
|
+
const key = product ? `${product}:${storageKey ?? label}` : (storageKey ?? label);
|
|
193
199
|
const buttonId = useId();
|
|
194
200
|
const listId = useId();
|
|
195
201
|
const listRef = useRef(null);
|
|
@@ -403,6 +409,11 @@ function useContentLoading({ navigating, pending }) {
|
|
|
403
409
|
}, [active, visible]);
|
|
404
410
|
return visible;
|
|
405
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Délai entre le clic vers un autre produit et le H : une page prérendue s'affiche avant (une ou
|
|
414
|
+
* deux images), le H n'apparaît alors jamais ; une page à charger le montre aussitôt après.
|
|
415
|
+
*/
|
|
416
|
+
export const PRODUCT_LEAVE_DELAY_MS = 100;
|
|
406
417
|
export function AppShell({ product, organization, productRail = true, accountHref, nav, links, account, sidebarFooter, header, onSearch, searchLabel, assistant, children, contentKey, navigating = false, mainClassName, statusPage, }) {
|
|
407
418
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
408
419
|
// Barre des produits élargie : la barre latérale se plie le temps du survol (ADR 0011).
|
|
@@ -420,15 +431,26 @@ export function AppShell({ product, organization, productRail = true, accountHre
|
|
|
420
431
|
setPagePending((count) => count + 1);
|
|
421
432
|
return () => setPagePending((count) => count - 1);
|
|
422
433
|
}, []);
|
|
423
|
-
|
|
434
|
+
// Après `PRODUCT_LEAVE_DELAY_MS` seulement : une page prérendue au survol s'affiche avant, sans
|
|
435
|
+
// que le H apparaisse (ADR 0020). Sinon le H couvre le contenu jusqu'au départ de la page.
|
|
436
|
+
const leaveTimer = useRef(undefined);
|
|
437
|
+
const leave = useCallback(() => {
|
|
438
|
+
window.clearTimeout(leaveTimer.current);
|
|
439
|
+
leaveTimer.current = window.setTimeout(() => setLeaving(true), PRODUCT_LEAVE_DELAY_MS);
|
|
440
|
+
}, []);
|
|
424
441
|
useEffect(() => {
|
|
425
442
|
// Retour arrière depuis l'autre produit (cache de page) : la page revient telle quelle.
|
|
426
443
|
const onPageShow = (event) => {
|
|
427
|
-
if (event.persisted)
|
|
428
|
-
|
|
444
|
+
if (!event.persisted)
|
|
445
|
+
return;
|
|
446
|
+
window.clearTimeout(leaveTimer.current);
|
|
447
|
+
setLeaving(false);
|
|
429
448
|
};
|
|
430
449
|
window.addEventListener("pageshow", onPageShow);
|
|
431
|
-
return () =>
|
|
450
|
+
return () => {
|
|
451
|
+
window.removeEventListener("pageshow", onPageShow);
|
|
452
|
+
window.clearTimeout(leaveTimer.current);
|
|
453
|
+
};
|
|
432
454
|
}, []);
|
|
433
455
|
const statusPageUrl = statusPage === false ? null : (statusPage ?? statusPageUrlFor(product));
|
|
434
456
|
const loading = useContentLoading({ navigating, pending: pagePending > 0 || leaving });
|
|
@@ -455,5 +477,5 @@ export function AppShell({ product, organization, productRail = true, accountHre
|
|
|
455
477
|
const assistantButtonRef = useRef(null);
|
|
456
478
|
useAssistantShortcut(assistant);
|
|
457
479
|
const footer = (_jsxs(_Fragment, { children: [account !== undefined ? sidebarFooter : null, _jsx(ShellCommonLinks, { links: links ?? {}, statusPageUrl: statusPageUrl }), account ?? sidebarFooter] }));
|
|
458
|
-
return (_jsx(ShellContext.Provider, { value: { closeMobileNav: () => setMenuOpen(false) }, children: _jsx(ProductRailShownContext, { value: productRail, children: _jsx(ShellLeaveContext, { value: leave, children: _jsx(StatusPageContext, { value: statusPageUrl, children: _jsxs("div", { "data-slot": "app-shell", "data-rail-peek": railPeek ? "" : undefined, "data-product-rail": productRail ? "" : undefined, "data-loading": loading ? "" : undefined, "data-hydrated": hydrated ? "" : undefined, "data-assistant": assistantOpen ? "" : undefined, className: cn("min-h-dvh lg:grid lg:h-dvh lg:bg-shell", productRail ? "lg:grid-cols-[auto_auto_minmax(0,1fr)_auto]" : "lg:grid-cols-[auto_minmax(0,1fr)_auto]"), children: [_jsx("a", { href: "#contenu", className: "sr-only z-50 rounded-xl bg-background px-4 py-2 text-sm font-medium focus:not-sr-only focus:fixed focus:top-3 focus:left-3 focus:ring-2 focus:ring-ring", children: "Aller au contenu" }), productRail ? (_jsx("div", { className: "contents", inert: pageBlocked || undefined, children: _jsx(ProductRail, { current: product, organization: organization, accountHref: accountHref, onExpandedChange: setRailPeek }) })) : null, _jsxs("aside", { "aria-label": "Barre lat\u00E9rale", inert: pageBlocked || undefined, className: "group/sidebar hidden overflow-hidden text-shell-foreground transition-[width] duration-(--duration-base) ease-ios motion-reduce:transition-none lg:flex lg:w-64 lg:flex-col lg:rail:w-16", children: [_jsx(SidebarHead, { product: product }), !productRail && organization ? _jsx("div", { className: "shrink-0 px-3 pb-1", children: organization }) : null, nav, _jsx("div", { "data-slot": "app-shell-footer", className: "grid gap-0.5 px-3 pt-1 pb-3 empty:hidden", children: footer })] }), _jsx("div", { inert: pageBlocked || undefined, className: "flex min-h-dvh flex-col lg:min-h-0 lg:p-2 lg:pl-0", children: _jsxs("div", { ref: contentRef, className: "relative flex flex-1 flex-col lg:min-h-0 lg:overflow-y-auto lg:rounded-xl lg:bg-background lg:[scrollbar-gutter:stable]", children: [_jsxs("header", { className: "sticky top-[env(safe-area-inset-top,0px)] z-30 flex min-h-14 shrink-0 flex-wrap items-center gap-x-1 gap-y-1 border-b border-foreground/5 bg-background px-2 py-1.5 sm:px-4 lg:top-0", children: [_jsx("button", { type: "button", onClick: () => setMenuOpen(true), "aria-label": "Ouvrir le menu", className: "touch-target grid size-10 shrink-0 place-items-center rounded-xl text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 lg:hidden", children: _jsx(HugeiconsIcon, { icon: Menu01Icon, "aria-hidden": true, className: "size-5" }) }), _jsxs("div", { "data-slot": "app-shell-brand", className: "flex shrink-0 items-center gap-2 pr-1 lg:hidden", children: [_jsx(ProductBadge, { product: product, className: "size-6" }), _jsx("span", { className: "font-heading text-[0.9375rem] font-semibold", children: productName(product) })] }), _jsx("div", { className: cn("flex min-w-0 items-center gap-2", header ? "flex-[1_1_7rem]" : "flex-1"), children: header }), onSearch ? (_jsx(CommandMenuButton, { onClick: onSearch, compact: "mobile", className: "shrink-0 lg:w-96 lg:max-w-none", ...(searchLabel !== undefined ? { label: searchLabel } : {}) })) : null, assistant ? (_jsx(AssistantButton, { assistant: assistant, panelId: assistantPanelId, buttonRef: assistantButtonRef })) : null] }), _jsx("div", { "data-slot": "app-shell-loading", "aria-hidden": !loading, className: CONTENT_LOADING_LAYER, children: loading ? (_jsx("span", { role: "status", className: "sr-only", children: "Chargement\u2026" })) : null }), _jsxs("main", { id: "contenu", tabIndex: -1, "aria-busy": loading ? true : undefined, inert: loading || undefined, className: cn("mx-auto w-full max-w-6xl flex-1 px-4 pt-6 pb-16 outline-none sm:px-6 lg:px-10 lg:pt-8", mainClassName), children: [_jsx(ServiceStatus, { pageUrl: statusPageUrl, className: "mb-6" }), _jsx(ContentLoadingContext, { value: registerPagePending, children: contentKey === undefined ? (children) : (_jsx(Suspense, { fallback: _jsx(PagePending, {}), children: children }, contentKey)) })] })] }) }), assistant && assistantMounted ? (_jsx(AssistantPanel, { assistant: assistant, panelId: assistantPanelId, wide: wide, openAtShellMount: assistantOpenAtMount, buttonRef: assistantButtonRef })) : null, hasRoot ? null : _jsx(FamilyLoaderHost, {}), _jsx(Drawer, { side: "bottom", open: menuOpen, onOpenChange: setMenuOpen, children: _jsxs(DrawerContent, { className: "lg:hidden", children: [_jsx(DrawerTitle, { className: "sr-only", children: "Menu" }), organization ? _jsx("div", { className: "px-3 pt-1", children: organization }) : null, nav, _jsx("div", { className: "grid gap-0.5 border-t border-foreground/5 px-3 pt-2 pb-[max(env(safe-area-inset-bottom),0.75rem)] empty:hidden", children: footer })] }) })] }) }) }) }) }));
|
|
480
|
+
return (_jsx(ShellContext.Provider, { value: { closeMobileNav: () => setMenuOpen(false), product }, children: _jsx(ProductRailShownContext, { value: productRail, children: _jsx(ShellLeaveContext, { value: leave, children: _jsx(StatusPageContext, { value: statusPageUrl, children: _jsxs("div", { "data-slot": "app-shell", "data-rail-peek": railPeek ? "" : undefined, "data-product-rail": productRail ? "" : undefined, "data-loading": loading ? "" : undefined, "data-hydrated": hydrated ? "" : undefined, "data-assistant": assistantOpen ? "" : undefined, className: cn("min-h-dvh lg:grid lg:h-dvh lg:bg-shell", productRail ? "lg:grid-cols-[auto_auto_minmax(0,1fr)_auto]" : "lg:grid-cols-[auto_minmax(0,1fr)_auto]"), children: [_jsx("a", { href: "#contenu", className: "sr-only z-50 rounded-xl bg-background px-4 py-2 text-sm font-medium focus:not-sr-only focus:fixed focus:top-3 focus:left-3 focus:ring-2 focus:ring-ring", children: "Aller au contenu" }), productRail ? (_jsx("div", { className: "contents", inert: pageBlocked || undefined, children: _jsx(ProductRail, { current: product, organization: organization, accountHref: accountHref, onExpandedChange: setRailPeek }) })) : null, _jsxs("aside", { "data-slot": "app-shell-sidebar", "aria-label": "Barre lat\u00E9rale", inert: pageBlocked || undefined, className: "group/sidebar hidden overflow-hidden text-shell-foreground transition-[width] duration-(--duration-base) ease-ios motion-reduce:transition-none lg:flex lg:w-64 lg:flex-col lg:rail:w-16", children: [_jsx(SidebarHead, { product: product }), !productRail && organization ? _jsx("div", { className: "shrink-0 px-3 pb-1", children: organization }) : null, nav, _jsx("div", { "data-slot": "app-shell-footer", className: "grid gap-0.5 px-3 pt-1 pb-3 empty:hidden", children: footer })] }), _jsx("div", { inert: pageBlocked || undefined, className: "flex min-h-dvh flex-col lg:min-h-0 lg:p-2 lg:pl-0", children: _jsxs("div", { ref: contentRef, className: "relative flex flex-1 flex-col lg:min-h-0 lg:overflow-y-auto lg:rounded-xl lg:bg-background lg:[scrollbar-gutter:stable]", children: [_jsxs("header", { "data-slot": "app-shell-header", className: "sticky top-[env(safe-area-inset-top,0px)] z-30 flex min-h-14 shrink-0 flex-wrap items-center gap-x-1 gap-y-1 border-b border-foreground/5 bg-background px-2 py-1.5 sm:px-4 lg:top-0", children: [_jsx("button", { type: "button", onClick: () => setMenuOpen(true), "aria-label": "Ouvrir le menu", className: "touch-target grid size-10 shrink-0 place-items-center rounded-xl text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 lg:hidden", children: _jsx(HugeiconsIcon, { icon: Menu01Icon, "aria-hidden": true, className: "size-5" }) }), _jsxs("div", { "data-slot": "app-shell-brand", className: "flex shrink-0 items-center gap-2 pr-1 lg:hidden", children: [_jsx(ProductBadge, { product: product, className: "size-6" }), _jsx("span", { className: "font-heading text-[0.9375rem] font-semibold", children: productName(product) })] }), _jsx("div", { className: cn("flex min-w-0 items-center gap-2", header ? "flex-[1_1_7rem]" : "flex-1"), children: header }), onSearch ? (_jsx(CommandMenuButton, { onClick: onSearch, compact: "mobile", className: "shrink-0 lg:w-96 lg:max-w-none", ...(searchLabel !== undefined ? { label: searchLabel } : {}) })) : null, assistant ? (_jsx(AssistantButton, { assistant: assistant, panelId: assistantPanelId, buttonRef: assistantButtonRef })) : null] }), _jsx("div", { "data-slot": "app-shell-loading", "aria-hidden": !loading, className: CONTENT_LOADING_LAYER, children: loading ? (_jsx("span", { role: "status", className: "sr-only", children: "Chargement\u2026" })) : null }), _jsxs("main", { id: "contenu", tabIndex: -1, "aria-busy": loading ? true : undefined, inert: loading || undefined, className: cn("mx-auto w-full max-w-6xl flex-1 px-4 pt-6 pb-16 outline-none sm:px-6 lg:px-10 lg:pt-8", mainClassName), children: [_jsx(ServiceStatus, { pageUrl: statusPageUrl, className: "mb-6" }), _jsx(ContentLoadingContext, { value: registerPagePending, children: contentKey === undefined ? (children) : (_jsx(Suspense, { fallback: _jsx(PagePending, {}), children: children }, contentKey)) })] })] }) }), assistant && assistantMounted ? (_jsx(AssistantPanel, { assistant: assistant, panelId: assistantPanelId, wide: wide, openAtShellMount: assistantOpenAtMount, buttonRef: assistantButtonRef })) : null, hasRoot ? null : _jsx(FamilyLoaderHost, {}), _jsx(Drawer, { side: "bottom", open: menuOpen, onOpenChange: setMenuOpen, children: _jsxs(DrawerContent, { className: "lg:hidden", children: [_jsx(DrawerTitle, { className: "sr-only", children: "Menu" }), organization ? _jsx("div", { className: "px-3 pt-1", children: organization }) : null, nav, _jsx("div", { className: "grid gap-0.5 border-t border-foreground/5 px-3 pt-2 pb-[max(env(safe-area-inset-bottom),0.75rem)] empty:hidden", children: footer })] }) })] }) }) }) }) }));
|
|
459
481
|
}
|
package/dist/product-rail.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
3
3
|
import { ProductBadge } from "./product-badge.js";
|
|
4
|
+
import { PRODUCT_PRERENDER_ATTRIBUTE } from "./product-switch.js";
|
|
4
5
|
import { productRailEntries } from "./products.js";
|
|
5
6
|
import { ShellPlacementContext, useLeaveOnClick } from "./shell-placement.js";
|
|
6
7
|
import { cn } from "./cn.js";
|
|
7
8
|
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
9
|
+
const noSubscription = () => () => { };
|
|
10
|
+
/** `location.origin` après l'hydratation, `undefined` au rendu serveur (liens absolus d'abord). */
|
|
11
|
+
function usePageOrigin() {
|
|
12
|
+
return useSyncExternalStore(noSubscription, () => window.location.origin, () => undefined);
|
|
13
|
+
}
|
|
8
14
|
/** Délai d'intention avant d'élargir la barre au survol : un passage de souris ne l'ouvre pas. */
|
|
9
15
|
export const PRODUCT_RAIL_OPEN_DELAY = 150;
|
|
10
16
|
function ProductLink({ entry }) {
|
|
11
17
|
// Chaque produit s'ouvre en document complet : la coque montre le H dès le clic.
|
|
12
18
|
const leaveOnClick = useLeaveOnClick();
|
|
13
|
-
return (_jsx("li", { children: _jsxs("a", { href: entry.href, onClick: leaveOnClick, "aria-current": entry.current ? "page" : undefined, className: cn("flex h-10 items-center gap-3 rounded-xl px-2 text-shell-muted coarse:h-11 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(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] })] }) }));
|
|
19
|
+
return (_jsx("li", { children: _jsxs("a", { href: entry.href, onClick: leaveOnClick, ...(entry.sameOrigin ? { [PRODUCT_PRERENDER_ATTRIBUTE]: "" } : {}), "aria-current": entry.current ? "page" : undefined, className: cn("flex h-10 items-center gap-3 rounded-xl px-2 text-shell-muted coarse:h-11 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(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] })] }) }));
|
|
14
20
|
}
|
|
15
21
|
/**
|
|
16
22
|
* La barre des produits de la famille, tout à gauche de la coque sur ordinateur : organisation en
|
|
@@ -35,7 +41,8 @@ export function ProductRail({ current, organization, accountHref, catalog, onExp
|
|
|
35
41
|
onExpandedChange?.(expanded);
|
|
36
42
|
}, [expanded, onExpandedChange]);
|
|
37
43
|
const ref = useRef(null);
|
|
38
|
-
const
|
|
44
|
+
const pageOrigin = usePageOrigin();
|
|
45
|
+
const { products, account } = productRailEntries({ current, accountHref, catalog, pageOrigin });
|
|
39
46
|
// Écouteurs natifs : la barre est un repère `nav`, pas un contrôle ; c'est son survol, le focus
|
|
40
47
|
// de ses liens et les menus qu'elle ouvre qui l'élargissent.
|
|
41
48
|
useEffect(() => {
|
|
@@ -74,6 +81,17 @@ export function ProductRail({ current, organization, accountHref, catalog, onExp
|
|
|
74
81
|
if (!(event.relatedTarget instanceof Node) || !rail.contains(event.relatedTarget))
|
|
75
82
|
setFocused(false);
|
|
76
83
|
};
|
|
84
|
+
// Un produit choisi : la barre se replie comme un menu qui se ferme, pendant que l'autre produit
|
|
85
|
+
// s'ouvre. La page d'arrivée la montre repliée : rien ne bouge au passage (ADR 0020).
|
|
86
|
+
const onClick = (event) => {
|
|
87
|
+
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
|
|
88
|
+
return;
|
|
89
|
+
if (!(event.target instanceof Element) || !event.target.closest("a[href]"))
|
|
90
|
+
return;
|
|
91
|
+
cancel();
|
|
92
|
+
setHovered(false);
|
|
93
|
+
setFocused(false);
|
|
94
|
+
};
|
|
77
95
|
const onKey = (event) => {
|
|
78
96
|
if (event.key !== "Escape")
|
|
79
97
|
return;
|
|
@@ -94,6 +112,7 @@ export function ProductRail({ current, organization, accountHref, catalog, onExp
|
|
|
94
112
|
rail.addEventListener("focusin", onFocusIn);
|
|
95
113
|
rail.addEventListener("focusout", onFocusOut);
|
|
96
114
|
rail.addEventListener("keydown", onKey);
|
|
115
|
+
rail.addEventListener("click", onClick);
|
|
97
116
|
return () => {
|
|
98
117
|
cancel();
|
|
99
118
|
observer.disconnect();
|
|
@@ -104,6 +123,7 @@ export function ProductRail({ current, organization, accountHref, catalog, onExp
|
|
|
104
123
|
rail.removeEventListener("focusin", onFocusIn);
|
|
105
124
|
rail.removeEventListener("focusout", onFocusOut);
|
|
106
125
|
rail.removeEventListener("keydown", onKey);
|
|
126
|
+
rail.removeEventListener("click", onClick);
|
|
107
127
|
};
|
|
108
128
|
}, []);
|
|
109
129
|
return (_jsx("div", { "data-slot": "product-rail", "data-expanded": expanded ? "" : undefined, className: cn("hidden w-14 shrink-0 overflow-hidden lg:block",
|
package/dist/product-switch.d.ts
CHANGED
|
@@ -4,13 +4,29 @@ export declare const ORG_SYNC_COOKIE = "hgdc-org-sync";
|
|
|
4
4
|
export declare const BACKGROUND_REFRESH_PATH = "/auth/hgdc?refresh=1&frame=1";
|
|
5
5
|
/** Abandon du cadre s'il ne s'est pas chargé : Account injoignable, réseau coupé. */
|
|
6
6
|
export declare const BACKGROUND_REFRESH_TIMEOUT_MS = 20000;
|
|
7
|
+
/** Marque d'un lien de la barre vers un autre produit servi sous la même origine : prérendu au survol. */
|
|
8
|
+
export declare const PRODUCT_PRERENDER_ATTRIBUTE = "data-hgdc-prerender";
|
|
7
9
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Règles de spéculation posées par la coque (Chrome et Edge ; ignorées ailleurs) :
|
|
11
|
+
*
|
|
12
|
+
* - **prérendu** au survol (`eagerness: "moderate"`, environ 200 ms) des liens marqués
|
|
13
|
+
* `PRODUCT_PRERENDER_ATTRIBUTE` vers l'entrée `refresh=background` d'un produit de la même
|
|
14
|
+
* origine (`app.hgdc.ch/<produit>`) : le motif est relatif, il ne vaut que pour l'origine de la
|
|
15
|
+
* page. L'entrée ne rend que la redirection d'une session ouverte ; sans session, elle répond 503
|
|
16
|
+
* à une requête `Sec-Purpose: prefetch;prerender`, le prérendu est jeté et le clic repart
|
|
17
|
+
* normalement. La page prérendue attend d'être affichée pour relire les organisations.
|
|
18
|
+
* - **préchargement** seul de l'entrée d'un produit encore sur son sous-domaine (même site, les
|
|
19
|
+
* cookies partent), comme avant (ADR 0016).
|
|
12
20
|
*/
|
|
21
|
+
export declare const PRODUCT_SPECULATION_RULES: string;
|
|
22
|
+
/** @deprecated Remplacé par `PRODUCT_SPECULATION_RULES` (prérendu sous la même origine, 0.38). */
|
|
13
23
|
export declare const PRODUCT_PREFETCH_RULES: string;
|
|
24
|
+
/**
|
|
25
|
+
* Lit et efface le cookie de relecture. Rend la base de chemin du produit (`""` à la racine de son
|
|
26
|
+
* origine, `"/analytics"` sous `app.hgdc.ch/analytics`), `null` s'il n'était pas posé. Sous une
|
|
27
|
+
* base, l'entrée le limite à ce chemin : un autre produit de la même origine ne le voit pas.
|
|
28
|
+
*/
|
|
29
|
+
export declare function takeOrgSync(doc?: Document): string | null;
|
|
14
30
|
/** Lit et efface le cookie de relecture. Vrai s'il était posé. */
|
|
15
31
|
export declare function takeOrgSyncCookie(doc?: Document): boolean;
|
|
16
32
|
/**
|
|
@@ -18,7 +34,8 @@ export declare function takeOrgSyncCookie(doc?: Document): boolean;
|
|
|
18
34
|
* ni script ni formulaire (`sandbox`), il suit seulement les redirections d'Account ; la nouvelle
|
|
19
35
|
* session du produit recopie les organisations, et les requêtes Convex abonnées se mettent à jour.
|
|
20
36
|
* Si la session d'Account a expiré, Account refuse d'être affiché dans un cadre : rien ne se passe,
|
|
21
|
-
* la session du produit reste celle d'avant.
|
|
37
|
+
* la session du produit reste celle d'avant. Une page prérendue au survol attend d'être affichée :
|
|
38
|
+
* un survol sans clic ne relit rien.
|
|
22
39
|
*/
|
|
23
40
|
export declare function startBackgroundRefresh(doc?: Document): HTMLIFrameElement | null;
|
|
24
41
|
/**
|
|
@@ -30,6 +47,8 @@ export declare function startBackgroundRefresh(doc?: Document): HTMLIFrameElemen
|
|
|
30
47
|
export declare function useBackgroundOrganizationRefresh(): void;
|
|
31
48
|
/**
|
|
32
49
|
* Dans la coque (`AppShell` le fait) : la relecture ci-dessus si la racine ne l'a pas déjà faite
|
|
33
|
-
* (le cookie ne sert qu'une fois), et le préchargement au survol des autres
|
|
50
|
+
* (le cookie ne sert qu'une fois), et le prérendu ou le préchargement au survol des autres
|
|
51
|
+
* produits. Les règles sont un `<script type="speculationrules">` en ligne : une CSP qui pose
|
|
52
|
+
* `script-src` doit y autoriser `'inline-speculation-rules'` (README).
|
|
34
53
|
*/
|
|
35
54
|
export declare function useProductSwitch(enabled?: boolean): void;
|
package/dist/product-switch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
2
|
/*
|
|
3
|
-
* Passage d'un produit à l'autre (ADR 0016) : la barre des produits pointe vers
|
|
3
|
+
* Passage d'un produit à l'autre (ADR 0016, 0020) : la barre des produits pointe vers
|
|
4
4
|
* `/auth/hgdc?refresh=background&next=/`. Avec une session du produit ouverte, l'entrée de
|
|
5
5
|
* `@hgdc/auth` va droit à la page et pose un cookie « relire les organisations » ; la coque le lit
|
|
6
6
|
* ici et relit les organisations dans un cadre caché, sans faire attendre la personne.
|
|
@@ -15,13 +15,33 @@ export const ORG_SYNC_COOKIE = "hgdc-org-sync";
|
|
|
15
15
|
export const BACKGROUND_REFRESH_PATH = "/auth/hgdc?refresh=1&frame=1";
|
|
16
16
|
/** Abandon du cadre s'il ne s'est pas chargé : Account injoignable, réseau coupé. */
|
|
17
17
|
export const BACKGROUND_REFRESH_TIMEOUT_MS = 20_000;
|
|
18
|
+
/** Marque d'un lien de la barre vers un autre produit servi sous la même origine : prérendu au survol. */
|
|
19
|
+
export const PRODUCT_PRERENDER_ATTRIBUTE = "data-hgdc-prerender";
|
|
18
20
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* Règles de spéculation posées par la coque (Chrome et Edge ; ignorées ailleurs) :
|
|
22
|
+
*
|
|
23
|
+
* - **prérendu** au survol (`eagerness: "moderate"`, environ 200 ms) des liens marqués
|
|
24
|
+
* `PRODUCT_PRERENDER_ATTRIBUTE` vers l'entrée `refresh=background` d'un produit de la même
|
|
25
|
+
* origine (`app.hgdc.ch/<produit>`) : le motif est relatif, il ne vaut que pour l'origine de la
|
|
26
|
+
* page. L'entrée ne rend que la redirection d'une session ouverte ; sans session, elle répond 503
|
|
27
|
+
* à une requête `Sec-Purpose: prefetch;prerender`, le prérendu est jeté et le clic repart
|
|
28
|
+
* normalement. La page prérendue attend d'être affichée pour relire les organisations.
|
|
29
|
+
* - **préchargement** seul de l'entrée d'un produit encore sur son sous-domaine (même site, les
|
|
30
|
+
* cookies partent), comme avant (ADR 0016).
|
|
23
31
|
*/
|
|
24
|
-
export const
|
|
32
|
+
export const PRODUCT_SPECULATION_RULES = JSON.stringify({
|
|
33
|
+
prerender: [
|
|
34
|
+
{
|
|
35
|
+
source: "document",
|
|
36
|
+
where: {
|
|
37
|
+
and: [
|
|
38
|
+
{ selector_matches: `a[${PRODUCT_PRERENDER_ATTRIBUTE}]` },
|
|
39
|
+
{ href_matches: "/*/auth/hgdc?refresh=background*" },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
eagerness: "moderate",
|
|
43
|
+
},
|
|
44
|
+
],
|
|
25
45
|
prefetch: [
|
|
26
46
|
{
|
|
27
47
|
source: "document",
|
|
@@ -30,24 +50,53 @@ export const PRODUCT_PREFETCH_RULES = JSON.stringify({
|
|
|
30
50
|
},
|
|
31
51
|
],
|
|
32
52
|
});
|
|
33
|
-
/**
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
53
|
+
/** @deprecated Remplacé par `PRODUCT_SPECULATION_RULES` (prérendu sous la même origine, 0.38). */
|
|
54
|
+
export const PRODUCT_PREFETCH_RULES = PRODUCT_SPECULATION_RULES;
|
|
55
|
+
/** Base de chemin d'un produit (`/analytics`), telle que l'entrée la pose dans le cookie. */
|
|
56
|
+
const BASE_PATH = /^(?:\/[A-Za-z0-9_~-][A-Za-z0-9._~-]*)+$/;
|
|
57
|
+
/**
|
|
58
|
+
* Lit et efface le cookie de relecture. Rend la base de chemin du produit (`""` à la racine de son
|
|
59
|
+
* origine, `"/analytics"` sous `app.hgdc.ch/analytics`), `null` s'il n'était pas posé. Sous une
|
|
60
|
+
* base, l'entrée le limite à ce chemin : un autre produit de la même origine ne le voit pas.
|
|
61
|
+
*/
|
|
62
|
+
export function takeOrgSync(doc = document) {
|
|
63
|
+
let base = null;
|
|
64
|
+
for (const part of doc.cookie.split(";")) {
|
|
65
|
+
const [name, value] = part.trim().split("=", 2);
|
|
66
|
+
if (name !== ORG_SYNC_COOKIE || value === undefined)
|
|
67
|
+
continue;
|
|
68
|
+
if (value === "1")
|
|
69
|
+
base = "";
|
|
70
|
+
else if (BASE_PATH.test(value))
|
|
71
|
+
base = value;
|
|
72
|
+
if (base !== null)
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
if (base !== null) {
|
|
37
76
|
const secure = doc.location.protocol === "https:" ? "; Secure" : "";
|
|
38
|
-
doc.cookie = `${ORG_SYNC_COOKIE}=; Path
|
|
77
|
+
doc.cookie = `${ORG_SYNC_COOKIE}=; Path=${base || "/"}; Max-Age=0; SameSite=Lax${secure}`;
|
|
39
78
|
}
|
|
40
|
-
return
|
|
79
|
+
return base;
|
|
80
|
+
}
|
|
81
|
+
/** Lit et efface le cookie de relecture. Vrai s'il était posé. */
|
|
82
|
+
export function takeOrgSyncCookie(doc = document) {
|
|
83
|
+
return takeOrgSync(doc) !== null;
|
|
41
84
|
}
|
|
42
85
|
/**
|
|
43
86
|
* Relit les organisations dans un cadre caché, une fois, si l'entrée l'a demandé. Le cadre n'a
|
|
44
87
|
* ni script ni formulaire (`sandbox`), il suit seulement les redirections d'Account ; la nouvelle
|
|
45
88
|
* session du produit recopie les organisations, et les requêtes Convex abonnées se mettent à jour.
|
|
46
89
|
* Si la session d'Account a expiré, Account refuse d'être affiché dans un cadre : rien ne se passe,
|
|
47
|
-
* la session du produit reste celle d'avant.
|
|
90
|
+
* la session du produit reste celle d'avant. Une page prérendue au survol attend d'être affichée :
|
|
91
|
+
* un survol sans clic ne relit rien.
|
|
48
92
|
*/
|
|
49
93
|
export function startBackgroundRefresh(doc = document) {
|
|
50
|
-
if (
|
|
94
|
+
if (doc.prerendering) {
|
|
95
|
+
doc.addEventListener("prerenderingchange", () => startBackgroundRefresh(doc), { once: true });
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const base = takeOrgSync(doc);
|
|
99
|
+
if (base === null)
|
|
51
100
|
return null;
|
|
52
101
|
const frame = doc.createElement("iframe");
|
|
53
102
|
frame.hidden = true;
|
|
@@ -63,7 +112,7 @@ export function startBackgroundRefresh(doc = document) {
|
|
|
63
112
|
};
|
|
64
113
|
const timer = window.setTimeout(done, BACKGROUND_REFRESH_TIMEOUT_MS);
|
|
65
114
|
frame.addEventListener("load", done, { once: true });
|
|
66
|
-
frame.src = BACKGROUND_REFRESH_PATH
|
|
115
|
+
frame.src = `${base}${BACKGROUND_REFRESH_PATH}`;
|
|
67
116
|
doc.body.append(frame);
|
|
68
117
|
return frame;
|
|
69
118
|
}
|
|
@@ -80,7 +129,9 @@ export function useBackgroundOrganizationRefresh() {
|
|
|
80
129
|
}
|
|
81
130
|
/**
|
|
82
131
|
* Dans la coque (`AppShell` le fait) : la relecture ci-dessus si la racine ne l'a pas déjà faite
|
|
83
|
-
* (le cookie ne sert qu'une fois), et le préchargement au survol des autres
|
|
132
|
+
* (le cookie ne sert qu'une fois), et le prérendu ou le préchargement au survol des autres
|
|
133
|
+
* produits. Les règles sont un `<script type="speculationrules">` en ligne : une CSP qui pose
|
|
134
|
+
* `script-src` doit y autoriser `'inline-speculation-rules'` (README).
|
|
84
135
|
*/
|
|
85
136
|
export function useProductSwitch(enabled = true) {
|
|
86
137
|
useEffect(() => {
|
|
@@ -95,7 +146,7 @@ export function useProductSwitch(enabled = true) {
|
|
|
95
146
|
// Créé par le DOM, pas par React : un script inséré par `innerHTML` n'est jamais lu.
|
|
96
147
|
const script = document.createElement("script");
|
|
97
148
|
script.type = "speculationrules";
|
|
98
|
-
script.textContent =
|
|
149
|
+
script.textContent = PRODUCT_SPECULATION_RULES;
|
|
99
150
|
document.head.append(script);
|
|
100
151
|
return () => script.remove();
|
|
101
152
|
}, [enabled]);
|
package/dist/products.d.ts
CHANGED
|
@@ -96,6 +96,11 @@ export declare function productMenuEntries(options?: {
|
|
|
96
96
|
accountHref?: string | undefined;
|
|
97
97
|
catalog?: ReadonlyArray<HgdcCatalogProduct> | undefined;
|
|
98
98
|
}): Array<ProductMenuEntry>;
|
|
99
|
+
/**
|
|
100
|
+
* Chemin de `href` s'il est servi sous `pageOrigin` (`/mail/auth/hgdc?…` depuis
|
|
101
|
+
* `https://app.hgdc.ch`), sinon `null`. Sans origine connue (rendu serveur), `null`.
|
|
102
|
+
*/
|
|
103
|
+
export declare function sameOriginPath(href: string, pageOrigin: string | undefined): string | null;
|
|
99
104
|
/** Une entrée de la barre des produits d'`AppShell`. */
|
|
100
105
|
export type ProductRailEntry = {
|
|
101
106
|
name: string;
|
|
@@ -105,17 +110,27 @@ export type ProductRailEntry = {
|
|
|
105
110
|
current?: boolean;
|
|
106
111
|
/** Texte discret sous le nom : « Préversion ». */
|
|
107
112
|
hint?: string;
|
|
113
|
+
/**
|
|
114
|
+
* Produit servi sous la même origine que la page : lien relatif, prérendu au survol
|
|
115
|
+
* (`PRODUCT_PRERENDER_ATTRIBUTE` de `@hgdc/ui/product-switch`) et transition de vue.
|
|
116
|
+
*/
|
|
117
|
+
sameOrigin?: boolean;
|
|
108
118
|
};
|
|
109
119
|
/**
|
|
110
120
|
* Les entrées de la barre des produits : les produits ouverts du catalogue et Account, affiché en
|
|
111
121
|
* tête (le compte commun, pas un produit). Chaque produit pointe vers son entrée
|
|
112
122
|
* `/auth/hgdc?refresh=background&next=/` (ADR 0010, 0013, 0016), Account vers sa racine (`accountHref` en
|
|
113
|
-
* développement). Le produit courant pointe vers
|
|
123
|
+
* développement). Le produit courant pointe vers sa racine, sans détour par l'entrée.
|
|
124
|
+
*
|
|
125
|
+
* `pageOrigin` (`location.origin`, absent au rendu serveur) : un produit servi sous cette origine
|
|
126
|
+
* (`PRODUCT_ORIGINS`, `app.hgdc.ch/<produit>`) reçoit un lien relatif et `sameOrigin` (ADR 0020),
|
|
127
|
+
* et le produit courant sa base (`/mail/`) plutôt que `/`.
|
|
114
128
|
*/
|
|
115
129
|
export declare function productRailEntries(options?: {
|
|
116
130
|
current?: string | undefined;
|
|
117
131
|
accountHref?: string | undefined;
|
|
118
132
|
catalog?: ReadonlyArray<HgdcCatalogProduct> | undefined;
|
|
133
|
+
pageOrigin?: string | undefined;
|
|
119
134
|
}): {
|
|
120
135
|
products: Array<ProductRailEntry>;
|
|
121
136
|
account: ProductRailEntry;
|
package/dist/products.js
CHANGED
|
@@ -33,6 +33,8 @@ export const PRODUCT_ENTRY_PATH = "/auth/hgdc";
|
|
|
33
33
|
export const PRODUCT_ORIGINS = {
|
|
34
34
|
analytics: { origin: "https://app.hgdc.ch", basePath: "/analytics" },
|
|
35
35
|
mail: { origin: "https://app.hgdc.ch", basePath: "/mail" },
|
|
36
|
+
host: { origin: "https://app.hgdc.ch", basePath: "/host" },
|
|
37
|
+
docs: { origin: "https://app.hgdc.ch", basePath: "/docs" },
|
|
36
38
|
};
|
|
37
39
|
/** Adresse de l'app d'un produit : celle de `PRODUCT_ORIGINS`, sinon `https://<mot>.hgdc.ch`. */
|
|
38
40
|
export function productOrigin(word, origins = PRODUCT_ORIGINS) {
|
|
@@ -103,22 +105,49 @@ export function productMenuEntries(options = {}) {
|
|
|
103
105
|
...(item.stage === "preview" ? { hint: PRODUCT_STAGE_LABEL.preview } : {}),
|
|
104
106
|
}));
|
|
105
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Chemin de `href` s'il est servi sous `pageOrigin` (`/mail/auth/hgdc?…` depuis
|
|
110
|
+
* `https://app.hgdc.ch`), sinon `null`. Sans origine connue (rendu serveur), `null`.
|
|
111
|
+
*/
|
|
112
|
+
export function sameOriginPath(href, pageOrigin) {
|
|
113
|
+
if (!pageOrigin)
|
|
114
|
+
return null;
|
|
115
|
+
try {
|
|
116
|
+
const url = new URL(href);
|
|
117
|
+
return url.origin === pageOrigin ? `${url.pathname}${url.search}${url.hash}` : null;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
106
123
|
/**
|
|
107
124
|
* Les entrées de la barre des produits : les produits ouverts du catalogue et Account, affiché en
|
|
108
125
|
* tête (le compte commun, pas un produit). Chaque produit pointe vers son entrée
|
|
109
126
|
* `/auth/hgdc?refresh=background&next=/` (ADR 0010, 0013, 0016), Account vers sa racine (`accountHref` en
|
|
110
|
-
* développement). Le produit courant pointe vers
|
|
127
|
+
* développement). Le produit courant pointe vers sa racine, sans détour par l'entrée.
|
|
128
|
+
*
|
|
129
|
+
* `pageOrigin` (`location.origin`, absent au rendu serveur) : un produit servi sous cette origine
|
|
130
|
+
* (`PRODUCT_ORIGINS`, `app.hgdc.ch/<produit>`) reçoit un lien relatif et `sameOrigin` (ADR 0020),
|
|
131
|
+
* et le produit courant sa base (`/mail/`) plutôt que `/`.
|
|
111
132
|
*/
|
|
112
133
|
export function productRailEntries(options = {}) {
|
|
113
134
|
const current = options.current?.toLowerCase();
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
const { pageOrigin } = options;
|
|
136
|
+
const entry = (item, href, product) => {
|
|
137
|
+
const base = {
|
|
138
|
+
name: item.name,
|
|
139
|
+
badge: item.badge,
|
|
140
|
+
...(item.stage === "preview" ? { hint: PRODUCT_STAGE_LABEL.preview } : {}),
|
|
141
|
+
};
|
|
142
|
+
if (item.badge === current) {
|
|
143
|
+
const app = productOrigin(item.badge);
|
|
144
|
+
return { ...base, href: pageOrigin === app.origin ? `${app.basePath}/` : "/", current: true };
|
|
145
|
+
}
|
|
146
|
+
const path = product ? sameOriginPath(href, pageOrigin) : null;
|
|
147
|
+
return path === null ? { ...base, href } : { ...base, href: path, sameOrigin: true };
|
|
148
|
+
};
|
|
120
149
|
return {
|
|
121
|
-
products: (options.catalog ?? HGDC_PRODUCTS).filter(isProductOpen).map((item) => entry(item, item.entryHref)),
|
|
122
|
-
account: entry(HGDC_ACCOUNT, options.accountHref ?? HGDC_ACCOUNT.entryHref),
|
|
150
|
+
products: (options.catalog ?? HGDC_PRODUCTS).filter(isProductOpen).map((item) => entry(item, item.entryHref, true)),
|
|
151
|
+
account: entry(HGDC_ACCOUNT, options.accountHref ?? HGDC_ACCOUNT.entryHref, false),
|
|
123
152
|
};
|
|
124
153
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hgdc/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "Interface de la H Collection : jetons de la DA, thèmes, polices et composants.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://github.com/hugogebel-boop/hgdc-kit/tree/main/packages/ui#readme",
|
package/styles.css
CHANGED
|
@@ -870,3 +870,43 @@
|
|
|
870
870
|
opacity 150ms var(--ease-out),
|
|
871
871
|
visibility 0s;
|
|
872
872
|
}
|
|
873
|
+
|
|
874
|
+
/* Passage d'un produit à l'autre sous la même origine (`app.hgdc.ch/<produit>`, ADR 0020) : une
|
|
875
|
+
transition de vue entre documents. La barre des produits, la barre latérale et la barre du haut
|
|
876
|
+
gardent leur place (même nom des deux côtés : elles ne bougent pas, leur contenu passe en
|
|
877
|
+
fondu) ; le reste de la page passe en fondu court. Le navigateur garde l'ancienne page à l'écran
|
|
878
|
+
jusqu'à ce que la nouvelle soit prête : pas de page blanche. Chrome, Edge et Safari 18.2+ ;
|
|
879
|
+
ailleurs, une navigation ordinaire. Entre deux origines (un produit encore sur son sous-domaine)
|
|
880
|
+
ou derrière une redirection vers Account, le navigateur n'en fait pas. Mouvement réduit : aucune
|
|
881
|
+
transition. */
|
|
882
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
883
|
+
@view-transition {
|
|
884
|
+
navigation: auto;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
[data-slot="product-rail"] {
|
|
888
|
+
view-transition-name: hgdc-product-rail;
|
|
889
|
+
}
|
|
890
|
+
[data-slot="app-shell-sidebar"] {
|
|
891
|
+
view-transition-name: hgdc-sidebar;
|
|
892
|
+
}
|
|
893
|
+
[data-slot="app-shell-header"] {
|
|
894
|
+
view-transition-name: hgdc-header;
|
|
895
|
+
}
|
|
896
|
+
::view-transition-old(root),
|
|
897
|
+
::view-transition-new(root) {
|
|
898
|
+
animation-duration: var(--duration-fast);
|
|
899
|
+
animation-timing-function: var(--ease-out);
|
|
900
|
+
}
|
|
901
|
+
::view-transition-group(hgdc-product-rail),
|
|
902
|
+
::view-transition-group(hgdc-sidebar),
|
|
903
|
+
::view-transition-group(hgdc-header),
|
|
904
|
+
::view-transition-old(hgdc-product-rail),
|
|
905
|
+
::view-transition-new(hgdc-product-rail),
|
|
906
|
+
::view-transition-old(hgdc-sidebar),
|
|
907
|
+
::view-transition-new(hgdc-sidebar),
|
|
908
|
+
::view-transition-old(hgdc-header),
|
|
909
|
+
::view-transition-new(hgdc-header) {
|
|
910
|
+
animation-duration: var(--duration-base);
|
|
911
|
+
animation-timing-function: var(--ease-ios);
|
|
912
|
+
}
|