@hgdc/ui 0.31.0 → 0.33.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/README.md +16 -14
- package/dist/app-shell.d.ts +48 -1
- package/dist/app-shell.js +125 -1
- package/dist/filter-bar.d.ts +15 -1
- package/dist/filter-bar.js +55 -6
- package/dist/project-switcher.d.ts +5 -1
- package/dist/project-switcher.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ Primitives déjà là : `avatar` `badge` `button` `dialog` `drawer` `dropdown-me
|
|
|
48
48
|
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
49
49
|
| `app-shell` | `<AppShell product="mail" organization={<OrgSwitcher …/>} nav={<SidebarNav label="Projet">…</SidebarNav>} links={{ documentation: "/docs", settings: { render: <Link to="/settings" /> } }} account={<AccountMenu …/>} onSearch={() => setOpen(true)} assistant={{ open, onOpenChange: setOpen, panel: <Conversation /> }}>` : Documentation, Réglages et État du service (la page de statut par défaut) juste au-dessus du compte ; bouton Assistant à côté de la recherche, panneau dans une colonne à droite (la page se rétrécit, vue dédiée sous 1280 px, Échap ferme, `shortcut: "j"` facultatif). `sidebarFooter` est déprécié |
|
|
50
50
|
| | `<SidebarNavItem icon={Rocket01Icon} label="Déploiements" render={<Link to="/deployments" />} />` |
|
|
51
|
+
| | Navigation imbriquée (l'espace d'une page, d'un projet) : `<SidebarNavBack label="Retour aux pages" render={<Link to="/pages" />} />` en tête, puis `<SidebarNavGroup label="Suivi">…</SidebarNavGroup>` (titre muet, trait en rail), ou `<SidebarNavGroup label="Réglages" collapsible icon={…} active={…}>` (entrée à flèche qui déroule ses rubriques, fermée par défaut, ouverte sur une rubrique active, mémorisée par navigateur) |
|
|
51
52
|
| `org-switcher` | `<OrgSwitcher organizations={orgs} activeId={org.id} onSelect={go} createHref="/organisations/nouvelle" />` |
|
|
52
53
|
| `project-switcher` | `<ProjectSwitcher projects={projects} activeId={project?.id} activeName={params.project} allHref="/o/acme" createHref="/o/acme/new" renderLink={(href) => <Link to={href} />} />` : projet actif ou « Tous les projets », marque du projet (`ProjectMark`), « Nouveau projet » |
|
|
53
54
|
| `account-menu` | `<AccountMenu user={user} onProfile={…} onSecurity={…} onSignOut={signOut} product="Mail" />` : Account et les produits ouverts du catalogue, Mail coché. Dans l'ordre : identité, Profil, Sécurité, Thème (Système, Clair, Sombre, `ThemeSwitch`), entrées du produit, Se déconnecter. Ni « État du service » ni raccourcis : la coque pose le premier au-dessus du compte, l'aide garde sa touche `?` |
|
|
@@ -116,20 +117,21 @@ Sur ordinateur, la coque ouvre sur la barre des produits (ADR 0011) : organisati
|
|
|
116
117
|
|
|
117
118
|
## Listes, journaux et retours
|
|
118
119
|
|
|
119
|
-
| Import | Exemple
|
|
120
|
-
| -------------------- |
|
|
121
|
-
| `filter-bar` | `<FilterBar search={{ value: q, onChange }} filters={[{ key: "status", label: "Statut", options }]} values={{ status }} onFilterChange={…} onReset={…} />`
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
124
|
-
| `cursor-
|
|
125
|
-
| `
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
120
|
+
| Import | Exemple |
|
|
121
|
+
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
122
|
+
| `filter-bar` | `<FilterBar search={{ value: q, onChange }} filters={[{ key: "status", label: "Statut", options }]} values={{ status }} onFilterChange={…} onReset={…} />` |
|
|
123
|
+
| | `variant="panel"` : un bouton « Filtres » (compteur des filtres posés) ouvre un panneau, bulle sur ordinateur et feuille du bas sur téléphone ; `onResetFilters` garde la recherche, `leading` pose une vue (`SegmentedControl`) avant la recherche |
|
|
124
|
+
| `data-table` | `<DataTable label="Déploiements" rows={rows} columns={columns} rowKey={(d) => d.id} sort={sort} onSortChange={…} filters={<FilterBar …/>} loadMore={…} />` |
|
|
125
|
+
| `cursor-pages` | `const pages = useCursorPages(JSON.stringify(filters))` : pile de curseurs, 200 lignes par page (`PAGE_SIZE`), retour à la page 1 quand la clé change, `pages.paginationOpts` pour `paginate` de Convex |
|
|
126
|
+
| `cursor-pager` | `<CursorPager pages={pages} result={result} scrollTarget={topRef} />` : Précédent, Suivant, « Lignes 201 à 400 », chargement sans saut ; règle des grandes listes dans le guide |
|
|
127
|
+
| `board` | `<Board label="Tableau du projet" columns={[{ id, label, title, cards }]} cardKey={(c) => c.id} cardLabel={(c) => c.title} renderCard={(c, { handle }) => …} onMove={…} />` : poignée souris, doigt, clavier |
|
|
128
|
+
| `dns-record-table` | `<DnsRecordTable records={[{ type: "MX", name: "send", value, priority: 10, status: "pending" }]} />` (Vérifié, En attente, Échec, À ajouter) |
|
|
129
|
+
| `log-viewer` | `<LogViewer lines={[{ id, time, level: "error", message }]} live />` : niveaux, recherche, « Suivre », copie ; seules les lignes visibles sont rendues |
|
|
130
|
+
| `sparkline` | `<Sparkline label="Envois sur 7 jours" values={[3, 8, 5]} color={2} />` |
|
|
131
|
+
| `middle-truncate` | `<MiddleTruncate text={deployment.url} />` : « …/routes/index.tsx », texte entier dans la page |
|
|
132
|
+
| `error-card` | `<ErrorCard title="Impossible de charger les journaux" detail={message} reference={requestId} onRetry={refetch} />` |
|
|
133
|
+
| `destructive-dialog` | `<DestructiveDialog open={open} onOpenChange={setOpen} title="Supprimer le domaine" question="…" name="acme.ch" onConfirm={remove} />` |
|
|
134
|
+
| `sonner` | `<Toaster />` une fois, puis `toastUndo("Domaine retiré", { onUndo: restore })` ; `errorMessage(error)` pour un toast d'erreur |
|
|
133
135
|
|
|
134
136
|
Filtres, tri et page vivent dans l'URL du produit : les composants sont contrôlés et ne connaissent pas le routeur.
|
|
135
137
|
|
package/dist/app-shell.d.ts
CHANGED
|
@@ -13,7 +13,10 @@ export declare function toggleSidebar(): void;
|
|
|
13
13
|
export declare function useAppShell(): {
|
|
14
14
|
closeMobileNav: () => void;
|
|
15
15
|
};
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* La navigation du produit : 4 à 7 entrées, à plat. Une navigation imbriquée (l'espace d'une page,
|
|
18
|
+
* d'un projet) commence par `SidebarNavBack` et range ses entrées dans des `SidebarNavGroup`.
|
|
19
|
+
*/
|
|
17
20
|
export declare function SidebarNav({ label, className, children, }: {
|
|
18
21
|
/** Nom de la navigation pour les lecteurs d'écran : « Projet », « Organisation ». */
|
|
19
22
|
label: string;
|
|
@@ -33,6 +36,50 @@ export declare function SidebarNavItem({ icon, label, active, href, render, }: {
|
|
|
33
36
|
/** Lien du routeur : `render={<Link to="…" />}`. */
|
|
34
37
|
render?: React.ReactElement;
|
|
35
38
|
}): React.JSX.Element;
|
|
39
|
+
/**
|
|
40
|
+
* Retour au niveau du dessus, en tête d'une navigation imbriquée : « Retour aux pages » quand la
|
|
41
|
+
* barre montre l'espace d'une page, d'un projet. Premier enfant de `SidebarNav`, au-dessus des
|
|
42
|
+
* groupes. Une pastille teintée, flèche à gauche, jamais « vous êtes ici ». En rail, la flèche seule
|
|
43
|
+
* et le texte en infobulle.
|
|
44
|
+
*/
|
|
45
|
+
export declare function SidebarNavBack({ label, href, render, }: {
|
|
46
|
+
/** « Retour aux pages » */
|
|
47
|
+
label: string;
|
|
48
|
+
href?: string;
|
|
49
|
+
/** Lien du routeur : `render={<Link to="…" />}`. */
|
|
50
|
+
render?: React.ReactElement;
|
|
51
|
+
}): React.JSX.Element;
|
|
52
|
+
/**
|
|
53
|
+
* Un groupe titré de la barre : « Suivi », « Réglages ». Ses `SidebarNavItem` en enfants. Le titre
|
|
54
|
+
* nomme la liste pour les lecteurs d'écran ; en rail, il cède la place à un trait.
|
|
55
|
+
*
|
|
56
|
+
* Avec `collapsible`, le titre devient une entrée à flèche qui déroule ses rubriques : fermé par
|
|
57
|
+
* défaut, ouvert dès qu'une de ses rubriques est la page ouverte, puis tel que la personne l'a
|
|
58
|
+
* laissé (mémorisé par navigateur). En rail, ses rubriques restent visibles, sous un trait.
|
|
59
|
+
*/
|
|
60
|
+
export declare function SidebarNavGroup(props: {
|
|
61
|
+
label: string;
|
|
62
|
+
children: ReactNode;
|
|
63
|
+
/** Le titre déroule ou replie le groupe. */
|
|
64
|
+
collapsible?: false;
|
|
65
|
+
}): React.JSX.Element;
|
|
66
|
+
export declare function SidebarNavGroup(props: {
|
|
67
|
+
label: string;
|
|
68
|
+
children: ReactNode;
|
|
69
|
+
collapsible: true;
|
|
70
|
+
/** Icône de l'entrée qui déroule le groupe, comme une rubrique. */
|
|
71
|
+
icon?: IconSvgElement;
|
|
72
|
+
/**
|
|
73
|
+
* Une rubrique du groupe est la page ouverte : le groupe s'ouvre, dès le rendu serveur. Sans cette
|
|
74
|
+
* valeur, la coque le lit dans les liens (`aria-current="page"`, `data-status="active"`) après
|
|
75
|
+
* l'hydratation.
|
|
76
|
+
*/
|
|
77
|
+
active?: boolean;
|
|
78
|
+
/** Ouvert tant que la personne n'a rien choisi. Fermé par défaut. */
|
|
79
|
+
defaultOpen?: boolean;
|
|
80
|
+
/** Clé de mémorisation, par défaut le titre : à donner quand deux groupes portent le même. */
|
|
81
|
+
storageKey?: string;
|
|
82
|
+
}): React.JSX.Element;
|
|
36
83
|
/**
|
|
37
84
|
* Un accès commun de la barre : une adresse, ou un lien du routeur (`{ render: <Link to="/settings" /> }`,
|
|
38
85
|
* dont le `data-status="active"` marque la page ouverte), ou une adresse avec `active`.
|
package/dist/app-shell.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import Activity01Icon from "@hugeicons/core-free-icons/Activity01Icon";
|
|
3
3
|
import AiMagicIcon from "@hugeicons/core-free-icons/AiMagicIcon";
|
|
4
4
|
import ArrowLeft01Icon from "@hugeicons/core-free-icons/ArrowLeft01Icon";
|
|
5
|
+
import ArrowRight01Icon from "@hugeicons/core-free-icons/ArrowRight01Icon";
|
|
5
6
|
import BookOpen01Icon from "@hugeicons/core-free-icons/BookOpen01Icon";
|
|
6
7
|
import Cancel01Icon from "@hugeicons/core-free-icons/Cancel01Icon";
|
|
7
8
|
import Menu01Icon from "@hugeicons/core-free-icons/Menu01Icon";
|
|
@@ -81,7 +82,10 @@ const ShellContext = createContext({ closeMobileNav: () => { } });
|
|
|
81
82
|
export function useAppShell() {
|
|
82
83
|
return useContext(ShellContext);
|
|
83
84
|
}
|
|
84
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* La navigation du produit : 4 à 7 entrées, à plat. Une navigation imbriquée (l'espace d'une page,
|
|
87
|
+
* d'un projet) commence par `SidebarNavBack` et range ses entrées dans des `SidebarNavGroup`.
|
|
88
|
+
*/
|
|
85
89
|
export function SidebarNav({ label, className, children, }) {
|
|
86
90
|
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 }) }));
|
|
87
91
|
}
|
|
@@ -106,6 +110,126 @@ export function SidebarNavItem({ icon, label, active, href, render, }) {
|
|
|
106
110
|
});
|
|
107
111
|
return (_jsx("li", { children: _jsxs(Tooltip, { disabled: !rail, children: [_jsx(TooltipTrigger, { render: link }), _jsx(TooltipContent, { side: "right", children: label })] }) }));
|
|
108
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Retour au niveau du dessus, en tête d'une navigation imbriquée : « Retour aux pages » quand la
|
|
115
|
+
* barre montre l'espace d'une page, d'un projet. Premier enfant de `SidebarNav`, au-dessus des
|
|
116
|
+
* groupes. Une pastille teintée, flèche à gauche, jamais « vous êtes ici ». En rail, la flèche seule
|
|
117
|
+
* et le texte en infobulle.
|
|
118
|
+
*/
|
|
119
|
+
export function SidebarNavBack({ label, href, render, }) {
|
|
120
|
+
const rail = useSidebarRail();
|
|
121
|
+
const { closeMobileNav } = useAppShell();
|
|
122
|
+
const link = useRender({
|
|
123
|
+
defaultTagName: "a",
|
|
124
|
+
render,
|
|
125
|
+
props: {
|
|
126
|
+
href,
|
|
127
|
+
onClick: closeMobileNav,
|
|
128
|
+
"data-slot": "sidebar-nav-back",
|
|
129
|
+
className: cn(
|
|
130
|
+
// Jamais marqué actif : le lien du routeur pose `data-status="active"` sur un parent ouvert.
|
|
131
|
+
"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"),
|
|
132
|
+
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 })] })),
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
return (_jsx("li", { className: "pb-2", children: _jsxs(Tooltip, { disabled: !rail, children: [_jsx(TooltipTrigger, { render: link }), _jsx(TooltipContent, { side: "right", children: label })] }) }));
|
|
136
|
+
}
|
|
137
|
+
// ── Groupes repliables : ouverts ou fermés par la personne, mémorisés par navigateur ──────
|
|
138
|
+
const GROUP_KEY_PREFIX = "sidebar-group:";
|
|
139
|
+
const GROUP_EVENT = "sidebargroupchange";
|
|
140
|
+
function subscribeGroups(onChange) {
|
|
141
|
+
window.addEventListener(GROUP_EVENT, onChange);
|
|
142
|
+
window.addEventListener("storage", onChange);
|
|
143
|
+
return () => {
|
|
144
|
+
window.removeEventListener(GROUP_EVENT, onChange);
|
|
145
|
+
window.removeEventListener("storage", onChange);
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function readGroup(key) {
|
|
149
|
+
try {
|
|
150
|
+
const value = localStorage.getItem(GROUP_KEY_PREFIX + key);
|
|
151
|
+
return value === "open" ? true : value === "closed" ? false : null;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function writeGroup(key, open) {
|
|
158
|
+
try {
|
|
159
|
+
localStorage.setItem(GROUP_KEY_PREFIX + key, open ? "open" : "closed");
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// Stockage bloqué : le choix vaut pour cette page seulement.
|
|
163
|
+
}
|
|
164
|
+
window.dispatchEvent(new Event(GROUP_EVENT));
|
|
165
|
+
}
|
|
166
|
+
/** Une entrée de la liste est-elle la page ouverte ? Suit les attributs que pose le routeur. */
|
|
167
|
+
function useActiveDescendant(ref) {
|
|
168
|
+
const [active, setActive] = useState(false);
|
|
169
|
+
useLayoutEffect(() => {
|
|
170
|
+
const element = ref.current;
|
|
171
|
+
if (!element)
|
|
172
|
+
return;
|
|
173
|
+
const read = () => setActive(element.querySelector('[aria-current="page"], [data-status="active"]') !== null);
|
|
174
|
+
read();
|
|
175
|
+
const observer = new MutationObserver(read);
|
|
176
|
+
observer.observe(element, {
|
|
177
|
+
subtree: true,
|
|
178
|
+
childList: true,
|
|
179
|
+
attributes: true,
|
|
180
|
+
attributeFilter: ["aria-current", "data-status"],
|
|
181
|
+
});
|
|
182
|
+
return () => observer.disconnect();
|
|
183
|
+
}, [ref]);
|
|
184
|
+
return active;
|
|
185
|
+
}
|
|
186
|
+
export function SidebarNavGroup(props) {
|
|
187
|
+
if (props.collapsible)
|
|
188
|
+
return _jsx(CollapsibleNavGroup, { ...props });
|
|
189
|
+
return _jsx(TitledNavGroup, { label: props.label, children: props.children });
|
|
190
|
+
}
|
|
191
|
+
function CollapsibleNavGroup({ label, children, icon, active, defaultOpen = false, storageKey, }) {
|
|
192
|
+
const key = storageKey ?? label;
|
|
193
|
+
const buttonId = useId();
|
|
194
|
+
const listId = useId();
|
|
195
|
+
const listRef = useRef(null);
|
|
196
|
+
const stored = useSyncExternalStore(subscribeGroups, () => readGroup(key), () => null);
|
|
197
|
+
const activeInDom = useActiveDescendant(listRef);
|
|
198
|
+
const current = active ?? activeInDom;
|
|
199
|
+
// Refermé par la personne alors qu'une de ses rubriques est ouverte : il le reste jusqu'à ce
|
|
200
|
+
// qu'elle en sorte. La navigation suivante vers une rubrique du groupe le rouvre.
|
|
201
|
+
const [dismissed, setDismissed] = useState(false);
|
|
202
|
+
const [wasCurrent, setWasCurrent] = useState(current);
|
|
203
|
+
if (wasCurrent !== current) {
|
|
204
|
+
setWasCurrent(current);
|
|
205
|
+
setDismissed(false);
|
|
206
|
+
}
|
|
207
|
+
const open = current ? !dismissed : (stored ?? defaultOpen);
|
|
208
|
+
// Pas d'animation au premier rendu : l'état lu dans le navigateur s'applique sans glisser.
|
|
209
|
+
const [ready, setReady] = useState(false);
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
const frame = requestAnimationFrame(() => setReady(true));
|
|
212
|
+
return () => cancelAnimationFrame(frame);
|
|
213
|
+
}, []);
|
|
214
|
+
const toggle = () => {
|
|
215
|
+
const next = !open;
|
|
216
|
+
if (current)
|
|
217
|
+
setDismissed(!next);
|
|
218
|
+
writeGroup(key, next);
|
|
219
|
+
};
|
|
220
|
+
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",
|
|
221
|
+
// Une rubrique du groupe est ouverte et le groupe est replié : l'entrée le dit, sans fond.
|
|
222
|
+
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 &&
|
|
223
|
+
"transition-[grid-template-rows,visibility] duration-(--duration-base) ease-ios motion-reduce:transition-none",
|
|
224
|
+
// En rail, les rubriques restent là, sous le trait : seules leurs icônes se voient.
|
|
225
|
+
"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",
|
|
226
|
+
// Rubriques en retrait, sous un filet aligné sur l'icône du titre.
|
|
227
|
+
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 }) })] }));
|
|
228
|
+
}
|
|
229
|
+
function TitledNavGroup({ label, children }) {
|
|
230
|
+
const id = useId();
|
|
231
|
+
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 })] }));
|
|
232
|
+
}
|
|
109
233
|
const COMMON_LINKS = [
|
|
110
234
|
{ key: "documentation", label: "Documentation", icon: BookOpen01Icon },
|
|
111
235
|
{ key: "settings", label: "Réglages", icon: Settings01Icon },
|
package/dist/filter-bar.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export declare function filterSummary(filter: FilterDefinition, selected?: Reado
|
|
|
20
20
|
export declare function activeFilterCount(values?: FilterValues, search?: string): number;
|
|
21
21
|
/** Ajoute ou retire une valeur d'un filtre à choix multiple, dans l'ordre des options. */
|
|
22
22
|
export declare function toggleFilterValue(filter: FilterDefinition, selected: ReadonlyArray<string> | undefined, value: string, checked: boolean): Array<string>;
|
|
23
|
+
/** Nombre de filtres posés, sans la recherche : le compteur du bouton « Filtres ». */
|
|
24
|
+
export declare function activeFilterOnlyCount(values?: FilterValues): number;
|
|
23
25
|
/**
|
|
24
26
|
* Recherche et filtres en pastilles au-dessus d'une liste. Entièrement contrôlée, sans routeur :
|
|
25
27
|
* le produit lit les valeurs dans les paramètres d'URL et les y réécrit dans les callbacks, pour
|
|
@@ -28,8 +30,12 @@ export declare function toggleFilterValue(filter: FilterDefinition, selected: Re
|
|
|
28
30
|
* La recherche n'écrit qu'après 250 ms de calme (Entrée pour tout de suite, Échap pour vider).
|
|
29
31
|
* Un filtre à choix multiple reste ouvert pendant qu'on coche ; `single` en fait un choix unique
|
|
30
32
|
* avec « Tous ». « Effacer » apparaît dès qu'un critère est posé.
|
|
33
|
+
*
|
|
34
|
+
* `variant="panel"` range les filtres derrière un seul bouton « Filtres » (icône, compteur des
|
|
35
|
+
* filtres posés) qui ouvre un panneau : bulle sur ordinateur, feuille du bas sur téléphone. La
|
|
36
|
+
* recherche reste visible. Pour une liste qui a plus de deux filtres, ou un en-tête déjà chargé.
|
|
31
37
|
*/
|
|
32
|
-
export declare function FilterBar({ search, filters, values, onFilterChange, onReset, actions, className, }: {
|
|
38
|
+
export declare function FilterBar({ search, filters, values, onFilterChange, onReset, onResetFilters, variant, panelLabel, leading, actions, className, }: {
|
|
33
39
|
search?: {
|
|
34
40
|
value: string;
|
|
35
41
|
onChange: (value: string) => void;
|
|
@@ -44,6 +50,14 @@ export declare function FilterBar({ search, filters, values, onFilterChange, onR
|
|
|
44
50
|
onFilterChange?: (key: string, values: Array<string>) => void;
|
|
45
51
|
/** Retire tous les critères, recherche comprise. */
|
|
46
52
|
onReset?: () => void;
|
|
53
|
+
/** `panel` : « Réinitialiser » du panneau, qui retire les filtres et garde la recherche. */
|
|
54
|
+
onResetFilters?: () => void;
|
|
55
|
+
/** `pills` (par défaut) : une pastille par filtre. `panel` : un bouton « Filtres » et son panneau. */
|
|
56
|
+
variant?: "pills" | "panel";
|
|
57
|
+
/** Nom du bouton et du panneau en `panel`, « Filtres » par défaut. */
|
|
58
|
+
panelLabel?: string;
|
|
59
|
+
/** Avant la recherche : une vue (`SegmentedControl`) qui choisit la liste plutôt que de la filtrer. */
|
|
60
|
+
leading?: ReactNode;
|
|
47
61
|
/** À droite : la période, un bouton d'export. */
|
|
48
62
|
actions?: ReactNode;
|
|
49
63
|
className?: string;
|
package/dist/filter-bar.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import ArrowDown01Icon from "@hugeicons/core-free-icons/ArrowDown01Icon";
|
|
3
3
|
import Cancel01Icon from "@hugeicons/core-free-icons/Cancel01Icon";
|
|
4
|
+
import FilterHorizontalIcon from "@hugeicons/core-free-icons/FilterHorizontalIcon";
|
|
4
5
|
import Search01Icon from "@hugeicons/core-free-icons/Search01Icon";
|
|
5
6
|
import { HugeiconsIcon } from "@hugeicons/react";
|
|
6
|
-
import { useEffect, useRef, useState } from "react";
|
|
7
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
7
8
|
import { Button, buttonVariants } from "./button.js";
|
|
8
9
|
import { cn } from "./cn.js";
|
|
9
10
|
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, } from "./dropdown-menu.js";
|
|
11
|
+
import { Drawer, DrawerBody, DrawerCloseButton, DrawerContent, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger, } from "./drawer.js";
|
|
10
12
|
import { Input } from "./input.js";
|
|
13
|
+
import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from "./popover.js";
|
|
14
|
+
import { useIsMobile } from "./use-media-query.js";
|
|
11
15
|
/** Le texte de la pastille : « Statut », « Statut : Erreur », « Statut : Erreur +2 ». */
|
|
12
16
|
export function filterSummary(filter, selected = []) {
|
|
13
17
|
const labels = filter.options.filter((o) => selected.includes(o.value)).map((o) => o.label);
|
|
@@ -58,9 +62,9 @@ function useDebouncedSearch(value, onChange, delay) {
|
|
|
58
62
|
};
|
|
59
63
|
return { draft, setDraft, flush };
|
|
60
64
|
}
|
|
61
|
-
function SearchField({ value, onChange, placeholder = "Rechercher…", label = "Rechercher", delay = 250, }) {
|
|
65
|
+
function SearchField({ value, onChange, placeholder = "Rechercher…", label = "Rechercher", delay = 250, className, }) {
|
|
62
66
|
const { draft, setDraft, flush } = useDebouncedSearch(value, onChange, delay);
|
|
63
|
-
return (_jsxs("div", { className: "relative min-w-0 flex-1 basis-full sm:basis-56 sm:max-w-sm", children: [_jsx(HugeiconsIcon, { icon: Search01Icon, strokeWidth: 2, "aria-hidden": true, className: "pointer-events-none absolute top-1/2 left-3.5 size-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { type: "search", "aria-label": label, placeholder: placeholder, autoComplete: "off", spellCheck: false, value: draft, onChange: (event) => setDraft(event.target.value), onKeyDown: (event) => {
|
|
67
|
+
return (_jsxs("div", { className: cn("relative min-w-0 flex-1 basis-full sm:basis-56 sm:max-w-sm", className), children: [_jsx(HugeiconsIcon, { icon: Search01Icon, strokeWidth: 2, "aria-hidden": true, className: "pointer-events-none absolute top-1/2 left-3.5 size-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { type: "search", "aria-label": label, placeholder: placeholder, autoComplete: "off", spellCheck: false, value: draft, onChange: (event) => setDraft(event.target.value), onKeyDown: (event) => {
|
|
64
68
|
if (event.key === "Enter")
|
|
65
69
|
flush(draft);
|
|
66
70
|
if (event.key === "Escape" && draft !== "") {
|
|
@@ -73,6 +77,45 @@ function FilterPill({ filter, selected = [], onChange, }) {
|
|
|
73
77
|
const active = selected.length > 0;
|
|
74
78
|
return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { "data-slot": "filter-pill", "data-active": active || undefined, className: cn(buttonVariants({ variant: "outline", size: "sm" }), "max-w-64 gap-1 pr-2.5 font-normal text-muted-foreground", active && "bg-foreground/[0.06] font-medium text-foreground ring-0 dark:bg-foreground/[0.08]"), children: [_jsx("span", { className: "truncate", children: filterSummary(filter, selected) }), _jsx(HugeiconsIcon, { icon: ArrowDown01Icon, strokeWidth: 2, "aria-hidden": true, className: "size-3.5 opacity-60" })] }), _jsx(DropdownMenuContent, { className: "w-auto", children: filter.single ? (_jsxs(DropdownMenuRadioGroup, { label: filter.label, value: selected[0] ?? "", onValueChange: (value) => onChange(value === "" ? [] : [value]), children: [_jsx(DropdownMenuRadioItem, { value: "", children: "Tous" }), filter.options.map((option) => (_jsx(DropdownMenuRadioItem, { value: option.value, children: option.label }, option.value)))] })) : (_jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: filter.label }), filter.options.map((option) => (_jsx(DropdownMenuCheckboxItem, { checked: selected.includes(option.value), onCheckedChange: (checked) => onChange(toggleFilterValue(filter, selected, option.value, checked)), children: option.label }, option.value)))] })) })] }));
|
|
75
79
|
}
|
|
80
|
+
/** Nombre de filtres posés, sans la recherche : le compteur du bouton « Filtres ». */
|
|
81
|
+
export function activeFilterOnlyCount(values = {}) {
|
|
82
|
+
return activeFilterCount(values, "");
|
|
83
|
+
}
|
|
84
|
+
/** Une option du panneau : une pastille qui se coche (choix multiple) ou se choisit (choix unique). */
|
|
85
|
+
function OptionChip({ label, checked, kind, onClick, }) {
|
|
86
|
+
return (_jsx("button", { type: "button", role: kind, "aria-checked": checked, onClick: onClick, className: cn("touch-target inline-flex h-9 items-center rounded-full px-3.5 text-sm outline-none transition-[background-color,color,box-shadow] duration-(--duration-fast) ease-out focus-visible:ring-3 focus-visible:ring-ring/50 lg:h-8 lg:px-3", checked
|
|
87
|
+
? "bg-foreground font-medium text-background"
|
|
88
|
+
: "bg-background text-foreground ring-1 ring-border/70 ring-inset hover:bg-muted"), children: label }));
|
|
89
|
+
}
|
|
90
|
+
/** Un filtre du panneau : son nom, puis ses options en pastilles. */
|
|
91
|
+
function PanelFilter({ filter, selected = [], onChange, }) {
|
|
92
|
+
const id = useId();
|
|
93
|
+
return (_jsxs("div", { className: "grid gap-2", children: [_jsx("p", { id: id, className: "text-xs font-medium text-muted-foreground", children: filter.label }), _jsxs("div", { role: filter.single ? "radiogroup" : "group", "aria-labelledby": id, className: "flex flex-wrap gap-1.5", children: [filter.single ? (_jsx(OptionChip, { label: "Tous", kind: "radio", checked: selected.length === 0, onClick: () => onChange([]) })) : null, filter.options.map((option) => {
|
|
94
|
+
const checked = selected.includes(option.value);
|
|
95
|
+
return (_jsx(OptionChip, { label: option.label, kind: filter.single ? "radio" : "checkbox", checked: checked, onClick: () => onChange(filter.single
|
|
96
|
+
? checked
|
|
97
|
+
? []
|
|
98
|
+
: [option.value]
|
|
99
|
+
: toggleFilterValue(filter, selected, option.value, !checked)) }, option.value));
|
|
100
|
+
})] })] }));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Le bouton « Filtres » et son panneau : une bulle sous le bouton sur ordinateur, une feuille du
|
|
104
|
+
* bas sur téléphone. Chaque choix s'applique aussitôt ; « Réinitialiser » retire les filtres, pas
|
|
105
|
+
* la recherche.
|
|
106
|
+
*/
|
|
107
|
+
function FilterPanel({ filters, values, onFilterChange, onReset, label, }) {
|
|
108
|
+
const isMobile = useIsMobile();
|
|
109
|
+
const [open, setOpen] = useState(false);
|
|
110
|
+
const count = activeFilterOnlyCount(values);
|
|
111
|
+
const trigger = (_jsx("button", { type: "button", "data-slot": "filter-panel-trigger", "data-active": count > 0 || undefined, "aria-label": count > 0 ? `${label}, ${count} actif${count > 1 ? "s" : ""}` : label, className: cn(buttonVariants({ variant: "outline", size: "sm" }), "gap-2 font-normal", count > 0 && "font-medium") }));
|
|
112
|
+
const content = (_jsx(_Fragment, { children: filters.map((filter) => (_jsx(PanelFilter, { filter: filter, ...(values[filter.key] !== undefined ? { selected: values[filter.key] } : {}), onChange: (next) => onFilterChange?.(filter.key, next) }, filter.key))) }));
|
|
113
|
+
const face = (_jsxs(_Fragment, { children: [_jsx(HugeiconsIcon, { icon: FilterHorizontalIcon, strokeWidth: 2, "aria-hidden": true, className: "size-4" }), _jsx("span", { children: label }), count > 0 ? (_jsx("span", { "aria-hidden": true, className: "grid h-5 min-w-5 place-items-center rounded-full bg-foreground px-1.5 text-xs font-medium text-background tabular-nums", children: count })) : null] }));
|
|
114
|
+
const reset = onReset && count > 0 ? (_jsx(Button, { variant: "ghost", size: "sm", onClick: onReset, className: "text-muted-foreground", children: "R\u00E9initialiser" })) : null;
|
|
115
|
+
if (isMobile)
|
|
116
|
+
return (_jsxs(Drawer, { side: "bottom", open: open, onOpenChange: setOpen, children: [_jsx(DrawerTrigger, { render: trigger, children: face }), _jsxs(DrawerContent, { children: [_jsx(DrawerHeader, { children: _jsx(DrawerTitle, { children: label }) }), _jsx(DrawerCloseButton, {}), _jsx(DrawerBody, { className: "grid gap-5 pb-4", children: content }), _jsxs(DrawerFooter, { children: [_jsx(Button, { onClick: () => setOpen(false), children: "Voir les r\u00E9sultats" }), reset] })] })] }));
|
|
117
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { render: trigger, children: face }), _jsxs(PopoverContent, { align: "end", className: "w-[22rem] gap-0 p-0", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-4 pt-3.5 pb-1", children: [_jsx(PopoverTitle, { children: label }), reset ?? _jsx("span", { className: "h-10 lg:h-9", "aria-hidden": true })] }), _jsx("div", { className: "grid max-h-[min(28rem,70svh)] gap-4 overflow-y-auto px-4 pt-1 pb-4", children: content })] })] }));
|
|
118
|
+
}
|
|
76
119
|
/**
|
|
77
120
|
* Recherche et filtres en pastilles au-dessus d'une liste. Entièrement contrôlée, sans routeur :
|
|
78
121
|
* le produit lit les valeurs dans les paramètres d'URL et les y réécrit dans les callbacks, pour
|
|
@@ -81,8 +124,14 @@ function FilterPill({ filter, selected = [], onChange, }) {
|
|
|
81
124
|
* La recherche n'écrit qu'après 250 ms de calme (Entrée pour tout de suite, Échap pour vider).
|
|
82
125
|
* Un filtre à choix multiple reste ouvert pendant qu'on coche ; `single` en fait un choix unique
|
|
83
126
|
* avec « Tous ». « Effacer » apparaît dès qu'un critère est posé.
|
|
127
|
+
*
|
|
128
|
+
* `variant="panel"` range les filtres derrière un seul bouton « Filtres » (icône, compteur des
|
|
129
|
+
* filtres posés) qui ouvre un panneau : bulle sur ordinateur, feuille du bas sur téléphone. La
|
|
130
|
+
* recherche reste visible. Pour une liste qui a plus de deux filtres, ou un en-tête déjà chargé.
|
|
84
131
|
*/
|
|
85
|
-
export function FilterBar({ search, filters = [], values = {}, onFilterChange, onReset, actions, className, }) {
|
|
132
|
+
export function FilterBar({ search, filters = [], values = {}, onFilterChange, onReset, onResetFilters, variant = "pills", panelLabel = "Filtres", leading, actions, className, }) {
|
|
86
133
|
const count = activeFilterCount(values, search?.value);
|
|
87
|
-
|
|
134
|
+
if (variant === "panel")
|
|
135
|
+
return (_jsxs("div", { role: "search", "data-slot": "filter-bar", className: cn("flex flex-wrap items-center gap-2", className), children: [leading, search ? _jsx(SearchField, { ...search, className: "basis-40 sm:basis-56" }) : null, filters.length ? (_jsx(FilterPanel, { filters: filters, values: values, onFilterChange: onFilterChange, onReset: onResetFilters, label: panelLabel })) : null, actions ? _jsx("div", { className: "ml-auto flex items-center gap-2", children: actions }) : null] }));
|
|
136
|
+
return (_jsxs("div", { role: "search", "data-slot": "filter-bar", className: cn("flex flex-wrap items-center gap-2", className), children: [leading, search ? _jsx(SearchField, { ...search }) : null, filters.map((filter) => (_jsx(FilterPill, { filter: filter, ...(values[filter.key] !== undefined ? { selected: values[filter.key] } : {}), onChange: (next) => onFilterChange?.(filter.key, next) }, filter.key))), onReset && count > 0 ? (_jsx(Button, { variant: "ghost", size: "sm", onClick: onReset, className: "text-muted-foreground", children: "Effacer" })) : null, actions ? _jsx("div", { className: "ml-auto flex items-center gap-2", children: actions }) : null] }));
|
|
88
137
|
}
|
|
@@ -43,10 +43,14 @@ export declare function ProjectSwitcher({ projects, activeId, activeName, onSele
|
|
|
43
43
|
onCreate?: () => void;
|
|
44
44
|
/** Rend un lien du routeur du produit ; par défaut un `<a href>`. */
|
|
45
45
|
renderLink?: (href: string) => ReactElement;
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Ce que l'on choisit : `{ one: "site", many: "sites" }` pour Analytics. Un nom féminin porte
|
|
48
|
+
* `feminine` : « Toutes les pages », « Page active », « Nouvelle page » (Status).
|
|
49
|
+
*/
|
|
47
50
|
noun?: {
|
|
48
51
|
one: string;
|
|
49
52
|
many: string;
|
|
53
|
+
feminine?: boolean;
|
|
50
54
|
};
|
|
51
55
|
createLabel?: string;
|
|
52
56
|
className?: string;
|
package/dist/project-switcher.js
CHANGED
|
@@ -39,12 +39,12 @@ function SwitcherEntry({ href, onSelect, renderLink, children, }) {
|
|
|
39
39
|
* pendant le chargement : le déclencheur montre déjà `activeName` (le segment de l'URL), le menu
|
|
40
40
|
* reste utilisable pour « Tous les projets » et « Nouveau projet ».
|
|
41
41
|
*/
|
|
42
|
-
export function ProjectSwitcher({ projects, activeId, activeName, onSelect, allHref, createHref, onCreate, renderLink, noun = { one: "projet", many: "projets" }, createLabel =
|
|
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, }) {
|
|
43
43
|
const capitalized = (word) => word.charAt(0).toUpperCase() + word.slice(1);
|
|
44
|
-
const all =
|
|
44
|
+
const all = `${noun.feminine ? "Toutes" : "Tous"} les ${noun.many}`;
|
|
45
45
|
const hasActive = activeId !== undefined && activeId !== null;
|
|
46
46
|
const current = hasActive ? projects?.find((p) => p.id === activeId) : undefined;
|
|
47
47
|
const label = hasActive ? (current?.name ?? activeName ?? "") : all;
|
|
48
48
|
const canCreate = createHref !== undefined || onCreate !== undefined;
|
|
49
|
-
return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: _jsx("button", { type: "button", "aria-label": hasActive ? `${capitalized(noun.one)} ${label}, changer de ${noun.one}` : `${all}, changer de ${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)} 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] })] }));
|
|
49
|
+
return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: _jsx("button", { type: "button", "aria-label": hasActive ? `${capitalized(noun.one)} ${label}, changer de ${noun.one}` : `${all}, changer de ${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
50
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hgdc/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.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",
|