@hgdc/ui 0.8.0 → 0.10.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 CHANGED
@@ -39,18 +39,31 @@ Primitives déjà là : `avatar` `badge` `button` `dialog` `drawer` `dropdown-me
39
39
 
40
40
  ## Coque des consoles
41
41
 
42
- | Import | Exemple |
43
- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
- | `app-shell` | `<AppShell sidebarHeader={<OrgSwitcher …/>} nav={<SidebarNav label="Projet">…</SidebarNav>} sidebarFooter={<AccountMenu …/>}>` |
45
- | | `<SidebarNavItem icon={Rocket01Icon} label="Déploiements" render={<Link to="/deployments" />} />` |
46
- | `org-switcher` | `<OrgSwitcher organizations={orgs} activeId={org.id} onSelect={go} createHref="/organisations/nouvelle" />` |
47
- | `account-menu` | `<AccountMenu user={user} onProfile={…} onSecurity={…} onSignOut={signOut} product="Mail" />` : Account et les produits ouverts du catalogue, Mail coché |
48
- | `command-menu` | `<CommandMenu groups={[{ label: "Aller à", items }, { label: "Créer", items }, { label: "Changer", items }]} />` (`Ctrl+K`), `<CommandMenuButton compact="mobile" …/>` |
49
- | `products` | `HGDC_PRODUCTS` (mot, nom, sous-domaine, phrase, icône, état, `entryHref`), `HGDC_ACCOUNT`, `productMenuEntries({ current: "Mail" })` : chaque produit du menu pointe vers son entrée `/auth/hgdc` (ADR 0010) |
50
- | `kbd` | `<KbdGroup><Kbd>Ctrl</Kbd><Kbd>K</Kbd></KbdGroup>` |
51
- | `page-header` | `<PageHeader title="Domaines" breadcrumbs={[{ label: "Acme", render: <Link to="/o/acme" /> }, { label: "Domaines" }]} actions={<Button>Ajouter</Button>} />` |
52
-
53
- La barre se réduit en rail (`Ctrl+B`, mémorisé) : poser `sidebarInitScript` dans le `<head>`. Le kit ne connaît ni routeur ni authentification : liens par `render={<Link />}`, actions par callbacks. Rôles d'organisation : `owner`, `admin`, `member` (Propriétaire, Admin, Membre).
42
+ | Import | Exemple |
43
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
+ | `app-shell` | `<AppShell product="mail" organization={<OrgSwitcher …/>} nav={<SidebarNav label="Projet">…</SidebarNav>} sidebarFooter={<AccountMenu …/>}>` |
45
+ | | `<SidebarNavItem icon={Rocket01Icon} label="Déploiements" render={<Link to="/deployments" />} />` |
46
+ | `org-switcher` | `<OrgSwitcher organizations={orgs} activeId={org.id} onSelect={go} createHref="/organisations/nouvelle" />` |
47
+ | `account-menu` | `<AccountMenu user={user} onProfile={…} onSecurity={…} onSignOut={signOut} product="Mail" />` : Account et les produits ouverts du catalogue, Mail coché |
48
+ | `command-menu` | `<CommandMenu groups={[{ label: "Aller à", items }, { label: "Créer", items }, { label: "Changer", items }]} />` (`Ctrl+K`), `<CommandMenuButton compact="mobile" …/>` |
49
+ | `products` | `HGDC_PRODUCTS` (mot, nom, sous-domaine, phrase, pastille `badge`, icône, état, `entryHref`), `HGDC_ACCOUNT`, `productMenuEntries({ current: "Mail" })` : chaque produit du menu pointe vers son entrée `/auth/hgdc` (ADR 0010) |
50
+ | `kbd` | `<KbdGroup><Kbd>Ctrl</Kbd><Kbd>K</Kbd></KbdGroup>` |
51
+ | `page-header` | `<PageHeader title="Domaines" breadcrumbs={[{ label: "Acme", render: <Link to="/o/acme" /> }, { label: "Domaines" }]} actions={<Button>Ajouter</Button>} />` |
52
+
53
+ Le menu « Produits » et la palette (`badge` d'une commande) montrent la pastille de chaque produit.
54
+
55
+ ## Identité H family
56
+
57
+ | Import | Rôle |
58
+ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
59
+ | `product-badge` | `<ProductBadge product="host" className="size-8" />`, `<FamilyMark />` (le H tricolore), `PRODUCT_GRADIENT` |
60
+ | `@hgdc/ui/family/<mot>.svg` | Fichiers des pastilles (`account`, `host`, `mail`, `portal`, `status`, `analytics`, `docs`), à copier en `favicon.svg` |
61
+ | `@hgdc/ui/family/hfamily*.svg` | `hfamily-icon.svg` (le H seul), `hfamily.svg` et `hfamily-dark.svg` (logo sur fond clair ou sombre, jamais recoloré) |
62
+ | `styles.css` | `--product-<mot>-from` / `--product-<mot>-to` (dégradé de chaque pastille), `--glow-blue`, `--glow-violet`, `--glow-pink`, `--glow-orange` |
63
+
64
+ Couleurs pleines ; pour éclaircir, mélanger au blanc `color-mix(in oklab, …)`, jamais `in oklch`. Une pastille reste seule, jamais dans une tuile de couleur. Les originaux vivent dans le dépôt du site hgdc.ch : les recopier dans `packages/ui/family/`, puis `bun scripts/family.ts && bun run format` (SVG embarqués et PNG des e-mails).
65
+
66
+ Sur ordinateur, la coque ouvre sur la barre des produits (ADR 0011) : organisation en haut, pastilles des produits ouverts, Account en bas, chacun par son entrée `/auth/hgdc?next=/`, le produit courant en teinte. Au survol (150 ms d'intention) ou au focus clavier, elle s'élargit par-dessus la page et écrit les noms. `productRail={false}` la coupe ; `accountHref` pointe vers un Account local. La barre latérale porte la marque du produit et se réduit en rail (`Ctrl+B`, mémorisé) : poser `sidebarInitScript` dans le `<head>`. En rail, la pastille du produit devient le bouton « Déplier » au survol. Le kit ne connaît ni routeur ni authentification : liens par `render={<Link />}`, actions par callbacks. Rôles d'organisation : `owner`, `admin`, `member` (Propriétaire, Admin, Membre).
54
67
 
55
68
  ## Contenu et données
56
69
 
@@ -99,7 +112,7 @@ Filtres, tri et page vivent dans l'URL du produit : les composants sont contrôl
99
112
  | `money-field` | `<MoneyField value={centimes} onChange={…} />` : CHF en centimes, « 1’250.50 », accepte « 12.– » ; `formatChf`, `parseMoney` |
100
113
  | `address-fields` | `<AddressFields value={address} onChange={…} errors={validatePostalAddress(address)} />` : rue, numéro, NPA, localité, pays (facture QR) |
101
114
 
102
- `access-request` : `<AccessRequest endpoint="https://<deploiement>.convex.site/f/<id>" product="HGDC Mail" signInRender={<Link to="/sign-in" />} />`, la page « Demander un accès » d'un produit (les comptes HGDC s'ouvrent sur invitation). Poste en JSON vers un formulaire HGDC Mail Forms : prénom, nom, entreprise, e-mail professionnel, message facultatif, `produit` caché, piège `_gotcha`. États envoi, réussite, erreur, champ refusé ; `submitAccessRequest` s'appelle seul.
115
+ `access-request` : `<AccessRequest endpoint="https://<deploiement>.convex.site/f/<id>" product="Mail" signInRender={<Link to="/sign-in" />} />`, la page « Demander un accès » d'un produit (les comptes HGDC s'ouvrent sur invitation). Poste en JSON vers un formulaire HGDC Mail Forms : prénom, nom, entreprise, e-mail professionnel, message facultatif, `produit` caché, piège `_gotcha`. États envoi, réussite, erreur, champ refusé ; `submitAccessRequest` s'appelle seul.
103
116
 
104
117
  Les champs de date sont ceux du navigateur, sans calendrier maison (ADR 0006). Les fonctions pures (`parseMoney`, `contrastRatio`, `validatePostalAddress`, `formatDateRange`) s'importent seules, côté serveur aussi.
105
118
 
@@ -129,20 +142,20 @@ const { html, text } = await renderEmail(
129
142
  )
130
143
  ```
131
144
 
132
- | Bloc | Rôle |
133
- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
134
- | `EmailLayout` | Icône et nom du produit, carte, pied « Propulsé par HGDC Mail » et mentions légales ; `footer` (désabonnement), `name`, `iconSrc`, `accent` |
135
- | `EmailHeading` | Titre |
136
- | `EmailText` | Paragraphe ; `small` pour une note secondaire |
137
- | `EmailButton` | Lien direct, jamais réécrit ; absolu (`https:`, `mailto:`) ou erreur |
138
- | `EmailLink` | Lien dans le texte |
139
- | `EmailCode` | Code à usage unique, en deux groupes à l'œil, d'un bloc au copier |
140
- | `EmailCallout` | Encadré `neutral`, `info`, `success`, `warning`, `danger`, avec titre |
141
- | `EmailDivider` | Séparateur |
142
- | `EmailDetails` | Liste clé / valeur |
143
- | `renderEmail` | `{ html, text }` : HTML pour clients mail, texte tiré du même rendu |
144
-
145
- L'accent vient du catalogue (`accent` de `@hgdc/ui/products`), les icônes de `https://hgdc.ch/email/<mot>.png` (fichiers dans `@hgdc/ui/email-icons/`, régénérés par `bun scripts/email-icons.ts`). Un exemple par type : `scripts/email-showcase.tsx`.
145
+ | Bloc | Rôle |
146
+ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
147
+ | `EmailLayout` | Icône et nom du produit, carte, pied « Envoyé avec Mail de la H family » et mentions légales ; `footer` (désabonnement), `name`, `iconSrc`, `accent` |
148
+ | `EmailHeading` | Titre |
149
+ | `EmailText` | Paragraphe ; `small` pour une note secondaire |
150
+ | `EmailButton` | Lien direct, jamais réécrit ; absolu (`https:`, `mailto:`) ou erreur |
151
+ | `EmailLink` | Lien dans le texte |
152
+ | `EmailCode` | Code à usage unique, en deux groupes à l'œil, d'un bloc au copier |
153
+ | `EmailCallout` | Encadré `neutral`, `info`, `success`, `warning`, `danger`, avec titre |
154
+ | `EmailDivider` | Séparateur |
155
+ | `EmailDetails` | Liste clé / valeur |
156
+ | `renderEmail` | `{ html, text }` : HTML pour clients mail, texte tiré du même rendu |
157
+
158
+ L'accent vient du catalogue (`accent` de `@hgdc/ui/products`), les icônes de `https://hgdc.ch/email/<mot>.png` (fichiers dans `@hgdc/ui/email-icons/`, tirés des pastilles par `bun scripts/family.ts`). Un exemple par type : `scripts/email-showcase.tsx`.
146
159
 
147
160
  ## Règles de la DA
148
161
 
