@hgdc/ui 0.64.0 → 0.66.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/app-shell.d.ts +11 -1
- package/dist/app-shell.js +6 -4
- package/dist/module-bar-store.d.ts +4 -2
- package/dist/module-bar-store.js +6 -2
- package/dist/module-bar.d.ts +4 -1
- package/dist/module-bar.js +4 -2
- package/dist/product-bar.d.ts +8 -1
- package/dist/product-bar.js +62 -2
- package/dist/shell-project.d.ts +30 -0
- package/dist/shell-project.js +69 -0
- package/package.json +1 -1
package/dist/app-shell.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ModuleWord } from "./modules.ts";
|
|
1
2
|
import { type ReactNode } from "react";
|
|
2
3
|
import type { ProductView } from "./addresses.ts";
|
|
3
4
|
import type { FamilyProduct } from "./family-svg.ts";
|
|
@@ -20,7 +21,7 @@ export type { AppShellAssistant } from "./shell-assistant.tsx";
|
|
|
20
21
|
* Le kit ne connaît ni le routeur ni l'authentification : le produit passe ses éléments
|
|
21
22
|
* (`OrgSwitcher`, `SidebarNav`, `AccountMenu`) et ses liens.
|
|
22
23
|
*/
|
|
23
|
-
export declare function AppShell({ product, organization, productRail: productRailOption, view: viewOption, accountHref, nav, links, account, sidebarFooter, header, onSearch, searchLabel, assistant, children, contentKey, navigating, mainClassName, statusPage, }: {
|
|
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, project, }: {
|
|
24
25
|
/** Le produit affiché (`"host"`) : son nom au-dessus de ses onglets, « vous êtes ici » dans la liste des modules. */
|
|
25
26
|
product: FamilyProduct;
|
|
26
27
|
/**
|
|
@@ -101,4 +102,13 @@ export declare function AppShell({ product, organization, productRail: productRa
|
|
|
101
102
|
* de test), ou `false` pour ne rien lire. L'accès « État du service » y mène.
|
|
102
103
|
*/
|
|
103
104
|
statusPage?: string | false | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* Le projet choisi en haut de page (`useShellProject`) : la liste des modules se limite aux siens, et
|
|
107
|
+
* « Réglages du projet » paraît en bas de la barre. Absent : tous les projets.
|
|
108
|
+
*/
|
|
109
|
+
project?: {
|
|
110
|
+
name: string;
|
|
111
|
+
modules: ReadonlyArray<ModuleWord>;
|
|
112
|
+
settingsHref: string;
|
|
113
|
+
} | undefined;
|
|
104
114
|
}): React.JSX.Element;
|
package/dist/app-shell.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Settings02Icon from "@hugeicons/core-free-icons/Settings02Icon";
|
|
3
|
+
import { SIDEBAR_ITEM } from "./sidebar-nav.js";
|
|
2
4
|
import Menu01Icon from "@hugeicons/core-free-icons/Menu01Icon";
|
|
3
5
|
import { HugeiconsIcon } from "@hugeicons/react";
|
|
4
6
|
import { Suspense, useCallback, useContext, useEffect, useId, useRef, useState } from "react";
|
|
@@ -38,11 +40,11 @@ export { SidebarNavGroup } from "./sidebar-nav-group.js";
|
|
|
38
40
|
* Le kit ne connaît ni le routeur ni l'authentification : le produit passe ses éléments
|
|
39
41
|
* (`OrgSwitcher`, `SidebarNav`, `AccountMenu`) et ses liens.
|
|
40
42
|
*/
|
|
41
|
-
export function AppShell({ product, organization, productRail: productRailOption, view: viewOption, accountHref, nav, links, account, sidebarFooter, header, onSearch, searchLabel, assistant, children, contentKey, navigating = false, mainClassName, statusPage, }) {
|
|
43
|
+
export function AppShell({ product, organization, productRail: productRailOption, view: viewOption, accountHref, nav, links, account, sidebarFooter, header, onSearch, searchLabel, assistant, children, contentKey, navigating = false, mainClassName, statusPage, project, }) {
|
|
42
44
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
43
45
|
const preference = useProductBarPreference();
|
|
44
46
|
// Relu chez Account seulement par un produit passé aux deux vues (il donne `view`, ADR 0014).
|
|
45
|
-
useProductBarSync(productRailOption
|
|
47
|
+
useProductBarSync(productRailOption !== false);
|
|
46
48
|
const productRail = productRailOption ?? (viewOption === undefined || productBarShown(viewOption, preference));
|
|
47
49
|
const [navLevel, setNavLevel] = useState("tabs");
|
|
48
50
|
useToggleShortcut();
|
|
@@ -100,7 +102,7 @@ export function AppShell({ product, organization, productRail: productRailOption
|
|
|
100
102
|
useAssistantShortcut(assistant);
|
|
101
103
|
const otherView = viewOption === undefined ? null : _jsx(OtherViewLink, { product: product, view: viewOption });
|
|
102
104
|
// Dans la barre de Basalte, seul Réglages reste au-dessus du compte : le reste passe dans son menu.
|
|
103
|
-
const footer = (_jsxs(_Fragment, { children: [account !== undefined ? sidebarFooter : null, _jsx(ShellCommonLinks, { links: productRail ? { ...(links?.settings ? { settings: links.settings } : {}), status: false } : (links ?? {}), statusPageUrl: statusPageUrl, otherView: productRail ? null : otherView }), account ?? sidebarFooter] }));
|
|
105
|
+
const footer = (_jsxs(_Fragment, { children: [account !== undefined ? sidebarFooter : null, productRail && project ? (_jsx("div", { className: "px-3 pb-1", children: _jsxs("a", { href: project.settingsHref, className: SIDEBAR_ITEM, children: [_jsx(HugeiconsIcon, { icon: Settings02Icon, strokeWidth: 1.8, "aria-hidden": true, className: "size-5 shrink-0 lg:size-[1.125rem]" }), _jsx("span", { className: "truncate lg:rail:sr-only", children: "R\u00E9glages du projet" })] }) })) : null, _jsx(ShellCommonLinks, { links: productRail ? { ...(links?.settings ? { settings: links.settings } : {}), status: false } : (links ?? {}), statusPageUrl: statusPageUrl, otherView: productRail ? null : otherView }), account ?? sidebarFooter] }));
|
|
104
106
|
const menuLinks = productRail
|
|
105
107
|
? {
|
|
106
108
|
documentation: links?.documentation,
|
|
@@ -108,6 +110,6 @@ export function AppShell({ product, organization, productRail: productRailOption
|
|
|
108
110
|
otherView: viewOption === undefined ? undefined : otherViewMenuLink(product, viewOption),
|
|
109
111
|
}
|
|
110
112
|
: {};
|
|
111
|
-
const navigation = productRail ? (_jsx(ShellNavigation, { product: product, nav: nav, accountHref: accountHref, level: navLevel, onLevelChange: setNavLevel })) : (nav);
|
|
113
|
+
const navigation = productRail ? (_jsx(ShellNavigation, { product: product, nav: nav, accountHref: accountHref, level: navLevel, onLevelChange: setNavLevel, projectModules: project?.modules })) : (nav);
|
|
112
114
|
return (_jsx(ShellContext.Provider, { value: { closeMobileNav: () => setMenuOpen(false), product }, children: _jsx(ModuleBarShownContext, { value: productRail, children: _jsx(ShellMenuLinksContext, { value: menuLinks, children: _jsx(ShellLeaveContext, { value: leave, children: _jsx(StatusPageContext, { value: statusPageUrl, children: _jsxs("div", { "data-slot": "app-shell", "data-module-bar": productRail ? "" : undefined, "data-loading": loading ? "" : undefined, "data-hydrated": hydrated ? "" : undefined, "data-assistant": assistantOpen ? "" : undefined, className: "min-h-dvh lg:grid lg:h-dvh lg:grid-cols-[auto_minmax(0,1fr)_auto] lg:bg-shell", 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" }), _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: [productRail ? (_jsx(ShellOrganizationHead, { organization: organization })) : (_jsxs(_Fragment, { children: [_jsx(SidebarHead, { product: product }), organization ? _jsx("div", { className: "shrink-0 px-3 pb-1", children: organization }) : null] })), navigation, _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, navigation, _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 })] }) })] }) }) }) }) }) }));
|
|
113
115
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type ModuleWord } from "./modules.ts";
|
|
2
2
|
/**
|
|
3
|
-
* L'ordre et le choix des modules de la barre, propres à chaque personne
|
|
4
|
-
*
|
|
3
|
+
* L'ordre et le choix des modules de la barre, propres à chaque personne : Account les garde
|
|
4
|
+
* (`product-bar.ts`), et chaque origine en retient la dernière valeur, pour s'afficher sans attendre.
|
|
5
5
|
*/
|
|
6
6
|
export declare const MODULE_BAR_KEY = "hgdc-shell:modules";
|
|
7
7
|
export declare function saveModuleBar(order: ReadonlyArray<ModuleWord>): void;
|
|
8
|
+
/** L'ordre retenu dans cette origine, lu hors de React. */
|
|
9
|
+
export declare function storedModuleBar(): ReadonlyArray<ModuleWord>;
|
|
8
10
|
/** L'ordre retenu ; celui par défaut au rendu serveur. */
|
|
9
11
|
export declare function useModuleBar(): ReadonlyArray<ModuleWord>;
|
package/dist/module-bar-store.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useSyncExternalStore } from "react";
|
|
2
2
|
import { DEFAULT_MODULE_BAR, parseModuleBar } from "./modules.js";
|
|
3
3
|
/**
|
|
4
|
-
* L'ordre et le choix des modules de la barre, propres à chaque personne
|
|
5
|
-
*
|
|
4
|
+
* L'ordre et le choix des modules de la barre, propres à chaque personne : Account les garde
|
|
5
|
+
* (`product-bar.ts`), et chaque origine en retient la dernière valeur, pour s'afficher sans attendre.
|
|
6
6
|
*/
|
|
7
7
|
export const MODULE_BAR_KEY = "hgdc-shell:modules";
|
|
8
8
|
const EVENT = "hgdc-module-bar";
|
|
@@ -50,6 +50,10 @@ export function saveModuleBar(order) {
|
|
|
50
50
|
}
|
|
51
51
|
window.dispatchEvent(new Event(EVENT));
|
|
52
52
|
}
|
|
53
|
+
/** L'ordre retenu dans cette origine, lu hors de React. */
|
|
54
|
+
export function storedModuleBar() {
|
|
55
|
+
return read();
|
|
56
|
+
}
|
|
53
57
|
/** L'ordre retenu ; celui par défaut au rendu serveur. */
|
|
54
58
|
export function useModuleBar() {
|
|
55
59
|
return useSyncExternalStore(subscribe, read, () => DEFAULT_MODULE_BAR);
|
package/dist/module-bar.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import type { FamilyProduct } from "./family-svg.ts";
|
|
3
|
+
import { type ModuleWord } from "./modules.ts";
|
|
3
4
|
/** Ce que montre le milieu de la barre : les onglets du module ouvert, la liste des modules, ou son réglage. */
|
|
4
5
|
export type ShellNavLevel = "tabs" | "modules" | "edit";
|
|
5
6
|
/**
|
|
@@ -7,10 +8,12 @@ export type ShellNavLevel = "tabs" | "modules" | "edit";
|
|
|
7
8
|
* ses onglets (`nav`) sous « Retour aux modules » ; un clic y affiche la liste sans rien recharger ;
|
|
8
9
|
* un clic sur un autre module ouvre sa console, préchargée au survol sous la même origine.
|
|
9
10
|
*/
|
|
10
|
-
export declare function ShellNavigation({ product, nav, accountHref, level, onLevelChange, }: {
|
|
11
|
+
export declare function ShellNavigation({ product, nav, accountHref, level, onLevelChange, projectModules, }: {
|
|
11
12
|
product: FamilyProduct;
|
|
12
13
|
nav: ReactNode;
|
|
13
14
|
accountHref?: string | undefined;
|
|
14
15
|
level: ShellNavLevel;
|
|
15
16
|
onLevelChange: (level: ShellNavLevel) => void;
|
|
17
|
+
/** Modules activés sur le projet choisi : la liste s'y limite. Absent : tous les projets. */
|
|
18
|
+
projectModules?: ReadonlyArray<ModuleWord> | undefined;
|
|
16
19
|
}): React.JSX.Element;
|
package/dist/module-bar.js
CHANGED
|
@@ -11,6 +11,7 @@ import { useState, useSyncExternalStore } from "react";
|
|
|
11
11
|
import { Button } from "./button.js";
|
|
12
12
|
import { cn } from "./cn.js";
|
|
13
13
|
import { saveModuleBar, useModuleBar } from "./module-bar-store.js";
|
|
14
|
+
import { pushModuleBar } from "./product-bar.js";
|
|
14
15
|
import { hiddenModules, moduleByWord, moveModule, shownModules } from "./modules.js";
|
|
15
16
|
import { ProductBadge } from "./product-badge.js";
|
|
16
17
|
import { PRODUCT_PRERENDER_ATTRIBUTE } from "./product-switch.js";
|
|
@@ -80,12 +81,13 @@ function ModuleBarEditor({ order, onSave, onCancel, }) {
|
|
|
80
81
|
* ses onglets (`nav`) sous « Retour aux modules » ; un clic y affiche la liste sans rien recharger ;
|
|
81
82
|
* un clic sur un autre module ouvre sa console, préchargée au survol sous la même origine.
|
|
82
83
|
*/
|
|
83
|
-
export function ShellNavigation({ product, nav, accountHref, level, onLevelChange, }) {
|
|
84
|
+
export function ShellNavigation({ product, nav, accountHref, level, onLevelChange, projectModules, }) {
|
|
84
85
|
const pageOrigin = usePageOrigin();
|
|
85
86
|
const order = useModuleBar();
|
|
86
87
|
const { products, account } = shellProductEntries({ current: product, accountHref, pageOrigin });
|
|
87
|
-
return (_jsxs("div", { "data-slot": "shell-navigation", className: "flex min-h-0 flex-1 flex-col", children: [_jsxs("div", { "data-slot": "shell-manager", className: "shrink-0 px-3 pt-1", children: [_jsx("ul", { children: _jsx(ShellEntryLink, { entry: account, icon: UserGroupIcon }) }), _jsx("div", { "aria-hidden": true, className: "mx-2 mt-2 h-px bg-foreground/8" })] }), level === "modules" ? (_jsx(ModuleList, { entries: products, order: shownModules(order, product), onOpenCurrent: () => onLevelChange("tabs"), onCustomize: () => onLevelChange("edit") })) : level === "edit" ? (_jsx(ModuleBarEditor, { order: order, onSave: (next) => {
|
|
88
|
+
return (_jsxs("div", { "data-slot": "shell-navigation", className: "flex min-h-0 flex-1 flex-col", children: [_jsxs("div", { "data-slot": "shell-manager", className: "shrink-0 px-3 pt-1", children: [_jsx("ul", { children: _jsx(ShellEntryLink, { entry: account, icon: UserGroupIcon }) }), _jsx("div", { "aria-hidden": true, className: "mx-2 mt-2 h-px bg-foreground/8" })] }), level === "modules" ? (_jsx(ModuleList, { entries: products, order: shownModules(order, product).filter((word) => !projectModules || word === product || projectModules.includes(word)), onOpenCurrent: () => onLevelChange("tabs"), onCustomize: () => onLevelChange("edit") })) : level === "edit" ? (_jsx(ModuleBarEditor, { order: order, onSave: (next) => {
|
|
88
89
|
saveModuleBar(next);
|
|
90
|
+
void pushModuleBar(next);
|
|
89
91
|
onLevelChange("modules");
|
|
90
92
|
}, onCancel: () => onLevelChange("modules") })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "shrink-0 px-3 pt-2", children: [_jsx("ul", { children: _jsx(SidebarNavBack, { label: "Retour aux modules", keepMenuOpen: true, render: _jsx("button", { type: "button", className: "w-full", onClick: () => onLevelChange("modules") }) }) }), _jsxs("div", { "data-slot": "shell-module-title", className: "flex min-h-9 items-center gap-2.5 px-3 text-sm font-semibold lg:text-shell-foreground lg:rail:hidden", children: [_jsx(ProductBadge, { product: product, className: "size-[1.125rem] shrink-0" }), _jsx("span", { className: "truncate font-heading", children: familyProductName(product) })] })] }), nav] }))] }));
|
|
91
93
|
}
|
package/dist/product-bar.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ProductView } from "./addresses.ts";
|
|
2
|
+
import { type ModuleWord } from "./modules.ts";
|
|
2
3
|
/**
|
|
3
4
|
* Réglage « Barre des produits » (ADR 0014 de la famille), choisi dans Account :
|
|
4
5
|
* - `auto` : la barre sous `app.hgdc.ch`, pas sur `<mot>.hgdc.ch` ;
|
|
@@ -13,7 +14,7 @@ export type ProductBarPreference = "auto" | "always" | "never";
|
|
|
13
14
|
export declare const PRODUCT_BAR_PREFERENCES: ReadonlyArray<ProductBarPreference>;
|
|
14
15
|
/** Libellés du réglage, dans l'ordre d'affichage. */
|
|
15
16
|
export declare const PRODUCT_BAR_LABELS: Readonly<Record<ProductBarPreference, string>>;
|
|
16
|
-
/**
|
|
17
|
+
/** Réglages chez Account : `{ productBar, moduleBar }`, rien d'autre. */
|
|
17
18
|
export declare const ACCOUNT_PREFERENCES_URL = "https://account.hgdc.ch/api/preferences";
|
|
18
19
|
/** Valeur reçue (Account, stockage) : un réglage connu, sinon `auto`. */
|
|
19
20
|
export declare function parseProductBar(value: unknown): ProductBarPreference;
|
|
@@ -25,5 +26,11 @@ export declare function rememberProductBar(preference: ProductBarPreference): vo
|
|
|
25
26
|
export declare function useProductBarPreference(): ProductBarPreference;
|
|
26
27
|
/** Relit le réglage chez Account une fois par chargement de page ; une erreur garde la dernière valeur. */
|
|
27
28
|
export declare function syncProductBar(fetchImpl?: typeof fetch): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Enregistre la barre des modules chez Account, pour la retrouver sur un autre appareil. Un échec n'est
|
|
31
|
+
* pas signalé : la barre reste réglée dans cette origine, et repart au prochain chargement.
|
|
32
|
+
* `keepalive` : un clic sur un autre module recharge la page, l'envoi doit y survivre.
|
|
33
|
+
*/
|
|
34
|
+
export declare function pushModuleBar(order: ReadonlyArray<ModuleWord>, fetchImpl?: typeof fetch): Promise<void>;
|
|
28
35
|
/** À monter une fois (la coque le fait) : relecture du réglage après l'hydratation. */
|
|
29
36
|
export declare function useProductBarSync(enabled?: boolean): void;
|
package/dist/product-bar.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { useEffect, useSyncExternalStore } from "react";
|
|
2
|
+
import { saveModuleBar, storedModuleBar } from "./module-bar-store.js";
|
|
3
|
+
import { parseModuleBar } from "./modules.js";
|
|
2
4
|
export const PRODUCT_BAR_PREFERENCES = ["auto", "always", "never"];
|
|
3
5
|
/** Libellés du réglage, dans l'ordre d'affichage. */
|
|
4
6
|
export const PRODUCT_BAR_LABELS = {
|
|
@@ -6,10 +8,12 @@ export const PRODUCT_BAR_LABELS = {
|
|
|
6
8
|
always: "Toujours",
|
|
7
9
|
never: "Jamais",
|
|
8
10
|
};
|
|
9
|
-
/**
|
|
11
|
+
/** Réglages chez Account : `{ productBar, moduleBar }`, rien d'autre. */
|
|
10
12
|
export const ACCOUNT_PREFERENCES_URL = "https://account.hgdc.ch/api/preferences";
|
|
11
13
|
const STORAGE_KEY = "hgdc-product-bar";
|
|
12
14
|
const EVENT = "hgdc-product-bar";
|
|
15
|
+
/** Présent tant qu'Account n'a pas confirmé la barre des modules réglée ici : elle ne doit pas être écrasée. */
|
|
16
|
+
const MODULE_BAR_UNSENT_KEY = "hgdc-shell:modules-unsent";
|
|
13
17
|
/** Valeur reçue (Account, stockage) : un réglage connu, sinon `auto`. */
|
|
14
18
|
export function parseProductBar(value) {
|
|
15
19
|
return PRODUCT_BAR_PREFERENCES.includes(value) ? value : "auto";
|
|
@@ -62,13 +66,69 @@ export async function syncProductBar(fetchImpl = fetch) {
|
|
|
62
66
|
if (!response.ok)
|
|
63
67
|
return;
|
|
64
68
|
const body = await response.json();
|
|
65
|
-
if (typeof body
|
|
69
|
+
if (typeof body !== "object" || body === null)
|
|
70
|
+
return;
|
|
71
|
+
if ("productBar" in body)
|
|
66
72
|
rememberProductBar(parseProductBar(body.productBar));
|
|
73
|
+
if (moduleBarUnsent()) {
|
|
74
|
+
await pushModuleBar(storedModuleBar(), fetchImpl);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Absente : la personne n'a encore rien réglé, cette origine garde ce qu'elle a.
|
|
78
|
+
if ("moduleBar" in body) {
|
|
79
|
+
const order = parseModuleBar(body.moduleBar);
|
|
80
|
+
if (order)
|
|
81
|
+
saveModuleBar(order);
|
|
82
|
+
}
|
|
67
83
|
}
|
|
68
84
|
catch {
|
|
69
85
|
// Account injoignable ou hors ligne : rien ne change.
|
|
70
86
|
}
|
|
71
87
|
}
|
|
88
|
+
function moduleBarUnsent() {
|
|
89
|
+
try {
|
|
90
|
+
return window.localStorage.getItem(MODULE_BAR_UNSENT_KEY) !== null;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function markModuleBarUnsent(unsent) {
|
|
97
|
+
try {
|
|
98
|
+
if (unsent)
|
|
99
|
+
window.localStorage.setItem(MODULE_BAR_UNSENT_KEY, "1");
|
|
100
|
+
else
|
|
101
|
+
window.localStorage.removeItem(MODULE_BAR_UNSENT_KEY);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Stockage bloqué : la prochaine relecture reprendra la valeur d'Account.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Enregistre la barre des modules chez Account, pour la retrouver sur un autre appareil. Un échec n'est
|
|
109
|
+
* pas signalé : la barre reste réglée dans cette origine, et repart au prochain chargement.
|
|
110
|
+
* `keepalive` : un clic sur un autre module recharge la page, l'envoi doit y survivre.
|
|
111
|
+
*/
|
|
112
|
+
export async function pushModuleBar(order, fetchImpl = fetch) {
|
|
113
|
+
// Account n'accepte que les consoles de `hgdc.ch` : en local, en test, rien à envoyer.
|
|
114
|
+
if (!window.location.hostname.endsWith(".hgdc.ch"))
|
|
115
|
+
return;
|
|
116
|
+
markModuleBarUnsent(true);
|
|
117
|
+
try {
|
|
118
|
+
const response = await fetchImpl(ACCOUNT_PREFERENCES_URL, {
|
|
119
|
+
method: "PUT",
|
|
120
|
+
credentials: "include",
|
|
121
|
+
keepalive: true,
|
|
122
|
+
headers: { "content-type": "application/json" },
|
|
123
|
+
body: JSON.stringify({ moduleBar: order }),
|
|
124
|
+
});
|
|
125
|
+
if (response.ok)
|
|
126
|
+
markModuleBarUnsent(false);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Account injoignable ou hors ligne.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
72
132
|
let synced = false;
|
|
73
133
|
/** À monter une fois (la coque le fait) : relecture du réglage après l'hydratation. */
|
|
74
134
|
export function useProductBarSync(enabled = true) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ModuleWord } from "./modules.ts";
|
|
2
|
+
export declare function saveShellProject(organizationId: string, projectId: string | null): void;
|
|
3
|
+
/** L'identifiant du projet choisi ; `null` : tous les projets, et toujours au rendu serveur. */
|
|
4
|
+
export declare function useShellProject(organizationId: string | null | undefined): string | null;
|
|
5
|
+
/** Un projet commun, tel que le rend `listCoreProjects` de `@hgdc/auth/core`. */
|
|
6
|
+
export type ShellCoreProject = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
modules: ReadonlyArray<string>;
|
|
10
|
+
};
|
|
11
|
+
/** Le projet choisi, s'il existe encore et si le module y est activé. Sinon : tous les projets. */
|
|
12
|
+
export declare function activeCoreProject<Project extends ShellCoreProject>(projects: ReadonlyArray<Project> | undefined, activeId: string | null): Project | null;
|
|
13
|
+
/** Ce que `project` d'`AppShell` attend pour le projet choisi. */
|
|
14
|
+
export declare function shellProjectFrom(project: ShellCoreProject | null, settingsHref: string): {
|
|
15
|
+
name: string;
|
|
16
|
+
modules: ReadonlyArray<ModuleWord>;
|
|
17
|
+
settingsHref: string;
|
|
18
|
+
} | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Le sélecteur de projet de Basalte, dans `header` d'`AppShell`. Le choix vaut pour toutes les consoles.
|
|
21
|
+
* `onChange` : ce que le module fait en plus, par exemple ouvrir la page du projet. Rien à montrer
|
|
22
|
+
* tant que l'organisation n'a aucun projet.
|
|
23
|
+
*/
|
|
24
|
+
export declare function CoreProjectSwitcher({ organizationId, projects, createHref, onChange, }: {
|
|
25
|
+
organizationId: string;
|
|
26
|
+
projects: ReadonlyArray<ShellCoreProject> | undefined;
|
|
27
|
+
/** « Nouveau projet » : la page Projets de Manager. */
|
|
28
|
+
createHref?: string;
|
|
29
|
+
onChange?: (projectId: string | null) => void;
|
|
30
|
+
}): React.JSX.Element | null;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useSyncExternalStore } from "react";
|
|
3
|
+
import { ProjectSwitcher } from "./project-switcher.js";
|
|
4
|
+
/**
|
|
5
|
+
* Le projet choisi en haut de page, le même dans toutes les consoles de Basalte : sous `app.hgdc.ch`,
|
|
6
|
+
* elles partagent l'origine, donc ce réglage. Une valeur par organisation : changer d'organisation
|
|
7
|
+
* ne garde pas le projet d'une autre. Propre à chaque personne et à chaque navigateur.
|
|
8
|
+
*/
|
|
9
|
+
const keyOf = (organizationId) => `hgdc-shell:project:${organizationId}`;
|
|
10
|
+
const EVENT = "hgdc-shell-project";
|
|
11
|
+
function subscribe(onChange) {
|
|
12
|
+
window.addEventListener(EVENT, onChange);
|
|
13
|
+
window.addEventListener("storage", onChange);
|
|
14
|
+
return () => {
|
|
15
|
+
window.removeEventListener(EVENT, onChange);
|
|
16
|
+
window.removeEventListener("storage", onChange);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function saveShellProject(organizationId, projectId) {
|
|
20
|
+
try {
|
|
21
|
+
if (projectId === null)
|
|
22
|
+
window.localStorage.removeItem(keyOf(organizationId));
|
|
23
|
+
else
|
|
24
|
+
window.localStorage.setItem(keyOf(organizationId), projectId);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Stockage bloqué : le choix vaut seulement pour cette page.
|
|
28
|
+
}
|
|
29
|
+
window.dispatchEvent(new Event(EVENT));
|
|
30
|
+
}
|
|
31
|
+
/** L'identifiant du projet choisi ; `null` : tous les projets, et toujours au rendu serveur. */
|
|
32
|
+
export function useShellProject(organizationId) {
|
|
33
|
+
return useSyncExternalStore(subscribe, () => {
|
|
34
|
+
if (!organizationId)
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
return window.localStorage.getItem(keyOf(organizationId));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}, () => null);
|
|
43
|
+
}
|
|
44
|
+
/** Le projet choisi, s'il existe encore et si le module y est activé. Sinon : tous les projets. */
|
|
45
|
+
export function activeCoreProject(projects, activeId) {
|
|
46
|
+
return projects?.find((project) => project.id === activeId) ?? null;
|
|
47
|
+
}
|
|
48
|
+
/** Ce que `project` d'`AppShell` attend pour le projet choisi. */
|
|
49
|
+
export function shellProjectFrom(project, settingsHref) {
|
|
50
|
+
if (!project)
|
|
51
|
+
return undefined;
|
|
52
|
+
return { name: project.name, modules: project.modules, settingsHref };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Le sélecteur de projet de Basalte, dans `header` d'`AppShell`. Le choix vaut pour toutes les consoles.
|
|
56
|
+
* `onChange` : ce que le module fait en plus, par exemple ouvrir la page du projet. Rien à montrer
|
|
57
|
+
* tant que l'organisation n'a aucun projet.
|
|
58
|
+
*/
|
|
59
|
+
export function CoreProjectSwitcher({ organizationId, projects, createHref, onChange, }) {
|
|
60
|
+
const activeId = useShellProject(organizationId);
|
|
61
|
+
if (projects !== undefined && projects.length === 0)
|
|
62
|
+
return null;
|
|
63
|
+
const known = activeCoreProject(projects, activeId);
|
|
64
|
+
const entries = projects?.map(({ id, name }) => ({ id, name }));
|
|
65
|
+
return (_jsx(ProjectSwitcher, { projects: entries, activeId: known?.id ?? null, ...(createHref ? { createHref } : {}), onSelect: (id) => {
|
|
66
|
+
saveShellProject(organizationId, id);
|
|
67
|
+
onChange?.(id);
|
|
68
|
+
} }));
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hgdc/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.66.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",
|