@hgdc/ui 0.38.0 → 0.39.1
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 -1
- package/dist/account-menu.js +6 -1
- package/dist/app-shell.d.ts +9 -3
- package/dist/app-shell.js +25 -11
- package/dist/family-loader.js +2 -1
- package/dist/org-switcher.js +9 -0
- package/dist/product-rail.d.ts +18 -1
- package/dist/product-rail.js +44 -4
- package/dist/products.js +1 -1
- package/dist/shell-frame.d.ts +21 -0
- package/dist/shell-frame.js +33 -0
- package/dist/shell-memory.d.ts +65 -0
- package/dist/shell-memory.js +169 -0
- package/package.json +1 -1
- package/styles.css +110 -13
package/README.md
CHANGED
|
@@ -65,11 +65,21 @@ Le menu « Produits » et la palette (`badge` d'une commande) montrent la pastil
|
|
|
65
65
|
|
|
66
66
|
### D'un produit à l'autre sous `app.hgdc.ch` (ADR 0020)
|
|
67
67
|
|
|
68
|
-
Entre deux produits servis sous la même origine, la coque fait comme une seule app : transition de vue entre documents
|
|
68
|
+
Entre deux produits servis sous la même origine, la coque fait comme une seule app : transition de vue entre documents, 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
|
+
Pendant le passage (0.39) :
|
|
71
|
+
|
|
72
|
+
- **Barre des produits** : immobile, sans fondu, seul l'indicateur du produit change. La page d'arrivée la peint identique dès sa première image, même quand ses données ne sont pas là : `OrgPlaceholder` tient la place de l'organisation (`organization` absent) avec la marque de la dernière organisation affichée sous l'origine (initiales ou logo, retenus par `OrgSwitcher`, oubliés à la déconnexion par `AccountMenu`), sinon un squelette de la même taille. Le clic la replie sans transition et la page d'arrivée ne l'élargit pas sous la souris restée dessus.
|
|
73
|
+
- **Barre latérale** : la marque du produit et sa navigation passent en fondu de 200 ms. Une page qui attend hors de la coque (`PagePending` sous `FamilyLoaderRoot`) dessine la coque entière : barre des produits, marque du produit de l'adresse, navigation en squelette à la hauteur retenue pour ce produit, barre du haut.
|
|
74
|
+
- **H** : un seul nom de transition (`hgdc-loader`), sans fondu, calé sur l'horloge : celui de l'arrivée reprend à la même phase.
|
|
75
|
+
- **Contenu** : fondu de 150 ms.
|
|
76
|
+
|
|
77
|
+
`sidebarInitScript` (dans le `<head>`, déjà posé par tous les produits) applique cette mémoire sur `<html>` avant la première peinture : `data-hgdc-product`, `data-hgdc-org`, `--hgdc-org-initials`, `--hgdc-org-logo` (adresse `https:` seulement), `--hgdc-nav-height`. Elle vit dans `localStorage` (`hgdc-shell:org`, `hgdc-shell:nav:<produit>`) et `sessionStorage` (`hgdc-shell:switch`) ; rien d'autre que des initiales, un logo et une hauteur.
|
|
69
78
|
|
|
70
79
|
Ce que chaque produit fait :
|
|
71
80
|
|
|
72
81
|
- Monter `@hgdc/ui` et `@hgdc/auth` : `styles.css` déclare `@view-transition`, `AppShell` pose les noms et les règles. Rien d'autre à écrire.
|
|
82
|
+
- Pendant le chargement de ses données, rendre `AppShell` sans `organization` (pas `organization={null}`) : la place reste tenue.
|
|
73
83
|
- 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
84
|
- 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
85
|
- `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`).
|
package/dist/account-menu.js
CHANGED
|
@@ -12,6 +12,7 @@ import { initials } from "./initials.js";
|
|
|
12
12
|
import { MENU_LINK_ITEM } from "./org-switcher.js";
|
|
13
13
|
import { ProductBadge } from "./product-badge.js";
|
|
14
14
|
import { productMenuEntries } from "./products.js";
|
|
15
|
+
import { forgetShellOrganization } from "./shell-memory.js";
|
|
15
16
|
import { useLeaveOnClick, useProductRailShown } from "./shell-placement.js";
|
|
16
17
|
import { ThemeSwitch } from "./theme-switch.js";
|
|
17
18
|
/** L'avatar du compte : la photo, sinon les initiales sur la teinte stable de l'adresse. */
|
|
@@ -38,7 +39,11 @@ export function AccountMenu({ user, onProfile, onSecurity, onSignOut, products:
|
|
|
38
39
|
const products = productsProp ?? productMenuEntries({ current: currentProduct, accountHref });
|
|
39
40
|
const railShown = useProductRailShown();
|
|
40
41
|
const leaveOnClick = useLeaveOnClick();
|
|
41
|
-
return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: variant === "row" ? (_jsx("button", { type: "button", "aria-label": `Compte de ${user.name}`, className: "flex w-full min-w-0 items-center gap-2.5 rounded-xl p-1.5 text-left outline-none transition-colors duration-(--duration-fast) ease-out hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50 aria-expanded:bg-muted lg:hover:bg-shell-hover lg:aria-expanded:bg-shell-hover lg:rail:justify-center" })) : (_jsx("button", { type: "button", "aria-label": `Compte de ${user.name}`, className: "touch-target grid size-9 place-items-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring/50" })), children: [_jsx(AccountAvatar, { user: user }), variant === "row" ? (_jsxs("span", { className: "min-w-0 flex-1 lg:rail:hidden", children: [_jsx("span", { className: "block truncate text-sm font-medium", children: user.name }), _jsx("span", { className: "block truncate text-xs text-muted-foreground lg:text-shell-muted", children: user.email })] })) : null] }), _jsxs(DropdownMenuContent, { align: variant === "row" ? "start" : "end", side: variant === "row" ? "top" : "bottom", className: "w-72", children: [_jsx(DropdownMenuGroup, { children: _jsxs(DropdownMenuLabel, { className: "flex items-center gap-2.5 py-2", children: [_jsx(AccountAvatar, { user: user }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-sm font-medium text-foreground", children: user.name }), _jsx("span", { className: "block truncate", children: user.email })] })] }) }), _jsx(DropdownMenuSeparator, {}), onProfile ? (_jsxs(DropdownMenuItem, { onClick: onProfile, children: [_jsx(HugeiconsIcon, { icon: UserIcon, "aria-hidden": true }), "Profil"] })) : null, onSecurity ? (_jsxs(DropdownMenuItem, { onClick: onSecurity, children: [_jsx(HugeiconsIcon, { icon: SecurityLockIcon, "aria-hidden": true }), "S\u00E9curit\u00E9"] })) : null, _jsx(ThemeSwitch, {}), children ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), children] })) : null, products.length ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, { className: railShown ? "lg:hidden" : undefined }), _jsxs(DropdownMenuGroup, { className: railShown ? "lg:hidden" : undefined, children: [_jsx(DropdownMenuLabel, { children: "Produits" }), products.map((product) => product.current ? (_jsxs(DropdownMenuItem, { disabled: true, className: "data-disabled:opacity-100", children: [_jsx(ProductMenuIcon, { product: product }), _jsx("span", { className: "flex-1", children: product.name }), _jsx(HugeiconsIcon, { icon: Tick02Icon, "aria-label": "Produit ouvert" })] }, product.name)) : (_jsxs(MenuPrimitive.LinkItem, { href: product.href, onClick: leaveOnClick, className: MENU_LINK_ITEM, children: [_jsx(ProductMenuIcon, { product: product }), _jsx("span", { className: "flex-1", children: product.name }), product.hint ? (_jsx("span", { className: "text-xs font-normal text-muted-foreground", children: product.hint })) : null] }, product.name)))] })] })) : null, _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick:
|
|
42
|
+
return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: variant === "row" ? (_jsx("button", { type: "button", "aria-label": `Compte de ${user.name}`, className: "flex w-full min-w-0 items-center gap-2.5 rounded-xl p-1.5 text-left outline-none transition-colors duration-(--duration-fast) ease-out hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50 aria-expanded:bg-muted lg:hover:bg-shell-hover lg:aria-expanded:bg-shell-hover lg:rail:justify-center" })) : (_jsx("button", { type: "button", "aria-label": `Compte de ${user.name}`, className: "touch-target grid size-9 place-items-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring/50" })), children: [_jsx(AccountAvatar, { user: user }), variant === "row" ? (_jsxs("span", { className: "min-w-0 flex-1 lg:rail:hidden", children: [_jsx("span", { className: "block truncate text-sm font-medium", children: user.name }), _jsx("span", { className: "block truncate text-xs text-muted-foreground lg:text-shell-muted", children: user.email })] })) : null] }), _jsxs(DropdownMenuContent, { align: variant === "row" ? "start" : "end", side: variant === "row" ? "top" : "bottom", className: "w-72", children: [_jsx(DropdownMenuGroup, { children: _jsxs(DropdownMenuLabel, { className: "flex items-center gap-2.5 py-2", children: [_jsx(AccountAvatar, { user: user }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-sm font-medium text-foreground", children: user.name }), _jsx("span", { className: "block truncate", children: user.email })] })] }) }), _jsx(DropdownMenuSeparator, {}), onProfile ? (_jsxs(DropdownMenuItem, { onClick: onProfile, children: [_jsx(HugeiconsIcon, { icon: UserIcon, "aria-hidden": true }), "Profil"] })) : null, onSecurity ? (_jsxs(DropdownMenuItem, { onClick: onSecurity, children: [_jsx(HugeiconsIcon, { icon: SecurityLockIcon, "aria-hidden": true }), "S\u00E9curit\u00E9"] })) : null, _jsx(ThemeSwitch, {}), children ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), children] })) : null, products.length ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, { className: railShown ? "lg:hidden" : undefined }), _jsxs(DropdownMenuGroup, { className: railShown ? "lg:hidden" : undefined, children: [_jsx(DropdownMenuLabel, { children: "Produits" }), products.map((product) => product.current ? (_jsxs(DropdownMenuItem, { disabled: true, className: "data-disabled:opacity-100", children: [_jsx(ProductMenuIcon, { product: product }), _jsx("span", { className: "flex-1", children: product.name }), _jsx(HugeiconsIcon, { icon: Tick02Icon, "aria-label": "Produit ouvert" })] }, product.name)) : (_jsxs(MenuPrimitive.LinkItem, { href: product.href, onClick: leaveOnClick, className: MENU_LINK_ITEM, children: [_jsx(ProductMenuIcon, { product: product }), _jsx("span", { className: "flex-1", children: product.name }), product.hint ? (_jsx("span", { className: "text-xs font-normal text-muted-foreground", children: product.hint })) : null] }, product.name)))] })] })) : null, _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: () => {
|
|
43
|
+
// La marque de l'organisation retenue pour la coque ne survit pas à la session.
|
|
44
|
+
forgetShellOrganization();
|
|
45
|
+
onSignOut();
|
|
46
|
+
}, children: [_jsx(HugeiconsIcon, { icon: Logout01Icon, "aria-hidden": true }), "Se d\u00E9connecter"] })] })] }));
|
|
42
47
|
}
|
|
43
48
|
function ProductMenuIcon({ product }) {
|
|
44
49
|
if (product.badge)
|
package/dist/app-shell.d.ts
CHANGED
|
@@ -2,8 +2,10 @@ import { type IconSvgElement } from "@hugeicons/react";
|
|
|
2
2
|
import { type ReactNode } from "react";
|
|
3
3
|
import type { FamilyProduct } from "./family-svg.ts";
|
|
4
4
|
/**
|
|
5
|
-
* À poser dans le `<head>`, avec `themeInitScript` :
|
|
6
|
-
*
|
|
5
|
+
* À poser dans le `<head>`, avec `themeInitScript` : avant la première peinture, il remet le
|
|
6
|
+
* rail (la variante CSS `rail:` le lit, rien ne saute au chargement) et la mémoire de la coque
|
|
7
|
+
* partagée sous la même origine (`shell-memory` : produit de l'adresse, dernière organisation,
|
|
8
|
+
* hauteur de la navigation), que le cadre du chargeur et `OrgPlaceholder` lisent (ADR 0020).
|
|
7
9
|
*/
|
|
8
10
|
export declare const sidebarInitScript: string;
|
|
9
11
|
/** La barre latérale est-elle réduite en rail ? L'état vit sur `<html data-sidebar="rail">`. */
|
|
@@ -126,7 +128,11 @@ export declare const PRODUCT_LEAVE_DELAY_MS = 100;
|
|
|
126
128
|
export declare function AppShell({ product, organization, productRail, accountHref, nav, links, account, sidebarFooter, header, onSearch, searchLabel, assistant, children, contentKey, navigating, mainClassName, statusPage, }: {
|
|
127
129
|
/** Le produit affiché (`"host"`) : sa marque en haut de la barre, « vous êtes ici » dans la barre des produits. */
|
|
128
130
|
product: FamilyProduct;
|
|
129
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* `OrgSwitcher` : en haut de la barre des produits, en haut du tiroir sur téléphone. Absent
|
|
133
|
+
* pendant que les données du produit arrivent : la barre lui garde sa place, avec la marque de
|
|
134
|
+
* la dernière organisation affichée (`OrgPlaceholder`, ADR 0020). `null` : aucune organisation.
|
|
135
|
+
*/
|
|
130
136
|
organization?: ReactNode;
|
|
131
137
|
/**
|
|
132
138
|
* `false` : pas de barre des produits (un écran hors de la famille). L'organisation passe alors
|
package/dist/app-shell.js
CHANGED
|
@@ -20,8 +20,9 @@ import { CONTENT_LOADING_LAYER, ContentLoadingContext, PAGE_PENDING_MIN_MS, PAGE
|
|
|
20
20
|
import { ProductBadge } from "./product-badge.js";
|
|
21
21
|
import { ProductRail } from "./product-rail.js";
|
|
22
22
|
import { useProductSwitch } from "./product-switch.js";
|
|
23
|
-
import { HGDC_ACCOUNT, HGDC_PRODUCTS } from "./products.js";
|
|
24
23
|
import { ProductRailShownContext, ShellLeaveContext, StatusPageContext } from "./shell-placement.js";
|
|
24
|
+
import { familyProductName, SIDEBAR_BRAND, SIDEBAR_BRAND_NAME, SIDEBAR_HEAD } from "./shell-frame.js";
|
|
25
|
+
import { rememberShellNavHeight, shellBootScript } from "./shell-memory.js";
|
|
25
26
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
|
|
26
27
|
import { cn } from "./cn.js";
|
|
27
28
|
import { ServiceStatus, statusPageUrlFor } from "./service-status.js";
|
|
@@ -30,10 +31,12 @@ import { useMediaQuery } from "./use-media-query.js";
|
|
|
30
31
|
const RAIL_KEY = "sidebar";
|
|
31
32
|
const RAIL_EVENT = "sidebarchange";
|
|
32
33
|
/**
|
|
33
|
-
* À poser dans le `<head>`, avec `themeInitScript` :
|
|
34
|
-
*
|
|
34
|
+
* À poser dans le `<head>`, avec `themeInitScript` : avant la première peinture, il remet le
|
|
35
|
+
* rail (la variante CSS `rail:` le lit, rien ne saute au chargement) et la mémoire de la coque
|
|
36
|
+
* partagée sous la même origine (`shell-memory` : produit de l'adresse, dernière organisation,
|
|
37
|
+
* hauteur de la navigation), que le cadre du chargeur et `OrgPlaceholder` lisent (ADR 0020).
|
|
35
38
|
*/
|
|
36
|
-
export const sidebarInitScript =
|
|
39
|
+
export const sidebarInitScript = shellBootScript();
|
|
37
40
|
function subscribeRail(onChange) {
|
|
38
41
|
window.addEventListener(RAIL_EVENT, onChange);
|
|
39
42
|
return () => window.removeEventListener(RAIL_EVENT, onChange);
|
|
@@ -344,11 +347,6 @@ const TOGGLE = "touch-target relative grid h-9 w-10 shrink-0 place-items-center
|
|
|
344
347
|
function ShortcutHint({ label }) {
|
|
345
348
|
return (_jsxs("span", { className: "flex items-center gap-2", children: [label, _jsxs(KbdGroup, { children: [_jsx(Kbd, { children: isApplePlatform() ? "⌘" : "Ctrl" }), _jsx(Kbd, { children: "B" })] })] }));
|
|
346
349
|
}
|
|
347
|
-
function productName(product) {
|
|
348
|
-
if (product === HGDC_ACCOUNT.badge)
|
|
349
|
-
return HGDC_ACCOUNT.name;
|
|
350
|
-
return HGDC_PRODUCTS.find((item) => item.badge === product)?.name ?? `HGDC ${product}`;
|
|
351
|
-
}
|
|
352
350
|
/**
|
|
353
351
|
* Haut de la barre latérale. Dépliée : la marque du produit, et le bouton « Réduire » à droite.
|
|
354
352
|
* En rail : la marque seule, qui devient le bouton « Déplier » au survol de la barre ou au focus,
|
|
@@ -356,7 +354,7 @@ function productName(product) {
|
|
|
356
354
|
* choisis par la variante CSS `rail:`, donc justes dès la première peinture.
|
|
357
355
|
*/
|
|
358
356
|
function SidebarHead({ product }) {
|
|
359
|
-
return (_jsxs("div", { className:
|
|
357
|
+
return (_jsxs("div", { className: SIDEBAR_HEAD, children: [_jsxs("div", { "data-slot": "sidebar-brand", className: cn(SIDEBAR_BRAND, "rail:hidden"), children: [_jsx(ProductBadge, { product: product, className: "size-6" }), _jsx("span", { className: SIDEBAR_BRAND_NAME, children: familyProductName(product) })] }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx("button", { type: "button", "data-slot": "sidebar-collapse", onClick: toggleSidebar, "aria-label": "R\u00E9duire la barre lat\u00E9rale", "aria-keyshortcuts": "Control+B Meta+B", className: cn(TOGGLE, "rail:hidden") }), children: _jsx(HugeiconsIcon, { icon: COLLAPSE_ICON, strokeWidth: 1.8, "aria-hidden": true, className: "size-[1.125rem]" }) }), _jsx(TooltipContent, { side: "right", children: _jsx(ShortcutHint, { label: "R\u00E9duire" }) })] }), _jsxs(Tooltip, { children: [_jsxs(TooltipTrigger, { render: _jsx("button", { type: "button", "data-slot": "sidebar-expand", onClick: toggleSidebar, "aria-label": "D\u00E9plier la barre lat\u00E9rale", "aria-keyshortcuts": "Control+B Meta+B", className: cn(TOGGLE, "group/toggle hidden rail:grid") }), children: [_jsx(ProductBadge, { product: product, className: "col-start-1 row-start-1 size-6 transition-opacity duration-(--duration-fast) ease-out group-hover/sidebar:opacity-0 group-focus-visible/toggle:opacity-0 motion-reduce:transition-none" }), _jsx(HugeiconsIcon, { icon: EXPAND_ICON, strokeWidth: 1.8, "aria-hidden": true, className: "col-start-1 row-start-1 size-[1.125rem] opacity-0 transition-opacity duration-(--duration-fast) ease-out group-hover/sidebar:opacity-100 group-focus-visible/toggle:opacity-100 motion-reduce:transition-none" })] }), _jsx(TooltipContent, { side: "right", children: _jsx(ShortcutHint, { label: "D\u00E9plier" }) })] })] }));
|
|
360
358
|
}
|
|
361
359
|
/**
|
|
362
360
|
* La coque commune des consoles HGDC, celle de Host. Sur ordinateur, de gauche à droite : la barre
|
|
@@ -445,6 +443,7 @@ export function AppShell({ product, organization, productRail = true, accountHre
|
|
|
445
443
|
return;
|
|
446
444
|
window.clearTimeout(leaveTimer.current);
|
|
447
445
|
setLeaving(false);
|
|
446
|
+
delete document.documentElement.dataset.hgdcSwitching;
|
|
448
447
|
};
|
|
449
448
|
window.addEventListener("pageshow", onPageShow);
|
|
450
449
|
return () => {
|
|
@@ -459,6 +458,21 @@ export function AppShell({ product, organization, productRail = true, accountHre
|
|
|
459
458
|
const hydrated = useHydrated();
|
|
460
459
|
const hasRoot = useContext(FamilyLoaderRootContext);
|
|
461
460
|
const contentRef = useRef(null);
|
|
461
|
+
const sidebarRef = useRef(null);
|
|
462
|
+
// Mémoire de la coque (ADR 0020) : le produit affiché, pour le cadre d'un chargement qui suivrait
|
|
463
|
+
// hors de la coque dans ce document, et la hauteur de sa navigation, pour le squelette de la
|
|
464
|
+
// barre latérale à la prochaine arrivée sur ce produit.
|
|
465
|
+
useIsomorphicLayoutEffect(() => {
|
|
466
|
+
document.documentElement.dataset.hgdcProduct = product;
|
|
467
|
+
}, [product]);
|
|
468
|
+
const navHeight = useRef(0);
|
|
469
|
+
useEffect(() => {
|
|
470
|
+
const list = sidebarRef.current?.querySelector(":scope > nav > ul");
|
|
471
|
+
if (!(list instanceof HTMLElement) || list.offsetHeight === navHeight.current)
|
|
472
|
+
return;
|
|
473
|
+
navHeight.current = list.offsetHeight;
|
|
474
|
+
rememberShellNavHeight(product, list.offsetHeight);
|
|
475
|
+
});
|
|
462
476
|
useEffect(() => {
|
|
463
477
|
// Sur ordinateur, la zone de contenu défile seule : le calque couvre sa partie visible, en haut.
|
|
464
478
|
if (loading && contentRef.current)
|
|
@@ -477,5 +491,5 @@ export function AppShell({ product, organization, productRail = true, accountHre
|
|
|
477
491
|
const assistantButtonRef = useRef(null);
|
|
478
492
|
useAssistantShortcut(assistant);
|
|
479
493
|
const footer = (_jsxs(_Fragment, { children: [account !== undefined ? sidebarFooter : null, _jsx(ShellCommonLinks, { links: links ?? {}, statusPageUrl: statusPageUrl }), account ?? sidebarFooter] }));
|
|
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:
|
|
494
|
+
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", { ref: sidebarRef, "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: familyProductName(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 })] }) })] }) }) }) }) }));
|
|
481
495
|
}
|
package/dist/family-loader.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { createContext, useContext, useEffect, useLayoutEffect, useRef } from "react";
|
|
3
3
|
import { cn } from "./cn.js";
|
|
4
4
|
import { useBackgroundOrganizationRefresh } from "./product-switch.js";
|
|
5
|
+
import { ShellFrame } from "./shell-frame.js";
|
|
5
6
|
const SIZES = { sm: 24, md: 40, lg: 64 };
|
|
6
7
|
/** Durée de la boucle du H (`styles.css`, `.family-loader [data-fill]`). */
|
|
7
8
|
export const FAMILY_LOADER_PERIOD_MS = 2000;
|
|
@@ -92,7 +93,7 @@ function FamilyMark({ px, label, className, }) {
|
|
|
92
93
|
export function FamilyLoaderHost() {
|
|
93
94
|
const ref = useRef(null);
|
|
94
95
|
useClockSync(ref);
|
|
95
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: ref, "data-slot": "family-loader-root", "aria-hidden": true, children: [_jsx("div", { "data-frame": true }), _jsx("div", { "data-stage": true, children: _jsx("span", { "data-slot": "family-loader", className: "family-loader", style: { ["--family-loader-size"]: "64px" }, children: _jsx(Mark, {}) }) })] }), _jsx("script", { dangerouslySetInnerHTML: { __html: SYNC_SCRIPT } })] }));
|
|
96
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: ref, "data-slot": "family-loader-root", "aria-hidden": true, children: [_jsx("div", { "data-frame": true, children: _jsx(ShellFrame, {}) }), _jsx("div", { "data-stage": true, children: _jsx("span", { "data-slot": "family-loader", className: "family-loader", style: { ["--family-loader-size"]: "64px" }, children: _jsx(Mark, {}) }) })] }), _jsx("script", { dangerouslySetInnerHTML: { __html: SYNC_SCRIPT } })] }));
|
|
96
97
|
}
|
|
97
98
|
/**
|
|
98
99
|
* Le chargeur du document, à poser une fois dans le `body` de la route racine, autour de l'app :
|
package/dist/org-switcher.js
CHANGED
|
@@ -4,9 +4,11 @@ import Tick02Icon from "@hugeicons/core-free-icons/Tick02Icon";
|
|
|
4
4
|
import UnfoldMoreIcon from "@hugeicons/core-free-icons/UnfoldMoreIcon";
|
|
5
5
|
import { HugeiconsIcon } from "@hugeicons/react";
|
|
6
6
|
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
|
|
7
|
+
import { useEffect } from "react";
|
|
7
8
|
import { Badge } from "./badge.js";
|
|
8
9
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "./dropdown-menu.js";
|
|
9
10
|
import { initials } from "./initials.js";
|
|
11
|
+
import { rememberShellOrganization } from "./shell-memory.js";
|
|
10
12
|
import { useShellPlacement } from "./shell-placement.js";
|
|
11
13
|
import { cn } from "./cn.js";
|
|
12
14
|
export const ORG_ROLE_LABEL = {
|
|
@@ -39,6 +41,13 @@ export function OrgMark({ org, className, }) {
|
|
|
39
41
|
export function OrgSwitcher({ organizations, activeId, onSelect, createHref, onCreate, className, }) {
|
|
40
42
|
const placement = useShellPlacement();
|
|
41
43
|
const active = organizations.find((org) => org.id === activeId);
|
|
44
|
+
// Retenue pour la première image du prochain produit ouvert sous la même origine (ADR 0020).
|
|
45
|
+
const activeName = active?.name;
|
|
46
|
+
const activeLogo = active?.logo;
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (activeName !== undefined)
|
|
49
|
+
rememberShellOrganization({ name: activeName, logo: activeLogo });
|
|
50
|
+
}, [activeName, activeLogo]);
|
|
42
51
|
if (!active)
|
|
43
52
|
return null;
|
|
44
53
|
// En haut de la barre des produits : la marque seule dans 40 px, le nom quand la barre s'élargit.
|
package/dist/product-rail.d.ts
CHANGED
|
@@ -2,6 +2,20 @@ import { type ReactNode } from "react";
|
|
|
2
2
|
import { type HgdcCatalogProduct } from "./products.ts";
|
|
3
3
|
/** Délai d'intention avant d'élargir la barre au survol : un passage de souris ne l'ouvre pas. */
|
|
4
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;
|
|
5
19
|
/**
|
|
6
20
|
* La barre des produits de la famille, tout à gauche de la coque sur ordinateur : organisation en
|
|
7
21
|
* haut, Account, puis les pastilles des produits. 56 px de large ; au survol (après un court délai
|
|
@@ -14,7 +28,10 @@ export declare const PRODUCT_RAIL_OPEN_DELAY = 150;
|
|
|
14
28
|
export declare function ProductRail({ current, organization, accountHref, catalog, onExpandedChange, }: {
|
|
15
29
|
/** Le mot du produit affiché (« host ») : marqué en teinte. */
|
|
16
30
|
current?: string | undefined;
|
|
17
|
-
/**
|
|
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
|
+
*/
|
|
18
35
|
organization?: ReactNode;
|
|
19
36
|
/** Adresse d'Account (un Account local en développement). */
|
|
20
37
|
accountHref?: string | undefined;
|
package/dist/product-rail.js
CHANGED
|
@@ -3,6 +3,7 @@ import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } fr
|
|
|
3
3
|
import { ProductBadge } from "./product-badge.js";
|
|
4
4
|
import { PRODUCT_PRERENDER_ATTRIBUTE } from "./product-switch.js";
|
|
5
5
|
import { productRailEntries } from "./products.js";
|
|
6
|
+
import { markProductSwitch, takeProductSwitch } from "./shell-memory.js";
|
|
6
7
|
import { ShellPlacementContext, useLeaveOnClick } from "./shell-placement.js";
|
|
7
8
|
import { cn } from "./cn.js";
|
|
8
9
|
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
@@ -13,10 +14,41 @@ function usePageOrigin() {
|
|
|
13
14
|
}
|
|
14
15
|
/** Délai d'intention avant d'élargir la barre au survol : un passage de souris ne l'ouvre pas. */
|
|
15
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
|
+
}
|
|
16
26
|
function ProductLink({ entry }) {
|
|
17
27
|
// Chaque produit s'ouvre en document complet : la coque montre le H dès le clic.
|
|
18
28
|
const leaveOnClick = useLeaveOnClick();
|
|
19
|
-
return (_jsx("li", { children:
|
|
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))) })] }) }));
|
|
20
52
|
}
|
|
21
53
|
/**
|
|
22
54
|
* La barre des produits de la famille, tout à gauche de la coque sur ordinateur : organisation en
|
|
@@ -50,17 +82,21 @@ export function ProductRail({ current, organization, accountHref, catalog, onExp
|
|
|
50
82
|
if (!rail)
|
|
51
83
|
return;
|
|
52
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();
|
|
53
88
|
const cancel = () => {
|
|
54
89
|
window.clearTimeout(timer);
|
|
55
90
|
timer = undefined;
|
|
56
91
|
};
|
|
57
92
|
const onEnter = (event) => {
|
|
58
|
-
if (event.pointerType !== "mouse")
|
|
93
|
+
if (event.pointerType !== "mouse" || arrivedUnderPointer)
|
|
59
94
|
return;
|
|
60
95
|
cancel();
|
|
61
96
|
timer = window.setTimeout(() => setHovered(true), PRODUCT_RAIL_OPEN_DELAY);
|
|
62
97
|
};
|
|
63
98
|
const onLeave = () => {
|
|
99
|
+
arrivedUnderPointer = false;
|
|
64
100
|
cancel();
|
|
65
101
|
setHovered(false);
|
|
66
102
|
};
|
|
@@ -88,6 +124,10 @@ export function ProductRail({ current, organization, accountHref, catalog, onExp
|
|
|
88
124
|
return;
|
|
89
125
|
if (!(event.target instanceof Element) || !event.target.closest("a[href]"))
|
|
90
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();
|
|
91
131
|
cancel();
|
|
92
132
|
setHovered(false);
|
|
93
133
|
setFocused(false);
|
|
@@ -126,10 +166,10 @@ export function ProductRail({ current, organization, accountHref, catalog, onExp
|
|
|
126
166
|
rail.removeEventListener("click", onClick);
|
|
127
167
|
};
|
|
128
168
|
}, []);
|
|
129
|
-
return (_jsx("div", { "data-slot": "product-rail", "data-expanded": expanded ? "" : undefined, className: cn(
|
|
169
|
+
return (_jsx("div", { "data-slot": "product-rail", "data-expanded": expanded ? "" : undefined, className: cn(RAIL_COLUMN,
|
|
130
170
|
// Même durée et même courbe que la barre latérale qui se plie : la somme des deux largeurs
|
|
131
171
|
// reste constante (56 + 256 = 248 + 64), la page ne bouge pas.
|
|
132
|
-
"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:
|
|
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)))] })] }) }));
|
|
133
173
|
}
|
|
134
174
|
function focusVisible(target) {
|
|
135
175
|
if (!(target instanceof Element))
|
package/dist/products.js
CHANGED
|
@@ -75,7 +75,7 @@ const account = product("Account", "Ton compte : profil, sécurité et organisat
|
|
|
75
75
|
export const HGDC_ACCOUNT = { ...account, entryHref: account.href };
|
|
76
76
|
/** Les produits de la famille, dans leur ordre d'affichage. Account n'en fait pas partie (`HGDC_ACCOUNT`). */
|
|
77
77
|
export const HGDC_PRODUCTS = [
|
|
78
|
-
product("Host", "Ton site en ligne, déployé à chaque modification.", Rocket01Icon, "
|
|
78
|
+
product("Host", "Ton site en ligne, déployé à chaque modification.", Rocket01Icon, "live"),
|
|
79
79
|
product("Mail", "Tes e-mails, newsletters et formulaires, envoyés depuis ton adresse.", Mail01Icon, "live"),
|
|
80
80
|
product("Portal", "Un espace partagé où ton client suit son projet, signe et paie.", Briefcase01Icon, "live"),
|
|
81
81
|
product("Status", "La surveillance de tes sites et ta page de statut.", Activity01Icon, "live"),
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type FamilyProduct } from "./family-svg.ts";
|
|
2
|
+
/** Nom affiché d'un produit de la famille (« Mail »), Account compris. */
|
|
3
|
+
export declare function familyProductName(product: FamilyProduct): string;
|
|
4
|
+
/** Haut de la barre latérale (marque du produit) : le même dans la coque et dans le cadre. */
|
|
5
|
+
export declare const SIDEBAR_HEAD = "mt-2 flex h-14 shrink-0 items-center gap-1 px-3";
|
|
6
|
+
export declare const SIDEBAR_BRAND = "flex min-w-0 flex-1 items-center gap-2.5 pl-2.25";
|
|
7
|
+
export declare const SIDEBAR_BRAND_NAME = "truncate font-heading text-[0.9375rem] font-semibold";
|
|
8
|
+
/** Même boîte qu'une entrée de la barre en rail : 40 × 36 px. */
|
|
9
|
+
export declare const SIDEBAR_RAIL_BOX = "grid h-9 w-10 shrink-0 place-items-center rounded-xl";
|
|
10
|
+
/**
|
|
11
|
+
* Le cadre de la coque, dessiné par le chargeur du document (`FamilyLoaderRoot`) quand une page
|
|
12
|
+
* attend hors de la coque : barre des produits complète (organisation retenue, pastilles, produit
|
|
13
|
+
* courant), barre latérale du produit (sa marque, la navigation en squelette à sa hauteur
|
|
14
|
+
* retenue), carte du contenu et sa barre du haut. Mêmes boîtes que la coque, au pixel : la
|
|
15
|
+
* transition de vue d'un produit à l'autre n'a rien à faire bouger, et la coque qui monte ensuite
|
|
16
|
+
* prend la place sans saut.
|
|
17
|
+
*
|
|
18
|
+
* Le produit vient de l'adresse (`<html data-hgdc-product>`, `sidebarInitScript`) : le rendu
|
|
19
|
+
* serveur dessine toutes les marques, le CSS montre la bonne.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ShellFrame(): React.JSX.Element;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { FAMILY_PRODUCTS } from "./family-svg.js";
|
|
3
|
+
import { ProductBadge } from "./product-badge.js";
|
|
4
|
+
import { ProductRailFrame } from "./product-rail.js";
|
|
5
|
+
import { HGDC_ACCOUNT, HGDC_PRODUCTS } from "./products.js";
|
|
6
|
+
/** Nom affiché d'un produit de la famille (« Mail »), Account compris. */
|
|
7
|
+
export function familyProductName(product) {
|
|
8
|
+
if (product === HGDC_ACCOUNT.badge)
|
|
9
|
+
return HGDC_ACCOUNT.name;
|
|
10
|
+
return HGDC_PRODUCTS.find((item) => item.badge === product)?.name ?? `HGDC ${product}`;
|
|
11
|
+
}
|
|
12
|
+
/** Haut de la barre latérale (marque du produit) : le même dans la coque et dans le cadre. */
|
|
13
|
+
export const SIDEBAR_HEAD = "mt-2 flex h-14 shrink-0 items-center gap-1 px-3";
|
|
14
|
+
export const SIDEBAR_BRAND = "flex min-w-0 flex-1 items-center gap-2.5 pl-2.25";
|
|
15
|
+
export const SIDEBAR_BRAND_NAME = "truncate font-heading text-[0.9375rem] font-semibold";
|
|
16
|
+
/** Même boîte qu'une entrée de la barre en rail : 40 × 36 px. */
|
|
17
|
+
export const SIDEBAR_RAIL_BOX = "grid h-9 w-10 shrink-0 place-items-center rounded-xl";
|
|
18
|
+
/** Lignes de squelette de la navigation, coupées à la hauteur retenue (`--hgdc-nav-height`). */
|
|
19
|
+
const SKELETON_ROWS = 12;
|
|
20
|
+
/**
|
|
21
|
+
* Le cadre de la coque, dessiné par le chargeur du document (`FamilyLoaderRoot`) quand une page
|
|
22
|
+
* attend hors de la coque : barre des produits complète (organisation retenue, pastilles, produit
|
|
23
|
+
* courant), barre latérale du produit (sa marque, la navigation en squelette à sa hauteur
|
|
24
|
+
* retenue), carte du contenu et sa barre du haut. Mêmes boîtes que la coque, au pixel : la
|
|
25
|
+
* transition de vue d'un produit à l'autre n'a rien à faire bouger, et la coque qui monte ensuite
|
|
26
|
+
* prend la place sans saut.
|
|
27
|
+
*
|
|
28
|
+
* Le produit vient de l'adresse (`<html data-hgdc-product>`, `sidebarInitScript`) : le rendu
|
|
29
|
+
* serveur dessine toutes les marques, le CSS montre la bonne.
|
|
30
|
+
*/
|
|
31
|
+
export function ShellFrame() {
|
|
32
|
+
return (_jsxs("div", { "data-slot": "shell-frame", "aria-hidden": true, className: "hidden h-full lg:flex", children: [_jsx(ProductRailFrame, {}), _jsxs("div", { "data-slot": "shell-frame-sidebar", className: "flex w-64 shrink-0 flex-col overflow-hidden rail:w-16", children: [_jsx("div", { className: SIDEBAR_HEAD, children: FAMILY_PRODUCTS.map((product) => (_jsxs("div", { "data-product": product, className: "contents", children: [_jsxs("div", { className: `${SIDEBAR_BRAND} rail:hidden`, children: [_jsx(ProductBadge, { product: product, className: "size-6" }), _jsx("span", { className: SIDEBAR_BRAND_NAME, children: familyProductName(product) })] }), _jsx("div", { className: `${SIDEBAR_RAIL_BOX} hidden rail:grid`, children: _jsx(ProductBadge, { product: product, className: "size-6" }) })] }, product))) }), _jsx("div", { className: "px-3 py-2", children: _jsx("div", { "data-slot": "shell-frame-nav", className: "grid gap-0.5 overflow-hidden", children: Array.from({ length: SKELETON_ROWS }, (_, index) => (_jsxs("div", { className: "flex h-9 shrink-0 items-center gap-2.5 px-3 rail:justify-center rail:px-0", children: [_jsx("span", { className: "size-[1.125rem] shrink-0 rounded-md bg-shell-hover" }), _jsx("span", { className: "h-2.5 w-24 rounded-full bg-shell-hover rail:hidden" })] }, index))) }) })] }), _jsx("div", { className: "min-w-0 flex-1 py-2 pr-2", children: _jsx("div", { className: "h-full overflow-hidden rounded-xl bg-background", children: _jsx("div", { "data-slot": "shell-frame-header", className: "min-h-14 border-b border-foreground/5 bg-background" }) }) })] }));
|
|
33
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Organisation affichée en dernier : `{ initials, logo? }`, en `localStorage`. */
|
|
2
|
+
export declare const SHELL_ORG_KEY = "hgdc-shell:org";
|
|
3
|
+
/** Hauteur en pixels de la navigation d'un produit : `hgdc-shell:nav:<produit>`. */
|
|
4
|
+
export declare const SHELL_NAV_KEY_PREFIX = "hgdc-shell:nav:";
|
|
5
|
+
/** Passage d'un produit à l'autre (horodatage), en `sessionStorage`. */
|
|
6
|
+
export declare const SHELL_SWITCH_KEY = "hgdc-shell:switch";
|
|
7
|
+
/** Au-delà, un passage noté n'est plus celui qu'on vit (onglet laissé, retour bien plus tard). */
|
|
8
|
+
export declare const SHELL_SWITCH_MAX_AGE_MS = 10000;
|
|
9
|
+
export type ShellOrganization = {
|
|
10
|
+
initials: string;
|
|
11
|
+
logo?: string;
|
|
12
|
+
};
|
|
13
|
+
export type ShellBootConfig = {
|
|
14
|
+
/** Clé du rail de la barre latérale (`sidebar`), valeur `rail`. */
|
|
15
|
+
railKey: string;
|
|
16
|
+
orgKey: string;
|
|
17
|
+
navKeyPrefix: string;
|
|
18
|
+
/** Les produits de la famille, par mot en minuscules. */
|
|
19
|
+
products: ReadonlyArray<string>;
|
|
20
|
+
/** `[produit, origine, base]` des produits servis sous une base de chemin (`PRODUCT_ORIGINS`). */
|
|
21
|
+
origins: ReadonlyArray<readonly [string, string, string]>;
|
|
22
|
+
/** Domaine des produits sur leur sous-domaine (`hgdc.ch` : `mail.hgdc.ch` est Mail). */
|
|
23
|
+
domain: string;
|
|
24
|
+
};
|
|
25
|
+
type BootWindow = {
|
|
26
|
+
document: {
|
|
27
|
+
documentElement: HTMLElement;
|
|
28
|
+
};
|
|
29
|
+
location: {
|
|
30
|
+
origin: string;
|
|
31
|
+
hostname: string;
|
|
32
|
+
pathname: string;
|
|
33
|
+
};
|
|
34
|
+
localStorage: Pick<Storage, "getItem">;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Applique la mémoire de la coque sur `<html>` : `data-sidebar="rail"`, `data-hgdc-product`
|
|
38
|
+
* (le produit de l'adresse), `data-hgdc-org` et les variables `--hgdc-org-initials`,
|
|
39
|
+
* `--hgdc-org-logo`, `--hgdc-nav-height` que lit `styles.css`. Autonome : sa source est recopiée
|
|
40
|
+
* telle quelle dans le script en ligne du `<head>`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function shellBoot(config: ShellBootConfig, win: BootWindow): void;
|
|
43
|
+
/** Configuration de la famille : les produits du kit et `PRODUCT_ORIGINS`. */
|
|
44
|
+
export declare const SHELL_BOOT_CONFIG: ShellBootConfig;
|
|
45
|
+
/** Le script en ligne du `<head>` : `shellBoot` et sa configuration, sans dépendance. */
|
|
46
|
+
export declare function shellBootScript(config?: ShellBootConfig): string;
|
|
47
|
+
/** Ce que la coque retient d'une organisation : ses initiales et son logo (`https:` seulement). */
|
|
48
|
+
export declare function shellOrganization(org: {
|
|
49
|
+
name: string;
|
|
50
|
+
logo?: string | undefined;
|
|
51
|
+
}): ShellOrganization;
|
|
52
|
+
/** Retient l'organisation affichée (`OrgSwitcher` le fait) et l'applique à la page. */
|
|
53
|
+
export declare function rememberShellOrganization(org: {
|
|
54
|
+
name: string;
|
|
55
|
+
logo?: string | undefined;
|
|
56
|
+
}): void;
|
|
57
|
+
/** Oublie l'organisation (déconnexion) : la page suivante montre un squelette. */
|
|
58
|
+
export declare function forgetShellOrganization(): void;
|
|
59
|
+
/** Retient la hauteur de la navigation d'un produit (`AppShell` le fait après l'affichage). */
|
|
60
|
+
export declare function rememberShellNavHeight(product: string, height: number): void;
|
|
61
|
+
/** Note le départ vers un autre produit (clic dans la barre des produits). */
|
|
62
|
+
export declare function markProductSwitch(now?: number): void;
|
|
63
|
+
/** Lit et efface la note : vrai si la page vient d'un passage depuis la barre, il y a peu. */
|
|
64
|
+
export declare function takeProductSwitch(now?: number): boolean;
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { initials } from "./initials.js";
|
|
2
|
+
import { FAMILY_PRODUCTS } from "./family-svg.js";
|
|
3
|
+
import { PRODUCT_ORIGINS } from "./products.js";
|
|
4
|
+
/*
|
|
5
|
+
* Mémoire de la coque, partagée par les produits servis sous la même origine (`app.hgdc.ch/<produit>`,
|
|
6
|
+
* ADR 0020) : ce qu'il faut pour que la première image d'un produit ait déjà la barre des produits
|
|
7
|
+
* complète et la barre latérale à sa taille, avant que ses données n'arrivent.
|
|
8
|
+
*
|
|
9
|
+
* - l'organisation affichée en dernier : ses initiales et son logo (rien d'autre, pas le nom) ;
|
|
10
|
+
* - la hauteur de la navigation de chaque produit, pour un squelette à la bonne taille ;
|
|
11
|
+
* - le passage en cours d'un produit à l'autre (session de l'onglet), pour que la barre d'arrivée
|
|
12
|
+
* ne s'élargisse pas sous une souris restée immobile.
|
|
13
|
+
*
|
|
14
|
+
* `shellBoot` applique la mémoire sur `<html>` avant la première peinture : il tourne en ligne
|
|
15
|
+
* dans le `<head>` (`sidebarInitScript`), donc il ne dépend de rien d'autre que de ses arguments.
|
|
16
|
+
*/
|
|
17
|
+
/** Organisation affichée en dernier : `{ initials, logo? }`, en `localStorage`. */
|
|
18
|
+
export const SHELL_ORG_KEY = "hgdc-shell:org";
|
|
19
|
+
/** Hauteur en pixels de la navigation d'un produit : `hgdc-shell:nav:<produit>`. */
|
|
20
|
+
export const SHELL_NAV_KEY_PREFIX = "hgdc-shell:nav:";
|
|
21
|
+
/** Passage d'un produit à l'autre (horodatage), en `sessionStorage`. */
|
|
22
|
+
export const SHELL_SWITCH_KEY = "hgdc-shell:switch";
|
|
23
|
+
/** Au-delà, un passage noté n'est plus celui qu'on vit (onglet laissé, retour bien plus tard). */
|
|
24
|
+
export const SHELL_SWITCH_MAX_AGE_MS = 10_000;
|
|
25
|
+
/**
|
|
26
|
+
* Applique la mémoire de la coque sur `<html>` : `data-sidebar="rail"`, `data-hgdc-product`
|
|
27
|
+
* (le produit de l'adresse), `data-hgdc-org` et les variables `--hgdc-org-initials`,
|
|
28
|
+
* `--hgdc-org-logo`, `--hgdc-nav-height` que lit `styles.css`. Autonome : sa source est recopiée
|
|
29
|
+
* telle quelle dans le script en ligne du `<head>`.
|
|
30
|
+
*/
|
|
31
|
+
export function shellBoot(config, win) {
|
|
32
|
+
// Ni fonction imbriquée ni import : un outil de construction n'a rien à y injecter.
|
|
33
|
+
const root = win.document.documentElement;
|
|
34
|
+
let rail = null;
|
|
35
|
+
let rawOrg = null;
|
|
36
|
+
try {
|
|
37
|
+
rail = win.localStorage.getItem(config.railKey);
|
|
38
|
+
rawOrg = win.localStorage.getItem(config.orgKey);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Stockage bloqué : rien à reprendre.
|
|
42
|
+
}
|
|
43
|
+
if (rail === "rail")
|
|
44
|
+
root.dataset.sidebar = "rail";
|
|
45
|
+
let product = "";
|
|
46
|
+
for (const [word, origin, base] of config.origins) {
|
|
47
|
+
const path = win.location.pathname;
|
|
48
|
+
if (win.location.origin === origin && (path === base || path.startsWith(`${base}/`)))
|
|
49
|
+
product = word;
|
|
50
|
+
}
|
|
51
|
+
const host = win.location.hostname;
|
|
52
|
+
const suffix = `.${config.domain}`;
|
|
53
|
+
if (!product && host.endsWith(suffix)) {
|
|
54
|
+
const word = host.slice(0, -suffix.length);
|
|
55
|
+
if (config.products.includes(word))
|
|
56
|
+
product = word;
|
|
57
|
+
}
|
|
58
|
+
// La coque, une fois montée, sait mieux que l'adresse : on ne réécrit pas son produit.
|
|
59
|
+
if (product && !root.dataset.hgdcProduct)
|
|
60
|
+
root.dataset.hgdcProduct = product;
|
|
61
|
+
product = root.dataset.hgdcProduct ?? "";
|
|
62
|
+
let org = null;
|
|
63
|
+
try {
|
|
64
|
+
org = JSON.parse(rawOrg ?? "null");
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
org = null;
|
|
68
|
+
}
|
|
69
|
+
const letters = org && typeof org.initials === "string" ? org.initials.replace(/[^\p{L}\p{N}]/gu, "").slice(0, 4) : "";
|
|
70
|
+
const logo = org && typeof org.logo === "string" && org.logo.length < 2048 && /^https:\/\/[^\s"'()\\]+$/.test(org.logo)
|
|
71
|
+
? org.logo
|
|
72
|
+
: "";
|
|
73
|
+
if (letters || logo) {
|
|
74
|
+
root.dataset.hgdcOrg = "";
|
|
75
|
+
root.style.setProperty("--hgdc-org-initials", logo ? '""' : `"${letters}"`);
|
|
76
|
+
if (logo)
|
|
77
|
+
root.style.setProperty("--hgdc-org-logo", `url("${logo}")`);
|
|
78
|
+
else
|
|
79
|
+
root.style.removeProperty("--hgdc-org-logo");
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
delete root.dataset.hgdcOrg;
|
|
83
|
+
root.style.removeProperty("--hgdc-org-initials");
|
|
84
|
+
root.style.removeProperty("--hgdc-org-logo");
|
|
85
|
+
}
|
|
86
|
+
let height = 0;
|
|
87
|
+
try {
|
|
88
|
+
height = product ? Number(win.localStorage.getItem(config.navKeyPrefix + product)) : 0;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
height = 0;
|
|
92
|
+
}
|
|
93
|
+
if (height > 0 && height < 2000)
|
|
94
|
+
root.style.setProperty("--hgdc-nav-height", `${Math.round(height)}px`);
|
|
95
|
+
}
|
|
96
|
+
/** Configuration de la famille : les produits du kit et `PRODUCT_ORIGINS`. */
|
|
97
|
+
export const SHELL_BOOT_CONFIG = {
|
|
98
|
+
railKey: "sidebar",
|
|
99
|
+
orgKey: SHELL_ORG_KEY,
|
|
100
|
+
navKeyPrefix: SHELL_NAV_KEY_PREFIX,
|
|
101
|
+
products: FAMILY_PRODUCTS,
|
|
102
|
+
origins: Object.entries(PRODUCT_ORIGINS).map(([word, { origin, basePath }]) => [word, origin, basePath]),
|
|
103
|
+
domain: "hgdc.ch",
|
|
104
|
+
};
|
|
105
|
+
/** Le script en ligne du `<head>` : `shellBoot` et sa configuration, sans dépendance. */
|
|
106
|
+
export function shellBootScript(config = SHELL_BOOT_CONFIG) {
|
|
107
|
+
return `try{(${shellBoot.toString()})(${JSON.stringify(config)},window)}catch(e){}`;
|
|
108
|
+
}
|
|
109
|
+
function browser() {
|
|
110
|
+
return typeof window === "undefined" ? null : window;
|
|
111
|
+
}
|
|
112
|
+
function storageSet(storage, key, value) {
|
|
113
|
+
try {
|
|
114
|
+
if (value === null)
|
|
115
|
+
window[storage].removeItem(key);
|
|
116
|
+
else
|
|
117
|
+
window[storage].setItem(key, value);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Stockage bloqué : la première image d'un autre produit montrera un squelette, rien de plus.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Ce que la coque retient d'une organisation : ses initiales et son logo (`https:` seulement). */
|
|
124
|
+
export function shellOrganization(org) {
|
|
125
|
+
return { initials: initials(org.name), ...(org.logo ? { logo: org.logo } : {}) };
|
|
126
|
+
}
|
|
127
|
+
/** Retient l'organisation affichée (`OrgSwitcher` le fait) et l'applique à la page. */
|
|
128
|
+
export function rememberShellOrganization(org) {
|
|
129
|
+
const win = browser();
|
|
130
|
+
if (!win)
|
|
131
|
+
return;
|
|
132
|
+
storageSet("localStorage", SHELL_ORG_KEY, JSON.stringify(shellOrganization(org)));
|
|
133
|
+
shellBoot(SHELL_BOOT_CONFIG, win);
|
|
134
|
+
}
|
|
135
|
+
/** Oublie l'organisation (déconnexion) : la page suivante montre un squelette. */
|
|
136
|
+
export function forgetShellOrganization() {
|
|
137
|
+
const win = browser();
|
|
138
|
+
if (!win)
|
|
139
|
+
return;
|
|
140
|
+
storageSet("localStorage", SHELL_ORG_KEY, null);
|
|
141
|
+
shellBoot(SHELL_BOOT_CONFIG, win);
|
|
142
|
+
}
|
|
143
|
+
/** Retient la hauteur de la navigation d'un produit (`AppShell` le fait après l'affichage). */
|
|
144
|
+
export function rememberShellNavHeight(product, height) {
|
|
145
|
+
if (!browser() || !(height > 0))
|
|
146
|
+
return;
|
|
147
|
+
storageSet("localStorage", SHELL_NAV_KEY_PREFIX + product, String(Math.round(height)));
|
|
148
|
+
}
|
|
149
|
+
/** Note le départ vers un autre produit (clic dans la barre des produits). */
|
|
150
|
+
export function markProductSwitch(now = Date.now()) {
|
|
151
|
+
if (!browser())
|
|
152
|
+
return;
|
|
153
|
+
storageSet("sessionStorage", SHELL_SWITCH_KEY, String(now));
|
|
154
|
+
}
|
|
155
|
+
/** Lit et efface la note : vrai si la page vient d'un passage depuis la barre, il y a peu. */
|
|
156
|
+
export function takeProductSwitch(now = Date.now()) {
|
|
157
|
+
if (!browser())
|
|
158
|
+
return false;
|
|
159
|
+
let value = null;
|
|
160
|
+
try {
|
|
161
|
+
value = window.sessionStorage.getItem(SHELL_SWITCH_KEY);
|
|
162
|
+
window.sessionStorage.removeItem(SHELL_SWITCH_KEY);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
const at = Number(value);
|
|
168
|
+
return value !== null && at > 0 && now - at >= 0 && now - at < SHELL_SWITCH_MAX_AGE_MS;
|
|
169
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hgdc/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.1",
|
|
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
|
@@ -838,20 +838,58 @@
|
|
|
838
838
|
:root:not(:has([data-slot="app-shell"])) [data-slot="family-loader-root"] > [data-frame] {
|
|
839
839
|
display: block;
|
|
840
840
|
}
|
|
841
|
+
/* Sur ordinateur, le cadre est la coque elle-même (`ShellFrame`) : barre des produits complète,
|
|
842
|
+
marque du produit, navigation en squelette, carte du contenu (ADR 0020). */
|
|
841
843
|
@media (width >= 64rem) {
|
|
842
844
|
[data-slot="family-loader-root"] > [data-frame] {
|
|
843
|
-
background:
|
|
844
|
-
linear-gradient(var(--shell-rail), var(--shell-rail)) left / var(--hgdc-rail-w) 100% no-repeat,
|
|
845
|
-
var(--shell);
|
|
846
|
-
}
|
|
847
|
-
[data-slot="family-loader-root"] > [data-frame]::after {
|
|
848
|
-
content: "";
|
|
849
|
-
position: absolute;
|
|
850
|
-
inset: 0.5rem 0.5rem 0.5rem var(--hgdc-content-left);
|
|
851
|
-
border-radius: var(--radius-xl);
|
|
852
|
-
background: var(--background);
|
|
845
|
+
background: var(--shell);
|
|
853
846
|
}
|
|
854
847
|
}
|
|
848
|
+
/* La marque du produit de l'adresse (`<html data-hgdc-product>`, `sidebarInitScript`), seule ; le
|
|
849
|
+
produit courant marqué dans la barre, comme `aria-current` dans la coque. */
|
|
850
|
+
[data-slot="shell-frame"] [data-product] {
|
|
851
|
+
display: none;
|
|
852
|
+
}
|
|
853
|
+
[data-slot="shell-frame-rail"] [data-product] {
|
|
854
|
+
display: flex;
|
|
855
|
+
}
|
|
856
|
+
:root[data-hgdc-product="account"] [data-slot="shell-frame"] [data-product="account"],
|
|
857
|
+
:root[data-hgdc-product="host"] [data-slot="shell-frame"] [data-product="host"],
|
|
858
|
+
:root[data-hgdc-product="mail"] [data-slot="shell-frame"] [data-product="mail"],
|
|
859
|
+
:root[data-hgdc-product="portal"] [data-slot="shell-frame"] [data-product="portal"],
|
|
860
|
+
:root[data-hgdc-product="status"] [data-slot="shell-frame"] [data-product="status"],
|
|
861
|
+
:root[data-hgdc-product="analytics"] [data-slot="shell-frame"] [data-product="analytics"],
|
|
862
|
+
:root[data-hgdc-product="docs"] [data-slot="shell-frame"] [data-product="docs"] {
|
|
863
|
+
display: contents;
|
|
864
|
+
}
|
|
865
|
+
:root[data-hgdc-product="account"] [data-slot="shell-frame-rail"] [data-product="account"],
|
|
866
|
+
:root[data-hgdc-product="host"] [data-slot="shell-frame-rail"] [data-product="host"],
|
|
867
|
+
:root[data-hgdc-product="mail"] [data-slot="shell-frame-rail"] [data-product="mail"],
|
|
868
|
+
:root[data-hgdc-product="portal"] [data-slot="shell-frame-rail"] [data-product="portal"],
|
|
869
|
+
:root[data-hgdc-product="status"] [data-slot="shell-frame-rail"] [data-product="status"],
|
|
870
|
+
:root[data-hgdc-product="analytics"] [data-slot="shell-frame-rail"] [data-product="analytics"],
|
|
871
|
+
:root[data-hgdc-product="docs"] [data-slot="shell-frame-rail"] [data-product="docs"] {
|
|
872
|
+
display: flex;
|
|
873
|
+
background: var(--shell-active);
|
|
874
|
+
color: var(--shell-foreground);
|
|
875
|
+
}
|
|
876
|
+
/* Navigation en squelette, à la hauteur retenue pour ce produit (`--hgdc-nav-height`). */
|
|
877
|
+
[data-slot="shell-frame-nav"] {
|
|
878
|
+
max-height: var(--hgdc-nav-height, 7.5rem);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/* La place de l'organisation (`OrgPlaceholder`) : la marque retenue (`<html data-hgdc-org>`,
|
|
882
|
+
initiales ou logo), comme `OrgMark` ; sinon un carré de squelette de la même taille. */
|
|
883
|
+
[data-slot="org-placeholder-mark"] {
|
|
884
|
+
background: var(--shell-hover);
|
|
885
|
+
}
|
|
886
|
+
:root[data-hgdc-org] [data-slot="org-placeholder-mark"] {
|
|
887
|
+
background: var(--hgdc-org-logo, var(--foreground)) center / cover no-repeat;
|
|
888
|
+
color: var(--background);
|
|
889
|
+
}
|
|
890
|
+
:root[data-hgdc-org] [data-slot="org-placeholder-mark"]::before {
|
|
891
|
+
content: var(--hgdc-org-initials, "");
|
|
892
|
+
}
|
|
855
893
|
|
|
856
894
|
/* Calque de la zone de contenu (`AppShell`) : opaque, au-dessus de l'ancienne page, sous le H du
|
|
857
895
|
document. Mêmes conditions et même rythme que le H : ils apparaissent et s'effacent ensemble. */
|
|
@@ -893,16 +931,41 @@
|
|
|
893
931
|
[data-slot="app-shell-header"] {
|
|
894
932
|
view-transition-name: hgdc-header;
|
|
895
933
|
}
|
|
934
|
+
/* Le cadre du chargeur (hors de la coque) et le H lui-même portent les mêmes noms, seulement quand
|
|
935
|
+
ils sont à l'écran : mêmes conditions que l'affichage du chargeur, plus haut. */
|
|
936
|
+
:root:has([data-slot="page-pending"]):not(:has([data-slot="app-shell"])) {
|
|
937
|
+
& [data-slot="shell-frame-rail"] {
|
|
938
|
+
view-transition-name: hgdc-product-rail;
|
|
939
|
+
}
|
|
940
|
+
& [data-slot="shell-frame-sidebar"] {
|
|
941
|
+
view-transition-name: hgdc-sidebar;
|
|
942
|
+
}
|
|
943
|
+
& [data-slot="shell-frame-header"] {
|
|
944
|
+
view-transition-name: hgdc-header;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
:root:has([data-slot="page-pending"]):not(:has([data-slot="app-shell"])),
|
|
948
|
+
:root:has([data-slot="app-shell"][data-loading]),
|
|
949
|
+
:root:has([data-slot="app-shell"]:not([data-hydrated]) [data-slot="page-pending"]) {
|
|
950
|
+
& [data-slot="family-loader-root"] > [data-stage] {
|
|
951
|
+
view-transition-name: hgdc-loader;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
/* Un clic dans la barre des produits : la barre se replie et la barre latérale revient d'un coup,
|
|
955
|
+
pour que l'image de départ soit déjà celle de l'arrivée (`ProductRail`). */
|
|
956
|
+
:root[data-hgdc-switching] [data-slot="product-rail"],
|
|
957
|
+
:root[data-hgdc-switching] [data-slot="app-shell-sidebar"] {
|
|
958
|
+
transition: none;
|
|
959
|
+
}
|
|
896
960
|
::view-transition-old(root),
|
|
897
961
|
::view-transition-new(root) {
|
|
898
962
|
animation-duration: var(--duration-fast);
|
|
899
963
|
animation-timing-function: var(--ease-out);
|
|
900
964
|
}
|
|
901
|
-
|
|
965
|
+
/* Barre latérale et barre du haut : à la même place des deux côtés, leur contenu (un autre
|
|
966
|
+
produit) passe en fondu en place. */
|
|
902
967
|
::view-transition-group(hgdc-sidebar),
|
|
903
968
|
::view-transition-group(hgdc-header),
|
|
904
|
-
::view-transition-old(hgdc-product-rail),
|
|
905
|
-
::view-transition-new(hgdc-product-rail),
|
|
906
969
|
::view-transition-old(hgdc-sidebar),
|
|
907
970
|
::view-transition-new(hgdc-sidebar),
|
|
908
971
|
::view-transition-old(hgdc-header),
|
|
@@ -910,3 +973,37 @@
|
|
|
910
973
|
animation-duration: var(--duration-base);
|
|
911
974
|
animation-timing-function: var(--ease-ios);
|
|
912
975
|
}
|
|
976
|
+
/* Barre des produits et H : identiques des deux côtés (la barre ne change que d'indicateur, le H
|
|
977
|
+
est à la même phase, calé sur l'horloge). Ni fondu ni mouvement : l'ancienne image est masquée,
|
|
978
|
+
la nouvelle, vivante, s'affiche telle quelle. Présents d'un seul côté (une page sans coque,
|
|
979
|
+
une page prête sans H), ils apparaissent ou s'effacent en fondu court. */
|
|
980
|
+
::view-transition-group(hgdc-product-rail),
|
|
981
|
+
::view-transition-group(hgdc-loader),
|
|
982
|
+
::view-transition-new(hgdc-product-rail),
|
|
983
|
+
::view-transition-new(hgdc-loader) {
|
|
984
|
+
animation: none;
|
|
985
|
+
}
|
|
986
|
+
::view-transition-old(hgdc-product-rail),
|
|
987
|
+
::view-transition-old(hgdc-loader) {
|
|
988
|
+
animation: none;
|
|
989
|
+
opacity: 0;
|
|
990
|
+
}
|
|
991
|
+
::view-transition-old(hgdc-product-rail):only-child,
|
|
992
|
+
::view-transition-old(hgdc-loader):only-child {
|
|
993
|
+
opacity: 1;
|
|
994
|
+
animation: hgdc-view-out var(--duration-fast) var(--ease-out) both;
|
|
995
|
+
}
|
|
996
|
+
::view-transition-new(hgdc-product-rail):only-child,
|
|
997
|
+
::view-transition-new(hgdc-loader):only-child {
|
|
998
|
+
animation: hgdc-view-in var(--duration-fast) var(--ease-out) both;
|
|
999
|
+
}
|
|
1000
|
+
@keyframes hgdc-view-out {
|
|
1001
|
+
to {
|
|
1002
|
+
opacity: 0;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
@keyframes hgdc-view-in {
|
|
1006
|
+
from {
|
|
1007
|
+
opacity: 0;
|
|
1008
|
+
}
|
|
1009
|
+
}
|