@hgdc/ui 0.66.1 → 0.67.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.
|
@@ -38,6 +38,7 @@ export function DestructiveDialog({ open, onOpenChange, title, question, consequ
|
|
|
38
38
|
const view = shown.current;
|
|
39
39
|
const [typed, setTyped] = useState("");
|
|
40
40
|
const [pending, setPending] = useState(false);
|
|
41
|
+
const confirmButton = useRef(null);
|
|
41
42
|
const ready = !pending && (view.name === undefined || matchesConfirmation(typed, view.name));
|
|
42
43
|
const [wasOpen, setWasOpen] = useState(open);
|
|
43
44
|
if (open !== wasOpen) {
|
|
@@ -55,6 +56,11 @@ export function DestructiveDialog({ open, onOpenChange, title, question, consequ
|
|
|
55
56
|
}
|
|
56
57
|
catch (error) {
|
|
57
58
|
toast.error(formatError(error));
|
|
59
|
+
// Le bouton se désactive pendant l'envoi et le focus part sur `body`, hors de la boîte restée ouverte.
|
|
60
|
+
requestAnimationFrame(() => {
|
|
61
|
+
if (document.activeElement === document.body)
|
|
62
|
+
confirmButton.current?.focus();
|
|
63
|
+
});
|
|
58
64
|
}
|
|
59
65
|
finally {
|
|
60
66
|
setPending(false);
|
|
@@ -69,5 +75,5 @@ export function DestructiveDialog({ open, onOpenChange, title, question, consequ
|
|
|
69
75
|
}, children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: view.title }), _jsxs(DialogDescription, { render: _jsx("div", {}), className: "grid gap-1 text-[15px]", children: [_jsx("p", { children: view.question }), view.consequence ? _jsx("p", { children: view.consequence }) : null, _jsx("p", { className: "font-semibold text-destructive", children: "Impossible de revenir en arri\u00E8re." })] })] }), view.name !== undefined ? (_jsxs("form", { className: "grid gap-3", onSubmit: (event) => {
|
|
70
76
|
event.preventDefault();
|
|
71
77
|
void confirm();
|
|
72
|
-
}, children: [_jsxs("p", { className: "text-[15px] leading-7 text-muted-foreground", children: ["Tape ", _jsx(NameChip, { name: view.name }), " pour confirmer."] }), _jsx(Input, { "aria-label": `Tape ${view.name} pour confirmer`, autoFocus: true, autoComplete: "off", autoCapitalize: "off", spellCheck: false, value: typed, onChange: (event) => setTyped(event.target.value), placeholder: view.name, "aria-invalid": typed.length > 0 && !view.name.startsWith(typed) ? true : undefined })] })) : null, _jsxs(DialogFooter, { className: "sm:justify-start", children: [_jsxs(Button, { variant: "destructive", isLoading: pending, disabled: !ready, onClick: () => void confirm(), children: [view.confirmLabel ?? view.title, _jsx(Kbd, { "aria-hidden": true, className: "ml-1 hidden bg-current/10 text-current lg:inline-flex", children: mod }), _jsx(Kbd, { "aria-hidden": true, className: "hidden bg-current/10 text-current lg:inline-flex", children: "\u21B5" })] }), _jsxs(Button, { variant: "outline", disabled: pending, onClick: () => onOpenChange(false), children: ["Annuler", _jsx(Kbd, { "aria-hidden": true, className: "ml-1 hidden lg:inline-flex", children: "\u00C9chap" })] })] })] }) }));
|
|
78
|
+
}, children: [_jsxs("p", { className: "text-[15px] leading-7 text-muted-foreground", children: ["Tape ", _jsx(NameChip, { name: view.name }), " pour confirmer."] }), _jsx(Input, { "aria-label": `Tape ${view.name} pour confirmer`, autoFocus: true, autoComplete: "off", autoCapitalize: "off", spellCheck: false, value: typed, onChange: (event) => setTyped(event.target.value), placeholder: view.name, "aria-invalid": typed.length > 0 && !view.name.startsWith(typed) ? true : undefined })] })) : null, _jsxs(DialogFooter, { className: "sm:justify-start", children: [_jsxs(Button, { ref: confirmButton, variant: "destructive", isLoading: pending, disabled: !ready, onClick: () => void confirm(), children: [view.confirmLabel ?? view.title, _jsx(Kbd, { "aria-hidden": true, className: "ml-1 hidden bg-current/10 text-current lg:inline-flex", children: mod }), _jsx(Kbd, { "aria-hidden": true, className: "hidden bg-current/10 text-current lg:inline-flex", children: "\u21B5" })] }), _jsxs(Button, { variant: "outline", disabled: pending, onClick: () => onOpenChange(false), children: ["Annuler", _jsx(Kbd, { "aria-hidden": true, className: "ml-1 hidden lg:inline-flex", children: "\u00C9chap" })] })] })] }) }));
|
|
73
79
|
}
|
package/dist/project-switcher.js
CHANGED
|
@@ -22,6 +22,8 @@ export function ProjectMark({ project, className, }) {
|
|
|
22
22
|
const mark = (project.mark ?? project.name.trim().charAt(0)) || "?";
|
|
23
23
|
return (_jsx("span", { "aria-hidden": true, "data-slot": "project-mark", className: cn(base, "font-mono text-[10px] font-semibold text-foreground uppercase", className), children: mark }));
|
|
24
24
|
}
|
|
25
|
+
const startsWithVowel = (word) => /^[aeiouyàâéèêh]/i.test(word);
|
|
26
|
+
const changeLabel = (noun) => `changer ${startsWithVowel(noun) ? "d'" : "de "}${noun}`;
|
|
25
27
|
/** Rend une destination du menu : un lien (`href`, par `renderLink`) ou une action. */
|
|
26
28
|
function SwitcherEntry({ href, onSelect, renderLink, children, }) {
|
|
27
29
|
if (href !== undefined)
|
|
@@ -39,12 +41,14 @@ function SwitcherEntry({ href, onSelect, renderLink, children, }) {
|
|
|
39
41
|
* pendant le chargement : le déclencheur montre déjà `activeName` (le segment de l'URL), le menu
|
|
40
42
|
* reste utilisable pour « Tous les projets » et « Nouveau projet ».
|
|
41
43
|
*/
|
|
42
|
-
export function ProjectSwitcher({ projects, activeId, activeName, onSelect, allHref, createHref, onCreate, renderLink, noun = { one: "projet", many: "projets" }, createLabel = `${noun.feminine ? "Nouvelle" : "Nouveau"} ${noun.one}`, className, }) {
|
|
44
|
+
export function ProjectSwitcher({ projects, activeId, activeName, onSelect, allHref, createHref, onCreate, renderLink, noun = { one: "projet", many: "projets" }, createLabel = `${noun.feminine ? "Nouvelle" : startsWithVowel(noun.one) ? "Nouvel" : "Nouveau"} ${noun.one}`, className, }) {
|
|
43
45
|
const capitalized = (word) => word.charAt(0).toUpperCase() + word.slice(1);
|
|
44
46
|
const all = `${noun.feminine ? "Toutes" : "Tous"} les ${noun.many}`;
|
|
45
47
|
const hasActive = activeId !== undefined && activeId !== null;
|
|
46
48
|
const current = hasActive ? projects?.find((p) => p.id === activeId) : undefined;
|
|
47
49
|
const label = hasActive ? (current?.name ?? activeName ?? "") : all;
|
|
48
50
|
const canCreate = createHref !== undefined || onCreate !== undefined;
|
|
49
|
-
return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: _jsx("button", { type: "button", "aria-label": hasActive
|
|
51
|
+
return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: _jsx("button", { type: "button", "aria-label": hasActive
|
|
52
|
+
? `${capitalized(noun.one)} ${label}, ${changeLabel(noun.one)}`
|
|
53
|
+
: `${all}, ${changeLabel(noun.one)}`, className: cn("flex h-10 max-w-full min-w-0 items-center gap-2 rounded-xl px-2.5 text-sm font-medium outline-none transition-colors duration-(--duration-fast) ease-out hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50 aria-expanded:bg-muted coarse:h-11", className) }), children: [hasActive ? (current ? (_jsx(ProjectMark, { project: current })) : (_jsx("span", { "aria-hidden": true, className: "size-6 shrink-0 rounded-md bg-muted skeleton-pulse" }))) : (_jsx(HugeiconsIcon, { icon: DashboardSquare01Icon, "aria-hidden": true, className: "size-4.5 shrink-0 text-muted-foreground" })), _jsx("span", { className: "min-w-0 truncate", children: label }), _jsx(HugeiconsIcon, { icon: UnfoldMoreIcon, "aria-hidden": true, className: "size-4 shrink-0 text-muted-foreground" })] }), _jsxs(DropdownMenuContent, { align: "start", className: "w-72 max-w-[calc(100vw-2rem)]", children: [_jsxs(SwitcherEntry, { href: allHref, onSelect: () => onSelect?.(null), renderLink: renderLink, children: [_jsx(HugeiconsIcon, { icon: DashboardSquare01Icon, "aria-hidden": true, className: "size-4" }), _jsx("span", { className: "flex-1", children: all }), hasActive ? null : _jsx(HugeiconsIcon, { icon: Tick02Icon, "aria-label": "Vue active", className: "size-4" })] }), projects?.length ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: capitalized(noun.many) }), projects.map((project) => (_jsxs(SwitcherEntry, { href: project.href, onSelect: () => onSelect?.(project.id), renderLink: renderLink, children: [_jsx(ProjectMark, { project: project }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: project.name }), project.id === activeId ? (_jsx(HugeiconsIcon, { icon: Tick02Icon, "aria-label": `${capitalized(noun.one)} ${noun.feminine ? "active" : "actif"}`, className: "size-4" })) : null] }, project.id)))] })] })) : null, canCreate ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(SwitcherEntry, { href: createHref, onSelect: onCreate, renderLink: renderLink, children: [_jsx(HugeiconsIcon, { icon: PlusSignIcon, "aria-hidden": true, className: "size-4" }), createLabel] })] })) : null] })] }));
|
|
50
54
|
}
|
package/dist/settings-section.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useId } from "react";
|
|
2
|
+
import { useId, useRef } from "react";
|
|
3
3
|
import { Button } from "./button.js";
|
|
4
4
|
import { cn } from "./cn.js";
|
|
5
5
|
/**
|
|
@@ -26,8 +26,9 @@ import { cn } from "./cn.js";
|
|
|
26
26
|
export function SettingsSection({ title, description, children, footer, footerNote, tone = "default", onSave, dirty = true, saving = false, saveLabel = "Enregistrer", className, }) {
|
|
27
27
|
const titleId = useId();
|
|
28
28
|
const descriptionId = useId();
|
|
29
|
+
const heading = useRef(null);
|
|
29
30
|
const hasFooter = Boolean(footer || footerNote || onSave);
|
|
30
|
-
const body = (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid gap-4 p-5 sm:p-6", children: [_jsxs("div", { children: [_jsx("h2", { id: titleId, className: "text-base font-semibold text-balance", children: title }), description ? (_jsx("p", { id: descriptionId, className: "mt-1 max-w-prose text-sm text-pretty text-muted-foreground", children: description })) : null] }), children] }), hasFooter ? (_jsxs("div", { "data-slot": "settings-section-footer", className: "flex flex-wrap items-center justify-end gap-3 border-t border-border bg-muted/40 px-5 py-3 sm:px-6", children: [footerNote ? _jsx("p", { className: "mr-auto text-sm text-muted-foreground", children: footerNote }) : null, footer, onSave ? (_jsx(Button, { type: "submit", size: "sm", disabled: !dirty, isLoading: saving, children: saveLabel })) : null] })) : null] }));
|
|
31
|
+
const body = (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid gap-4 p-5 sm:p-6", children: [_jsxs("div", { children: [_jsx("h2", { ref: heading, id: titleId, tabIndex: -1, className: "text-base font-semibold text-balance outline-none", children: title }), description ? (_jsx("p", { id: descriptionId, className: "mt-1 max-w-prose text-sm text-pretty text-muted-foreground", children: description })) : null] }), children] }), hasFooter ? (_jsxs("div", { "data-slot": "settings-section-footer", className: "flex flex-wrap items-center justify-end gap-3 border-t border-border bg-muted/40 px-5 py-3 sm:px-6", children: [footerNote ? _jsx("p", { className: "mr-auto text-sm text-muted-foreground", children: footerNote }) : null, footer, onSave ? (_jsx(Button, { type: "submit", size: "sm", disabled: !dirty, isLoading: saving, children: saveLabel })) : null] })) : null] }));
|
|
31
32
|
const shared = {
|
|
32
33
|
"data-slot": "settings-section",
|
|
33
34
|
"data-tone": tone,
|
|
@@ -41,7 +42,14 @@ export function SettingsSection({ title, description, children, footer, footerNo
|
|
|
41
42
|
event.preventDefault();
|
|
42
43
|
if (!dirty || saving)
|
|
43
44
|
return;
|
|
44
|
-
|
|
45
|
+
// Le bouton se désactive pendant l'envoi et le focus part sur `body` : il revient au titre du bloc.
|
|
46
|
+
// Un focus resté dans un champ (Entrée) n'y est pas déplacé.
|
|
47
|
+
void Promise.resolve(onSave()).finally(() => {
|
|
48
|
+
requestAnimationFrame(() => {
|
|
49
|
+
if (document.activeElement === document.body)
|
|
50
|
+
heading.current?.focus();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
45
53
|
};
|
|
46
54
|
// Un `<form>` nommé par son titre a le rôle « form » : il apparaît dans les repères du lecteur d'écran.
|
|
47
55
|
return (_jsx("form", { ...shared, onSubmit: submit, children: body }));
|
package/dist/shell-project.d.ts
CHANGED
|
@@ -16,6 +16,30 @@ export declare function shellProjectFrom(project: ShellCoreProject | null, setti
|
|
|
16
16
|
modules: ReadonlyArray<ModuleWord>;
|
|
17
17
|
settingsHref: string;
|
|
18
18
|
} | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Un module montre un seul sélecteur en haut de page : celui de Basalte dès que l'organisation a des
|
|
21
|
+
* projets, ou le sien sinon (démo, espace pas encore relié). Pendant le chargement, celui de Basalte.
|
|
22
|
+
*/
|
|
23
|
+
export declare function coreSwitcherShown(organizationId: string | null | undefined, projects: ReadonlyArray<ShellCoreProject> | undefined): boolean;
|
|
24
|
+
/** Ce qu'un module a pour un projet : un site, une doc, un assistant, une page de statut. */
|
|
25
|
+
export type ProjectResource = {
|
|
26
|
+
coreProjectId?: string | null | undefined;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Où mène un projet choisi dans le sélecteur : sa ressource s'il n'y en a qu'une, sinon la liste du
|
|
30
|
+
* module, déjà filtrée. `null` : on ne bouge pas (liste en chargement, ou déjà sur la liste).
|
|
31
|
+
*/
|
|
32
|
+
export declare function projectDestination<Resource extends ProjectResource>(resources: ReadonlyArray<Resource> | undefined, projectId: string | null, resourceOpen: boolean): {
|
|
33
|
+
to: "resource";
|
|
34
|
+
resource: Resource;
|
|
35
|
+
} | {
|
|
36
|
+
to: "list";
|
|
37
|
+
} | null;
|
|
38
|
+
/**
|
|
39
|
+
* Une ressource ouverte par son adresse (lien, palette, liste) : le sélecteur montre son projet. Écrit
|
|
40
|
+
* à l'ouverture seulement : revenir sur « Tous les projets » n'est pas annulé avant de quitter la page.
|
|
41
|
+
*/
|
|
42
|
+
export declare function useShellProjectOfResource(organizationId: string | null | undefined, coreProjectId: string | null | undefined): void;
|
|
19
43
|
/**
|
|
20
44
|
* Le sélecteur de projet de Basalte, dans `header` d'`AppShell`. Le choix vaut pour toutes les consoles.
|
|
21
45
|
* `onChange` : ce que le module fait en plus, par exemple ouvrir la page du projet. Rien à montrer
|
package/dist/shell-project.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useSyncExternalStore } from "react";
|
|
2
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
3
3
|
import { ProjectSwitcher } from "./project-switcher.js";
|
|
4
4
|
/**
|
|
5
5
|
* Le projet choisi en haut de page, le même dans toutes les consoles de Basalte : sous `app.hgdc.ch`,
|
|
@@ -51,6 +51,37 @@ export function shellProjectFrom(project, settingsHref) {
|
|
|
51
51
|
return undefined;
|
|
52
52
|
return { name: project.name, modules: project.modules, settingsHref };
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Un module montre un seul sélecteur en haut de page : celui de Basalte dès que l'organisation a des
|
|
56
|
+
* projets, ou le sien sinon (démo, espace pas encore relié). Pendant le chargement, celui de Basalte.
|
|
57
|
+
*/
|
|
58
|
+
export function coreSwitcherShown(organizationId, projects) {
|
|
59
|
+
return Boolean(organizationId) && projects?.length !== 0;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Où mène un projet choisi dans le sélecteur : sa ressource s'il n'y en a qu'une, sinon la liste du
|
|
63
|
+
* module, déjà filtrée. `null` : on ne bouge pas (liste en chargement, ou déjà sur la liste).
|
|
64
|
+
*/
|
|
65
|
+
export function projectDestination(resources, projectId, resourceOpen) {
|
|
66
|
+
if (resources === undefined)
|
|
67
|
+
return null;
|
|
68
|
+
if (projectId !== null) {
|
|
69
|
+
const matching = resources.filter((resource) => resource.coreProjectId === projectId);
|
|
70
|
+
if (matching.length === 1 && matching[0])
|
|
71
|
+
return { to: "resource", resource: matching[0] };
|
|
72
|
+
}
|
|
73
|
+
return resourceOpen ? { to: "list" } : null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Une ressource ouverte par son adresse (lien, palette, liste) : le sélecteur montre son projet. Écrit
|
|
77
|
+
* à l'ouverture seulement : revenir sur « Tous les projets » n'est pas annulé avant de quitter la page.
|
|
78
|
+
*/
|
|
79
|
+
export function useShellProjectOfResource(organizationId, coreProjectId) {
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (organizationId && coreProjectId)
|
|
82
|
+
saveShellProject(organizationId, coreProjectId);
|
|
83
|
+
}, [organizationId, coreProjectId]);
|
|
84
|
+
}
|
|
54
85
|
/**
|
|
55
86
|
* Le sélecteur de projet de Basalte, dans `header` d'`AppShell`. Le choix vaut pour toutes les consoles.
|
|
56
87
|
* `onChange` : ce que le module fait en plus, par exemple ouvrir la page du projet. Rien à montrer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hgdc/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.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",
|