@@ -35,7 +35,7 @@ export declare function submitAccessRequest(endpoint: string, values: AccessRequ
35
35
  export declare function AccessRequest({ endpoint, product, title, description, messageLabel, signInHref, signInRender, homeHref, homeRender, fetcher, }: {
36
36
  /** Adresse d'envoi du formulaire Forms : `https://<deploiement>.convex.site/f/<id>`. */
37
37
  endpoint: string;
38
- /** Nom affiché et valeur du champ caché `produit` : « HGDC Mail ». */
38
+ /** Nom affiché et valeur du champ caché `produit` : « Mail ». */
39
39
  product: string;
40
40
  title?: string;
41
41
  description?: ReactNode;
@@ -49,7 +49,7 @@ function LinkSlot({ href, render, className, children, }) {
49
49
  props: mergeProps({ className, children }, render ? {} : { href }),
50
50
  });
51
51
  }
52
- export function AccessRequest({ endpoint, product, title = `Demander un accès à ${product}`, description = "Les comptes HGDC s'ouvrent sur invitation. Dis-nous qui tu es : on te répond rapidement et, si c'est bon, tu reçois une invitation par e-mail.", messageLabel = "Ton projet, en quelques mots (facultatif)", signInHref = "/sign-in", signInRender, homeHref = "/", homeRender, fetcher, }) {
52
+ export function AccessRequest({ endpoint, product, title = `Demander un accès à ${product}`, description = "Les comptes s'ouvrent sur invitation. Dis-nous qui tu es : on te répond rapidement et, si c'est bon, tu reçois une invitation par e-mail.", messageLabel = "Ton projet, en quelques mots (facultatif)", signInHref = "/sign-in", signInRender, homeHref = "/", homeRender, fetcher, }) {
53
53
  const [values, setValues] = useState(EMPTY_ACCESS_REQUEST);
54
54
  const [trap, setTrap] = useState("");
55
55
  const [pending, setPending] = useState(false);
@@ -75,10 +75,10 @@ export function AccessRequest({ endpoint, product, title = `Demander un accès
75
75
  setPending(false);
76
76
  }
77
77
  if (sentTo) {
78
- return (_jsxs("div", { "data-slot": "access-request", className: "grid rise gap-6", children: [_jsx(HugeiconsIcon, { icon: CheckmarkCircle02Icon, className: "size-10 pop-in text-status-active" }), _jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-semibold text-balance", children: "Demande envoy\u00E9e" }), _jsxs("p", { className: "mt-2 text-muted-foreground", children: ["Merci. Un accus\u00E9 de r\u00E9ception part vers ", sentTo, ". Si ta demande est accept\u00E9e, tu re\u00E7ois une invitation de HGDC Account pour cr\u00E9er ton compte."] })] }), _jsx(LinkSlot, { href: homeHref, render: homeRender, className: buttonVariants({ variant: "outline", size: "lg" }), children: "Retour \u00E0 l'accueil" })] }, "access-done"));
78
+ return (_jsxs("div", { "data-slot": "access-request", className: "grid rise gap-6", children: [_jsx(HugeiconsIcon, { icon: CheckmarkCircle02Icon, className: "size-10 pop-in text-status-active" }), _jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-semibold text-balance", children: "Demande envoy\u00E9e" }), _jsxs("p", { className: "mt-2 text-muted-foreground", children: ["Merci. Un accus\u00E9 de r\u00E9ception part vers ", sentTo, ". Si ta demande est accept\u00E9e, tu re\u00E7ois une invitation d'Account pour cr\u00E9er ton compte."] })] }), _jsx(LinkSlot, { href: homeHref, render: homeRender, className: buttonVariants({ variant: "outline", size: "lg" }), children: "Retour \u00E0 l'accueil" })] }, "access-done"));
79
79
  }
80
80
  const invalid = (key) => (fieldErrors[key] ? true : undefined);
81
81
  const field = (key, label, input) => (_jsxs(Field, { "data-invalid": invalid(key), children: [_jsx(FieldLabel, { htmlFor: `acces-${key}`, children: label }), input, fieldErrors[key] ? _jsx(FieldError, { children: fieldErrors[key] }) : null] }));
82
82
  const text = (key, autoComplete, max, type = "text") => (_jsx(Input, { id: `acces-${key}`, type: type, autoComplete: autoComplete, required: true, maxLength: max, value: values[key], onChange: set(key), "aria-invalid": invalid(key) }));
83
- return (_jsxs("form", { "data-slot": "access-request", onSubmit: submit, className: "grid rise gap-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-semibold text-balance", children: title }), description ? _jsx("p", { className: "mt-2 text-muted-foreground", children: description }) : null] }), failure ? (_jsx(Alert, { tone: "destructive", role: "alert", children: failure })) : null, _jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [field("prenom", "Prénom", text("prenom", "given-name", 100)), field("nom", "Nom", text("nom", "family-name", 100))] }), field("entreprise", "Entreprise", text("entreprise", "organization", 120)), field("email", "E-mail professionnel", text("email", "email", 320, "email")), field("message", messageLabel, _jsx(Textarea, { id: "acces-message", rows: 3, maxLength: 2000, value: values.message, onChange: set("message"), "aria-invalid": invalid("message") })), _jsx("input", { type: "text", name: "_gotcha", tabIndex: -1, autoComplete: "off", "aria-hidden": "true", value: trap, onChange: (e) => setTrap(e.target.value), className: "absolute -left-[9999px] size-px opacity-0" })] }), _jsx(Button, { type: "submit", size: "lg", isLoading: pending, children: "Envoyer la demande" }), _jsxs("p", { className: "text-sm text-muted-foreground", children: ["D\u00E9j\u00E0 un compte HGDC ?", " ", _jsx(LinkSlot, { href: signInHref, render: signInRender, className: "font-medium text-foreground underline-offset-4 hover:underline", children: "Se connecter" })] })] }, "access"));
83
+ return (_jsxs("form", { "data-slot": "access-request", onSubmit: submit, className: "grid rise gap-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-semibold text-balance", children: title }), description ? _jsx("p", { className: "mt-2 text-muted-foreground", children: description }) : null] }), failure ? (_jsx(Alert, { tone: "destructive", role: "alert", children: failure })) : null, _jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [field("prenom", "Prénom", text("prenom", "given-name", 100)), field("nom", "Nom", text("nom", "family-name", 100))] }), field("entreprise", "Entreprise", text("entreprise", "organization", 120)), field("email", "E-mail professionnel", text("email", "email", 320, "email")), field("message", messageLabel, _jsx(Textarea, { id: "acces-message", rows: 3, maxLength: 2000, value: values.message, onChange: set("message"), "aria-invalid": invalid("message") })), _jsx("input", { type: "text", name: "_gotcha", tabIndex: -1, autoComplete: "off", "aria-hidden": "true", value: trap, onChange: (e) => setTrap(e.target.value), className: "absolute -left-[9999px] size-px opacity-0" })] }), _jsx(Button, { type: "submit", size: "lg", isLoading: pending, children: "Envoyer la demande" }), _jsxs("p", { className: "text-sm text-muted-foreground", children: ["D\u00E9j\u00E0 un compte ?", " ", _jsx(LinkSlot, { href: signInHref, render: signInRender, className: "font-medium text-foreground underline-offset-4 hover:underline", children: "Se connecter" })] })] }, "access"));
84
84
  }
@@ -1,12 +1,15 @@
1
1
  import { type IconSvgElement } from "@hugeicons/react";
2
2
  import type { ReactNode } from "react";
3
+ import type { FamilyProduct } from "./family-svg.ts";
3
4
  export type HgdcProduct = {
4
- /** « HGDC Host ». */
5
+ /** « Host ». */
5
6
  name: string;
6
7
  href: string;
7
8
  /** Le produit ouvert : coché, sans lien. */
8
9
  current?: boolean;
9
- /** Icône du produit ; celle d'un tableau de bord par défaut. */
10
+ /** Pastille du produit (`host`, `mail`…), prioritaire sur `icon`. */
11
+ badge?: FamilyProduct;
12
+ /** Icône du produit sans pastille ; celle d'un tableau de bord par défaut. */
10
13
  icon?: IconSvgElement;
11
14
  /** Texte discret à droite : « Préversion ». */
12
15
  hint?: string;
@@ -22,7 +25,7 @@ export type AccountUser = {
22
25
  * l'avatar seul, pour une barre du haut. Le produit passe les actions ; le thème passe par
23
26
  * `ThemeProvider`.
24
27
  *
25
- * « Produits HGDC » liste par défaut Account et les produits ouverts du catalogue
28
+ * « Produits » liste par défaut Account et les produits ouverts du catalogue
26
29
  * (`productMenuEntries`, `@hgdc/ui/products`), avec `product` coché. `products` remplace cette
27
30
  * liste ; `products={[]}` la retire.
28
31
  */
@@ -13,6 +13,7 @@ import { Menu as MenuPrimitive } from "@base-ui/react/menu";
13
13
  import { Avatar, AvatarFallback, AvatarImage } from "./avatar.js";
14
14
  import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "./dropdown-menu.js";
15
15
  import { MENU_LINK_ITEM } from "./org-switcher.js";
16
+ import { ProductBadge } from "./product-badge.js";
16
17
  import { productMenuEntries } from "./products.js";
17
18
  import { useTheme } from "./theme.js";
18
19
  const THEMES = ["light", "dark", "system"];
@@ -30,7 +31,7 @@ function initials(name) {
30
31
  * l'avatar seul, pour une barre du haut. Le produit passe les actions ; le thème passe par
31
32
  * `ThemeProvider`.
32
33
  *
33
- * « Produits HGDC » liste par défaut Account et les produits ouverts du catalogue
34
+ * « Produits » liste par défaut Account et les produits ouverts du catalogue
34
35
  * (`productMenuEntries`, `@hgdc/ui/products`), avec `product` coché. `products` remplace cette
35
36
  * liste ; `products={[]}` la retire.
36
37
  */
@@ -41,5 +42,10 @@ export function AccountMenu({ user, onProfile, onSecurity, onShortcuts, onSignOu
41
42
  const next = THEMES.find((t) => t === value);
42
43
  if (next)
43
44
  setTheme(next);
44
- }, children: [_jsx(DropdownMenuRadioItem, { value: "light", children: "Clair" }), _jsx(DropdownMenuRadioItem, { value: "dark", children: "Sombre" }), _jsx(DropdownMenuRadioItem, { value: "system", children: "Syst\u00E8me" })] }) })] }), onShortcuts ? (_jsxs(DropdownMenuItem, { onClick: onShortcuts, children: [_jsx(HugeiconsIcon, { icon: KeyboardIcon, "aria-hidden": true }), "Raccourcis clavier", _jsx(DropdownMenuShortcut, { children: "?" })] })) : null, products.length ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: "Produits HGDC" }), products.map((product) => product.current ? (_jsxs(DropdownMenuItem, { disabled: true, className: "data-disabled:opacity-100", children: [_jsx(HugeiconsIcon, { icon: product.icon ?? DashboardSquare01Icon, "aria-hidden": true }), _jsx("span", { className: "flex-1", children: product.name }), _jsx(HugeiconsIcon, { icon: Tick02Icon, "aria-label": "Produit ouvert" })] }, product.name)) : (_jsxs(MenuPrimitive.LinkItem, { href: product.href, className: MENU_LINK_ITEM, children: [_jsx(HugeiconsIcon, { icon: product.icon ?? DashboardSquare01Icon, "aria-hidden": true }), _jsx("span", { className: "flex-1", children: product.name }), product.hint ? (_jsx("span", { className: "text-xs font-normal text-muted-foreground", children: product.hint })) : null] }, product.name)))] })] })) : null, _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: onSignOut, children: [_jsx(HugeiconsIcon, { icon: Logout01Icon, "aria-hidden": true }), "Se d\u00E9connecter"] })] })] }));
45
+ }, children: [_jsx(DropdownMenuRadioItem, { value: "light", children: "Clair" }), _jsx(DropdownMenuRadioItem, { value: "dark", children: "Sombre" }), _jsx(DropdownMenuRadioItem, { value: "system", children: "Syst\u00E8me" })] }) })] }), onShortcuts ? (_jsxs(DropdownMenuItem, { onClick: onShortcuts, children: [_jsx(HugeiconsIcon, { icon: KeyboardIcon, "aria-hidden": true }), "Raccourcis clavier", _jsx(DropdownMenuShortcut, { children: "?" })] })) : null, products.length ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: "Produits" }), products.map((product) => product.current ? (_jsxs(DropdownMenuItem, { disabled: true, className: "data-disabled:opacity-100", children: [_jsx(ProductMenuIcon, { product: product }), _jsx("span", { className: "flex-1", children: product.name }), _jsx(HugeiconsIcon, { icon: Tick02Icon, "aria-label": "Produit ouvert" })] }, product.name)) : (_jsxs(MenuPrimitive.LinkItem, { href: product.href, className: MENU_LINK_ITEM, children: [_jsx(ProductMenuIcon, { product: product }), _jsx("span", { className: "flex-1", children: product.name }), product.hint ? (_jsx("span", { className: "text-xs font-normal text-muted-foreground", children: product.hint })) : null] }, product.name)))] })] })) : null, _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: onSignOut, children: [_jsx(HugeiconsIcon, { icon: Logout01Icon, "aria-hidden": true }), "Se d\u00E9connecter"] })] })] }));
46
+ }
47
+ function ProductMenuIcon({ product }) {
48
+ if (product.badge)
49
+ return _jsx(ProductBadge, { product: product.badge });
50
+ return _jsx(HugeiconsIcon, { icon: product.icon ?? DashboardSquare01Icon, "aria-hidden": true });
45
51
  }
@@ -1,5 +1,6 @@
1
1
  import { type IconSvgElement } from "@hugeicons/react";
2
2
  import { type ReactNode } from "react";
3
+ import type { FamilyProduct } from "./family-svg.ts";
3
4
  /**
4
5
  * À poser dans le `<head>`, avec `themeInitScript` : il remet le rail avant la première
5
6
  * peinture, et la variante CSS `rail:` le lit, donc rien ne saute au chargement.
@@ -33,17 +34,28 @@ export declare function SidebarNavItem({ icon, label, active, href, render, }: {
33
34
  render?: React.ReactElement;
34
35
  }): React.JSX.Element;
35
36
  /**
36
- * La coque commune des consoles HGDC, celle de Host. Sur ordinateur : colonne teintée à gauche
37
- * (organisation en haut, navigation, compte en bas), réductible en rail (`Ctrl+B`, mémorisé), et
38
- * la page sur une carte à côté. Sur téléphone : une barre en haut, le menu dans un tiroir qui
39
- * monte du bas et se referme quand on choisit une page.
37
+ * La coque commune des consoles HGDC, celle de Host. Sur ordinateur, de gauche à droite : la barre
38
+ * des produits de la famille (organisation en haut, pastilles, Account en bas, élargie au survol
39
+ * par-dessus la page), la barre latérale du produit (sa marque, sa navigation, le compte en bas),
40
+ * réductible en rail (`Ctrl+B`, mémorisé), et la page sur une carte. Sur téléphone : une barre en
41
+ * haut, le menu dans un tiroir qui monte du bas et se referme quand on choisit une page ; les
42
+ * produits restent dans le menu de l'avatar.
40
43
  *
41
44
  * Le kit ne connaît ni le routeur ni l'authentification : le produit passe ses éléments
42
45
  * (`OrgSwitcher`, `SidebarNav`, `AccountMenu`) et ses liens.
43
46
  */
44
- export declare function AppShell({ sidebarHeader, nav, sidebarFooter, header, children, mainClassName, }: {
45
- /** Haut de la barre : `OrgSwitcher`. */
46
- sidebarHeader?: ReactNode;
47
+ export declare function AppShell({ product, organization, productRail, accountHref, nav, sidebarFooter, header, children, mainClassName, }: {
48
+ /** Le produit affiché (`"host"`) : sa marque en haut de la barre, « vous êtes ici » dans la barre des produits. */
49
+ product: FamilyProduct;
50
+ /** `OrgSwitcher` : en haut de la barre des produits, en haut du tiroir sur téléphone. */
51
+ organization?: ReactNode;
52
+ /**
53
+ * `false` : pas de barre des produits (un écran hors de la famille). L'organisation passe alors
54
+ * sous la marque, dans la barre latérale.
55
+ */
56
+ productRail?: boolean;
57
+ /** Adresse d'Account dans la barre des produits (un Account local en développement). */
58
+ accountHref?: string | undefined;
47
59
  /** `SidebarNav` et ses `SidebarNavItem`. */
48
60
  nav: ReactNode;
49
61
  /** Bas de la barre : liens secondaires (Docs, Paramètres) et `AccountMenu`. */
package/dist/app-shell.js CHANGED
@@ -6,6 +6,10 @@ import { HugeiconsIcon } from "@hugeicons/react";
6
6
  import { useRender } from "@base-ui/react/use-render";
7
7
  import { createContext, useContext, useEffect, useState, useSyncExternalStore } from "react";
8
8
  import { Drawer, DrawerContent, DrawerTitle } from "./drawer.js";
9
+ import { isApplePlatform, Kbd, KbdGroup } from "./kbd.js";
10
+ import { ProductBadge } from "./product-badge.js";
11
+ import { ProductRail } from "./product-rail.js";
12
+ import { HGDC_ACCOUNT, HGDC_PRODUCTS } from "./products.js";
9
13
  import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
10
14
  import { cn } from "./cn.js";
11
15
  // ── Rail : la barre latérale réduite aux icônes, mémorisée par navigateur ──────────────
@@ -90,18 +94,42 @@ export function SidebarNavItem({ icon, label, active, href, render, }) {
90
94
  return (_jsx("li", { children: _jsxs(Tooltip, { disabled: !rail, children: [_jsx(TooltipTrigger, { render: link }), _jsx(TooltipContent, { side: "right", children: label })] }) }));
91
95
  }
92
96
  // ── Coque ─────────────────────────────────────────────────────────────────────────────
97
+ // Les noms de Hugeicons disent l'état du panneau, pas le geste : « Open » dessine la flèche vers la
98
+ // gauche (réduire), « Close » vers la droite (déplier).
99
+ const COLLAPSE_ICON = PanelLeftOpenIcon;
100
+ const EXPAND_ICON = PanelLeftCloseIcon;
101
+ /** Même boîte qu'une entrée de la barre en rail : 40 × 36 px, icône de 18 px centrée. */
102
+ const TOGGLE = "relative grid h-9 w-10 shrink-0 place-items-center rounded-xl text-shell-muted outline-none transition-colors duration-(--duration-fast) ease-out hover:bg-shell-hover hover:text-shell-foreground focus-visible:ring-2 focus-visible:ring-ring/50";
103
+ function ShortcutHint({ label }) {
104
+ return (_jsxs("span", { className: "flex items-center gap-2", children: [label, _jsxs(KbdGroup, { children: [_jsx(Kbd, { children: isApplePlatform() ? "⌘" : "Ctrl" }), _jsx(Kbd, { children: "B" })] })] }));
105
+ }
106
+ function productName(product) {
107
+ if (product === HGDC_ACCOUNT.badge)
108
+ return HGDC_ACCOUNT.name;
109
+ return HGDC_PRODUCTS.find((item) => item.badge === product)?.name ?? `HGDC ${product}`;
110
+ }
111
+ /**
112
+ * Haut de la barre latérale. Dépliée : la marque du produit, et le bouton « Réduire » à droite.
113
+ * En rail : la marque seule, qui devient le bouton « Déplier » au survol de la barre ou au focus,
114
+ * à la même place : le bouton ne descend jamais sous le logo. Les deux états sont dans la page et
115
+ * choisis par la variante CSS `rail:`, donc justes dès la première peinture.
116
+ */
117
+ function SidebarHead({ product }) {
118
+ return (_jsxs("div", { className: "mt-2 flex h-14 shrink-0 items-center gap-1 px-3", children: [_jsxs("div", { "data-slot": "sidebar-brand", className: "flex min-w-0 flex-1 items-center gap-2.5 pl-2.25 rail:hidden", children: [_jsx(ProductBadge, { product: product, className: "size-6" }), _jsx("span", { className: "truncate font-heading text-[0.9375rem] font-semibold", children: productName(product) })] }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx("button", { type: "button", "data-slot": "sidebar-collapse", onClick: toggleSidebar, "aria-label": "R\u00E9duire la barre lat\u00E9rale", "aria-keyshortcuts": "Control+B Meta+B", className: cn(TOGGLE, "rail:hidden") }), children: _jsx(HugeiconsIcon, { icon: COLLAPSE_ICON, strokeWidth: 1.8, "aria-hidden": true, className: "size-[1.125rem]" }) }), _jsx(TooltipContent, { side: "right", children: _jsx(ShortcutHint, { label: "R\u00E9duire" }) })] }), _jsxs(Tooltip, { children: [_jsxs(TooltipTrigger, { render: _jsx("button", { type: "button", "data-slot": "sidebar-expand", onClick: toggleSidebar, "aria-label": "D\u00E9plier la barre lat\u00E9rale", "aria-keyshortcuts": "Control+B Meta+B", className: cn(TOGGLE, "group/toggle hidden rail:grid") }), children: [_jsx(ProductBadge, { product: product, className: "col-start-1 row-start-1 size-6 transition-opacity duration-(--duration-fast) ease-out group-hover/sidebar:opacity-0 group-focus-visible/toggle:opacity-0 motion-reduce:transition-none" }), _jsx(HugeiconsIcon, { icon: EXPAND_ICON, strokeWidth: 1.8, "aria-hidden": true, className: "col-start-1 row-start-1 size-[1.125rem] opacity-0 transition-opacity duration-(--duration-fast) ease-out group-hover/sidebar:opacity-100 group-focus-visible/toggle:opacity-100 motion-reduce:transition-none" })] }), _jsx(TooltipContent, { side: "right", children: _jsx(ShortcutHint, { label: "D\u00E9plier" }) })] })] }));
119
+ }
93
120
  /**
94
- * La coque commune des consoles HGDC, celle de Host. Sur ordinateur : colonne teintée à gauche
95
- * (organisation en haut, navigation, compte en bas), réductible en rail (`Ctrl+B`, mémorisé), et
96
- * la page sur une carte à côté. Sur téléphone : une barre en haut, le menu dans un tiroir qui
97
- * monte du bas et se referme quand on choisit une page.
121
+ * La coque commune des consoles HGDC, celle de Host. Sur ordinateur, de gauche à droite : la barre
122
+ * des produits de la famille (organisation en haut, pastilles, Account en bas, élargie au survol
123
+ * par-dessus la page), la barre latérale du produit (sa marque, sa navigation, le compte en bas),
124
+ * réductible en rail (`Ctrl+B`, mémorisé), et la page sur une carte. Sur téléphone : une barre en
125
+ * haut, le menu dans un tiroir qui monte du bas et se referme quand on choisit une page ; les
126
+ * produits restent dans le menu de l'avatar.
98
127
  *
99
128
  * Le kit ne connaît ni le routeur ni l'authentification : le produit passe ses éléments
100
129
  * (`OrgSwitcher`, `SidebarNav`, `AccountMenu`) et ses liens.
101
130
  */
102
- export function AppShell({ sidebarHeader, nav, sidebarFooter, header, children, mainClassName, }) {
103
- const rail = useSidebarRail();
131
+ export function AppShell({ product, organization, productRail = true, accountHref, nav, sidebarFooter, header, children, mainClassName, }) {
104
132
  const [menuOpen, setMenuOpen] = useState(false);
105
133
  useToggleShortcut();
106
- return (_jsx(ShellContext.Provider, { value: { closeMobileNav: () => setMenuOpen(false) }, children: _jsxs("div", { "data-slot": "app-shell", className: "min-h-dvh lg:grid lg:h-dvh lg:grid-cols-[auto_minmax(0,1fr)] 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", { "aria-label": "Barre lat\u00E9rale", className: "hidden overflow-hidden text-shell-foreground transition-[width] duration-(--duration-drawer) ease-ios motion-reduce:transition-none lg:flex lg:w-64 lg:flex-col lg:rail:w-16", children: [_jsxs("div", { className: "mt-2 flex min-h-14 shrink-0 items-center gap-1 px-3 rail:flex-col rail:justify-center rail:px-0 rail:py-1", children: [_jsx("div", { className: "min-w-0 flex-1 rail:flex-none", children: sidebarHeader }), _jsx("button", { type: "button", onClick: toggleSidebar, "aria-label": rail ? "Déplier la barre latérale" : "Réduire la barre latérale", "aria-keyshortcuts": "Control+B Meta+B", title: rail ? "Déplier (Ctrl+B)" : "Réduire (Ctrl+B)", className: "grid size-8 shrink-0 place-items-center rounded-lg text-shell-muted outline-none transition-colors duration-(--duration-fast) ease-out hover:bg-shell-hover hover:text-shell-foreground focus-visible:ring-2 focus-visible:ring-ring/50 active:scale-[0.96]", children: _jsx(HugeiconsIcon, { icon: rail ? PanelLeftCloseIcon : PanelLeftOpenIcon, strokeWidth: 1.8, "aria-hidden": true, className: "size-[1.125rem]" }) })] }), nav, sidebarFooter ? _jsx("div", { className: "grid gap-0.5 px-3 pt-1 pb-3", children: sidebarFooter }) : null] }), _jsx("div", { className: "flex min-h-dvh flex-col lg:min-h-0 lg:p-2 lg:pl-0", children: _jsxs("div", { className: "flex flex-1 flex-col lg:min-h-0 lg:overflow-y-auto lg:rounded-xl lg:bg-background", children: [_jsxs("header", { className: "sticky top-[env(safe-area-inset-top,0px)] z-30 flex h-14 shrink-0 items-center gap-1 border-b border-foreground/5 bg-background px-2 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" }) }), _jsx("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: header })] }), _jsx("main", { id: "contenu", tabIndex: -1, 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: children })] }) }), _jsx(Drawer, { side: "bottom", open: menuOpen, onOpenChange: setMenuOpen, children: _jsxs(DrawerContent, { className: "lg:hidden", children: [_jsx(DrawerTitle, { className: "sr-only", children: "Menu" }), sidebarHeader ? _jsx("div", { className: "px-3 pt-1", children: sidebarHeader }) : null, nav, sidebarFooter ? (_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)]", children: sidebarFooter })) : null] }) })] }) }));
134
+ return (_jsx(ShellContext.Provider, { value: { closeMobileNav: () => setMenuOpen(false) }, children: _jsxs("div", { "data-slot": "app-shell", className: cn("min-h-dvh lg:grid lg:h-dvh lg:bg-shell", productRail ? "lg:grid-cols-[auto_auto_minmax(0,1fr)]" : "lg:grid-cols-[auto_minmax(0,1fr)]"), children: [_jsx("a", { href: "#contenu", className: "sr-only z-50 rounded-xl bg-background px-4 py-2 text-sm font-medium focus:not-sr-only focus:fixed focus:top-3 focus:left-3 focus:ring-2 focus:ring-ring", children: "Aller au contenu" }), productRail ? _jsx(ProductRail, { current: product, organization: organization, accountHref: accountHref }) : null, _jsxs("aside", { "aria-label": "Barre lat\u00E9rale", className: "group/sidebar hidden overflow-hidden text-shell-foreground transition-[width] duration-(--duration-drawer) ease-ios motion-reduce:transition-none lg:flex lg:w-64 lg:flex-col lg:rail:w-16", children: [_jsx(SidebarHead, { product: product }), !productRail && organization ? _jsx("div", { className: "shrink-0 px-3 pb-1", children: organization }) : null, nav, sidebarFooter ? _jsx("div", { className: "grid gap-0.5 px-3 pt-1 pb-3", children: sidebarFooter }) : null] }), _jsx("div", { className: "flex min-h-dvh flex-col lg:min-h-0 lg:p-2 lg:pl-0", children: _jsxs("div", { className: "flex flex-1 flex-col lg:min-h-0 lg:overflow-y-auto lg:rounded-xl lg:bg-background", children: [_jsxs("header", { className: "sticky top-[env(safe-area-inset-top,0px)] z-30 flex h-14 shrink-0 items-center gap-1 border-b border-foreground/5 bg-background px-2 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" }) }), _jsx("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: header })] }), _jsx("main", { id: "contenu", tabIndex: -1, 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: children })] }) }), _jsx(Drawer, { side: "bottom", open: menuOpen, onOpenChange: setMenuOpen, children: _jsxs(DrawerContent, { className: "lg:hidden", children: [_jsx(DrawerTitle, { className: "sr-only", children: "Menu" }), organization ? _jsx("div", { className: "px-3 pt-1", children: organization }) : null, nav, sidebarFooter ? (_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)]", children: sidebarFooter })) : null] }) })] }) }));
107
135
  }
@@ -1,9 +1,12 @@
1
1
  import { type IconSvgElement } from "@hugeicons/react";
2
+ import type { FamilyProduct } from "./family-svg.ts";
2
3
  export type Command = {
3
4
  /** Unique dans toute la palette. */
4
5
  id: string;
5
6
  label: string;
6
7
  icon?: IconSvgElement;
8
+ /** Pastille d'un produit (« Ouvrir Mail »), à la place de `icon`. */
9
+ badge?: FamilyProduct;
7
10
  /** Autres mots qui trouvent la commande : « dns » pour « Ajouter un domaine ». */
8
11
  keywords?: Array<string>;
9
12
  /** Texte discret à droite : l'organisation d'un projet, « Page ». */
@@ -5,6 +5,7 @@ import Search01Icon from "@hugeicons/core-free-icons/Search01Icon";
5
5
  import { HugeiconsIcon } from "@hugeicons/react";
6
6
  import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
7
7
  import { FLOATING_SURFACE } from "./floating.js";
8
+ import { ProductBadge } from "./product-badge.js";
8
9
  import { Kbd, KbdGroup, isApplePlatform } from "./kbd.js";
9
10
  import { cn } from "./cn.js";
10
11
  function normalize(text) {
@@ -63,7 +64,7 @@ export function CommandMenu({ groups, open: openProp, onOpenChange, placeholder
63
64
  return (_jsx(DialogPrimitive.Root, { open: open, onOpenChange: setOpen, children: _jsxs(DialogPrimitive.Portal, { children: [_jsx(DialogPrimitive.Backdrop, { className: "fixed inset-0 z-50 bg-foreground/30 dark:bg-black/60" }), _jsx(DialogPrimitive.Viewport, { className: "fixed inset-0 z-50 flex items-start justify-center px-2 pt-[min(12dvh,6rem)] pb-2", children: _jsx(DialogPrimitive.Popup, { "data-slot": "command-menu", "aria-label": "Palette de commandes", className: cn(FLOATING_SURFACE, "flex max-h-[min(36rem,calc(100dvh-2rem))] w-full max-w-xl flex-col overflow-hidden rounded-3xl shadow-2xl shadow-black/10 outline-none"), children: _jsxs(Autocomplete.Root, { open: true, inline: true, items: visibleGroups, itemToStringValue: (command) => command.label, filter: (command, query) => matchCommand(command, query), autoHighlight: "always", keepHighlight: true, children: [_jsxs(Autocomplete.InputGroup, { className: "flex items-center gap-3 border-b border-foreground/5 px-4", children: [_jsx(HugeiconsIcon, { icon: Search01Icon, strokeWidth: 2, "aria-hidden": true, className: "size-4.5 shrink-0 text-muted-foreground" }), _jsx(Autocomplete.Input, { "aria-label": "Rechercher une commande", placeholder: placeholder, className: "h-14 w-full min-w-0 bg-transparent text-base outline-none placeholder:text-muted-foreground" }), _jsx(DialogPrimitive.Close, { className: "sr-only", children: "Fermer la palette" })] }), _jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto overscroll-contain p-1.5 [scroll-padding-block:0.375rem]", children: [_jsx(Autocomplete.Empty, { className: "px-3 py-10 text-center text-sm text-muted-foreground empty:hidden", children: emptyLabel }), _jsx(Autocomplete.List, { children: (group) => (_jsxs(Autocomplete.Group, { items: group.items, className: "not-last:mb-1.5", children: [_jsx(Autocomplete.GroupLabel, { className: "px-3 pt-2 pb-1.5 text-xs font-medium text-muted-foreground", children: group.label }), _jsx(Autocomplete.Collection, { children: (command) => (_jsxs(Autocomplete.Item, { value: command, onClick: () => {
64
65
  setOpen(false);
65
66
  command.onSelect();
66
- }, className: "flex min-h-11 cursor-default items-center gap-3 rounded-xl px-3 py-2 text-sm font-medium outline-none select-none data-highlighted:bg-muted lg:min-h-10", children: [command.icon ? (_jsx(HugeiconsIcon, { icon: command.icon, strokeWidth: 1.8, "aria-hidden": true, className: "size-4.5 shrink-0 text-muted-foreground" })) : null, _jsx("span", { className: "min-w-0 flex-1 truncate", children: command.label }), command.hint ? (_jsx("span", { className: "shrink-0 truncate text-xs font-normal text-muted-foreground", children: command.hint })) : null, command.shortcut ? (_jsx(KbdGroup, { className: "hidden shrink-0 sm:inline-flex", children: command.shortcut.map((key) => (_jsx(Kbd, { children: key }, key))) })) : null] }, command.id)) })] }, group.label)) })] }), _jsxs("div", { className: "hidden items-center gap-4 border-t border-foreground/5 px-4 py-2.5 text-xs text-muted-foreground sm:flex", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsxs(KbdGroup, { children: [_jsx(Kbd, { children: "\u2191" }), _jsx(Kbd, { children: "\u2193" })] }), "Choisir"] }), _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Kbd, { children: "Entr\u00E9e" }), "Ouvrir"] }), _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Kbd, { children: "\u00C9chap" }), "Fermer"] })] })] }) }) })] }) }));
67
+ }, className: "flex min-h-11 cursor-default items-center gap-3 rounded-xl px-3 py-2 text-sm font-medium outline-none select-none data-highlighted:bg-muted lg:min-h-10", children: [command.badge ? (_jsx(ProductBadge, { product: command.badge, className: "size-4.5" })) : command.icon ? (_jsx(HugeiconsIcon, { icon: command.icon, strokeWidth: 1.8, "aria-hidden": true, className: "size-4.5 shrink-0 text-muted-foreground" })) : null, _jsx("span", { className: "min-w-0 flex-1 truncate", children: command.label }), command.hint ? (_jsx("span", { className: "shrink-0 truncate text-xs font-normal text-muted-foreground", children: command.hint })) : null, command.shortcut ? (_jsx(KbdGroup, { className: "hidden shrink-0 sm:inline-flex", children: command.shortcut.map((key) => (_jsx(Kbd, { children: key }, key))) })) : null] }, command.id)) })] }, group.label)) })] }), _jsxs("div", { className: "hidden items-center gap-4 border-t border-foreground/5 px-4 py-2.5 text-xs text-muted-foreground sm:flex", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsxs(KbdGroup, { children: [_jsx(Kbd, { children: "\u2191" }), _jsx(Kbd, { children: "\u2193" })] }), "Choisir"] }), _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Kbd, { children: "Entr\u00E9e" }), "Ouvrir"] }), _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Kbd, { children: "\u00C9chap" }), "Fermer"] })] })] }) }) })] }) }));
67
68
  }
68
69
  function noop() { }
69
70
  function subscribeNever() {
package/dist/email.js CHANGED
@@ -85,7 +85,7 @@ export function EmailLayout({ product: ref, preview, children, name, iconSrc, ac
85
85
  border: `1px solid ${l.border}`,
86
86
  borderRadius: 16,
87
87
  padding: "32px 28px",
88
- }, children: children }), _jsxs(Section, { style: { padding: "20px 4px 0" }, children: [footer ? (_jsx(Text, { className: "hg-muted", style: { ...muted, margin: "0 0 12px" }, children: footer })) : null, _jsxs(Text, { className: "hg-muted", style: muted, children: [_jsx(Img, { src: emailIconUrl("mail"), width: 16, height: 16, alt: "", style: { display: "inline-block", verticalAlign: "middle", borderRadius: 8, marginRight: 6 } }), _jsxs("span", { style: { verticalAlign: "middle" }, children: ["Propuls\u00E9 par", " ", _jsx(Link, { href: MAIL_HREF, className: "hg-muted", style: { color: l.muted, textDecoration: "underline" }, children: "HGDC Mail" })] })] }), _jsx(Text, { className: "hg-muted", style: { ...muted, margin: "4px 0 0" }, children: LEGAL })] })] }) }) })] }));
88
+ }, children: children }), _jsxs(Section, { style: { padding: "20px 4px 0" }, children: [footer ? (_jsx(Text, { className: "hg-muted", style: { ...muted, margin: "0 0 12px" }, children: footer })) : null, _jsxs(Text, { className: "hg-muted", style: muted, children: [_jsx(Img, { src: emailIconUrl("mail"), width: 16, height: 16, alt: "", style: { display: "inline-block", verticalAlign: "middle", borderRadius: 8, marginRight: 6 } }), _jsxs("span", { style: { verticalAlign: "middle" }, children: ["Envoy\u00E9 avec", " ", _jsx(Link, { href: MAIL_HREF, className: "hg-muted", style: { color: l.muted, textDecoration: "underline" }, children: "Mail" }), " ", "de la H family"] })] }), _jsx(Text, { className: "hg-muted", style: { ...muted, margin: "4px 0 0" }, children: LEGAL })] })] }) }) })] }));
89
89
  }
90
90
  export function EmailHeading({ children }) {
91
91
  return (_jsx(Heading, { as: "h1", className: "hg-ink hg-heading", style: { color: EMAIL_LIGHT.ink, fontSize: 22, fontWeight: 600, lineHeight: "28px", margin: "0 0 12px" }, children: children }));
@@ -0,0 +1,9 @@
1
+ /** Les produits de la H family qui ont une pastille, par leur mot en minuscules. */
2
+ export declare const FAMILY_PRODUCTS: readonly ["account", "host", "mail", "portal", "status", "analytics", "docs"];
3
+ export type FamilyProduct = (typeof FAMILY_PRODUCTS)[number];
4
+ /** Vrai si le mot en minuscules (« host ») désigne un produit qui a une pastille. */
5
+ export declare function isFamilyProduct(word: string): word is FamilyProduct;
6
+ /** SVG de la pastille de chaque produit (`family/<mot>.svg`). */
7
+ export declare const PRODUCT_BADGE_SVG: Readonly<Record<FamilyProduct, string>>;
8
+ /** SVG du H tricolore, l'icône de la famille (`family/hfamily-icon.svg`). */
9
+ export declare const FAMILY_MARK_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 256 256\"><defs><linearGradient id=\"l\" x1=\"0\" y1=\"24\" x2=\"0\" y2=\"232\" gradientUnits=\"userSpaceOnUse\"><stop offset=\"0\" stop-color=\"#2c78fd\"/><stop offset=\"1\" stop-color=\"#7343fe\"/></linearGradient><linearGradient id=\"r\" x1=\"0\" y1=\"24\" x2=\"0\" y2=\"232\" gradientUnits=\"userSpaceOnUse\"><stop offset=\"0\" stop-color=\"#fe5292\"/><stop offset=\"1\" stop-color=\"#ff9a46\"/></linearGradient><linearGradient id=\"b\" x1=\"64\" y1=\"0\" x2=\"192\" y2=\"0\" gradientUnits=\"userSpaceOnUse\"><stop offset=\"0\" stop-color=\"#8145ff\"/><stop offset=\"1\" stop-color=\"#fe5292\"/></linearGradient></defs><rect x=\"43\" y=\"24\" width=\"57\" height=\"208\" rx=\"18\" fill=\"url(#l)\"/><rect x=\"156\" y=\"24\" width=\"57\" height=\"208\" rx=\"18\" fill=\"url(#r)\"/><rect x=\"67\" y=\"107\" width=\"122\" height=\"43\" rx=\"14\" fill=\"url(#b)\"/></svg>";
@@ -0,0 +1,19 @@
1
+ // Généré par `bun scripts/family.ts` depuis `packages/ui/family/`. Ne pas modifier à la main.
2
+ /** Les produits de la H family qui ont une pastille, par leur mot en minuscules. */
3
+ export const FAMILY_PRODUCTS = ["account", "host", "mail", "portal", "status", "analytics", "docs"];
4
+ /** Vrai si le mot en minuscules (« host ») désigne un produit qui a une pastille. */
5
+ export function isFamilyProduct(word) {
6
+ return FAMILY_PRODUCTS.includes(word);
7
+ }
8
+ /** SVG de la pastille de chaque produit (`family/<mot>.svg`). */
9
+ export const PRODUCT_BADGE_SVG = {
10
+ account: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="66 66 380 380"><defs><linearGradient id="sky" x1="96" y1="76" x2="416" y2="436" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff3b47"/><stop offset="1" stop-color="#ff6352"/></linearGradient><linearGradient id="acc" x1="150" y1="160" x2="362" y2="352" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff3b47"/><stop offset="1" stop-color="#ff6352"/></linearGradient></defs><circle cx="256" cy="256" r="190" fill="url(#sky)"/><rect x="128" y="162" width="256" height="188" rx="48" fill="#fff"/><g fill="url(#acc)"><circle cx="202" cy="232" r="30"/><path d="M154 314A48 44 0 0 1 250 314Z"/><rect x="276" y="216" width="72" height="24" rx="12"/><rect x="276" y="262" width="48" height="24" rx="12"/></g></svg>',
11
+ host: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="66 66 380 380"><defs><linearGradient id="sky" x1="96" y1="76" x2="416" y2="436" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#194ff7"/><stop offset="1" stop-color="#02b4ff"/></linearGradient><linearGradient id="acc" x1="150" y1="160" x2="362" y2="352" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#194ff7"/><stop offset="1" stop-color="#02b4ff"/></linearGradient></defs><circle cx="256" cy="256" r="190" fill="url(#sky)"/><g transform="translate(0 6)"><g fill="#fff"><path d="M158.7 331.3A262.3 262.3 0 0 0 353.3 331.3L334 283H178Z"/><circle cx="178" cy="283" r="52"/><circle cx="334" cy="283" r="52"/><circle cx="214" cy="238" r="54"/><circle cx="288" cy="222" r="72"/><rect x="178" y="232" width="156" height="100"/></g><path fill="none" stroke="url(#acc)" stroke-width="24" stroke-linecap="round" stroke-linejoin="round" d="M256 302V234M224 266L256 234L288 266"/></g></svg>',
12
+ mail: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="66 66 380 380"><defs><linearGradient id="sky" x1="96" y1="76" x2="416" y2="436" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f61f8e"/><stop offset="1" stop-color="#ff5cb8"/></linearGradient><linearGradient id="acc" x1="150" y1="160" x2="362" y2="352" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f61f8e"/><stop offset="1" stop-color="#ff5cb8"/></linearGradient></defs><circle cx="256" cy="256" r="190" fill="url(#sky)"/><rect x="128" y="162" width="256" height="188" rx="56" fill="#fff"/><path fill="url(#acc)" d="M182 194H330A22 22 0 0 1 341.82 234.56L271.04 279.62A28 28 0 0 1 240.96 279.62L170.18 234.56A22 22 0 0 1 182 194Z"/></svg>',
13
+ portal: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="66 66 380 380"><defs><linearGradient id="sky" x1="96" y1="76" x2="416" y2="436" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#4732ed"/><stop offset="1" stop-color="#b03cff"/></linearGradient><linearGradient id="acc" x1="150" y1="160" x2="362" y2="352" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#4732ed"/><stop offset="1" stop-color="#b03cff"/></linearGradient></defs><circle cx="256" cy="256" r="190" fill="url(#sky)"/><path fill="#fff" d="M168 160H222A24 24 0 0 1 240 168L258 188H344A40 40 0 0 1 384 228V316A40 40 0 0 1 344 356H168A40 40 0 0 1 128 316V200A40 40 0 0 1 168 160Z"/><path fill="none" stroke="url(#acc)" stroke-width="30" stroke-linecap="round" stroke-linejoin="round" d="M212 272L244 302L304 240"/></svg>',
14
+ status: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="66 66 380 380"><defs><linearGradient id="sky" x1="96" y1="76" x2="416" y2="436" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#199f62"/><stop offset="1" stop-color="#56db29"/></linearGradient><linearGradient id="acc" x1="150" y1="160" x2="362" y2="352" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#199f62"/><stop offset="1" stop-color="#56db29"/></linearGradient></defs><circle cx="256" cy="256" r="190" fill="url(#sky)"/><rect x="128" y="160" width="256" height="192" rx="48" fill="#fff"/><path fill="none" stroke="url(#acc)" stroke-width="26" stroke-linecap="round" stroke-linejoin="round" d="M164 262H206L230 214L262 304L288 248H348"/></svg>',
15
+ analytics: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="66 66 380 380"><defs><linearGradient id="sky" x1="96" y1="76" x2="416" y2="436" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fe5623"/><stop offset="1" stop-color="#ffb003"/></linearGradient><linearGradient id="acc" x1="150" y1="160" x2="362" y2="352" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fe5623"/><stop offset="1" stop-color="#ffb003"/></linearGradient></defs><circle cx="256" cy="256" r="190" fill="url(#sky)"/><rect x="128" y="160" width="256" height="192" rx="48" fill="#fff"/><path fill="none" stroke="url(#acc)" stroke-width="30" stroke-linecap="round" d="M198 304V276M256 304V240M314 304V206"/></svg>',
16
+ docs: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="66 66 380 380"><defs><linearGradient id="sky" x1="96" y1="76" x2="416" y2="436" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1bb9cf"/><stop offset="1" stop-color="#13e4c8"/></linearGradient><linearGradient id="acc" x1="150" y1="160" x2="362" y2="352" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1bb9cf"/><stop offset="1" stop-color="#13e4c8"/></linearGradient></defs><circle cx="256" cy="256" r="190" fill="url(#sky)"/><g fill="#fff"><path d="M248 196C222 176 184 168 146 174A14 14 0 0 0 134 188V324A12 12 0 0 0 148 336C184 332 220 338 248 356Z"/><path d="M264 196C290 176 328 168 366 174A14 14 0 0 1 378 188V324A12 12 0 0 1 364 336C328 332 292 338 264 356Z"/></g><path fill="none" stroke="url(#acc)" stroke-width="16" stroke-linecap="round" d="M166 226C188 224 208 228 224 238M166 266C188 264 208 268 224 278M288 238C304 228 324 224 346 226M288 278C304 268 324 264 346 266"/></svg>',
17
+ };
18
+ /** SVG du H tricolore, l'icône de la famille (`family/hfamily-icon.svg`). */
19
+ export const FAMILY_MARK_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><defs><linearGradient id="l" x1="0" y1="24" x2="0" y2="232" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2c78fd"/><stop offset="1" stop-color="#7343fe"/></linearGradient><linearGradient id="r" x1="0" y1="24" x2="0" y2="232" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fe5292"/><stop offset="1" stop-color="#ff9a46"/></linearGradient><linearGradient id="b" x1="64" y1="0" x2="192" y2="0" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8145ff"/><stop offset="1" stop-color="#fe5292"/></linearGradient></defs><rect x="43" y="24" width="57" height="208" rx="18" fill="url(#l)"/><rect x="156" y="24" width="57" height="208" rx="18" fill="url(#r)"/><rect x="67" y="107" width="122" height="43" rx="14" fill="url(#b)"/></svg>';
@@ -16,7 +16,9 @@ export declare function OrgMark({ org, className, }: {
16
16
  className?: string;
17
17
  }): React.JSX.Element;
18
18
  /**
19
- * Haut de la barre latérale : l'organisation active, et le menu pour en changer ou en créer une.
19
+ * L'organisation active, et le menu pour en changer ou en créer une. En haut de la barre des
20
+ * produits d'`AppShell` (la marque seule, le nom quand la barre s'élargit), dans le tiroir sur
21
+ * téléphone, ou en haut de la barre latérale quand le produit coupe la barre des produits.
20
22
  * Présentationnel : le produit passe la liste, l'active et ce qu'il faut faire au choix (changer
21
23
  * l'URL `/o/<slug>/…`), le kit n'appelle rien.
22
24
  */
@@ -5,6 +5,7 @@ import UnfoldMoreIcon from "@hugeicons/core-free-icons/UnfoldMoreIcon";
5
5
  import { HugeiconsIcon } from "@hugeicons/react";
6
6
  import { Menu as MenuPrimitive } from "@base-ui/react/menu";
7
7
  import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "./dropdown-menu.js";
8
+ import { useShellPlacement } from "./shell-placement.js";
8
9
  import { cn } from "./cn.js";
9
10
  export const ORG_ROLE_LABEL = {
10
11
  owner: "Propriétaire",
@@ -20,13 +21,24 @@ export function OrgMark({ org, className, }) {
20
21
  return (_jsx("span", { "aria-hidden": true, className: cn("grid size-8 shrink-0 place-items-center rounded-lg bg-foreground text-xs font-semibold text-background uppercase", className), children: org.name.slice(0, 2) }));
21
22
  }
22
23
  /**
23
- * Haut de la barre latérale : l'organisation active, et le menu pour en changer ou en créer une.
24
+ * L'organisation active, et le menu pour en changer ou en créer une. En haut de la barre des
25
+ * produits d'`AppShell` (la marque seule, le nom quand la barre s'élargit), dans le tiroir sur
26
+ * téléphone, ou en haut de la barre latérale quand le produit coupe la barre des produits.
24
27
  * Présentationnel : le produit passe la liste, l'active et ce qu'il faut faire au choix (changer
25
28
  * l'URL `/o/<slug>/…`), le kit n'appelle rien.
26
29
  */
27
30
  export function OrgSwitcher({ organizations, activeId, onSelect, createHref, onCreate, className, }) {
31
+ const placement = useShellPlacement();
28
32
  const active = organizations.find((org) => org.id === activeId);
29
33
  if (!active)
30
34
  return null;
31
- return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: _jsx("button", { type: "button", "aria-label": `Organisation ${active.name}, changer d'organisation`, className: cn("flex h-10 w-full min-w-0 items-center gap-2.5 rounded-xl px-1.5 text-left 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", "lg:hover:bg-shell-hover lg:aria-expanded:bg-shell-hover lg:rail:w-auto lg:rail:justify-center lg:rail:px-1", className) }), children: [_jsx(OrgMark, { org: active }), _jsxs("span", { className: "min-w-0 flex-1 lg:rail:hidden", children: [_jsx("span", { className: "block truncate text-sm font-medium", children: active.name }), active.role ? (_jsx("span", { className: "block truncate text-xs text-muted-foreground lg:text-shell-muted", children: ORG_ROLE_LABEL[active.role] })) : null] }), _jsx(HugeiconsIcon, { icon: UnfoldMoreIcon, "aria-hidden": true, className: "size-4 shrink-0 text-muted-foreground lg:text-shell-muted lg:rail:hidden" })] }), _jsxs(DropdownMenuContent, { align: "start", className: "w-64", children: [_jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: "Organisations" }), organizations.map((org) => (_jsxs(DropdownMenuItem, { onClick: () => onSelect(org.id), children: [_jsx(OrgMark, { org: org, className: "size-6 rounded-md bg-muted text-[10px] text-foreground" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: org.name }), org.id === activeId ? (_jsx(HugeiconsIcon, { icon: Tick02Icon, "aria-label": "Organisation active", className: "size-4" })) : org.role ? (_jsx("span", { className: "text-xs font-normal text-muted-foreground", children: ORG_ROLE_LABEL[org.role] })) : null] }, org.id)))] }), createHref !== undefined ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(MenuPrimitive.LinkItem, { href: createHref, className: MENU_LINK_ITEM, children: [_jsx(HugeiconsIcon, { icon: PlusSignIcon, "aria-hidden": true }), "Cr\u00E9er une organisation"] })] })) : onCreate !== undefined ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: onCreate, children: [_jsx(HugeiconsIcon, { icon: PlusSignIcon, "aria-hidden": true }), "Cr\u00E9er une organisation"] })] })) : null] })] }));
35
+ // En haut de la barre des produits : la marque seule dans 40 px, le nom quand la barre s'élargit.
36
+ const inRail = placement === "product-rail";
37
+ return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: _jsx("button", { type: "button", "aria-label": `Organisation ${active.name}, changer d'organisation`, className: cn("flex h-10 w-full min-w-0 items-center gap-2.5 rounded-xl px-1.5 text-left 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", inRail
38
+ ? "shrink-0 gap-3 px-1 hover:bg-shell-hover aria-expanded:bg-shell-hover"
39
+ : "lg:hover:bg-shell-hover lg:aria-expanded:bg-shell-hover lg:rail:w-auto lg:rail:justify-center lg:rail:px-1", className) }), children: [_jsx(OrgMark, { org: active }), _jsxs("span", { className: cn("min-w-0 flex-1", inRail
40
+ ? "opacity-0 transition-opacity duration-(--duration-fast) ease-out group-data-expanded/rail:opacity-100 motion-reduce:transition-none"
41
+ : "lg:rail:hidden"), children: [_jsx("span", { className: "block truncate text-sm font-medium", children: active.name }), active.role ? (_jsx("span", { className: "block truncate text-xs text-muted-foreground lg:text-shell-muted", children: ORG_ROLE_LABEL[active.role] })) : null] }), _jsx(HugeiconsIcon, { icon: UnfoldMoreIcon, "aria-hidden": true, className: cn("size-4 shrink-0 text-muted-foreground lg:text-shell-muted", inRail
42
+ ? "opacity-0 transition-opacity duration-(--duration-fast) group-data-expanded/rail:opacity-100"
43
+ : "lg:rail:hidden") })] }), _jsxs(DropdownMenuContent, { align: "start", className: "w-64", children: [_jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: "Organisations" }), organizations.map((org) => (_jsxs(DropdownMenuItem, { onClick: () => onSelect(org.id), children: [_jsx(OrgMark, { org: org, className: "size-6 rounded-md bg-muted text-[10px] text-foreground" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: org.name }), org.id === activeId ? (_jsx(HugeiconsIcon, { icon: Tick02Icon, "aria-label": "Organisation active", className: "size-4" })) : org.role ? (_jsx("span", { className: "text-xs font-normal text-muted-foreground", children: ORG_ROLE_LABEL[org.role] })) : null] }, org.id)))] }), createHref !== undefined ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(MenuPrimitive.LinkItem, { href: createHref, className: MENU_LINK_ITEM, children: [_jsx(HugeiconsIcon, { icon: PlusSignIcon, "aria-hidden": true }), "Cr\u00E9er une organisation"] })] })) : onCreate !== undefined ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: onCreate, children: [_jsx(HugeiconsIcon, { icon: PlusSignIcon, "aria-hidden": true }), "Cr\u00E9er une organisation"] })] })) : null] })] }));
32
44
  }
