@hgdc/ui 0.47.0 → 0.47.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/dist/app-shell.d.ts +18 -123
- package/dist/app-shell.js +18 -431
- package/dist/assistant-bar.d.ts +16 -0
- package/dist/assistant-bar.js +25 -0
- package/dist/assistant-labels.d.ts +29 -0
- package/dist/assistant-labels.js +42 -0
- package/dist/assistant-parts.d.ts +19 -0
- package/dist/assistant-parts.js +38 -0
- package/dist/assistant-text.d.ts +15 -0
- package/dist/assistant-text.js +30 -0
- package/dist/assistant-types.d.ts +59 -0
- package/dist/assistant-types.js +1 -0
- package/dist/assistant-view.d.ts +7 -120
- package/dist/assistant-view.js +9 -127
- package/dist/shell-assistant.d.ts +37 -0
- package/dist/shell-assistant.js +83 -0
- package/dist/shell-context.d.ts +11 -0
- package/dist/shell-context.js +8 -0
- package/dist/shell-links.d.ts +35 -0
- package/dist/shell-links.js +46 -0
- package/dist/sidebar-head.d.ts +10 -0
- package/dist/sidebar-head.js +28 -0
- package/dist/sidebar-nav-group.d.ts +33 -0
- package/dist/sidebar-nav-group.js +105 -0
- package/dist/sidebar-nav.d.ts +38 -0
- package/dist/sidebar-nav.js +60 -0
- package/dist/sidebar-rail.d.ts +12 -0
- package/dist/sidebar-rail.js +53 -0
- package/dist/use-content-loading.d.ts +13 -0
- package/dist/use-content-loading.js +42 -0
- package/dist/use-product-leave.d.ts +9 -0
- package/dist/use-product-leave.js +32 -0
- package/package.json +1 -1
package/dist/app-shell.d.ts
CHANGED
|
@@ -1,131 +1,26 @@
|
|
|
1
|
-
import { type IconSvgElement } from "@hugeicons/react";
|
|
2
1
|
import { type ReactNode } from "react";
|
|
2
|
+
import type { ProductView } from "./addresses.ts";
|
|
3
3
|
import type { FamilyProduct } from "./family-svg.ts";
|
|
4
|
-
import { type
|
|
4
|
+
import { type AppShellAssistant } from "./shell-assistant.tsx";
|
|
5
|
+
import { type AppShellLinks } from "./shell-links.tsx";
|
|
6
|
+
export { sidebarInitScript, toggleSidebar, useSidebarRail } from "./sidebar-rail.ts";
|
|
7
|
+
export { PRODUCT_LEAVE_DELAY_MS } from "./use-product-leave.ts";
|
|
8
|
+
export { useAppShell } from "./shell-context.ts";
|
|
9
|
+
export { SidebarNav, SidebarNavBack, SidebarNavItem } from "./sidebar-nav.tsx";
|
|
10
|
+
export { SidebarNavGroup } from "./sidebar-nav-group.tsx";
|
|
11
|
+
export type { AppShellLinks, ShellLink } from "./shell-links.tsx";
|
|
12
|
+
export type { AppShellAssistant } from "./shell-assistant.tsx";
|
|
5
13
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/** La barre latérale est-elle réduite en rail ? L'état vit sur `<html data-sidebar="rail">`. */
|
|
13
|
-
export declare function useSidebarRail(): boolean;
|
|
14
|
-
export declare function toggleSidebar(): void;
|
|
15
|
-
/** Pour fermer le tiroir mobile depuis un élément de la barre (un sélecteur, un bouton). */
|
|
16
|
-
export declare function useAppShell(): {
|
|
17
|
-
closeMobileNav: () => void;
|
|
18
|
-
};
|
|
19
|
-
/**
|
|
20
|
-
* La navigation du produit : 4 à 7 entrées, à plat. Une navigation imbriquée (l'espace d'une page,
|
|
21
|
-
* d'un projet) commence par `SidebarNavBack` et range ses entrées dans des `SidebarNavGroup`.
|
|
22
|
-
*/
|
|
23
|
-
export declare function SidebarNav({ label, className, children, }: {
|
|
24
|
-
/** Nom de la navigation pour les lecteurs d'écran : « Projet », « Organisation ». */
|
|
25
|
-
label: string;
|
|
26
|
-
className?: string;
|
|
27
|
-
children: ReactNode;
|
|
28
|
-
}): React.JSX.Element;
|
|
29
|
-
/**
|
|
30
|
-
* Une entrée de la barre. « Vous êtes ici » par `active` (ou `data-status="active"`, que pose le
|
|
31
|
-
* `Link` de TanStack Router) : teinte de la coque, jamais l'accent. En rail, l'icône seule, et le
|
|
32
|
-
* nom en infobulle.
|
|
33
|
-
*/
|
|
34
|
-
export declare function SidebarNavItem({ icon, label, active, href, render, }: {
|
|
35
|
-
icon: IconSvgElement;
|
|
36
|
-
label: string;
|
|
37
|
-
active?: boolean;
|
|
38
|
-
href?: string;
|
|
39
|
-
/** Lien du routeur : `render={<Link to="…" />}`. */
|
|
40
|
-
render?: React.ReactElement;
|
|
41
|
-
}): React.JSX.Element;
|
|
42
|
-
/**
|
|
43
|
-
* Retour au niveau du dessus, en tête d'une navigation imbriquée : « Retour aux pages » quand la
|
|
44
|
-
* barre montre l'espace d'une page, d'un projet. Premier enfant de `SidebarNav`, au-dessus des
|
|
45
|
-
* groupes. Une pastille teintée, flèche à gauche, jamais « vous êtes ici ». En rail, la flèche seule
|
|
46
|
-
* et le texte en infobulle.
|
|
47
|
-
*/
|
|
48
|
-
export declare function SidebarNavBack({ label, href, render, }: {
|
|
49
|
-
/** « Retour aux pages » */
|
|
50
|
-
label: string;
|
|
51
|
-
href?: string;
|
|
52
|
-
/** Lien du routeur : `render={<Link to="…" />}`. */
|
|
53
|
-
render?: React.ReactElement;
|
|
54
|
-
}): React.JSX.Element;
|
|
55
|
-
/**
|
|
56
|
-
* Un groupe titré de la barre : « Suivi », « Réglages ». Ses `SidebarNavItem` en enfants. Le titre
|
|
57
|
-
* nomme la liste pour les lecteurs d'écran ; en rail, il cède la place à un trait.
|
|
14
|
+
* La coque commune des consoles HGDC, celle de Host. Sur ordinateur, de gauche à droite : la barre
|
|
15
|
+
* des produits de la famille (organisation, Account, pastilles ; élargie au survol à la place
|
|
16
|
+
* de la barre latérale, qui se plie le temps du survol), la barre latérale du produit (sa marque, sa navigation, le compte en bas),
|
|
17
|
+
* réductible en rail (`Ctrl+B`, mémorisé), et la page sur une carte. Sur téléphone : une barre en
|
|
18
|
+
* haut, le menu dans un tiroir qui monte du bas et se referme quand on choisit une page ; les
|
|
19
|
+
* produits restent dans le menu de l'avatar.
|
|
58
20
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* laissé (mémorisé par navigateur). En rail, ses rubriques restent visibles, sous un trait.
|
|
62
|
-
*/
|
|
63
|
-
export declare function SidebarNavGroup(props: {
|
|
64
|
-
label: string;
|
|
65
|
-
children: ReactNode;
|
|
66
|
-
/** Le titre déroule ou replie le groupe. */
|
|
67
|
-
collapsible?: false;
|
|
68
|
-
}): React.JSX.Element;
|
|
69
|
-
export declare function SidebarNavGroup(props: {
|
|
70
|
-
label: string;
|
|
71
|
-
children: ReactNode;
|
|
72
|
-
collapsible: true;
|
|
73
|
-
/** Icône de l'entrée qui déroule le groupe, comme une rubrique. */
|
|
74
|
-
icon?: IconSvgElement;
|
|
75
|
-
/**
|
|
76
|
-
* Une rubrique du groupe est la page ouverte : le groupe s'ouvre, dès le rendu serveur. Sans cette
|
|
77
|
-
* valeur, la coque le lit dans les liens (`aria-current="page"`, `data-status="active"`) après
|
|
78
|
-
* l'hydratation.
|
|
79
|
-
*/
|
|
80
|
-
active?: boolean;
|
|
81
|
-
/** Ouvert tant que la personne n'a rien choisi. Fermé par défaut. */
|
|
82
|
-
defaultOpen?: boolean;
|
|
83
|
-
/** Clé de mémorisation, par défaut le titre, propre au produit de la coque : à donner quand deux groupes portent le même. */
|
|
84
|
-
storageKey?: string;
|
|
85
|
-
}): React.JSX.Element;
|
|
86
|
-
/**
|
|
87
|
-
* Un accès commun de la barre : une adresse, ou un lien du routeur (`{ render: <Link to="/settings" /> }`,
|
|
88
|
-
* dont le `data-status="active"` marque la page ouverte), ou une adresse avec `active`.
|
|
89
|
-
*/
|
|
90
|
-
export type ShellLink = string | {
|
|
91
|
-
href?: string;
|
|
92
|
-
render?: React.ReactElement;
|
|
93
|
-
active?: boolean;
|
|
94
|
-
};
|
|
95
|
-
/** Les trois accès que la coque pose juste au-dessus du compte, toujours dans cet ordre. */
|
|
96
|
-
export type AppShellLinks = {
|
|
97
|
-
/** Documentation du produit. */
|
|
98
|
-
documentation?: ShellLink;
|
|
99
|
-
/** Réglages du produit ou de l'organisation. */
|
|
100
|
-
settings?: ShellLink;
|
|
101
|
-
/** État du service : la page de statut de la coque (`statusPage`) par défaut, `false` pour le retirer. */
|
|
102
|
-
status?: ShellLink | false;
|
|
103
|
-
};
|
|
104
|
-
/**
|
|
105
|
-
* L'assistant IA du produit. La coque pose son bouton juste à côté de la recherche et l'ouvre dans
|
|
106
|
-
* une colonne à droite de la page, sur le fond de l'application : la page se rétrécit et se
|
|
107
|
-
* réorganise, jamais recouverte. Sous 1280 px, là où les deux ne tiennent plus côte à côte, une
|
|
108
|
-
* vue dédiée plein écran avec « Retour à la page ». La page reste montée dans les deux cas, et le
|
|
109
|
-
* panneau, une fois ouvert, aussi : rien ne se perd en fermant ou en changeant de largeur.
|
|
110
|
-
*/
|
|
111
|
-
export type AppShellAssistant = {
|
|
112
|
-
open: boolean;
|
|
113
|
-
onOpenChange: (open: boolean) => void;
|
|
114
|
-
/**
|
|
115
|
-
* Le corps de l'assistant : conversation et saisie (`@hgdc/ui/assistant-chat`). La coque pose le
|
|
116
|
-
* titre, la fermeture et le retour à la page ; le panneau occupe la hauteur restante et défile.
|
|
117
|
-
*/
|
|
118
|
-
panel: ReactNode;
|
|
119
|
-
/** Nom du bouton et titre du panneau : « Assistant » par défaut. */
|
|
120
|
-
label?: string;
|
|
121
|
-
/** Lettre du raccourci `Ctrl` (`⌘`) + lettre qui ouvre et ferme l'assistant (`"j"`). Aucun par défaut. */
|
|
122
|
-
shortcut?: string;
|
|
123
|
-
};
|
|
124
|
-
/**
|
|
125
|
-
* Délai entre le clic vers un autre produit et le H : une page prérendue s'affiche avant (une ou
|
|
126
|
-
* deux images), le H n'apparaît alors jamais ; une page à charger le montre aussitôt après.
|
|
21
|
+
* Le kit ne connaît ni le routeur ni l'authentification : le produit passe ses éléments
|
|
22
|
+
* (`OrgSwitcher`, `SidebarNav`, `AccountMenu`) et ses liens.
|
|
127
23
|
*/
|
|
128
|
-
export declare const PRODUCT_LEAVE_DELAY_MS = 100;
|
|
129
24
|
export declare function AppShell({ product, organization, productRail: productRailOption, view: viewOption, accountHref, nav, links, account, sidebarFooter, header, onSearch, searchLabel, assistant, children, contentKey, navigating, mainClassName, statusPage, }: {
|
|
130
25
|
/** Le produit affiché (`"host"`) : sa marque en haut de la barre, « vous êtes ici » dans la barre des produits. */
|
|
131
26
|
product: FamilyProduct;
|
package/dist/app-shell.js
CHANGED
|
@@ -1,379 +1,33 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import Activity01Icon from "@hugeicons/core-free-icons/Activity01Icon";
|
|
3
|
-
import AiMagicIcon from "@hugeicons/core-free-icons/AiMagicIcon";
|
|
4
|
-
import ArrowLeft01Icon from "@hugeicons/core-free-icons/ArrowLeft01Icon";
|
|
5
|
-
import ArrowRight01Icon from "@hugeicons/core-free-icons/ArrowRight01Icon";
|
|
6
|
-
import BookOpen01Icon from "@hugeicons/core-free-icons/BookOpen01Icon";
|
|
7
|
-
import Cancel01Icon from "@hugeicons/core-free-icons/Cancel01Icon";
|
|
8
|
-
import LinkSquare02Icon from "@hugeicons/core-free-icons/LinkSquare02Icon";
|
|
9
2
|
import Menu01Icon from "@hugeicons/core-free-icons/Menu01Icon";
|
|
10
|
-
import PanelLeftCloseIcon from "@hugeicons/core-free-icons/PanelLeftCloseIcon";
|
|
11
|
-
import PanelLeftOpenIcon from "@hugeicons/core-free-icons/PanelLeftOpenIcon";
|
|
12
|
-
import Settings01Icon from "@hugeicons/core-free-icons/Settings01Icon";
|
|
13
3
|
import { HugeiconsIcon } from "@hugeicons/react";
|
|
14
|
-
import {
|
|
15
|
-
import { createContext, Suspense, useCallback, useContext, useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore, } from "react";
|
|
4
|
+
import { Suspense, useCallback, useContext, useEffect, useId, useRef, useState } from "react";
|
|
16
5
|
import { CommandMenuButton } from "./command-menu.js";
|
|
17
6
|
import { Drawer, DrawerContent, DrawerTitle } from "./drawer.js";
|
|
18
7
|
import { FamilyLoaderHost, FamilyLoaderRootContext } from "./family-loader.js";
|
|
19
|
-
import {
|
|
20
|
-
import { CONTENT_LOADING_LAYER, ContentLoadingContext, PAGE_PENDING_MIN_MS, PAGE_PENDING_MS, PagePending, } from "./page-pending.js";
|
|
21
|
-
import { soloHref } from "./addresses.js";
|
|
8
|
+
import { CONTENT_LOADING_LAYER, ContentLoadingContext, PagePending } from "./page-pending.js";
|
|
22
9
|
import { ProductBadge } from "./product-badge.js";
|
|
23
10
|
import { productBarShown, useProductBarPreference, useProductBarSync } from "./product-bar.js";
|
|
24
|
-
import { otherViewHref, otherViewLabel } from "./products.js";
|
|
25
11
|
import { ProductRail } from "./product-rail.js";
|
|
26
12
|
import { useProductSwitch } from "./product-switch.js";
|
|
27
|
-
import { ProductRailShownContext, ShellLeaveContext, StatusPageContext } from "./shell-placement.js";
|
|
28
|
-
import { familyProductName, SIDEBAR_BRAND, SIDEBAR_BRAND_NAME, SIDEBAR_HEAD } from "./shell-frame.js";
|
|
29
|
-
import { rememberShellNavHeight, shellBootScript } from "./shell-memory.js";
|
|
30
|
-
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
|
|
31
13
|
import { cn } from "./cn.js";
|
|
32
14
|
import { ServiceStatus, statusPageUrlFor } from "./service-status.js";
|
|
15
|
+
import { AssistantButton, AssistantPanel, ASSISTANT_SIDE_BY_SIDE, useAssistantShortcut, } from "./shell-assistant.js";
|
|
16
|
+
import { ShellContext } from "./shell-context.js";
|
|
17
|
+
import { familyProductName } from "./shell-frame.js";
|
|
18
|
+
import { OtherViewLink, ShellCommonLinks } from "./shell-links.js";
|
|
19
|
+
import { rememberShellNavHeight } from "./shell-memory.js";
|
|
20
|
+
import { ProductRailShownContext, ShellLeaveContext, StatusPageContext } from "./shell-placement.js";
|
|
21
|
+
import { SidebarHead } from "./sidebar-head.js";
|
|
22
|
+
import { useToggleShortcut } from "./sidebar-rail.js";
|
|
23
|
+
import { useContentLoading, useHydrated, useIsomorphicLayoutEffect } from "./use-content-loading.js";
|
|
24
|
+
import { useProductLeave } from "./use-product-leave.js";
|
|
33
25
|
import { useMediaQuery } from "./use-media-query.js";
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* rail (la variante CSS `rail:` le lit, rien ne saute au chargement) et la mémoire de la coque
|
|
40
|
-
* partagée sous la même origine (`shell-memory` : produit de l'adresse, dernière organisation,
|
|
41
|
-
* hauteur de la navigation), que le cadre du chargeur et `OrgPlaceholder` lisent (ADR 0020).
|
|
42
|
-
*/
|
|
43
|
-
export const sidebarInitScript = shellBootScript();
|
|
44
|
-
function subscribeRail(onChange) {
|
|
45
|
-
window.addEventListener(RAIL_EVENT, onChange);
|
|
46
|
-
return () => window.removeEventListener(RAIL_EVENT, onChange);
|
|
47
|
-
}
|
|
48
|
-
/** La barre latérale est-elle réduite en rail ? L'état vit sur `<html data-sidebar="rail">`. */
|
|
49
|
-
export function useSidebarRail() {
|
|
50
|
-
return useSyncExternalStore(subscribeRail, () => document.documentElement.dataset.sidebar === "rail", () => false);
|
|
51
|
-
}
|
|
52
|
-
export function toggleSidebar() {
|
|
53
|
-
const root = document.documentElement;
|
|
54
|
-
const rail = root.dataset.sidebar !== "rail";
|
|
55
|
-
if (rail)
|
|
56
|
-
root.dataset.sidebar = "rail";
|
|
57
|
-
else
|
|
58
|
-
delete root.dataset.sidebar;
|
|
59
|
-
try {
|
|
60
|
-
if (rail)
|
|
61
|
-
localStorage.setItem(RAIL_KEY, "rail");
|
|
62
|
-
else
|
|
63
|
-
localStorage.removeItem(RAIL_KEY);
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
// Stockage bloqué : le choix vaut pour cette page seulement.
|
|
67
|
-
}
|
|
68
|
-
window.dispatchEvent(new Event(RAIL_EVENT));
|
|
69
|
-
}
|
|
70
|
-
/** `Ctrl+B` (`⌘B`) réduit ou déplie la barre latérale, hors des champs de saisie. */
|
|
71
|
-
function useToggleShortcut() {
|
|
72
|
-
useEffect(() => {
|
|
73
|
-
const onKey = (event) => {
|
|
74
|
-
if (event.key.toLowerCase() !== "b" || !(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey)
|
|
75
|
-
return;
|
|
76
|
-
const target = event.target;
|
|
77
|
-
if (target instanceof Element && target.closest("input, textarea, select, [contenteditable=true]"))
|
|
78
|
-
return;
|
|
79
|
-
event.preventDefault();
|
|
80
|
-
toggleSidebar();
|
|
81
|
-
};
|
|
82
|
-
document.addEventListener("keydown", onKey);
|
|
83
|
-
return () => document.removeEventListener("keydown", onKey);
|
|
84
|
-
}, []);
|
|
85
|
-
}
|
|
86
|
-
// ── Navigation ──────────────────────────────────────────────────────────────────────
|
|
87
|
-
const ShellContext = createContext({
|
|
88
|
-
closeMobileNav: () => { },
|
|
89
|
-
});
|
|
90
|
-
/** Pour fermer le tiroir mobile depuis un élément de la barre (un sélecteur, un bouton). */
|
|
91
|
-
export function useAppShell() {
|
|
92
|
-
return useContext(ShellContext);
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* La navigation du produit : 4 à 7 entrées, à plat. Une navigation imbriquée (l'espace d'une page,
|
|
96
|
-
* d'un projet) commence par `SidebarNavBack` et range ses entrées dans des `SidebarNavGroup`.
|
|
97
|
-
*/
|
|
98
|
-
export function SidebarNav({ label, className, children, }) {
|
|
99
|
-
return (_jsx("nav", { "aria-label": label, className: cn("min-h-0 flex-1 overflow-y-auto px-3 py-2", className), children: _jsx("ul", { className: "grid gap-0.5", children: children }) }));
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Une entrée de la barre. « Vous êtes ici » par `active` (ou `data-status="active"`, que pose le
|
|
103
|
-
* `Link` de TanStack Router) : teinte de la coque, jamais l'accent. En rail, l'icône seule, et le
|
|
104
|
-
* nom en infobulle.
|
|
105
|
-
*/
|
|
106
|
-
export function SidebarNavItem({ icon, label, active, href, render, }) {
|
|
107
|
-
const rail = useSidebarRail();
|
|
108
|
-
const { closeMobileNav } = useAppShell();
|
|
109
|
-
const link = useRender({
|
|
110
|
-
defaultTagName: "a",
|
|
111
|
-
render,
|
|
112
|
-
props: {
|
|
113
|
-
href,
|
|
114
|
-
"aria-current": active ? "page" : undefined,
|
|
115
|
-
onClick: closeMobileNav,
|
|
116
|
-
className: cn("flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium whitespace-nowrap text-muted-foreground outline-none transition-colors duration-(--duration-fast) ease-out", "hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50", "aria-[current=page]:bg-muted aria-[current=page]:text-foreground data-[status=active]:bg-muted data-[status=active]:text-foreground", "min-h-11 lg:min-h-9 coarse:min-h-11 lg:gap-2.5 lg:py-2 lg:text-shell-muted lg:hover:bg-shell-hover lg:hover:text-shell-foreground", "lg:aria-[current=page]:bg-shell-active lg:aria-[current=page]:text-shell-foreground lg:data-[status=active]:bg-shell-active lg:data-[status=active]:text-shell-foreground", "lg:rail:justify-center lg:rail:px-0"),
|
|
117
|
-
children: (_jsxs(_Fragment, { children: [_jsx(HugeiconsIcon, { icon: icon, strokeWidth: 1.8, "aria-hidden": true, className: "size-5 shrink-0 lg:size-[1.125rem]" }), _jsx("span", { className: "truncate lg:rail:sr-only", children: label })] })),
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
return (_jsx("li", { children: _jsxs(Tooltip, { disabled: !rail, children: [_jsx(TooltipTrigger, { render: link }), _jsx(TooltipContent, { side: "right", children: label })] }) }));
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Retour au niveau du dessus, en tête d'une navigation imbriquée : « Retour aux pages » quand la
|
|
124
|
-
* barre montre l'espace d'une page, d'un projet. Premier enfant de `SidebarNav`, au-dessus des
|
|
125
|
-
* groupes. Une pastille teintée, flèche à gauche, jamais « vous êtes ici ». En rail, la flèche seule
|
|
126
|
-
* et le texte en infobulle.
|
|
127
|
-
*/
|
|
128
|
-
export function SidebarNavBack({ label, href, render, }) {
|
|
129
|
-
const rail = useSidebarRail();
|
|
130
|
-
const { closeMobileNav } = useAppShell();
|
|
131
|
-
const link = useRender({
|
|
132
|
-
defaultTagName: "a",
|
|
133
|
-
render,
|
|
134
|
-
props: {
|
|
135
|
-
href,
|
|
136
|
-
onClick: closeMobileNav,
|
|
137
|
-
"data-slot": "sidebar-nav-back",
|
|
138
|
-
className: cn(
|
|
139
|
-
// Jamais marqué actif : le lien du routeur pose `data-status="active"` sur un parent ouvert.
|
|
140
|
-
"group/back flex items-center gap-2 rounded-xl bg-muted/70 px-3 py-2.5 text-sm font-medium whitespace-nowrap text-foreground outline-none transition-colors duration-(--duration-fast) ease-out", "hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50", "min-h-11 lg:min-h-9 coarse:min-h-11 lg:py-2 lg:bg-shell-hover lg:text-shell-foreground lg:hover:bg-shell-active", "lg:rail:justify-center lg:rail:px-0"),
|
|
141
|
-
children: (_jsxs(_Fragment, { children: [_jsx(HugeiconsIcon, { icon: ArrowLeft01Icon, strokeWidth: 2, "aria-hidden": true, className: "size-4.5 shrink-0 text-muted-foreground transition-transform duration-(--duration-fast) ease-out group-hover/back:-translate-x-0.5 motion-reduce:transition-none lg:size-4 lg:text-shell-muted" }), _jsx("span", { className: "truncate lg:rail:sr-only", children: label })] })),
|
|
142
|
-
},
|
|
143
|
-
});
|
|
144
|
-
return (_jsx("li", { className: "pb-2", children: _jsxs(Tooltip, { disabled: !rail, children: [_jsx(TooltipTrigger, { render: link }), _jsx(TooltipContent, { side: "right", children: label })] }) }));
|
|
145
|
-
}
|
|
146
|
-
// ── Groupes repliables : ouverts ou fermés par la personne, mémorisés par navigateur ──────
|
|
147
|
-
const GROUP_KEY_PREFIX = "sidebar-group:";
|
|
148
|
-
const GROUP_EVENT = "sidebargroupchange";
|
|
149
|
-
function subscribeGroups(onChange) {
|
|
150
|
-
window.addEventListener(GROUP_EVENT, onChange);
|
|
151
|
-
window.addEventListener("storage", onChange);
|
|
152
|
-
return () => {
|
|
153
|
-
window.removeEventListener(GROUP_EVENT, onChange);
|
|
154
|
-
window.removeEventListener("storage", onChange);
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
function readGroup(key) {
|
|
158
|
-
try {
|
|
159
|
-
const value = localStorage.getItem(GROUP_KEY_PREFIX + key);
|
|
160
|
-
return value === "open" ? true : value === "closed" ? false : null;
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
function writeGroup(key, open) {
|
|
167
|
-
try {
|
|
168
|
-
localStorage.setItem(GROUP_KEY_PREFIX + key, open ? "open" : "closed");
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
// Stockage bloqué : le choix vaut pour cette page seulement.
|
|
172
|
-
}
|
|
173
|
-
window.dispatchEvent(new Event(GROUP_EVENT));
|
|
174
|
-
}
|
|
175
|
-
/** Une entrée de la liste est-elle la page ouverte ? Suit les attributs que pose le routeur. */
|
|
176
|
-
function useActiveDescendant(ref) {
|
|
177
|
-
const [active, setActive] = useState(false);
|
|
178
|
-
useLayoutEffect(() => {
|
|
179
|
-
const element = ref.current;
|
|
180
|
-
if (!element)
|
|
181
|
-
return;
|
|
182
|
-
const read = () => setActive(element.querySelector('[aria-current="page"], [data-status="active"]') !== null);
|
|
183
|
-
read();
|
|
184
|
-
const observer = new MutationObserver(read);
|
|
185
|
-
observer.observe(element, {
|
|
186
|
-
subtree: true,
|
|
187
|
-
childList: true,
|
|
188
|
-
attributes: true,
|
|
189
|
-
attributeFilter: ["aria-current", "data-status"],
|
|
190
|
-
});
|
|
191
|
-
return () => observer.disconnect();
|
|
192
|
-
}, [ref]);
|
|
193
|
-
return active;
|
|
194
|
-
}
|
|
195
|
-
export function SidebarNavGroup(props) {
|
|
196
|
-
if (props.collapsible)
|
|
197
|
-
return _jsx(CollapsibleNavGroup, { ...props });
|
|
198
|
-
return _jsx(TitledNavGroup, { label: props.label, children: props.children });
|
|
199
|
-
}
|
|
200
|
-
function CollapsibleNavGroup({ label, children, icon, active, defaultOpen = false, storageKey, }) {
|
|
201
|
-
// Propre au produit : sous une origine partagée (`app.hgdc.ch/<produit>`), `localStorage` est
|
|
202
|
-
// commun à tous. Le thème et la barre réduite le sont volontairement (`theme`, `sidebar`), un
|
|
203
|
-
// groupe « Réglages » de Mail et de Host ne l'est pas (ADR 0020).
|
|
204
|
-
const { product } = useContext(ShellContext);
|
|
205
|
-
const key = product ? `${product}:${storageKey ?? label}` : (storageKey ?? label);
|
|
206
|
-
const buttonId = useId();
|
|
207
|
-
const listId = useId();
|
|
208
|
-
const listRef = useRef(null);
|
|
209
|
-
const stored = useSyncExternalStore(subscribeGroups, () => readGroup(key), () => null);
|
|
210
|
-
const activeInDom = useActiveDescendant(listRef);
|
|
211
|
-
const current = active ?? activeInDom;
|
|
212
|
-
// Refermé par la personne alors qu'une de ses rubriques est ouverte : il le reste jusqu'à ce
|
|
213
|
-
// qu'elle en sorte. La navigation suivante vers une rubrique du groupe le rouvre.
|
|
214
|
-
const [dismissed, setDismissed] = useState(false);
|
|
215
|
-
const [wasCurrent, setWasCurrent] = useState(current);
|
|
216
|
-
if (wasCurrent !== current) {
|
|
217
|
-
setWasCurrent(current);
|
|
218
|
-
setDismissed(false);
|
|
219
|
-
}
|
|
220
|
-
const open = current ? !dismissed : (stored ?? defaultOpen);
|
|
221
|
-
// Pas d'animation au premier rendu : l'état lu dans le navigateur s'applique sans glisser.
|
|
222
|
-
const [ready, setReady] = useState(false);
|
|
223
|
-
useEffect(() => {
|
|
224
|
-
const frame = requestAnimationFrame(() => setReady(true));
|
|
225
|
-
return () => cancelAnimationFrame(frame);
|
|
226
|
-
}, []);
|
|
227
|
-
const toggle = () => {
|
|
228
|
-
const next = !open;
|
|
229
|
-
if (current)
|
|
230
|
-
setDismissed(!next);
|
|
231
|
-
writeGroup(key, next);
|
|
232
|
-
};
|
|
233
|
-
return (_jsxs("li", { "data-slot": "sidebar-nav-group", "data-collapsible": "", "data-open": open ? "" : undefined, className: "pt-1 first:pt-0", children: [_jsx("span", { "aria-hidden": true, className: "mx-auto mt-3 mb-2 hidden h-px w-6 bg-border lg:rail:block" }), _jsxs("button", { type: "button", id: buttonId, "aria-expanded": open, "aria-controls": listId, onClick: toggle, className: cn("group/disclosure flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm font-medium whitespace-nowrap text-muted-foreground outline-none transition-colors duration-(--duration-fast) ease-out", "hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50", "min-h-11 lg:min-h-9 coarse:min-h-11 lg:gap-2.5 lg:py-2 lg:text-shell-muted lg:hover:bg-shell-hover lg:hover:text-shell-foreground",
|
|
234
|
-
// Une rubrique du groupe est ouverte et le groupe est replié : l'entrée le dit, sans fond.
|
|
235
|
-
current && !open && "text-foreground lg:text-shell-foreground", "lg:rail:hidden"), children: [icon ? (_jsx(HugeiconsIcon, { icon: icon, strokeWidth: 1.8, "aria-hidden": true, className: "size-5 shrink-0 lg:size-[1.125rem]" })) : null, _jsx("span", { className: "truncate", children: label }), _jsx(HugeiconsIcon, { icon: ArrowRight01Icon, strokeWidth: 2, "aria-hidden": true, className: cn("ml-auto size-4 shrink-0 opacity-70 transition-transform duration-(--duration-base) ease-ios motion-reduce:transition-none lg:size-3.5", open && "rotate-90") })] }), _jsx("div", { "data-slot": "sidebar-nav-group-panel", className: cn("grid", open ? "visible grid-rows-[1fr]" : "invisible grid-rows-[0fr]", ready &&
|
|
236
|
-
"transition-[grid-template-rows,visibility] duration-(--duration-base) ease-ios motion-reduce:transition-none",
|
|
237
|
-
// En rail, les rubriques restent là, sous le trait : seules leurs icônes se voient.
|
|
238
|
-
"lg:rail:visible lg:rail:grid-rows-[1fr]"), children: _jsx("ul", { ref: listRef, id: listId, "aria-labelledby": buttonId, className: cn("grid min-h-0 gap-0.5 overflow-hidden",
|
|
239
|
-
// Rubriques en retrait, sous un filet aligné sur l'icône du titre.
|
|
240
|
-
icon ? "ml-[1.375rem] border-l border-border pl-2 lg:ml-5" : "pl-2", "pt-0.5 lg:rail:ml-0 lg:rail:border-l-0 lg:rail:pt-0 lg:rail:pl-0"), children: children }) })] }));
|
|
241
|
-
}
|
|
242
|
-
function TitledNavGroup({ label, children }) {
|
|
243
|
-
const id = useId();
|
|
244
|
-
return (_jsxs("li", { "data-slot": "sidebar-nav-group", className: "pt-4 first:pt-0", children: [_jsx("p", { id: id, className: "px-3 pb-1.5 text-xs font-medium text-muted-foreground lg:text-shell-muted lg:rail:sr-only", children: label }), _jsx("span", { "aria-hidden": true, className: "mx-auto mb-2 hidden h-px w-6 bg-border lg:rail:block" }), _jsx("ul", { "aria-labelledby": id, className: "grid gap-0.5", children: children })] }));
|
|
245
|
-
}
|
|
246
|
-
const COMMON_LINKS = [
|
|
247
|
-
{ key: "documentation", label: "Documentation", icon: BookOpen01Icon },
|
|
248
|
-
{ key: "settings", label: "Réglages", icon: Settings01Icon },
|
|
249
|
-
{ key: "status", label: "État du service", icon: Activity01Icon },
|
|
250
|
-
];
|
|
251
|
-
function ShellCommonLinks({ links, statusPageUrl, otherView, }) {
|
|
252
|
-
const resolved = {
|
|
253
|
-
documentation: links.documentation,
|
|
254
|
-
settings: links.settings,
|
|
255
|
-
status: links.status === false ? undefined : (links.status ?? statusPageUrl ?? undefined),
|
|
256
|
-
};
|
|
257
|
-
const items = COMMON_LINKS.flatMap(({ key, label, icon }) => {
|
|
258
|
-
const link = resolved[key];
|
|
259
|
-
if (link === undefined)
|
|
260
|
-
return [];
|
|
261
|
-
const props = typeof link === "string" ? { href: link } : link;
|
|
262
|
-
return [
|
|
263
|
-
_jsx(SidebarNavItem, { icon: icon, label: label, ...(props.href !== undefined ? { href: props.href } : {}), ...(props.render !== undefined ? { render: props.render } : {}), ...(props.active !== undefined ? { active: props.active } : {}) }, key),
|
|
264
|
-
];
|
|
265
|
-
});
|
|
266
|
-
if (!items.length)
|
|
267
|
-
return null;
|
|
268
|
-
return (_jsx("nav", { "aria-label": "Acc\u00E8s communs", children: _jsxs("ul", { "data-slot": "app-shell-links", className: "grid gap-0.5", children: [items, otherView] }) }));
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* « Ouvrir seul » (vers `<mot>.hgdc.ch`) ou « Ouvrir dans app.hgdc.ch », discret, sous les accès communs.
|
|
272
|
-
* Le chemin courant n'est lu qu'au clic : le lien rendu par le serveur mène à l'accueil.
|
|
273
|
-
*/
|
|
274
|
-
function OtherViewLink({ product, view }) {
|
|
275
|
-
const href = otherViewHref(product, view);
|
|
276
|
-
if (!href)
|
|
277
|
-
return null;
|
|
278
|
-
const onClick = (event) => {
|
|
279
|
-
const path = soloHref(`/${product}`, `${window.location.pathname}${window.location.search}`);
|
|
280
|
-
event.currentTarget.href = otherViewHref(product, view, path) ?? href;
|
|
281
|
-
};
|
|
282
|
-
return (_jsx(SidebarNavItem, { icon: LinkSquare02Icon, label: otherViewLabel(view), render: _jsx("a", { href: href, onClick: onClick }) }));
|
|
283
|
-
}
|
|
284
|
-
/** Largeur à partir de laquelle la page et l'assistant tiennent côte à côte (`xl`). */
|
|
285
|
-
const ASSISTANT_SIDE_BY_SIDE = "(min-width: 80rem)";
|
|
286
|
-
const FOCUSABLE = 'textarea, input, [contenteditable="true"], [autofocus]';
|
|
287
|
-
function AssistantButton({ assistant, panelId, buttonRef, }) {
|
|
288
|
-
const label = assistant.label ?? "Assistant";
|
|
289
|
-
const letter = assistant.shortcut?.toUpperCase();
|
|
290
|
-
return (_jsxs(Tooltip, { children: [_jsxs(TooltipTrigger, { render: _jsx("button", { ref: buttonRef, type: "button", "data-slot": "app-shell-assistant-button", "aria-expanded": assistant.open, "aria-controls": panelId, ...(letter ? { "aria-keyshortcuts": `Control+${letter} Meta+${letter}` } : {}), onClick: () => assistant.onOpenChange(!assistant.open), className: cn("touch-target flex h-9 shrink-0 items-center gap-2 rounded-xl px-3 text-sm font-medium text-muted-foreground outline-none", "transition-colors duration-(--duration-fast) ease-out hover:bg-foreground/[0.05] hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50", "aria-expanded:bg-foreground/[0.08] aria-expanded:text-foreground", "max-lg:w-9 max-lg:justify-center max-lg:px-0") }), children: [_jsx(HugeiconsIcon, { icon: AiMagicIcon, strokeWidth: 1.8, "aria-hidden": true, className: "size-[1.125rem] shrink-0" }), _jsx("span", { className: "max-lg:sr-only", children: label })] }), _jsx(TooltipContent, { children: _jsxs("span", { className: "flex items-center gap-2", children: [assistant.open ? `Fermer ${label.toLocaleLowerCase("fr-CH")}` : label, letter ? (_jsxs(KbdGroup, { children: [_jsx(Kbd, { children: isApplePlatform() ? "⌘" : "Ctrl" }), _jsx(Kbd, { children: letter })] })) : null] }) })] }));
|
|
291
|
-
}
|
|
292
|
-
/** `Ctrl` (`⌘`) + la lettre de l'assistant : l'ouvre ou le ferme, où que soit le focus. */
|
|
293
|
-
function useAssistantShortcut(assistant) {
|
|
294
|
-
const latest = useRef(assistant);
|
|
295
|
-
useEffect(() => {
|
|
296
|
-
latest.current = assistant;
|
|
297
|
-
});
|
|
298
|
-
const letter = assistant?.shortcut?.toLowerCase();
|
|
299
|
-
useEffect(() => {
|
|
300
|
-
if (!letter)
|
|
301
|
-
return undefined;
|
|
302
|
-
const onKey = (event) => {
|
|
303
|
-
if (event.key.toLowerCase() !== letter || !(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey)
|
|
304
|
-
return;
|
|
305
|
-
const current = latest.current;
|
|
306
|
-
if (!current)
|
|
307
|
-
return;
|
|
308
|
-
event.preventDefault();
|
|
309
|
-
current.onOpenChange(!current.open);
|
|
310
|
-
};
|
|
311
|
-
document.addEventListener("keydown", onKey);
|
|
312
|
-
return () => document.removeEventListener("keydown", onKey);
|
|
313
|
-
}, [letter]);
|
|
314
|
-
}
|
|
315
|
-
function AssistantPanel({ assistant, panelId, wide, openAtShellMount, buttonRef, }) {
|
|
316
|
-
const { open, onOpenChange } = assistant;
|
|
317
|
-
const label = assistant.label ?? "Assistant";
|
|
318
|
-
const titleId = useId();
|
|
319
|
-
const panelRef = useRef(null);
|
|
320
|
-
// Ouvert dès le premier rendu de la coque (état restauré) : le focus reste où le navigateur le met.
|
|
321
|
-
const wasOpen = useRef(openAtShellMount);
|
|
322
|
-
// Ouvert : le focus entre dans le panneau (sa saisie, sinon le panneau). Fermé : il revient au
|
|
323
|
-
// bouton, s'il était dans le panneau ou perdu sur la page.
|
|
324
|
-
useEffect(() => {
|
|
325
|
-
const panel = panelRef.current;
|
|
326
|
-
if (open && !wasOpen.current && panel && !panel.contains(document.activeElement)) {
|
|
327
|
-
const target = panel.querySelector(FOCUSABLE) ?? panel;
|
|
328
|
-
target.focus({ preventScroll: true });
|
|
329
|
-
}
|
|
330
|
-
if (!open && wasOpen.current) {
|
|
331
|
-
const active = document.activeElement;
|
|
332
|
-
if (!active || active === document.body || panel?.contains(active))
|
|
333
|
-
buttonRef.current?.focus();
|
|
334
|
-
}
|
|
335
|
-
wasOpen.current = open;
|
|
336
|
-
}, [open, buttonRef]);
|
|
337
|
-
// Échap, le focus dans le panneau : il se ferme (sauf si un élément du panneau l'a déjà traité).
|
|
338
|
-
useEffect(() => {
|
|
339
|
-
const panel = panelRef.current;
|
|
340
|
-
if (!open || !panel)
|
|
341
|
-
return undefined;
|
|
342
|
-
const onKey = (event) => {
|
|
343
|
-
if (event.key !== "Escape" || event.defaultPrevented)
|
|
344
|
-
return;
|
|
345
|
-
event.preventDefault();
|
|
346
|
-
onOpenChange(false);
|
|
347
|
-
};
|
|
348
|
-
panel.addEventListener("keydown", onKey);
|
|
349
|
-
return () => panel.removeEventListener("keydown", onKey);
|
|
350
|
-
}, [open, onOpenChange]);
|
|
351
|
-
const close = () => onOpenChange(false);
|
|
352
|
-
return (_jsx("section", { ref: panelRef, id: panelId, tabIndex: -1, "data-slot": "app-shell-assistant", "data-open": open ? "" : undefined, "aria-labelledby": titleId, inert: !open || undefined, ...(open && !wide ? { role: "dialog", "aria-modal": true } : {}), className: cn("outline-none",
|
|
353
|
-
// Vue dédiée, sous 1280 px : plein écran, la page reste montée dessous.
|
|
354
|
-
"hidden data-open:max-xl:fixed data-open:max-xl:inset-0 data-open:max-xl:z-40 data-open:max-xl:flex data-open:max-xl:flex-col data-open:max-xl:bg-background", "data-open:max-xl:pt-[env(safe-area-inset-top,0px)] data-open:max-xl:soft-in motion-reduce:animate-none",
|
|
355
|
-
// Côte à côte : une colonne de la grille, qui s'ouvre en largeur ; la page se réorganise.
|
|
356
|
-
"xl:invisible xl:flex xl:w-0 xl:flex-col xl:overflow-hidden xl:transition-[width,visibility] xl:duration-(--duration-slow) xl:ease-ios motion-reduce:transition-none", "xl:data-open:visible xl:data-open:w-[26rem] 2xl:data-open:w-[28rem]"), children: _jsxs("div", { className: "flex min-h-0 flex-1 flex-col xl:w-[26rem] xl:py-2 xl:pr-2 2xl:w-[28rem]", children: [_jsxs("div", { className: "flex min-h-14 shrink-0 items-center gap-1 border-b border-foreground/5 px-2 sm:px-4 xl:border-0 xl:pr-1 xl:pl-3", children: [_jsxs("button", { type: "button", onClick: close, className: "touch-target flex h-9 shrink-0 items-center gap-1.5 rounded-xl pr-3 pl-2 text-sm font-medium text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 xl:hidden", children: [_jsx(HugeiconsIcon, { icon: ArrowLeft01Icon, strokeWidth: 2, "aria-hidden": true, className: "size-4" }), "Retour \u00E0 la page"] }), _jsxs("h2", { id: titleId, className: "flex min-w-0 flex-1 items-center gap-2 truncate font-heading text-[0.9375rem] font-semibold max-xl:justify-end max-xl:pr-2", children: [_jsx(HugeiconsIcon, { icon: AiMagicIcon, strokeWidth: 1.8, "aria-hidden": true, className: "size-[1.125rem] shrink-0" }), label] }), _jsx("button", { type: "button", onClick: close, "aria-label": `Fermer ${label.toLocaleLowerCase("fr-CH")}`, className: "touch-target grid size-9 shrink-0 place-items-center rounded-xl text-muted-foreground outline-none hover:bg-shell-hover hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 max-xl:hidden", children: _jsx(HugeiconsIcon, { icon: Cancel01Icon, strokeWidth: 1.8, "aria-hidden": true, className: "size-[1.125rem]" }) })] }), _jsx("div", { "data-slot": "app-shell-assistant-body", className: "flex min-h-0 flex-1 flex-col overflow-y-auto", children: assistant.panel })] }) }));
|
|
357
|
-
}
|
|
358
|
-
// ── Coque ─────────────────────────────────────────────────────────────────────────────
|
|
359
|
-
// Les noms de Hugeicons disent l'état du panneau, pas le geste : « Open » dessine la flèche vers la
|
|
360
|
-
// gauche (réduire), « Close » vers la droite (déplier).
|
|
361
|
-
const COLLAPSE_ICON = PanelLeftOpenIcon;
|
|
362
|
-
const EXPAND_ICON = PanelLeftCloseIcon;
|
|
363
|
-
/** Même boîte qu'une entrée de la barre en rail : 40 × 36 px, icône de 18 px centrée. */
|
|
364
|
-
const TOGGLE = "touch-target relative grid h-9 w-10 shrink-0 place-items-center rounded-xl text-shell-muted 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";
|
|
365
|
-
function ShortcutHint({ label }) {
|
|
366
|
-
return (_jsxs("span", { className: "flex items-center gap-2", children: [label, _jsxs(KbdGroup, { children: [_jsx(Kbd, { children: isApplePlatform() ? "⌘" : "Ctrl" }), _jsx(Kbd, { children: "B" })] })] }));
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* Haut de la barre latérale. Dépliée : la marque du produit, et le bouton « Réduire » à droite.
|
|
370
|
-
* En rail : la marque seule, qui devient le bouton « Déplier » au survol de la barre ou au focus,
|
|
371
|
-
* à la même place : le bouton ne descend jamais sous le logo. Les deux états sont dans la page et
|
|
372
|
-
* choisis par la variante CSS `rail:`, donc justes dès la première peinture.
|
|
373
|
-
*/
|
|
374
|
-
function SidebarHead({ product }) {
|
|
375
|
-
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" }) })] })] }));
|
|
376
|
-
}
|
|
26
|
+
export { sidebarInitScript, toggleSidebar, useSidebarRail } from "./sidebar-rail.js";
|
|
27
|
+
export { PRODUCT_LEAVE_DELAY_MS } from "./use-product-leave.js";
|
|
28
|
+
export { useAppShell } from "./shell-context.js";
|
|
29
|
+
export { SidebarNav, SidebarNavBack, SidebarNavItem } from "./sidebar-nav.js";
|
|
30
|
+
export { SidebarNavGroup } from "./sidebar-nav-group.js";
|
|
377
31
|
/**
|
|
378
32
|
* La coque commune des consoles HGDC, celle de Host. Sur ordinateur, de gauche à droite : la barre
|
|
379
33
|
* des produits de la famille (organisation, Account, pastilles ; élargie au survol à la place
|
|
@@ -385,51 +39,6 @@ function SidebarHead({ product }) {
|
|
|
385
39
|
* Le kit ne connaît ni le routeur ni l'authentification : le produit passe ses éléments
|
|
386
40
|
* (`OrgSwitcher`, `SidebarNav`, `AccountMenu`) et ses liens.
|
|
387
41
|
*/
|
|
388
|
-
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
389
|
-
/** Faux au rendu serveur et à l'hydratation, vrai avant la première image qui suit. */
|
|
390
|
-
function useHydrated() {
|
|
391
|
-
const [hydrated, setHydrated] = useState(false);
|
|
392
|
-
useIsomorphicLayoutEffect(() => setHydrated(true), []);
|
|
393
|
-
return hydrated;
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* Le chargement de la zone de contenu : `navigating` après 50 ms, `pending` aussitôt, puis au
|
|
397
|
-
* moins 300 ms. Effets de mise en page : une page qui passe en attente est couverte avant d'être
|
|
398
|
-
* peinte, jamais une image vide entre les deux.
|
|
399
|
-
*/
|
|
400
|
-
function useContentLoading({ navigating, pending }) {
|
|
401
|
-
const [navigatingLong, setNavigatingLong] = useState(false);
|
|
402
|
-
useEffect(() => {
|
|
403
|
-
if (!navigating)
|
|
404
|
-
return undefined;
|
|
405
|
-
const timer = window.setTimeout(() => setNavigatingLong(true), PAGE_PENDING_MS);
|
|
406
|
-
return () => {
|
|
407
|
-
window.clearTimeout(timer);
|
|
408
|
-
setNavigatingLong(false);
|
|
409
|
-
};
|
|
410
|
-
}, [navigating]);
|
|
411
|
-
const active = pending || (navigating && navigatingLong);
|
|
412
|
-
const [visible, setVisible] = useState(false);
|
|
413
|
-
const shownAt = useRef(0);
|
|
414
|
-
useIsomorphicLayoutEffect(() => {
|
|
415
|
-
if (active && !visible) {
|
|
416
|
-
shownAt.current = performance.now();
|
|
417
|
-
setVisible(true);
|
|
418
|
-
return undefined;
|
|
419
|
-
}
|
|
420
|
-
if (!active && visible) {
|
|
421
|
-
const timer = window.setTimeout(() => setVisible(false), Math.max(0, shownAt.current + PAGE_PENDING_MIN_MS - performance.now()));
|
|
422
|
-
return () => window.clearTimeout(timer);
|
|
423
|
-
}
|
|
424
|
-
return undefined;
|
|
425
|
-
}, [active, visible]);
|
|
426
|
-
return visible;
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* Délai entre le clic vers un autre produit et le H : une page prérendue s'affiche avant (une ou
|
|
430
|
-
* deux images), le H n'apparaît alors jamais ; une page à charger le montre aussitôt après.
|
|
431
|
-
*/
|
|
432
|
-
export const PRODUCT_LEAVE_DELAY_MS = 100;
|
|
433
42
|
export function AppShell({ product, organization, productRail: productRailOption, view: viewOption, accountHref, nav, links, account, sidebarFooter, header, onSearch, searchLabel, assistant, children, contentKey, navigating = false, mainClassName, statusPage, }) {
|
|
434
43
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
435
44
|
const preference = useProductBarPreference();
|
|
@@ -445,34 +54,12 @@ export function AppShell({ product, organization, productRail: productRailOption
|
|
|
445
54
|
// qui attend (`PagePending`, aussitôt : le routeur a déjà attendu), ou le départ vers un autre
|
|
446
55
|
// produit (aussitôt). Une fois montré, au moins 300 ms. Le H est celui du document, en position
|
|
447
56
|
// fixe : il ne bouge pas d'un état à l'autre et n'est jamais démonté.
|
|
448
|
-
const [leaving, setLeaving] = useState(false);
|
|
449
57
|
const [pagePending, setPagePending] = useState(0);
|
|
450
58
|
const registerPagePending = useCallback(() => {
|
|
451
59
|
setPagePending((count) => count + 1);
|
|
452
60
|
return () => setPagePending((count) => count - 1);
|
|
453
61
|
}, []);
|
|
454
|
-
|
|
455
|
-
// que le H apparaisse (ADR 0020). Sinon le H couvre le contenu jusqu'au départ de la page.
|
|
456
|
-
const leaveTimer = useRef(undefined);
|
|
457
|
-
const leave = useCallback(() => {
|
|
458
|
-
window.clearTimeout(leaveTimer.current);
|
|
459
|
-
leaveTimer.current = window.setTimeout(() => setLeaving(true), PRODUCT_LEAVE_DELAY_MS);
|
|
460
|
-
}, []);
|
|
461
|
-
useEffect(() => {
|
|
462
|
-
// Retour arrière depuis l'autre produit (cache de page) : la page revient telle quelle.
|
|
463
|
-
const onPageShow = (event) => {
|
|
464
|
-
if (!event.persisted)
|
|
465
|
-
return;
|
|
466
|
-
window.clearTimeout(leaveTimer.current);
|
|
467
|
-
setLeaving(false);
|
|
468
|
-
delete document.documentElement.dataset.hgdcSwitching;
|
|
469
|
-
};
|
|
470
|
-
window.addEventListener("pageshow", onPageShow);
|
|
471
|
-
return () => {
|
|
472
|
-
window.removeEventListener("pageshow", onPageShow);
|
|
473
|
-
window.clearTimeout(leaveTimer.current);
|
|
474
|
-
};
|
|
475
|
-
}, []);
|
|
62
|
+
const { leaving, leave } = useProductLeave();
|
|
476
63
|
const statusPageUrl = statusPage === false ? null : (statusPage ?? statusPageUrlFor(product));
|
|
477
64
|
const loading = useContentLoading({ navigating, pending: pagePending > 0 || leaving });
|
|
478
65
|
// Avant l'hydratation, le CSS montre le calque et le H dès qu'une page attend (`:has`) ; ensuite
|