@hgdc/ui 0.44.0 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addresses.d.ts +28 -1
- package/dist/addresses.js +46 -1
- package/dist/app-shell.d.ts +10 -2
- package/dist/app-shell.js +26 -4
- package/dist/product-bar.d.ts +29 -0
- package/dist/product-bar.js +82 -0
- package/dist/products.d.ts +11 -0
- package/dist/products.js +20 -1
- package/package.json +1 -1
package/dist/addresses.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare const APP_ORIGIN = "https://app.hgdc.ch";
|
|
|
15
15
|
* console seule, `status.hgdc.ch` garde les pages publiques et la console de secours. Vérifiée contre
|
|
16
16
|
* le manifeste privé de la famille (`famille check`) : un produit relayé s'ajoute ici et là.
|
|
17
17
|
*/
|
|
18
|
-
export declare const APP_PRODUCTS: readonly ["analytics", "mail", "host", "docs", "status", "iassistant"];
|
|
18
|
+
export declare const APP_PRODUCTS: readonly ["analytics", "mail", "host", "docs", "status", "iassistant", "portal"];
|
|
19
19
|
/** Vrai si le produit est servi sous `app.hgdc.ch`. */
|
|
20
20
|
export declare function isAppProduct(product: string): boolean;
|
|
21
21
|
/**
|
|
@@ -62,3 +62,30 @@ export type ProductAddresses = {
|
|
|
62
62
|
* `x-forwarded-host` valent l'origine relayée).
|
|
63
63
|
*/
|
|
64
64
|
export declare function productAddresses(product: string, env?: AddressOverrides): ProductAddresses;
|
|
65
|
+
/**
|
|
66
|
+
* Vue d'un produit : `app` sous `https://app.hgdc.ch/<mot>` (barre des produits), `seul` sur
|
|
67
|
+
* `https://<mot>.hgdc.ch` (le produit seul). Même build : la passerelle ajoute la base sur
|
|
68
|
+
* `<mot>.hgdc.ch`, toute adresse `/<mot>/…` y reste valide.
|
|
69
|
+
*/
|
|
70
|
+
export type ProductView = "app" | "seul";
|
|
71
|
+
/** Origine de la vue seule d'un produit : `https://mail.hgdc.ch`. */
|
|
72
|
+
export declare function soloOrigin(product: string): string;
|
|
73
|
+
/** Origine publique d'une vue : une constante, jamais tirée de la requête. */
|
|
74
|
+
export declare function viewOrigin(product: string, view: ProductView): string;
|
|
75
|
+
/**
|
|
76
|
+
* Vue d'après le nom de la page (`location.hostname`) : `seul` sur `<mot>.hgdc.ch` d'un produit servi
|
|
77
|
+
* sous `app.hgdc.ch`, `app` sinon (Account n'a qu'une vue).
|
|
78
|
+
*/
|
|
79
|
+
export declare function viewFromHostname(product: string, hostname: string): ProductView;
|
|
80
|
+
/**
|
|
81
|
+
* Origines publiques des consoles, pour une liste fermée (CORS d'Account) : `app.hgdc.ch` et la vue
|
|
82
|
+
* seule de chaque produit servi sous `app.hgdc.ch`.
|
|
83
|
+
*/
|
|
84
|
+
export declare const CONSOLE_ORIGINS: ReadonlyArray<string>;
|
|
85
|
+
/**
|
|
86
|
+
* Adresse affichée en vue seule : la base retirée (`/mail/o/acme` devient `/o/acme`). À donner au
|
|
87
|
+
* routeur dans le navigateur, sur `<mot>.hgdc.ch` seulement :
|
|
88
|
+
* `createBrowserHistory({ createHref: (href) => soloHref(basePath, href) })`. Le serveur reçoit
|
|
89
|
+
* toujours la base (la passerelle l'ajoute) ; un chemin qui la garde reste valide.
|
|
90
|
+
*/
|
|
91
|
+
export declare function soloHref(basePath: string, href: string): string;
|
package/dist/addresses.js
CHANGED
|
@@ -15,7 +15,7 @@ export const APP_ORIGIN = "https://app.hgdc.ch";
|
|
|
15
15
|
* console seule, `status.hgdc.ch` garde les pages publiques et la console de secours. Vérifiée contre
|
|
16
16
|
* le manifeste privé de la famille (`famille check`) : un produit relayé s'ajoute ici et là.
|
|
17
17
|
*/
|
|
18
|
-
export const APP_PRODUCTS = ["analytics", "mail", "host", "docs", "status", "iassistant"];
|
|
18
|
+
export const APP_PRODUCTS = ["analytics", "mail", "host", "docs", "status", "iassistant", "portal"];
|
|
19
19
|
/** Vrai si le produit est servi sous `app.hgdc.ch`. */
|
|
20
20
|
export function isAppProduct(product) {
|
|
21
21
|
return APP_PRODUCTS.includes(product.toLowerCase());
|
|
@@ -71,3 +71,48 @@ export function productAddresses(product, env = {}) {
|
|
|
71
71
|
withBase: (path) => `${basePath}${path}`,
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
+
/** Origine de la vue seule d'un produit : `https://mail.hgdc.ch`. */
|
|
75
|
+
export function soloOrigin(product) {
|
|
76
|
+
const word = product.toLowerCase();
|
|
77
|
+
if (!/^[a-z]+$/.test(word))
|
|
78
|
+
throw new Error(`@hgdc/ui/addresses : produit invalide « ${product} »`);
|
|
79
|
+
return `https://${word}.hgdc.ch`;
|
|
80
|
+
}
|
|
81
|
+
/** Origine publique d'une vue : une constante, jamais tirée de la requête. */
|
|
82
|
+
export function viewOrigin(product, view) {
|
|
83
|
+
return view === "seul" ? soloOrigin(product) : APP_ORIGIN;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Vue d'après le nom de la page (`location.hostname`) : `seul` sur `<mot>.hgdc.ch` d'un produit servi
|
|
87
|
+
* sous `app.hgdc.ch`, `app` sinon (Account n'a qu'une vue).
|
|
88
|
+
*/
|
|
89
|
+
export function viewFromHostname(product, hostname) {
|
|
90
|
+
if (!isAppProduct(product))
|
|
91
|
+
return "app";
|
|
92
|
+
return `https://${hostname.toLowerCase()}` === soloOrigin(product) ? "seul" : "app";
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Origines publiques des consoles, pour une liste fermée (CORS d'Account) : `app.hgdc.ch` et la vue
|
|
96
|
+
* seule de chaque produit servi sous `app.hgdc.ch`.
|
|
97
|
+
*/
|
|
98
|
+
export const CONSOLE_ORIGINS = [APP_ORIGIN, ...APP_PRODUCTS.map(soloOrigin)];
|
|
99
|
+
/**
|
|
100
|
+
* Adresse affichée en vue seule : la base retirée (`/mail/o/acme` devient `/o/acme`). À donner au
|
|
101
|
+
* routeur dans le navigateur, sur `<mot>.hgdc.ch` seulement :
|
|
102
|
+
* `createBrowserHistory({ createHref: (href) => soloHref(basePath, href) })`. Le serveur reçoit
|
|
103
|
+
* toujours la base (la passerelle l'ajoute) ; un chemin qui la garde reste valide.
|
|
104
|
+
*/
|
|
105
|
+
export function soloHref(basePath, href) {
|
|
106
|
+
const base = normalizeProductBasePath(basePath);
|
|
107
|
+
if (!base || !href.startsWith(base))
|
|
108
|
+
return href;
|
|
109
|
+
const rest = href.slice(base.length);
|
|
110
|
+
if (rest === "")
|
|
111
|
+
return "/";
|
|
112
|
+
if (rest.startsWith("?") || rest.startsWith("#"))
|
|
113
|
+
return `/${rest}`;
|
|
114
|
+
// `/mailbox` n'est pas sous `/mail` ; `//x` resterait une adresse externe.
|
|
115
|
+
if (!rest.startsWith("/") || rest.startsWith("//") || rest.includes("\\"))
|
|
116
|
+
return href;
|
|
117
|
+
return rest;
|
|
118
|
+
}
|
package/dist/app-shell.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type IconSvgElement } from "@hugeicons/react";
|
|
2
2
|
import { type ReactNode } from "react";
|
|
3
3
|
import type { FamilyProduct } from "./family-svg.ts";
|
|
4
|
+
import { type ProductView } from "./addresses.ts";
|
|
4
5
|
/**
|
|
5
6
|
* À poser dans le `<head>`, avec `themeInitScript` : avant la première peinture, il remet le
|
|
6
7
|
* rail (la variante CSS `rail:` le lit, rien ne saute au chargement) et la mémoire de la coque
|
|
@@ -125,7 +126,7 @@ export type AppShellAssistant = {
|
|
|
125
126
|
* deux images), le H n'apparaît alors jamais ; une page à charger le montre aussitôt après.
|
|
126
127
|
*/
|
|
127
128
|
export declare const PRODUCT_LEAVE_DELAY_MS = 100;
|
|
128
|
-
export declare function AppShell({ product, organization, productRail, accountHref, nav, links, account, sidebarFooter, header, onSearch, searchLabel, assistant, children, contentKey, navigating, mainClassName, statusPage, }: {
|
|
129
|
+
export declare function AppShell({ product, organization, productRail: productRailOption, view: viewOption, accountHref, nav, links, account, sidebarFooter, header, onSearch, searchLabel, assistant, children, contentKey, navigating, mainClassName, statusPage, }: {
|
|
129
130
|
/** Le produit affiché (`"host"`) : sa marque en haut de la barre, « vous êtes ici » dans la barre des produits. */
|
|
130
131
|
product: FamilyProduct;
|
|
131
132
|
/**
|
|
@@ -136,9 +137,16 @@ export declare function AppShell({ product, organization, productRail, accountHr
|
|
|
136
137
|
organization?: ReactNode;
|
|
137
138
|
/**
|
|
138
139
|
* `false` : pas de barre des produits (un écran hors de la famille). L'organisation passe alors
|
|
139
|
-
* sous la marque, dans la barre latérale.
|
|
140
|
+
* sous la marque, dans la barre latérale. Absent : présente, ou, si `view` est donné, selon la vue
|
|
141
|
+
* et le réglage « Barre des produits » d'Account (ADR 0014), relu par la coque.
|
|
140
142
|
*/
|
|
141
143
|
productRail?: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Vue de la page : `app` sous `app.hgdc.ch/<mot>`, `seul` sur `<mot>.hgdc.ch` (ADR 0014). Donnée
|
|
146
|
+
* par le serveur (`requestView` de `@hgdc/auth/server`), la même au rendu et dans le navigateur.
|
|
147
|
+
* Absent : un produit qui n'a qu'une vue, barre présente et pas de lien vers l'autre vue.
|
|
148
|
+
*/
|
|
149
|
+
view?: ProductView | undefined;
|
|
142
150
|
/** Adresse d'Account dans la barre des produits (un Account local en développement). */
|
|
143
151
|
accountHref?: string | undefined;
|
|
144
152
|
/** `SidebarNav` et ses `SidebarNavItem`. */
|
package/dist/app-shell.js
CHANGED
|
@@ -5,6 +5,7 @@ import ArrowLeft01Icon from "@hugeicons/core-free-icons/ArrowLeft01Icon";
|
|
|
5
5
|
import ArrowRight01Icon from "@hugeicons/core-free-icons/ArrowRight01Icon";
|
|
6
6
|
import BookOpen01Icon from "@hugeicons/core-free-icons/BookOpen01Icon";
|
|
7
7
|
import Cancel01Icon from "@hugeicons/core-free-icons/Cancel01Icon";
|
|
8
|
+
import LinkSquare02Icon from "@hugeicons/core-free-icons/LinkSquare02Icon";
|
|
8
9
|
import Menu01Icon from "@hugeicons/core-free-icons/Menu01Icon";
|
|
9
10
|
import PanelLeftCloseIcon from "@hugeicons/core-free-icons/PanelLeftCloseIcon";
|
|
10
11
|
import PanelLeftOpenIcon from "@hugeicons/core-free-icons/PanelLeftOpenIcon";
|
|
@@ -17,7 +18,10 @@ import { Drawer, DrawerContent, DrawerTitle } from "./drawer.js";
|
|
|
17
18
|
import { FamilyLoaderHost, FamilyLoaderRootContext } from "./family-loader.js";
|
|
18
19
|
import { isApplePlatform, Kbd, KbdGroup } from "./kbd.js";
|
|
19
20
|
import { CONTENT_LOADING_LAYER, ContentLoadingContext, PAGE_PENDING_MIN_MS, PAGE_PENDING_MS, PagePending, } from "./page-pending.js";
|
|
21
|
+
import { soloHref } from "./addresses.js";
|
|
20
22
|
import { ProductBadge } from "./product-badge.js";
|
|
23
|
+
import { productBarShown, useProductBarPreference, useProductBarSync } from "./product-bar.js";
|
|
24
|
+
import { otherViewHref, otherViewLabel } from "./products.js";
|
|
21
25
|
import { ProductRail } from "./product-rail.js";
|
|
22
26
|
import { useProductSwitch } from "./product-switch.js";
|
|
23
27
|
import { ProductRailShownContext, ShellLeaveContext, StatusPageContext } from "./shell-placement.js";
|
|
@@ -244,7 +248,7 @@ const COMMON_LINKS = [
|
|
|
244
248
|
{ key: "settings", label: "Réglages", icon: Settings01Icon },
|
|
245
249
|
{ key: "status", label: "État du service", icon: Activity01Icon },
|
|
246
250
|
];
|
|
247
|
-
function ShellCommonLinks({ links, statusPageUrl, }) {
|
|
251
|
+
function ShellCommonLinks({ links, statusPageUrl, otherView, }) {
|
|
248
252
|
const resolved = {
|
|
249
253
|
documentation: links.documentation,
|
|
250
254
|
settings: links.settings,
|
|
@@ -261,7 +265,21 @@ function ShellCommonLinks({ links, statusPageUrl, }) {
|
|
|
261
265
|
});
|
|
262
266
|
if (!items.length)
|
|
263
267
|
return null;
|
|
264
|
-
return (_jsx("nav", { "aria-label": "Acc\u00E8s communs", children:
|
|
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 }) }));
|
|
265
283
|
}
|
|
266
284
|
/** Largeur à partir de laquelle la page et l'assistant tiennent côte à côte (`xl`). */
|
|
267
285
|
const ASSISTANT_SIDE_BY_SIDE = "(min-width: 80rem)";
|
|
@@ -412,8 +430,12 @@ function useContentLoading({ navigating, pending }) {
|
|
|
412
430
|
* deux images), le H n'apparaît alors jamais ; une page à charger le montre aussitôt après.
|
|
413
431
|
*/
|
|
414
432
|
export const PRODUCT_LEAVE_DELAY_MS = 100;
|
|
415
|
-
export function AppShell({ product, organization, productRail
|
|
433
|
+
export function AppShell({ product, organization, productRail: productRailOption, view: viewOption, accountHref, nav, links, account, sidebarFooter, header, onSearch, searchLabel, assistant, children, contentKey, navigating = false, mainClassName, statusPage, }) {
|
|
416
434
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
435
|
+
const preference = useProductBarPreference();
|
|
436
|
+
// Relu chez Account seulement par un produit passé aux deux vues (il donne `view`, ADR 0014).
|
|
437
|
+
useProductBarSync(productRailOption === undefined && viewOption !== undefined);
|
|
438
|
+
const productRail = productRailOption ?? (viewOption === undefined || productBarShown(viewOption, preference));
|
|
417
439
|
// Barre des produits élargie : la barre latérale se plie le temps du survol (ADR 0011).
|
|
418
440
|
const [railPeek, setRailPeek] = useState(false);
|
|
419
441
|
useToggleShortcut();
|
|
@@ -490,6 +512,6 @@ export function AppShell({ product, organization, productRail = true, accountHre
|
|
|
490
512
|
const assistantPanelId = useId();
|
|
491
513
|
const assistantButtonRef = useRef(null);
|
|
492
514
|
useAssistantShortcut(assistant);
|
|
493
|
-
const footer = (_jsxs(_Fragment, { children: [account !== undefined ? sidebarFooter : null, _jsx(ShellCommonLinks, { links: links ?? {}, statusPageUrl: statusPageUrl }), account ?? sidebarFooter] }));
|
|
515
|
+
const footer = (_jsxs(_Fragment, { children: [account !== undefined ? sidebarFooter : null, _jsx(ShellCommonLinks, { links: links ?? {}, statusPageUrl: statusPageUrl, otherView: viewOption === undefined ? null : _jsx(OtherViewLink, { product: product, view: viewOption }) }), account ?? sidebarFooter] }));
|
|
494
516
|
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 })] }) })] }) }) }) }) }));
|
|
495
517
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ProductView } from "./addresses.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Réglage « Barre des produits » (ADR 0014 de la famille), choisi dans Account :
|
|
4
|
+
* - `auto` : la barre sous `app.hgdc.ch`, pas sur `<mot>.hgdc.ch` ;
|
|
5
|
+
* - `always` : toujours ;
|
|
6
|
+
* - `never` : jamais.
|
|
7
|
+
*
|
|
8
|
+
* Account le garde ; chaque produit le relit (`ACCOUNT_PREFERENCES_URL`, CORS avec le cookie
|
|
9
|
+
* d'Account, même site `hgdc.ch`) et retient la dernière valeur dans `localStorage` de son origine.
|
|
10
|
+
* Account injoignable : dernière valeur connue, sinon `auto`.
|
|
11
|
+
*/
|
|
12
|
+
export type ProductBarPreference = "auto" | "always" | "never";
|
|
13
|
+
export declare const PRODUCT_BAR_PREFERENCES: ReadonlyArray<ProductBarPreference>;
|
|
14
|
+
/** Libellés du réglage, dans l'ordre d'affichage. */
|
|
15
|
+
export declare const PRODUCT_BAR_LABELS: Readonly<Record<ProductBarPreference, string>>;
|
|
16
|
+
/** Lecture du réglage chez Account : `{ productBar }`, rien d'autre. */
|
|
17
|
+
export declare const ACCOUNT_PREFERENCES_URL = "https://account.hgdc.ch/api/preferences";
|
|
18
|
+
/** Valeur reçue (Account, stockage) : un réglage connu, sinon `auto`. */
|
|
19
|
+
export declare function parseProductBar(value: unknown): ProductBarPreference;
|
|
20
|
+
/** La barre des produits est-elle montrée dans cette vue, avec ce réglage ? */
|
|
21
|
+
export declare function productBarShown(view: ProductView, preference: ProductBarPreference): boolean;
|
|
22
|
+
/** Retient le réglage dans cette origine et prévient la coque. */
|
|
23
|
+
export declare function rememberProductBar(preference: ProductBarPreference): void;
|
|
24
|
+
/** Le réglage retenu dans cette origine ; `auto` au rendu serveur. */
|
|
25
|
+
export declare function useProductBarPreference(): ProductBarPreference;
|
|
26
|
+
/** Relit le réglage chez Account une fois par chargement de page ; une erreur garde la dernière valeur. */
|
|
27
|
+
export declare function syncProductBar(fetchImpl?: typeof fetch): Promise<void>;
|
|
28
|
+
/** À monter une fois (la coque le fait) : relecture du réglage après l'hydratation. */
|
|
29
|
+
export declare function useProductBarSync(enabled?: boolean): void;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
2
|
+
export const PRODUCT_BAR_PREFERENCES = ["auto", "always", "never"];
|
|
3
|
+
/** Libellés du réglage, dans l'ordre d'affichage. */
|
|
4
|
+
export const PRODUCT_BAR_LABELS = {
|
|
5
|
+
auto: "Automatique (selon l'adresse)",
|
|
6
|
+
always: "Toujours",
|
|
7
|
+
never: "Jamais",
|
|
8
|
+
};
|
|
9
|
+
/** Lecture du réglage chez Account : `{ productBar }`, rien d'autre. */
|
|
10
|
+
export const ACCOUNT_PREFERENCES_URL = "https://account.hgdc.ch/api/preferences";
|
|
11
|
+
const STORAGE_KEY = "hgdc-product-bar";
|
|
12
|
+
const EVENT = "hgdc-product-bar";
|
|
13
|
+
/** Valeur reçue (Account, stockage) : un réglage connu, sinon `auto`. */
|
|
14
|
+
export function parseProductBar(value) {
|
|
15
|
+
return PRODUCT_BAR_PREFERENCES.includes(value) ? value : "auto";
|
|
16
|
+
}
|
|
17
|
+
/** La barre des produits est-elle montrée dans cette vue, avec ce réglage ? */
|
|
18
|
+
export function productBarShown(view, preference) {
|
|
19
|
+
if (preference === "always")
|
|
20
|
+
return true;
|
|
21
|
+
if (preference === "never")
|
|
22
|
+
return false;
|
|
23
|
+
return view === "app";
|
|
24
|
+
}
|
|
25
|
+
function stored() {
|
|
26
|
+
try {
|
|
27
|
+
return parseProductBar(window.localStorage.getItem(STORAGE_KEY));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return "auto";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function subscribe(onChange) {
|
|
34
|
+
window.addEventListener(EVENT, onChange);
|
|
35
|
+
window.addEventListener("storage", onChange);
|
|
36
|
+
return () => {
|
|
37
|
+
window.removeEventListener(EVENT, onChange);
|
|
38
|
+
window.removeEventListener("storage", onChange);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Retient le réglage dans cette origine et prévient la coque. */
|
|
42
|
+
export function rememberProductBar(preference) {
|
|
43
|
+
try {
|
|
44
|
+
if (preference === "auto")
|
|
45
|
+
window.localStorage.removeItem(STORAGE_KEY);
|
|
46
|
+
else
|
|
47
|
+
window.localStorage.setItem(STORAGE_KEY, preference);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Stockage bloqué : la coque garde « automatique ».
|
|
51
|
+
}
|
|
52
|
+
window.dispatchEvent(new Event(EVENT));
|
|
53
|
+
}
|
|
54
|
+
/** Le réglage retenu dans cette origine ; `auto` au rendu serveur. */
|
|
55
|
+
export function useProductBarPreference() {
|
|
56
|
+
return useSyncExternalStore(subscribe, stored, () => "auto");
|
|
57
|
+
}
|
|
58
|
+
/** Relit le réglage chez Account une fois par chargement de page ; une erreur garde la dernière valeur. */
|
|
59
|
+
export async function syncProductBar(fetchImpl = fetch) {
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetchImpl(ACCOUNT_PREFERENCES_URL, { credentials: "include", cache: "no-store" });
|
|
62
|
+
if (!response.ok)
|
|
63
|
+
return;
|
|
64
|
+
const body = await response.json();
|
|
65
|
+
if (typeof body === "object" && body !== null && "productBar" in body)
|
|
66
|
+
rememberProductBar(parseProductBar(body.productBar));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Account injoignable ou hors ligne : rien ne change.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
let synced = false;
|
|
73
|
+
/** À monter une fois (la coque le fait) : relecture du réglage après l'hydratation. */
|
|
74
|
+
export function useProductBarSync(enabled = true) {
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
// Account n'accepte que les consoles de `hgdc.ch` : en local, en test, rien à demander.
|
|
77
|
+
if (!enabled || synced || !window.location.hostname.endsWith(".hgdc.ch"))
|
|
78
|
+
return;
|
|
79
|
+
synced = true;
|
|
80
|
+
void syncProductBar();
|
|
81
|
+
}, [enabled]);
|
|
82
|
+
}
|
package/dist/products.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IconSvgElement } from "@hugeicons/react";
|
|
2
|
+
import { type ProductView } from "./addresses.ts";
|
|
2
3
|
import { type EmailAccent } from "./email-palette.ts";
|
|
3
4
|
import { type FamilyProduct } from "./family-svg.ts";
|
|
4
5
|
/** Où en est un produit : seul ce qui répond s'ouvre depuis les menus. */
|
|
@@ -131,3 +132,13 @@ export declare function productRailEntries(options?: {
|
|
|
131
132
|
products: Array<ProductRailEntry>;
|
|
132
133
|
account: ProductRailEntry;
|
|
133
134
|
};
|
|
135
|
+
/**
|
|
136
|
+
* Lien vers l'autre vue du même produit (ADR 0014) : depuis `app.hgdc.ch/<mot>`, « Ouvrir seul » vers
|
|
137
|
+
* `<mot>.hgdc.ch` ; depuis `<mot>.hgdc.ch`, « Ouvrir dans app.hgdc.ch ». Passe par l'entrée
|
|
138
|
+
* (`/auth/hgdc?next=<chemin>`) : la session de l'autre origine s'ouvre sans clic si celle
|
|
139
|
+
* d'Account vit. `path` est un chemin du produit, sans la base ; un autre texte mène à l'accueil.
|
|
140
|
+
* `null` pour un produit qui n'a pas deux vues.
|
|
141
|
+
*/
|
|
142
|
+
export declare function otherViewHref(word: string, view: ProductView, path?: string): string | null;
|
|
143
|
+
/** Libellé du lien vers l'autre vue. */
|
|
144
|
+
export declare function otherViewLabel(view: ProductView): string;
|
package/dist/products.js
CHANGED
|
@@ -6,7 +6,7 @@ import Briefcase01Icon from "@hugeicons/core-free-icons/Briefcase01Icon";
|
|
|
6
6
|
import Mail01Icon from "@hugeicons/core-free-icons/Mail01Icon";
|
|
7
7
|
import Rocket01Icon from "@hugeicons/core-free-icons/Rocket01Icon";
|
|
8
8
|
import UserCircleIcon from "@hugeicons/core-free-icons/UserCircleIcon";
|
|
9
|
-
import { APP_ORIGIN, APP_PRODUCTS } from "./addresses.js";
|
|
9
|
+
import { APP_ORIGIN, APP_PRODUCTS, isAppProduct, soloOrigin } from "./addresses.js";
|
|
10
10
|
import { HGDC_EMAIL_ACCENT } from "./email-palette.js";
|
|
11
11
|
import { isFamilyProduct } from "./family-svg.js";
|
|
12
12
|
export const PRODUCT_STAGE_LABEL = {
|
|
@@ -145,3 +145,22 @@ export function productRailEntries(options = {}) {
|
|
|
145
145
|
account: entry(HGDC_ACCOUNT, options.accountHref ?? HGDC_ACCOUNT.entryHref, false),
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Lien vers l'autre vue du même produit (ADR 0014) : depuis `app.hgdc.ch/<mot>`, « Ouvrir seul » vers
|
|
150
|
+
* `<mot>.hgdc.ch` ; depuis `<mot>.hgdc.ch`, « Ouvrir dans app.hgdc.ch ». Passe par l'entrée
|
|
151
|
+
* (`/auth/hgdc?next=<chemin>`) : la session de l'autre origine s'ouvre sans clic si celle
|
|
152
|
+
* d'Account vit. `path` est un chemin du produit, sans la base ; un autre texte mène à l'accueil.
|
|
153
|
+
* `null` pour un produit qui n'a pas deux vues.
|
|
154
|
+
*/
|
|
155
|
+
export function otherViewHref(word, view, path = "/") {
|
|
156
|
+
const key = word.toLowerCase();
|
|
157
|
+
if (!isAppProduct(key))
|
|
158
|
+
return null;
|
|
159
|
+
const next = path.startsWith("/") && !path.startsWith("//") && !path.includes("\\") ? path : "/";
|
|
160
|
+
const target = view === "app" ? soloOrigin(key) : `${APP_ORIGIN}/${key}`;
|
|
161
|
+
return `${target}${PRODUCT_ENTRY_PATH}?next=${encodeURIComponent(next)}`;
|
|
162
|
+
}
|
|
163
|
+
/** Libellé du lien vers l'autre vue. */
|
|
164
|
+
export function otherViewLabel(view) {
|
|
165
|
+
return view === "app" ? "Ouvrir seul" : "Ouvrir dans app.hgdc.ch";
|
|
166
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hgdc/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.0",
|
|
4
4
|
"description": "Interface de la H Collection : jetons de la DA, thèmes, polices et composants.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://github.com/hugogebel-boop/hgdc-kit/tree/main/packages/ui#readme",
|