@@ -0,0 +1,24 @@
1
+ import { type FamilyProduct } from "./family-svg.ts";
2
+ /**
3
+ * Dégradé de chaque pastille, du coin haut gauche au coin bas droit. Mêmes valeurs que les jetons
4
+ * `--product-<mot>-from` / `--product-<mot>-to` de `styles.css` et que les SVG de `family/`.
5
+ */
6
+ export declare const PRODUCT_GRADIENT: Readonly<Record<FamilyProduct, {
7
+ from: string;
8
+ to: string;
9
+ }>>;
10
+ type MarkProps = {
11
+ className?: string;
12
+ /** Texte de remplacement. Absent : image décorative, le nom du produit est écrit à côté. */
13
+ label?: string;
14
+ };
15
+ /**
16
+ * Pastille d'un produit (dégradé plein, pictogramme blanc), toujours seule : jamais dans une tuile
17
+ * de couleur. `size-4` par défaut, à agrandir par `className`.
18
+ */
19
+ export declare function ProductBadge({ product, className, label }: MarkProps & {
20
+ product: FamilyProduct;
21
+ }): React.JSX.Element;
22
+ /** Le H tricolore de la famille (« Propulsé par », hub). `size-4` par défaut. */
23
+ export declare function FamilyMark({ className, label }: MarkProps): React.JSX.Element;
24
+ export {};
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from "./cn.js";
3
+ import { FAMILY_MARK_SVG, PRODUCT_BADGE_SVG } from "./family-svg.js";
4
+ /**
5
+ * Dégradé de chaque pastille, du coin haut gauche au coin bas droit. Mêmes valeurs que les jetons
6
+ * `--product-<mot>-from` / `--product-<mot>-to` de `styles.css` et que les SVG de `family/`.
7
+ */
8
+ export const PRODUCT_GRADIENT = {
9
+ account: { from: "#ff3b47", to: "#ff6352" },
10
+ host: { from: "#194ff7", to: "#02b4ff" },
11
+ mail: { from: "#f61f8e", to: "#ff5cb8" },
12
+ portal: { from: "#4732ed", to: "#b03cff" },
13
+ status: { from: "#199f62", to: "#56db29" },
14
+ analytics: { from: "#fe5623", to: "#ffb003" },
15
+ docs: { from: "#1bb9cf", to: "#13e4c8" },
16
+ };
17
+ // En image plutôt qu'en SVG en ligne : chaque pastille garde ses identifiants de dégradé pour elle,
18
+ // plusieurs pastilles dans une même page ne se marchent pas dessus.
19
+ function dataUri(svg) {
20
+ return `data:image/svg+xml,${encodeURIComponent(svg)}`;
21
+ }
22
+ /**
23
+ * Pastille d'un produit (dégradé plein, pictogramme blanc), toujours seule : jamais dans une tuile
24
+ * de couleur. `size-4` par défaut, à agrandir par `className`.
25
+ */
26
+ export function ProductBadge({ product, className, label }) {
27
+ return (_jsx("img", { src: dataUri(PRODUCT_BADGE_SVG[product]), alt: label ?? "", "aria-hidden": label ? undefined : true, width: 16, height: 16, draggable: false, className: cn("size-4 shrink-0 select-none", className) }));
28
+ }
29
+ /** Le H tricolore de la famille (« Propulsé par », hub). `size-4` par défaut. */
30
+ export function FamilyMark({ className, label }) {
31
+ return (_jsx("img", { src: dataUri(FAMILY_MARK_SVG), alt: label ?? "", "aria-hidden": label ? undefined : true, width: 16, height: 16, draggable: false, className: cn("size-4 shrink-0 select-none", className) }));
32
+ }
@@ -0,0 +1,19 @@
1
+ import { type ReactNode } from "react";
2
+ import { type HgdcCatalogProduct } from "./products.ts";
3
+ /** Délai d'intention avant d'élargir la barre au survol : un passage de souris ne l'ouvre pas. */
4
+ export declare const PRODUCT_RAIL_OPEN_DELAY = 150;
5
+ /**
6
+ * La barre des produits de la famille, tout à gauche de la coque sur ordinateur : organisation en
7
+ * haut, pastilles des produits, Account en bas. 56 px de large ; au survol (après un court délai
8
+ * d'intention) ou au focus clavier, elle s'élargit par-dessus la page et écrit le nom de chaque
9
+ * produit, sans rien décaler. Elle se referme dès que la souris sort.
10
+ */
11
+ export declare function ProductRail({ current, organization, accountHref, catalog, }: {
12
+ /** Le mot du produit affiché (« host ») : marqué en teinte. */
13
+ current?: string | undefined;
14
+ /** `OrgSwitcher`, rendu compact. */
15
+ organization?: ReactNode;
16
+ /** Adresse d'Account (un Account local en développement). */
17
+ accountHref?: string | undefined;
18
+ catalog?: ReadonlyArray<HgdcCatalogProduct> | undefined;
19
+ }): React.JSX.Element;
@@ -0,0 +1,85 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { ProductBadge } from "./product-badge.js";
4
+ import { productRailEntries } from "./products.js";
5
+ import { ShellPlacementContext } from "./shell-placement.js";
6
+ import { cn } from "./cn.js";
7
+ /** Délai d'intention avant d'élargir la barre au survol : un passage de souris ne l'ouvre pas. */
8
+ export const PRODUCT_RAIL_OPEN_DELAY = 150;
9
+ function ProductLink({ entry }) {
10
+ return (_jsx("li", { children: _jsxs("a", { href: entry.href, "aria-current": entry.current ? "page" : undefined, className: cn("flex h-10 items-center gap-3 rounded-xl px-2 text-shell-muted outline-none transition-colors duration-(--duration-fast) ease-out", "hover:bg-shell-hover hover:text-shell-foreground focus-visible:ring-2 focus-visible:ring-ring/50", "aria-[current=page]:bg-shell-active aria-[current=page]:text-shell-foreground"), children: [_jsx(ProductBadge, { product: entry.badge, className: "size-6" }), _jsxs("span", { className: "min-w-0 flex-1 truncate opacity-0 transition-opacity duration-(--duration-fast) ease-out group-data-expanded/rail:opacity-100 motion-reduce:transition-none", children: [_jsx("span", { className: "font-heading text-sm font-medium", children: entry.name }), entry.hint ? _jsx("span", { className: "sr-only", children: ", " }) : null, entry.hint ? _jsx("span", { className: "ml-1.5 text-xs text-shell-muted", children: entry.hint }) : null] })] }) }));
11
+ }
12
+ /**
13
+ * La barre des produits de la famille, tout à gauche de la coque sur ordinateur : organisation en
14
+ * haut, pastilles des produits, Account en bas. 56 px de large ; au survol (après un court délai
15
+ * d'intention) ou au focus clavier, elle s'élargit par-dessus la page et écrit le nom de chaque
16
+ * produit, sans rien décaler. Elle se referme dès que la souris sort.
17
+ */
18
+ export function ProductRail({ current, organization, accountHref, catalog, }) {
19
+ const [expanded, setExpanded] = useState(false);
20
+ const ref = useRef(null);
21
+ const { products, account } = productRailEntries({ current, accountHref, catalog });
22
+ // Écouteurs natifs : la barre est un repère `nav`, pas un contrôle ; c'est son survol et le
23
+ // focus de ses liens qui l'élargissent.
24
+ useEffect(() => {
25
+ const rail = ref.current;
26
+ if (!rail)
27
+ return;
28
+ let timer;
29
+ const cancel = () => {
30
+ window.clearTimeout(timer);
31
+ timer = undefined;
32
+ };
33
+ const onEnter = (event) => {
34
+ if (event.pointerType !== "mouse")
35
+ return;
36
+ cancel();
37
+ timer = window.setTimeout(() => setExpanded(true), PRODUCT_RAIL_OPEN_DELAY);
38
+ };
39
+ const onLeave = () => {
40
+ cancel();
41
+ setExpanded(false);
42
+ };
43
+ // Au clavier seulement : un clic de souris ne laisse pas la barre ouverte.
44
+ const onFocusIn = (event) => {
45
+ if (event.target && focusVisible(event.target))
46
+ setExpanded(true);
47
+ };
48
+ const onFocusOut = (event) => {
49
+ if (!(event.relatedTarget instanceof Node) || !rail.contains(event.relatedTarget))
50
+ setExpanded(false);
51
+ };
52
+ const onKey = (event) => {
53
+ if (event.key !== "Escape")
54
+ return;
55
+ cancel();
56
+ setExpanded(false);
57
+ };
58
+ rail.addEventListener("pointerenter", onEnter);
59
+ rail.addEventListener("pointerleave", onLeave);
60
+ rail.addEventListener("focusin", onFocusIn);
61
+ rail.addEventListener("focusout", onFocusOut);
62
+ rail.addEventListener("keydown", onKey);
63
+ return () => {
64
+ cancel();
65
+ rail.removeEventListener("pointerenter", onEnter);
66
+ rail.removeEventListener("pointerleave", onLeave);
67
+ rail.removeEventListener("focusin", onFocusIn);
68
+ rail.removeEventListener("focusout", onFocusOut);
69
+ rail.removeEventListener("keydown", onKey);
70
+ };
71
+ }, []);
72
+ return (_jsx("div", { "data-slot": "product-rail", className: "relative hidden w-14 shrink-0 lg:block", children: _jsxs("nav", { ref: ref, "aria-label": "Produits", "data-expanded": expanded ? "" : undefined, className: cn("group/rail absolute inset-y-0 left-0 z-40 flex w-14 flex-col gap-2 overflow-hidden bg-shell-rail px-2 py-3 text-shell-foreground", "transition-[width,box-shadow] duration-(--duration-base) ease-ios motion-reduce:transition-none",
73
+ // L'ombre seulement quand elle flotte ; en sombre, où l'ombre ne se voit pas, un filet.
74
+ "data-expanded:w-55 data-expanded:shadow-xl data-expanded:shadow-black/10 dark:data-expanded:shadow-black/50 dark:data-expanded:ring-1 dark:data-expanded:ring-white/8"), children: [organization ? (_jsxs(_Fragment, { children: [_jsx(ShellPlacementContext, { value: "product-rail", children: organization }), _jsx("div", { "aria-hidden": true, className: "mx-2 h-px shrink-0 bg-foreground/8" })] })) : null, _jsx("ul", { className: "grid min-h-0 gap-1 overflow-x-hidden overflow-y-auto", children: products.map((entry) => (_jsx(ProductLink, { entry: entry }, entry.badge))) }), _jsx("ul", { className: "mt-auto grid", children: _jsx(ProductLink, { entry: account }) })] }) }));
75
+ }
76
+ function focusVisible(target) {
77
+ if (!(target instanceof Element))
78
+ return false;
79
+ try {
80
+ return target.matches(":focus-visible");
81
+ }
82
+ catch {
83
+ return true;
84
+ }
85
+ }
@@ -1,13 +1,14 @@
1
1
  import type { IconSvgElement } from "@hugeicons/react";
2
2
  import { type EmailAccent } from "./email-palette.ts";
3
+ import { type FamilyProduct } from "./family-svg.ts";
3
4
  /** Où en est un produit : seul ce qui répond s'ouvre depuis les menus. */
4
5
  export type ProductStage = "live" | "preview" | "soon";
5
6
  export declare const PRODUCT_STAGE_LABEL: Readonly<Record<ProductStage, string>>;
6
7
  /** Un produit de la famille, tel que la vision le décrit. */
7
8
  export type HgdcCatalogProduct = {
8
- /** Le mot de « HGDC <mot> » : « Host ». */
9
+ /** Le mot du produit, qui sert de sous-domaine : « Host ». */
9
10
  word: string;
10
- /** « HGDC Host ». */
11
+ /** Nom affiché, le mot seul (« Host ») : la famille ne se répète pas devant chaque produit. */
11
12
  name: string;
12
13
  /** « host.hgdc.ch ». */
13
14
  subdomain: string;
@@ -20,6 +21,9 @@ export type HgdcCatalogProduct = {
20
21
  entryHref: string;
21
22
  /** Une phrase, au tutoiement. */
22
23
  pitch: string;
24
+ /** Pastille du produit (`ProductBadge` de `@hgdc/ui/product-badge`) : le mot en minuscules. */
25
+ badge: FamilyProduct;
26
+ /** Pictogramme Hugeicons, pour une liste qui en veut un (palette, navigation). Le logo est `badge`. */
23
27
  icon: IconSvgElement;
24
28
  stage: ProductStage;
25
29
  /**
@@ -39,12 +43,14 @@ export declare const HGDC_ACCOUNT: HgdcCatalogProduct;
39
43
  export declare const HGDC_PRODUCTS: ReadonlyArray<HgdcCatalogProduct>;
40
44
  /** Vrai si le produit répond déjà (en service ou en préversion). */
41
45
  export declare function isProductOpen(item: Pick<HgdcCatalogProduct, "stage">): boolean;
42
- /** Une entrée du menu « Produits HGDC » d'`AccountMenu`. */
46
+ /** Une entrée du menu « Produits » d'`AccountMenu`. */
43
47
  export type ProductMenuEntry = {
44
48
  name: string;
45
49
  href: string;
46
50
  /** Le produit ouvert : coché, sans lien. */
47
51
  current?: boolean;
52
+ /** Pastille du produit, affichée à la place de `icon`. */
53
+ badge?: FamilyProduct;
48
54
  icon?: IconSvgElement;
49
55
  /** Texte discret à droite : « Préversion ». */
50
56
  hint?: string;
@@ -60,3 +66,27 @@ export declare function productMenuEntries(options?: {
60
66
  accountHref?: string | undefined;
61
67
  catalog?: ReadonlyArray<HgdcCatalogProduct> | undefined;
62
68
  }): Array<ProductMenuEntry>;
69
+ /** Une entrée de la barre des produits d'`AppShell`. */
70
+ export type ProductRailEntry = {
71
+ name: string;
72
+ href: string;
73
+ badge: FamilyProduct;
74
+ /** Le produit ouvert : « vous êtes ici », lien vers sa propre racine. */
75
+ current?: boolean;
76
+ /** Texte discret sous le nom : « Préversion ». */
77
+ hint?: string;
78
+ };
79
+ /**
80
+ * Les entrées de la barre des produits : les produits ouverts du catalogue, puis Account, à part
81
+ * en bas (le compte commun, pas un produit). Chaque produit pointe vers son entrée
82
+ * `/auth/hgdc?next=/` (ADR 0010), Account vers sa racine (`accountHref` en développement). Le
83
+ * produit courant pointe vers `/`, sans détour par l'entrée.
84
+ */
85
+ export declare function productRailEntries(options?: {
86
+ current?: string | undefined;
87
+ accountHref?: string | undefined;
88
+ catalog?: ReadonlyArray<HgdcCatalogProduct> | undefined;
89
+ }): {
90
+ products: Array<ProductRailEntry>;
91
+ account: ProductRailEntry;
92
+ };
package/dist/products.js CHANGED
@@ -6,6 +6,7 @@ import Mail01Icon from "@hugeicons/core-free-icons/Mail01Icon";
6
6
  import Rocket01Icon from "@hugeicons/core-free-icons/Rocket01Icon";
7
7
  import UserCircleIcon from "@hugeicons/core-free-icons/UserCircleIcon";
8
8
  import { HGDC_EMAIL_ACCENT } from "./email-palette.js";
9
+ import { isFamilyProduct } from "./family-svg.js";
9
10
  export const PRODUCT_STAGE_LABEL = {
10
11
  live: "En service",
11
12
  preview: "Préversion",
@@ -18,22 +19,26 @@ export const PRODUCT_STAGE_LABEL = {
18
19
  */
19
20
  export const PRODUCT_ENTRY_PATH = "/auth/hgdc";
20
21
  function product(word, pitch, icon, stage) {
21
- const subdomain = `${word.toLowerCase()}.hgdc.ch`;
22
+ const badge = word.toLowerCase();
23
+ if (!isFamilyProduct(badge))
24
+ throw new Error(`@hgdc/ui/products : pas de pastille pour « ${word} »`);
25
+ const subdomain = `${badge}.hgdc.ch`;
22
26
  const href = `https://${subdomain}`;
23
27
  return {
24
28
  word,
25
- name: `HGDC ${word}`,
29
+ name: word,
26
30
  subdomain,
27
31
  href,
28
32
  entryHref: `${href}${PRODUCT_ENTRY_PATH}`,
29
33
  pitch,
34
+ badge,
30
35
  icon,
31
36
  stage,
32
37
  accent: HGDC_EMAIL_ACCENT,
33
38
  };
34
39
  }
35
40
  /** HGDC Account : le compte commun (profil, sécurité, organisations), hors du catalogue des produits. */
36
- const account = product("Account", "Ton compte HGDC : profil, sécurité et organisations, pour tous les produits.", UserCircleIcon, "live");
41
+ const account = product("Account", "Ton compte : profil, sécurité et organisations, pour tous les produits de la famille.", UserCircleIcon, "live");
37
42
  export const HGDC_ACCOUNT = { ...account, entryHref: account.href };
38
43
  /** Les produits de la famille, dans leur ordre d'affichage. Account n'en fait pas partie (`HGDC_ACCOUNT`). */
39
44
  export const HGDC_PRODUCTS = [
@@ -61,8 +66,30 @@ export function productMenuEntries(options = {}) {
61
66
  return [account, ...(options.catalog ?? HGDC_PRODUCTS).filter(isProductOpen)].map((item) => ({
62
67
  name: item.name,
63
68
  href: item.entryHref,
69
+ badge: item.badge,
64
70
  icon: item.icon,
65
71
  ...(item.word.toLowerCase() === current ? { current: true } : {}),
66
72
  ...(item.stage === "preview" ? { hint: PRODUCT_STAGE_LABEL.preview } : {}),
67
73
  }));
68
74
  }
75
+ /**
76
+ * Les entrées de la barre des produits : les produits ouverts du catalogue, puis Account, à part
77
+ * en bas (le compte commun, pas un produit). Chaque produit pointe vers son entrée
78
+ * `/auth/hgdc?next=/` (ADR 0010), Account vers sa racine (`accountHref` en développement). Le
79
+ * produit courant pointe vers `/`, sans détour par l'entrée.
80
+ */
81
+ export function productRailEntries(options = {}) {
82
+ const current = options.current?.toLowerCase();
83
+ const entry = (item, href) => ({
84
+ name: item.name,
85
+ badge: item.badge,
86
+ ...(item.badge === current ? { href: "/", current: true } : { href }),
87
+ ...(item.stage === "preview" ? { hint: PRODUCT_STAGE_LABEL.preview } : {}),
88
+ });
89
+ return {
90
+ products: (options.catalog ?? HGDC_PRODUCTS)
91
+ .filter(isProductOpen)
92
+ .map((item) => entry(item, `${item.entryHref}?next=/`)),
93
+ account: entry(HGDC_ACCOUNT, options.accountHref ?? HGDC_ACCOUNT.entryHref),
94
+ };
95
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Où `AppShell` pose un élément du produit (le sélecteur d'organisation) : dans la barre latérale
3
+ * ou le tiroir mobile, ou en haut de la barre des produits, où il n'a qu'une colonne de 56 px.
4
+ */
5
+ export type ShellPlacement = "sidebar" | "product-rail";
6
+ export declare const ShellPlacementContext: React.Context<ShellPlacement>;
7
+ export declare function useShellPlacement(): ShellPlacement;
@@ -0,0 +1,5 @@
1
+ import { createContext, useContext } from "react";
2
+ export const ShellPlacementContext = createContext("sidebar");
3
+ export function useShellPlacement() {
4
+ return useContext(ShellPlacementContext);
5
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="66 66 380 380"><defs><linearGradient id="sky" x1="96" y1="76" x2="416" y2="436" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff3b47"/><stop offset="1" stop-color="#ff6352"/></linearGradient><linearGradient id="acc" x1="150" y1="160" x2="362" y2="352" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff3b47"/><stop offset="1" stop-color="#ff6352"/></linearGradient></defs><circle cx="256" cy="256" r="190" fill="url(#sky)"/><rect x="128" y="162" width="256" height="188" rx="48" fill="#fff"/><g fill="url(#acc)"><circle cx="202" cy="232" r="30"/><path d="M154 314A48 44 0 0 1 250 314Z"/><rect x="276" y="216" width="72" height="24" rx="12"/><rect x="276" y="262" width="48" height="24" rx="12"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="66 66 380 380"><defs><linearGradient id="sky" x1="96" y1="76" x2="416" y2="436" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fe5623"/><stop offset="1" stop-color="#ffb003"/></linearGradient><linearGradient id="acc" x1="150" y1="160" x2="362" y2="352" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fe5623"/><stop offset="1" stop-color="#ffb003"/></linearGradient></defs><circle cx="256" cy="256" r="190" fill="url(#sky)"/><rect x="128" y="160" width="256" height="192" rx="48" fill="#fff"/><path fill="none" stroke="url(#acc)" stroke-width="30" stroke-linecap="round" d="M198 304V276M256 304V240M314 304V206"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="66 66 380 380"><defs><linearGradient id="sky" x1="96" y1="76" x2="416" y2="436" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1bb9cf"/><stop offset="1" stop-color="#13e4c8"/></linearGradient><linearGradient id="acc" x1="150" y1="160" x2="362" y2="352" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1bb9cf"/><stop offset="1" stop-color="#13e4c8"/></linearGradient></defs><circle cx="256" cy="256" r="190" fill="url(#sky)"/><g fill="#fff"><path d="M248 196C222 176 184 168 146 174A14 14 0 0 0 134 188V324A12 12 0 0 0 148 336C184 332 220 338 248 356Z"/><path d="M264 196C290 176 328 168 366 174A14 14 0 0 1 378 188V324A12 12 0 0 1 364 336C328 332 292 338 264 356Z"/></g><path fill="none" stroke="url(#acc)" stroke-width="16" stroke-linecap="round" d="M166 226C188 224 208 228 224 238M166 266C188 264 208 268 224 278M288 238C304 228 324 224 346 226M288 278C304 268 324 264 346 266"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="43 24 991.1 293.5"><defs><linearGradient id="l" x1="0" y1="24" x2="0" y2="232" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2c78fd"/><stop offset="1" stop-color="#7343fe"/></linearGradient><linearGradient id="r" x1="0" y1="24" x2="0" y2="232" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fe5292"/><stop offset="1" stop-color="#ff9a46"/></linearGradient><linearGradient id="b" x1="64" y1="0" x2="192" y2="0" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8145ff"/><stop offset="1" stop-color="#fe5292"/></linearGradient></defs><rect x="43" y="24" width="57" height="208" rx="18" fill="url(#l)"/><rect x="156" y="24" width="57" height="208" rx="18" fill="url(#r)"/><rect x="67" y="107" width="122" height="43" rx="14" fill="url(#b)"/><path fill="#fafafa" fill-rule="evenodd" d="M957.4 317.3C935 314.4 918.7 300.9 914.8 282C911.5 265.8 919.2 250 936.1 238.5C940.6 235.5 940.6 235.4 937.2 234.7C918.8 230.4 904.1 219.3 895.6 203.4C892.8 198.2 890 190.3 889.1 185.1C888.9 184.1 888.7 183.3 888.7 183.1C888.5 182.5 887.6 176.3 887.4 173.4C886.9 167.6 886.9 85.7 887.5 85.2C888 84.6 920.3 84.6 920.8 85.1C921 85.4 921.1 89 921.1 129C921.2 177.2 921.1 173.9 922.4 178.9C926.6 195.4 940 206.5 957.2 207.7C977.4 209.1 994.7 196.5 999.5 176.7C1000.8 171.3 1000.8 174.4 1000.8 127.5C1000.9 88.2 1000.9 85.2 1001.2 85C1001.4 84.8 1005 84.8 1017.6 84.8C1033.7 84.8 1033.7 84.8 1034 85.3C1034.5 86 1034.3 239.4 1033.8 244.1C1031.8 263.7 1027.6 275.8 1018 289.3C1004.4 308.5 979.3 320.1 957.4 317.3ZM970.7 289C979.9 287.4 988.4 281.2 993.7 272.2C996.4 267.8 998.8 260 999.7 253.2C1000.2 249.7 1000.5 220.8 1000.1 220C999.8 219.5 999.8 219.5 997.1 221.6C991.4 225.9 984 230.4 978 233.2C956.7 243.2 947.5 250.9 944 261.7C941.2 270.2 943.8 279 950.5 284.2C954 286.9 957.8 288.4 963.4 289.2C964.9 289.5 968.5 289.3 970.7 289ZM867 235.3C855.2 234.4 843.9 227.7 837.9 218.1C833 210.1 830.6 202.2 829.4 190.4C829.1 187.4 829 26.8 829.3 26C829.3 25.9 829.5 25.7 829.7 25.6C830.5 25.4 862.5 25.4 862.8 25.7C863.1 25.9 863.1 33.7 863.1 105.6C863.1 189.4 863.1 187.7 864 192.3C865.8 201.5 869.8 205.9 876.6 206.1C878.9 206.1 878.8 206 880 208.8C882.2 213.4 886.5 220.3 890.1 224.8C893.1 228.6 893.1 228.5 887.4 231.3C880.9 234.5 874 235.8 867 235.3ZM276 231.9C275.6 231.6 275.6 231.6 275.6 178.5C275.6 126.2 275.6 125.4 275.2 125.3C275 125.1 271.9 125.1 266.4 125.1C259.4 125.1 257.8 125 257.5 124.8C256.9 124.4 256.9 98.9 257.4 98.3C257.7 98 258.4 98 266.4 97.9C273.7 97.9 275 97.8 275.3 97.6C275.5 97.3 275.6 96.4 275.7 88.6C275.8 76.1 276.4 70.6 278.5 63.4C282.9 47.5 293.8 35.1 309.1 28.5C312.1 27.2 319.4 25.2 322.9 24.6C328.4 23.8 346.7 24 347.7 24.9C348.2 25.3 348.2 53.2 347.7 53.7C347.4 53.9 345.7 53.9 338.2 53.9C328.2 53.9 327.9 53.9 324 55.3C317.6 57.4 312.2 63.5 310.5 70.5C309.4 74.6 309.4 75.2 309.3 86.3C309.1 96.3 309.2 97.4 309.7 97.7C309.8 97.8 318.2 97.9 328.4 97.9C344.1 97.9 347 97.9 347.3 98.2C347.8 98.5 347.9 124.2 347.5 124.7C347.3 124.9 343.5 125 328.4 125.1C314.1 125.1 309.6 125.2 309.4 125.4C309.2 125.6 309.2 137.6 309.2 178.6C309.1 230.9 309.1 231.7 308.8 231.9C308.2 232.3 276.6 232.3 276 231.9ZM536.7 231.9C536.2 231.4 536.4 143.4 536.9 139.3C538 129.6 540 122.9 544.2 114.7C555.4 92.8 580.1 79.6 604.6 82.7C621.1 84.7 633.2 92 643.3 105.7C644.5 107.3 644.7 107.3 645.8 105.9C652.4 97.2 657.9 92.6 666.5 88.3C686.2 78.5 711.2 80.9 728.5 94.3C737.2 101 743 108.3 747.4 118C750.8 125.6 752.3 131.3 753.2 140.1C753.7 145.3 753.8 231.4 753.2 231.8C752.8 232.2 720.6 232.2 720.3 231.8C720.1 231.6 720 222.9 719.9 185.2C719.8 144.2 719.8 138.6 719.5 137.2C716.7 122.3 707.5 112.3 694.7 110.5C679.5 108.3 666 118 661.6 134C660.3 139 660.4 134.7 660.3 185.7C660.2 223 660.1 231.6 659.9 231.8C659.7 232.1 629.6 232.3 628.2 232.1C627.9 232 627.5 231.9 627.3 231.7C627.1 231.4 627 226.6 627 186.6C627 161.1 626.9 140.8 626.8 139.5C625.5 121.3 611.2 108.3 594.9 110.5C583.3 112.1 574.3 120.8 571.3 133.5C570.3 137.6 570.4 135.6 570.2 184.9C570.1 229.2 570.1 231.4 569.8 231.8C569.3 232.3 537.2 232.4 536.7 231.9ZM775.3 232C775 231.9 774.8 231.7 774.7 231.6C774.6 231.4 774.5 198.5 774.5 158.4C774.5 87.4 774.5 85.5 774.8 85.1C775.5 84.5 807.9 84.6 808.3 85.2C808.9 86 808.7 231.5 808.2 231.9C807.7 232.2 776 232.3 775.3 232ZM789.3 67.4C789 67.3 788 67.2 787.2 67C783.5 66.4 779.3 64.1 776.1 61.1C770.8 56 768.5 47.9 770.3 40.7C773.5 27.7 788.9 20.5 800.8 26.5C806.6 29.4 810.5 34 812.5 40.3C813.3 43.2 813.4 48.3 812.6 51.2C810.1 59.3 804 65.1 795.9 66.9C794.4 67.3 790.3 67.6 789.3 67.4ZM432.5 77.2A84.1 80.9 0 0 1 516.7 158.1L516.7 232L484.2 232L484.2 220.9A81.9 80.9 0 1 1 432.5 77.2ZM432.5 109.7A49.4 48.4 0 1 0 432.5 206.5A49.4 48.4 0 1 0 432.5 109.7Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><defs><linearGradient id="l" x1="0" y1="24" x2="0" y2="232" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2c78fd"/><stop offset="1" stop-color="#7343fe"/></linearGradient><linearGradient id="r" x1="0" y1="24" x2="0" y2="232" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fe5292"/><stop offset="1" stop-color="#ff9a46"/></linearGradient><linearGradient id="b" x1="64" y1="0" x2="192" y2="0" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8145ff"/><stop offset="1" stop-color="#fe5292"/></linearGradient></defs><rect x="43" y="24" width="57" height="208" rx="18" fill="url(#l)"/><rect x="156" y="24" width="57" height="208" rx="18" fill="url(#r)"/><rect x="67" y="107" width="122" height="43" rx="14" fill="url(#b)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="43 24 991.1 293.5"><defs><linearGradient id="l" x1="0" y1="24" x2="0" y2="232" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2c78fd"/><stop offset="1" stop-color="#7343fe"/></linearGradient><linearGradient id="r" x1="0" y1="24" x2="0" y2="232" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fe5292"/><stop offset="1" stop-color="#ff9a46"/></linearGradient><linearGradient id="b" x1="64" y1="0" x2="192" y2="0" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8145ff"/><stop offset="1" stop-color="#fe5292"/></linearGradient></defs><rect x="43" y="24" width="57" height="208" rx="18" fill="url(#l)"/><rect x="156" y="24" width="57" height="208" rx="18" fill="url(#r)"/><rect x="67" y="107" width="122" height="43" rx="14" fill="url(#b)"/><path fill="#0a0a0a" fill-rule="evenodd" d="M957.4 317.3C935 314.4 918.7 300.9 914.8 282C911.5 265.8 919.2 250 936.1 238.5C940.6 235.5 940.6 235.4 937.2 234.7C918.8 230.4 904.1 219.3 895.6 203.4C892.8 198.2 890 190.3 889.1 185.1C888.9 184.1 888.7 183.3 888.7 183.1C888.5 182.5 887.6 176.3 887.4 173.4C886.9 167.6 886.9 85.7 887.5 85.2C888 84.6 920.3 84.6 920.8 85.1C921 85.4 921.1 89 921.1 129C921.2 177.2 921.1 173.9 922.4 178.9C926.6 195.4 940 206.5 957.2 207.7C977.4 209.1 994.7 196.5 999.5 176.7C1000.8 171.3 1000.8 174.4 1000.8 127.5C1000.9 88.2 1000.9 85.2 1001.2 85C1001.4 84.8 1005 84.8 1017.6 84.8C1033.7 84.8 1033.7 84.8 1034 85.3C1034.5 86 1034.3 239.4 1033.8 244.1C1031.8 263.7 1027.6 275.8 1018 289.3C1004.4 308.5 979.3 320.1 957.4 317.3ZM970.7 289C979.9 287.4 988.4 281.2 993.7 272.2C996.4 267.8 998.8 260 999.7 253.2C1000.2 249.7 1000.5 220.8 1000.1 220C999.8 219.5 999.8 219.5 997.1 221.6C991.4 225.9 984 230.4 978 233.2C956.7 243.2 947.5 250.9 944 261.7C941.2 270.2 943.8 279 950.5 284.2C954 286.9 957.8 288.4 963.4 289.2C964.9 289.5 968.5 289.3 970.7 289ZM867 235.3C855.2 234.4 843.9 227.7 837.9 218.1C833 210.1 830.6 202.2 829.4 190.4C829.1 187.4 829 26.8 829.3 26C829.3 25.9 829.5 25.7 829.7 25.6C830.5 25.4 862.5 25.4 862.8 25.7C863.1 25.9 863.1 33.7 863.1 105.6C863.1 189.4 863.1 187.7 864 192.3C865.8 201.5 869.8 205.9 876.6 206.1C878.9 206.1 878.8 206 880 208.8C882.2 213.4 886.5 220.3 890.1 224.8C893.1 228.6 893.1 228.5 887.4 231.3C880.9 234.5 874 235.8 867 235.3ZM276 231.9C275.6 231.6 275.6 231.6 275.6 178.5C275.6 126.2 275.6 125.4 275.2 125.3C275 125.1 271.9 125.1 266.4 125.1C259.4 125.1 257.8 125 257.5 124.8C256.9 124.4 256.9 98.9 257.4 98.3C257.7 98 258.4 98 266.4 97.9C273.7 97.9 275 97.8 275.3 97.6C275.5 97.3 275.6 96.4 275.7 88.6C275.8 76.1 276.4 70.6 278.5 63.4C282.9 47.5 293.8 35.1 309.1 28.5C312.1 27.2 319.4 25.2 322.9 24.6C328.4 23.8 346.7 24 347.7 24.9C348.2 25.3 348.2 53.2 347.7 53.7C347.4 53.9 345.7 53.9 338.2 53.9C328.2 53.9 327.9 53.9 324 55.3C317.6 57.4 312.2 63.5 310.5 70.5C309.4 74.6 309.4 75.2 309.3 86.3C309.1 96.3 309.2 97.4 309.7 97.7C309.8 97.8 318.2 97.9 328.4 97.9C344.1 97.9 347 97.9 347.3 98.2C347.8 98.5 347.9 124.2 347.5 124.7C347.3 124.9 343.5 125 328.4 125.1C314.1 125.1 309.6 125.2 309.4 125.4C309.2 125.6 309.2 137.6 309.2 178.6C309.1 230.9 309.1 231.7 308.8 231.9C308.2 232.3 276.6 232.3 276 231.9ZM536.7 231.9C536.2 231.4 536.4 143.4 536.9 139.3C538 129.6 540 122.9 544.2 114.7C555.4 92.8 580.1 79.6 604.6 82.7C621.1 84.7 633.2 92 643.3 105.7C644.5 107.3 644.7 107.3 645.8 105.9C652.4 97.2 657.9 92.6 666.5 88.3C686.2 78.5 711.2 80.9 728.5 94.3C737.2 101 743 108.3 747.4 118C750.8 125.6 752.3 131.3 753.2 140.1C753.7 145.3 753.8 231.4 753.2 231.8C752.8 232.2 720.6 232.2 720.3 231.8C720.1 231.6 720 222.9 719.9 185.2C719.8 144.2 719.8 138.6 719.5 137.2C716.7 122.3 707.5 112.3 694.7 110.5C679.5 108.3 666 118 661.6 134C660.3 139 660.4 134.7 660.3 185.7C660.2 223 660.1 231.6 659.9 231.8C659.7 232.1 629.6 232.3 628.2 232.1C627.9 232 627.5 231.9 627.3 231.7C627.1 231.4 627 226.6 627 186.6C627 161.1 626.9 140.8 626.8 139.5C625.5 121.3 611.2 108.3 594.9 110.5C583.3 112.1 574.3 120.8 571.3 133.5C570.3 137.6 570.4 135.6 570.2 184.9C570.1 229.2 570.1 231.4 569.8 231.8C569.3 232.3 537.2 232.4 536.7 231.9ZM775.3 232C775 231.9 774.8 231.7 774.7 231.6C774.6 231.4 774.5 198.5 774.5 158.4C774.5 87.4 774.5 85.5 774.8 85.1C775.5 84.5 807.9 84.6 808.3 85.2C808.9 86 808.7 231.5 808.2 231.9C807.7 232.2 776 232.3 775.3 232ZM789.3 67.4C789 67.3 788 67.2 787.2 67C783.5 66.4 779.3 64.1 776.1 61.1C770.8 56 768.5 47.9 770.3 40.7C773.5 27.7 788.9 20.5 800.8 26.5C806.6 29.4 810.5 34 812.5 40.3C813.3 43.2 813.4 48.3 812.6 51.2C810.1 59.3 804 65.1 795.9 66.9C794.4 67.3 790.3 67.6 789.3 67.4ZM432.5 77.2A84.1 80.9 0 0 1 516.7 158.1L516.7 232L484.2 232L484.2 220.9A81.9 80.9 0 1 1 432.5 77.2ZM432.5 109.7A49.4 48.4 0 1 0 432.5 206.5A49.4 48.4 0 1 0 432.5 109.7Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="66 66 380 380"><defs><linearGradient id="sky" x1="96" y1="76" x2="416" y2="436" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#194ff7"/><stop offset="1" stop-color="#02b4ff"/></linearGradient><linearGradient id="acc" x1="150" y1="160" x2="362" y2="352" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#194ff7"/><stop offset="1" stop-color="#02b4ff"/></linearGradient></defs><circle cx="256" cy="256" r="190" fill="url(#sky)"/><g transform="translate(0 6)"><g fill="#fff"><path d="M158.7 331.3A262.3 262.3 0 0 0 353.3 331.3L334 283H178Z"/><circle cx="178" cy="283" r="52"/><circle cx="334" cy="283" r="52"/><circle cx="214" cy="238" r="54"/><circle cx="288" cy="222" r="72"/><rect x="178" y="232" width="156" height="100"/></g><path fill="none" stroke="url(#acc)" stroke-width="24" stroke-linecap="round" stroke-linejoin="round" d="M256 302V234M224 266L256 234L288 266"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="66 66 380 380"><defs><linearGradient id="sky" x1="96" y1="76" x2="416" y2="436" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f61f8e"/><stop offset="1" stop-color="#ff5cb8"/></linearGradient><linearGradient id="acc" x1="150" y1="160" x2="362" y2="352" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f61f8e"/><stop offset="1" stop-color="#ff5cb8"/></linearGradient></defs><circle cx="256" cy="256" r="190" fill="url(#sky)"/><rect x="128" y="162" width="256" height="188" rx="56" fill="#fff"/><path fill="url(#acc)" d="M182 194H330A22 22 0 0 1 341.82 234.56L271.04 279.62A28 28 0 0 1 240.96 279.62L170.18 234.56A22 22 0 0 1 182 194Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="66 66 380 380"><defs><linearGradient id="sky" x1="96" y1="76" x2="416" y2="436" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#4732ed"/><stop offset="1" stop-color="#b03cff"/></linearGradient><linearGradient id="acc" x1="150" y1="160" x2="362" y2="352" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#4732ed"/><stop offset="1" stop-color="#b03cff"/></linearGradient></defs><circle cx="256" cy="256" r="190" fill="url(#sky)"/><path fill="#fff" d="M168 160H222A24 24 0 0 1 240 168L258 188H344A40 40 0 0 1 384 228V316A40 40 0 0 1 344 356H168A40 40 0 0 1 128 316V200A40 40 0 0 1 168 160Z"/><path fill="none" stroke="url(#acc)" stroke-width="30" stroke-linecap="round" stroke-linejoin="round" d="M212 272L244 302L304 240"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="66 66 380 380"><defs><linearGradient id="sky" x1="96" y1="76" x2="416" y2="436" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#199f62"/><stop offset="1" stop-color="#56db29"/></linearGradient><linearGradient id="acc" x1="150" y1="160" x2="362" y2="352" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#199f62"/><stop offset="1" stop-color="#56db29"/></linearGradient></defs><circle cx="256" cy="256" r="190" fill="url(#sky)"/><rect x="128" y="160" width="256" height="192" rx="48" fill="#fff"/><path fill="none" stroke="url(#acc)" stroke-width="26" stroke-linecap="round" stroke-linejoin="round" d="M164 262H206L230 214L262 304L288 248H348"/></svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hgdc/ui",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Interface de la H family : 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",
@@ -17,7 +17,8 @@
17
17
  "files": [
18
18
  "dist",
19
19
  "styles.css",
20
- "email-icons"
20
+ "email-icons",
21
+ "family"
21
22
  ],
22
23
  "sideEffects": [
23
24
  "*.css"
@@ -26,6 +27,7 @@
26
27
  "./styles.css": "./styles.css",
27
28
  "./package.json": "./package.json",
28
29
  "./email-icons/*": "./email-icons/*",
30
+ "./family/*": "./family/*",
29
31
  "./*": {
30
32
  "types": "./dist/*.d.ts",
31
33
  "default": "./dist/*.js"
package/styles.css CHANGED
@@ -63,6 +63,7 @@
63
63
  --color-shell-muted: var(--shell-muted-foreground);
64
64
  --color-shell-hover: var(--shell-hover);
65
65
  --color-shell-active: var(--shell-active);
66
+ --color-shell-rail: var(--shell-rail);
66
67
  --color-success: var(--success);
67
68
  --color-success-foreground: var(--success-foreground);
68
69
  --color-status-active: var(--status-active);
@@ -135,6 +136,8 @@
135
136
  --shell-muted-foreground: oklch(0.45 0 0);
136
137
  --shell-hover: oklch(0 0 0 / 5%);
137
138
  --shell-active: oklch(0 0 0 / 8%);
139
+ /* Barre des produits, un cran sous la coque. */
140
+ --shell-rail: oklch(0.93 0 0);
138
141
  /* Séries de graphique, dans cet ordre, jamais recyclées : une 7e série se replie en « Autres ».
139
142
  Validées (daltonisme, écart entre voisines) sur `surface` en clair et sur `card` en sombre.
140
143
  En clair, 2 à 5 passent sous 3:1 : un graphique les accompagne d'une légende et de libellés. */
@@ -198,6 +201,7 @@
198
201
  --shell-muted-foreground: oklch(0.708 0 0);
199
202
  --shell-hover: oklch(1 0 0 / 6%);
200
203
  --shell-active: oklch(1 0 0 / 12%);
204
+ --shell-rail: oklch(0.05 0 0);
201
205
  --chart-1: oklch(0.622 0.161 255.1);
202
206
  --chart-2: oklch(0.622 0.173 40.1);
203
207
  --chart-3: oklch(0.621 0.128 163.1);
@@ -216,6 +220,31 @@
216
220
  --inverse: oklch(0.1 0 0);
217
221
  }
218
222
 
223
+ /* Identité H family, la même en clair et en sombre. Dégradé de chaque pastille produit
224
+ (`ProductBadge`, `family/<mot>.svg`) et palette du H tricolore. Réservés aux visuels de marque
225
+ (pastille, carte de produit), jamais au chrome. Couleurs pleines ; pour éclaircir, mélanger au
226
+ blanc `in oklab`, jamais `in oklch` (la teinte du blanc s'y mêle, tout vire au rose). */
227
+ :root {
228
+ --product-account-from: #ff3b47;
229
+ --product-account-to: #ff6352;
230
+ --product-host-from: #194ff7;
231
+ --product-host-to: #02b4ff;
232
+ --product-mail-from: #f61f8e;
233
+ --product-mail-to: #ff5cb8;
234
+ --product-portal-from: #4732ed;
235
+ --product-portal-to: #b03cff;
236
+ --product-status-from: #199f62;
237
+ --product-status-to: #56db29;
238
+ --product-analytics-from: #fe5623;
239
+ --product-analytics-to: #ffb003;
240
+ --product-docs-from: #1bb9cf;
241
+ --product-docs-to: #13e4c8;
242
+ --glow-blue: #2c78fd;
243
+ --glow-violet: #7343fe;
244
+ --glow-pink: #fe5292;
245
+ --glow-orange: #ff9a46;
246
+ }
247
+
219
248
  @layer base {
220
249
  /* Une grille sans colonnes déclarées a une seule piste qui ne s'élargit jamais pour un
221
250
  texte long : sans ça, `min-width: auto` pousse la page en défilement horizontal sur