@hgdc/ui 0.24.0 → 0.26.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 +13 -8
- package/dist/account-menu.d.ts +1 -1
- package/dist/account-menu.js +4 -13
- package/dist/brand.d.ts +14 -0
- package/dist/brand.js +58 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +47 -0
- package/dist/email-icon.d.ts +10 -0
- package/dist/email-icon.js +17 -0
- package/dist/email.d.ts +2 -4
- package/dist/email.js +2 -6
- package/dist/family-svg.d.ts +5 -0
- package/dist/family-svg.js +13 -0
- package/dist/theme-switch.d.ts +8 -0
- package/dist/theme-switch.js +42 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -67,14 +67,19 @@ Une seule règle dans les sept produits, posée par la coque :
|
|
|
67
67
|
|
|
68
68
|
## Identité H Collection
|
|
69
69
|
|
|
70
|
-
| Import | Rôle
|
|
71
|
-
| --------------------------------------- |
|
|
72
|
-
| `product-badge` | `<ProductBadge product="host" className="size-8" />` (carré blanc arrondi, la même forme partout), `<FamilyMark />` (le H « Double », clair et sombre selon le thème), `PRODUCT_GRADIENT`
|
|
73
|
-
| `@hgdc/ui/family/<mot>.svg` | Pictogrammes des produits (`account`, `host`, `mail`, `portal`, `status`, `analytics`, `docs`), originaux du site ; `family/tile/<mot>.svg` (carré blanc arrondi) ; `family/app/<mot>/` : favicon.svg, favicon.ico, apple-touch-icon.png, icon-192/512.png, icon-maskable-512.png,
|
|
74
|
-
| `@hgdc/ui/family/hcollection-icon*.svg` | Le H « Double », marque de la H Collection, sur fond clair (`hcollection-icon.svg`) ou sombre (`-dark`), jamais recoloré
|
|
75
|
-
| `styles.css` | `--product-<mot>-from` / `--product-<mot>-to` (dégradé de chaque icône sur fond clair), `--glow-blue`, `--glow-violet`, `--glow-pink`, `--glow-orange`
|
|
70
|
+
| Import | Rôle |
|
|
71
|
+
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
72
|
+
| `product-badge` | `<ProductBadge product="host" className="size-8" />` (carré blanc arrondi, la même forme partout), `<FamilyMark />` (le H « Double », clair et sombre selon le thème), `PRODUCT_GRADIENT` |
|
|
73
|
+
| `@hgdc/ui/family/<mot>.svg` | Pictogrammes des produits (`account`, `host`, `mail`, `portal`, `status`, `analytics`, `docs`), originaux du site ; `family/tile/<mot>.svg` (carré blanc arrondi) ; `family/app/<mot>/` : favicon.svg, favicon.ico, apple-touch-icon.png, icon-192/512.png, icon-maskable-512.png, posés dans `public/` au build par `hgdc-brand sync <mot>` (jamais versionnés) |
|
|
74
|
+
| `@hgdc/ui/family/hcollection-icon*.svg` | Le H « Double », marque de la H Collection, sur fond clair (`hcollection-icon.svg`) ou sombre (`-dark`), jamais recoloré |
|
|
75
|
+
| `styles.css` | `--product-<mot>-from` / `--product-<mot>-to` (dégradé de chaque icône sur fond clair), `--glow-blue`, `--glow-violet`, `--glow-pink`, `--glow-orange` |
|
|
76
76
|
|
|
77
|
-
Couleurs pleines ; pour éclaircir, mélanger au blanc `color-mix(in oklab, …)`, jamais `in oklch`. Une icône de produit se pose toujours sur son fond blanc, en clair comme en sombre, sans bordure ni ombre : carré arrondi 22 % partout, pictogramme à 12 % de marge (barre, menu, « Propulsé par », e-mails, icône d'app), plein cadre pour apple-touch et maskable. Le favicon est sans fond et suit le thème du système (ADR 0017). Le H reste sans fond. 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
|
|
77
|
+
Couleurs pleines ; pour éclaircir, mélanger au blanc `color-mix(in oklab, …)`, jamais `in oklch`. Une icône de produit se pose toujours sur son fond blanc, en clair comme en sombre, sans bordure ni ombre : carré arrondi 22 % partout, pictogramme à 12 % de marge (barre, menu, « Propulsé par », e-mails, icône d'app), plein cadre pour apple-touch et maskable. Le favicon est sans fond et suit le thème du système (ADR 0017). Le H reste sans fond. 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, PNG des e-mails, icônes d'application). Geste complet : `docs/runbooks/changer-une-icone.md`.
|
|
78
|
+
|
|
79
|
+
**`hgdc-brand`** (bin de ce paquet) pose les icônes du kit au build, rien n'est recopié à la main :
|
|
80
|
+
|
|
81
|
+
- produit : `"build": "hgdc-brand sync mail && vite build"` (et en tête de `dev`), les six fichiers de `family/app/<mot>/` dans `public/`, listés dans le `.gitignore` ; `--email-as <fichier>` y ajoute l'icône d'e-mail du produit sous un ancien nom ;
|
|
82
|
+
- site hgdc.ch : `hgdc-brand emails public/email`, les `<mot>.png` servis à `EMAIL_ICON_BASE`.
|
|
78
83
|
|
|
79
84
|
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).
|
|
80
85
|
|
|
@@ -172,7 +177,7 @@ const { html, text } = await renderEmail(
|
|
|
172
177
|
| `EmailDetails` | Liste clé / valeur |
|
|
173
178
|
| `renderEmail` | `{ html, text }` : HTML pour clients mail, texte tiré du même rendu |
|
|
174
179
|
|
|
175
|
-
L'accent vient du catalogue (`accent` de `@hgdc/ui/products`), les icônes de `https://hgdc.ch/email/<mot>.png
|
|
180
|
+
L'accent vient du catalogue (`accent` de `@hgdc/ui/products`), les icônes de `emailIconUrl(mot)` (aussi dans `@hgdc/ui/email-icon`, sans React Email, pour un HTML assemblé à la main) : `https://hgdc.ch/email/<mot>.png?v=<empreinte>` (fichiers dans `@hgdc/ui/email-icons/`, tirés des pastilles par `bun scripts/family.ts` ; l'empreinte `EMAIL_ICON_VERSION` change avec l'image et fait recharger les clients mail qui l'ont en cache). Un exemple par type : `scripts/email-showcase.tsx`.
|
|
176
181
|
|
|
177
182
|
## Règles de la DA
|
|
178
183
|
|
package/dist/account-menu.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type AccountUser = {
|
|
|
20
20
|
image?: string | null;
|
|
21
21
|
};
|
|
22
22
|
/**
|
|
23
|
-
* Le menu du compte, en bas de la barre latérale : profil, sécurité, thème, raccourcis, autres
|
|
23
|
+
* Le menu du compte, en bas de la barre latérale : profil, sécurité, choix du thème en trois icônes, raccourcis, autres
|
|
24
24
|
* produits HGDC, déconnexion. `row` : avatar, nom et adresse (l'avatar seul en rail) ; `avatar` :
|
|
25
25
|
* l'avatar seul, pour une barre du haut. Le produit passe les actions ; le thème passe par
|
|
26
26
|
* `ThemeProvider`.
|
package/dist/account-menu.js
CHANGED
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import ComputerIcon from "@hugeicons/core-free-icons/ComputerIcon";
|
|
3
2
|
import DashboardSquare01Icon from "@hugeicons/core-free-icons/DashboardSquare01Icon";
|
|
4
3
|
import KeyboardIcon from "@hugeicons/core-free-icons/KeyboardIcon";
|
|
5
4
|
import Logout01Icon from "@hugeicons/core-free-icons/Logout01Icon";
|
|
6
|
-
import Moon02Icon from "@hugeicons/core-free-icons/Moon02Icon";
|
|
7
5
|
import SecurityLockIcon from "@hugeicons/core-free-icons/SecurityLockIcon";
|
|
8
|
-
import Sun03Icon from "@hugeicons/core-free-icons/Sun03Icon";
|
|
9
6
|
import Tick02Icon from "@hugeicons/core-free-icons/Tick02Icon";
|
|
10
7
|
import UserIcon from "@hugeicons/core-free-icons/UserIcon";
|
|
11
8
|
import { HugeiconsIcon } from "@hugeicons/react";
|
|
12
9
|
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
|
|
13
10
|
import { Avatar, AvatarFallback, AvatarImage } from "./avatar.js";
|
|
14
|
-
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel,
|
|
11
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, } from "./dropdown-menu.js";
|
|
15
12
|
import { initials } from "./initials.js";
|
|
16
13
|
import { MENU_LINK_ITEM } from "./org-switcher.js";
|
|
17
14
|
import { ProductBadge } from "./product-badge.js";
|
|
18
15
|
import { productMenuEntries } from "./products.js";
|
|
19
16
|
import { useLeaveOnClick, useProductRailShown } from "./shell-placement.js";
|
|
20
|
-
import {
|
|
21
|
-
const THEMES = ["light", "dark", "system"];
|
|
17
|
+
import { ThemeSwitch } from "./theme-switch.js";
|
|
22
18
|
/**
|
|
23
|
-
* Le menu du compte, en bas de la barre latérale : profil, sécurité, thème, raccourcis, autres
|
|
19
|
+
* Le menu du compte, en bas de la barre latérale : profil, sécurité, choix du thème en trois icônes, raccourcis, autres
|
|
24
20
|
* produits HGDC, déconnexion. `row` : avatar, nom et adresse (l'avatar seul en rail) ; `avatar` :
|
|
25
21
|
* l'avatar seul, pour une barre du haut. Le produit passe les actions ; le thème passe par
|
|
26
22
|
* `ThemeProvider`.
|
|
@@ -31,15 +27,10 @@ const THEMES = ["light", "dark", "system"];
|
|
|
31
27
|
* liste ; `products={[]}` la retire.
|
|
32
28
|
*/
|
|
33
29
|
export function AccountMenu({ user, onProfile, onSecurity, onShortcuts, onSignOut, products: productsProp, product: currentProduct, accountHref, children, variant = "row", }) {
|
|
34
|
-
const { theme, setTheme } = useTheme();
|
|
35
30
|
const products = productsProp ?? productMenuEntries({ current: currentProduct, accountHref });
|
|
36
31
|
const railShown = useProductRailShown();
|
|
37
32
|
const leaveOnClick = useLeaveOnClick();
|
|
38
|
-
return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: variant === "row" ? (_jsx("button", { type: "button", "aria-label": `Compte de ${user.name}`, className: "flex w-full min-w-0 items-center gap-2.5 rounded-xl p-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:justify-center" })) : (_jsx("button", { type: "button", "aria-label": `Compte de ${user.name}`, className: "touch-target grid size-9 place-items-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring/50" })), children: [_jsxs(Avatar, { children: [user.image ? _jsx(AvatarImage, { src: user.image, alt: "" }) : null, _jsx(AvatarFallback, { className: "text-xs font-semibold uppercase", children: initials(user.name, user.email) })] }), variant === "row" ? (_jsxs("span", { className: "min-w-0 flex-1 lg:rail:hidden", children: [_jsx("span", { className: "block truncate text-sm font-medium", children: user.name }), _jsx("span", { className: "block truncate text-xs text-muted-foreground lg:text-shell-muted", children: user.email })] })) : null] }), _jsxs(DropdownMenuContent, { align: variant === "row" ? "start" : "end", side: variant === "row" ? "top" : "bottom", className: "w-64", children: [_jsx(DropdownMenuGroup, { children: _jsxs(DropdownMenuLabel, { children: [_jsx("span", { className: "block truncate text-foreground", children: user.name }), _jsx("span", { className: "block truncate", children: user.email })] }) }), _jsx(DropdownMenuSeparator, {}), onProfile ? (_jsxs(DropdownMenuItem, { onClick: onProfile, children: [_jsx(HugeiconsIcon, { icon: UserIcon, "aria-hidden": true }), "Profil"] })) : null, onSecurity ? (_jsxs(DropdownMenuItem, { onClick: onSecurity, children: [_jsx(HugeiconsIcon, { icon: SecurityLockIcon, "aria-hidden": true }), "S\u00E9curit\u00E9"] })) : null, children, _jsxs(
|
|
39
|
-
const next = THEMES.find((t) => t === value);
|
|
40
|
-
if (next)
|
|
41
|
-
setTheme(next);
|
|
42
|
-
}, 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, { className: railShown ? "lg:hidden" : undefined }), _jsxs(DropdownMenuGroup, { className: railShown ? "lg:hidden" : undefined, 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, onClick: leaveOnClick, 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"] })] })] }));
|
|
33
|
+
return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: variant === "row" ? (_jsx("button", { type: "button", "aria-label": `Compte de ${user.name}`, className: "flex w-full min-w-0 items-center gap-2.5 rounded-xl p-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:justify-center" })) : (_jsx("button", { type: "button", "aria-label": `Compte de ${user.name}`, className: "touch-target grid size-9 place-items-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring/50" })), children: [_jsxs(Avatar, { children: [user.image ? _jsx(AvatarImage, { src: user.image, alt: "" }) : null, _jsx(AvatarFallback, { className: "text-xs font-semibold uppercase", children: initials(user.name, user.email) })] }), variant === "row" ? (_jsxs("span", { className: "min-w-0 flex-1 lg:rail:hidden", children: [_jsx("span", { className: "block truncate text-sm font-medium", children: user.name }), _jsx("span", { className: "block truncate text-xs text-muted-foreground lg:text-shell-muted", children: user.email })] })) : null] }), _jsxs(DropdownMenuContent, { align: variant === "row" ? "start" : "end", side: variant === "row" ? "top" : "bottom", className: "w-64", children: [_jsx(DropdownMenuGroup, { children: _jsxs(DropdownMenuLabel, { children: [_jsx("span", { className: "block truncate text-foreground", children: user.name }), _jsx("span", { className: "block truncate", children: user.email })] }) }), _jsx(DropdownMenuSeparator, {}), onProfile ? (_jsxs(DropdownMenuItem, { onClick: onProfile, children: [_jsx(HugeiconsIcon, { icon: UserIcon, "aria-hidden": true }), "Profil"] })) : null, onSecurity ? (_jsxs(DropdownMenuItem, { onClick: onSecurity, children: [_jsx(HugeiconsIcon, { icon: SecurityLockIcon, "aria-hidden": true }), "S\u00E9curit\u00E9"] })) : null, children, _jsx(ThemeSwitch, {}), 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, { className: railShown ? "lg:hidden" : undefined }), _jsxs(DropdownMenuGroup, { className: railShown ? "lg:hidden" : undefined, 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, onClick: leaveOnClick, 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"] })] })] }));
|
|
43
34
|
}
|
|
44
35
|
function ProductMenuIcon({ product }) {
|
|
45
36
|
if (product.badge)
|
package/dist/brand.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Fichiers de `family/app/<mot>/`, servis à la racine du produit (favicon, icônes d'application). */
|
|
2
|
+
export declare const APP_ICON_FILES: readonly ["favicon.svg", "favicon.ico", "apple-touch-icon.png", "icon-192.png", "icon-512.png", "icon-maskable-512.png"];
|
|
3
|
+
export type Copy = Readonly<{
|
|
4
|
+
from: string;
|
|
5
|
+
to: string;
|
|
6
|
+
}>;
|
|
7
|
+
/**
|
|
8
|
+
* Copies qui posent les icônes d'un produit dans `publicDir` : `family/app/<mot>/*`, plus l'icône
|
|
9
|
+
* d'e-mail du produit sous chaque nom de `emailAs` (adresses déjà parties dans des e-mails).
|
|
10
|
+
*/
|
|
11
|
+
export declare function appIconCopies(word: string, publicDir: string, emailAs?: ReadonlyArray<string>): Array<Copy>;
|
|
12
|
+
/** Copies des icônes d'e-mail de tous les produits (`<mot>.png`) dans `dir`, pour le site qui les sert. */
|
|
13
|
+
export declare function emailIconCopies(dir: string): Array<Copy>;
|
|
14
|
+
export declare function runCopies(copies: ReadonlyArray<Copy>): Promise<void>;
|
package/dist/brand.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Copie des icônes du kit dans un produit ou un site, au build et au démarrage du dev (`hgdc-brand`).
|
|
2
|
+
// Les produits ne versionnent plus ces fichiers : les icônes suivent la version de `@hgdc/ui`
|
|
3
|
+
// installée (ADR 0017, runbook `docs/runbooks/changer-une-icone.md`).
|
|
4
|
+
import { copyFile, mkdir } from "node:fs/promises";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { FAMILY_PRODUCTS, isFamilyProduct } from "./family-svg.js";
|
|
8
|
+
const PACKAGE_ROOT = fileURLToPath(new URL("../", import.meta.url));
|
|
9
|
+
/** Fichiers de `family/app/<mot>/`, servis à la racine du produit (favicon, icônes d'application). */
|
|
10
|
+
export const APP_ICON_FILES = [
|
|
11
|
+
"favicon.svg",
|
|
12
|
+
"favicon.ico",
|
|
13
|
+
"apple-touch-icon.png",
|
|
14
|
+
"icon-192.png",
|
|
15
|
+
"icon-512.png",
|
|
16
|
+
"icon-maskable-512.png",
|
|
17
|
+
];
|
|
18
|
+
function assertProduct(word) {
|
|
19
|
+
if (!isFamilyProduct(word))
|
|
20
|
+
throw new Error(`produit inconnu « ${word} » (attendu : ${FAMILY_PRODUCTS.join(", ")})`);
|
|
21
|
+
return word;
|
|
22
|
+
}
|
|
23
|
+
/** Nom de fichier simple, sans dossier : une copie ne sort jamais du dossier cible. */
|
|
24
|
+
function assertFileName(name) {
|
|
25
|
+
if (!/^[\w.-]+$/.test(name) || name.startsWith("."))
|
|
26
|
+
throw new Error(`nom de fichier invalide « ${name} »`);
|
|
27
|
+
return name;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Copies qui posent les icônes d'un produit dans `publicDir` : `family/app/<mot>/*`, plus l'icône
|
|
31
|
+
* d'e-mail du produit sous chaque nom de `emailAs` (adresses déjà parties dans des e-mails).
|
|
32
|
+
*/
|
|
33
|
+
export function appIconCopies(word, publicDir, emailAs = []) {
|
|
34
|
+
const product = assertProduct(word);
|
|
35
|
+
return [
|
|
36
|
+
...APP_ICON_FILES.map((file) => ({
|
|
37
|
+
from: join(PACKAGE_ROOT, "family", "app", product, file),
|
|
38
|
+
to: join(publicDir, file),
|
|
39
|
+
})),
|
|
40
|
+
...emailAs.map((name) => ({
|
|
41
|
+
from: join(PACKAGE_ROOT, "email-icons", `${product}.png`),
|
|
42
|
+
to: join(publicDir, assertFileName(name)),
|
|
43
|
+
})),
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
/** Copies des icônes d'e-mail de tous les produits (`<mot>.png`) dans `dir`, pour le site qui les sert. */
|
|
47
|
+
export function emailIconCopies(dir) {
|
|
48
|
+
return FAMILY_PRODUCTS.map((product) => ({
|
|
49
|
+
from: join(PACKAGE_ROOT, "email-icons", `${product}.png`),
|
|
50
|
+
to: join(dir, `${product}.png`),
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
export async function runCopies(copies) {
|
|
54
|
+
for (const { from, to } of copies) {
|
|
55
|
+
await mkdir(dirname(to), { recursive: true });
|
|
56
|
+
await copyFile(from, to);
|
|
57
|
+
}
|
|
58
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hgdc-brand : pose les icônes du kit dans un produit ou un site (src/brand.ts).
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { appIconCopies, emailIconCopies, runCopies } from "./brand.js";
|
|
6
|
+
const USAGE = `Usage : hgdc-brand <commande>
|
|
7
|
+
|
|
8
|
+
sync <mot> [--public <dossier>] [--email-as <fichier>]...
|
|
9
|
+
pose favicon et icônes d'application du produit dans public/ (défaut : ./public) ;
|
|
10
|
+
--email-as copie aussi son icône d'e-mail sous ce nom (adresse déjà envoyée)
|
|
11
|
+
emails <dossier>
|
|
12
|
+
pose les icônes d'e-mail de tous les produits (<mot>.png) dans le dossier
|
|
13
|
+
|
|
14
|
+
Les fichiers posés ne se versionnent pas : les lister dans le .gitignore.`;
|
|
15
|
+
const { positionals, values } = parseArgs({
|
|
16
|
+
allowPositionals: true,
|
|
17
|
+
options: {
|
|
18
|
+
public: { type: "string", default: "public" },
|
|
19
|
+
"email-as": { type: "string", multiple: true, default: [] },
|
|
20
|
+
help: { type: "boolean", short: "h", default: false },
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
if (values.help) {
|
|
24
|
+
console.log(USAGE);
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
const [command, target] = positionals;
|
|
28
|
+
try {
|
|
29
|
+
if (command === "sync" && target) {
|
|
30
|
+
const copies = appIconCopies(target, resolve(values.public), values["email-as"]);
|
|
31
|
+
await runCopies(copies);
|
|
32
|
+
console.log(`hgdc-brand : ${copies.length} icônes de ${target} posées dans ${values.public}/`);
|
|
33
|
+
}
|
|
34
|
+
else if (command === "emails" && target) {
|
|
35
|
+
const copies = emailIconCopies(resolve(target));
|
|
36
|
+
await runCopies(copies);
|
|
37
|
+
console.log(`hgdc-brand : ${copies.length} icônes d'e-mail posées dans ${target}/`);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
console.error(USAGE);
|
|
41
|
+
process.exit(2);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error(`hgdc-brand : ${error instanceof Error ? error.message : String(error)}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seule adresse des icônes des e-mails : hgdc.ch les sert depuis `packages/ui/email-icons/`, posées
|
|
3
|
+
* à son build par `hgdc-brand emails public/email`.
|
|
4
|
+
*/
|
|
5
|
+
export declare const EMAIL_ICON_BASE = "https://hgdc.ch/email/";
|
|
6
|
+
/**
|
|
7
|
+
* Icône en PNG du produit (`mail` → `https://hgdc.ch/email/mail.png?v=<empreinte>`). L'empreinte
|
|
8
|
+
* change avec l'image : un client mail qui l'a gardée en cache charge la nouvelle.
|
|
9
|
+
*/
|
|
10
|
+
export declare function emailIconUrl(word: string): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Adresse des icônes des e-mails, sans React Email : un code serveur qui assemble son propre HTML
|
|
2
|
+
// (filigrane de Mail) l'importe sans tirer le modèle (`@hgdc/ui/email-icon`).
|
|
3
|
+
import { EMAIL_ICON_VERSION, isFamilyProduct } from "./family-svg.js";
|
|
4
|
+
/**
|
|
5
|
+
* Seule adresse des icônes des e-mails : hgdc.ch les sert depuis `packages/ui/email-icons/`, posées
|
|
6
|
+
* à son build par `hgdc-brand emails public/email`.
|
|
7
|
+
*/
|
|
8
|
+
export const EMAIL_ICON_BASE = "https://hgdc.ch/email/";
|
|
9
|
+
/**
|
|
10
|
+
* Icône en PNG du produit (`mail` → `https://hgdc.ch/email/mail.png?v=<empreinte>`). L'empreinte
|
|
11
|
+
* change avec l'image : un client mail qui l'a gardée en cache charge la nouvelle.
|
|
12
|
+
*/
|
|
13
|
+
export function emailIconUrl(word) {
|
|
14
|
+
const product = word.toLowerCase();
|
|
15
|
+
const url = `${EMAIL_ICON_BASE}${product}.png`;
|
|
16
|
+
return isFamilyProduct(product) ? `${url}?v=${EMAIL_ICON_VERSION[product]}` : url;
|
|
17
|
+
}
|
package/dist/email.d.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { type ReactElement, type ReactNode } from "react";
|
|
2
2
|
import { type EmailAccent, type EmailTone } from "./email-palette.ts";
|
|
3
|
+
import { EMAIL_ICON_BASE, emailIconUrl } from "./email-icon.ts";
|
|
3
4
|
import { type HgdcCatalogProduct } from "./products.ts";
|
|
4
5
|
/**
|
|
5
6
|
* Modèle des e-mails transactionnels de la famille : une mise en page (`EmailLayout`) et des blocs,
|
|
6
7
|
* rendus par React Email en HTML pour clients mail (tableaux, styles en ligne, 600 px) et en texte.
|
|
7
8
|
* Le rendu se fait côté serveur (action Convex `"use node"`, route serveur) : `renderEmail`.
|
|
8
9
|
*/
|
|
9
|
-
|
|
10
|
-
export declare const EMAIL_ICON_BASE = "https://hgdc.ch/email/";
|
|
11
|
-
/** Icône en PNG du produit (`mail` → `https://hgdc.ch/email/mail.png`). */
|
|
12
|
-
export declare function emailIconUrl(word: string): string;
|
|
10
|
+
export { EMAIL_ICON_BASE, emailIconUrl };
|
|
13
11
|
export type EmailLayoutProps = {
|
|
14
12
|
/** Produit qui écrit : son mot (« Status ») ou son entrée du catalogue. */
|
|
15
13
|
product: HgdcCatalogProduct | string;
|
package/dist/email.js
CHANGED
|
@@ -2,23 +2,19 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { createContext, useContext } from "react";
|
|
3
3
|
import { Body, Button, Column, Container, Head, Heading, Hr, Html, Img, Link, Preview, Row, Section, Text, render, toPlainText, } from "react-email";
|
|
4
4
|
import { EMAIL_DARK, EMAIL_LIGHT, EMAIL_TONES, HGDC_EMAIL_ACCENT, isHexColor, readableOn, } from "./email-palette.js";
|
|
5
|
+
import { EMAIL_ICON_BASE, emailIconUrl } from "./email-icon.js";
|
|
5
6
|
import { HGDC_ACCOUNT, HGDC_PRODUCTS } from "./products.js";
|
|
6
7
|
/**
|
|
7
8
|
* Modèle des e-mails transactionnels de la famille : une mise en page (`EmailLayout`) et des blocs,
|
|
8
9
|
* rendus par React Email en HTML pour clients mail (tableaux, styles en ligne, 600 px) et en texte.
|
|
9
10
|
* Le rendu se fait côté serveur (action Convex `"use node"`, route serveur) : `renderEmail`.
|
|
10
11
|
*/
|
|
11
|
-
|
|
12
|
-
export const EMAIL_ICON_BASE = "https://hgdc.ch/email/";
|
|
12
|
+
export { EMAIL_ICON_BASE, emailIconUrl };
|
|
13
13
|
const MAIL_HREF = "https://mail.hgdc.ch";
|
|
14
14
|
const LEGAL = "Hugo Gebel Développement Conseil, Lausanne";
|
|
15
15
|
// Pas de police web : celle du système, Inter ou Geist quand elles sont installées.
|
|
16
16
|
const FONT = 'Geist, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
17
17
|
const MONO = 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace';
|
|
18
|
-
/** Icône en PNG du produit (`mail` → `https://hgdc.ch/email/mail.png`). */
|
|
19
|
-
export function emailIconUrl(word) {
|
|
20
|
-
return `${EMAIL_ICON_BASE}${word.toLowerCase()}.png`;
|
|
21
|
-
}
|
|
22
18
|
function findProduct(product) {
|
|
23
19
|
if (typeof product !== "string")
|
|
24
20
|
return product;
|
package/dist/family-svg.d.ts
CHANGED
|
@@ -13,5 +13,10 @@ export type ThemedSvg = Readonly<{
|
|
|
13
13
|
* sombre (`family/tile/<mot>.svg`).
|
|
14
14
|
*/
|
|
15
15
|
export declare const PRODUCT_BADGE_SVG: Readonly<Record<FamilyProduct, string>>;
|
|
16
|
+
/**
|
|
17
|
+
* Empreinte de chaque icône d'e-mail (`email-icons/<mot>.png`), ajoutée à son adresse : un client
|
|
18
|
+
* mail qui garde l'ancienne image en cache (proxy d'images de Gmail) charge la nouvelle.
|
|
19
|
+
*/
|
|
20
|
+
export declare const EMAIL_ICON_VERSION: Readonly<Record<FamilyProduct, string>>;
|
|
16
21
|
/** SVG du H « Double », la marque de la H Collection, sans fond (`family/hcollection-icon(-dark).svg`). */
|
|
17
22
|
export declare const FAMILY_MARK_SVG: ThemedSvg;
|
package/dist/family-svg.js
CHANGED
|
@@ -18,6 +18,19 @@ export const PRODUCT_BADGE_SVG = {
|
|
|
18
18
|
analytics: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#fff"/><svg x="7.68" y="7.68" width="48.64" height="48.64" viewBox="15 15 34 34"><defs><linearGradient id="g" x1="14" y1="14" x2="50" y2="50" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffb020"/><stop offset="1" stop-color="#e58309"/></linearGradient></defs><path d="M18.3 34H21.7A2.8 2.8 0 0 1 24.5 36.8V46.2A2.8 2.8 0 0 1 21.7 49H18.3A2.8 2.8 0 0 1 15.5 46.2V36.8A2.8 2.8 0 0 1 18.3 34Z" fill="url(#g)" fill-opacity="0.4" fill-rule="evenodd"/><path d="M30.3 25H33.7A2.8 2.8 0 0 1 36.5 27.8V46.2A2.8 2.8 0 0 1 33.7 49H30.3A2.8 2.8 0 0 1 27.5 46.2V27.8A2.8 2.8 0 0 1 30.3 25Z" fill="url(#g)" fill-opacity="0.7" fill-rule="evenodd"/><path d="M42.3 15H45.7A2.8 2.8 0 0 1 48.5 17.8V46.2A2.8 2.8 0 0 1 45.7 49H42.3A2.8 2.8 0 0 1 39.5 46.2V17.8A2.8 2.8 0 0 1 42.3 15Z" fill="url(#g)" fill-rule="evenodd"/></svg></svg>',
|
|
19
19
|
docs: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#fff"/><svg x="7.68" y="7.68" width="48.64" height="48.64" viewBox="14 13 36 36"><defs><linearGradient id="g" x1="14" y1="14" x2="50" y2="50" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0ec1dc"/><stop offset="1" stop-color="#0aa3c1"/></linearGradient></defs><path d="M26 13H46A3 3 0 0 1 49 16V43A3 3 0 0 1 46 46H26A3 3 0 0 1 23 43V16A3 3 0 0 1 26 13Z" fill="url(#g)" fill-opacity="0.22" fill-rule="evenodd"/><path d="M18 18H32V24.5A2.5 2.5 0 0 0 34.5 27H41V46A3 3 0 0 1 38 49H18A3 3 0 0 1 15 46V21A3 3 0 0 1 18 18ZM21.3 33H34.7A1.3 1.3 0 0 1 36 34.3V34.300000000000004A1.3 1.3 0 0 1 34.7 35.6H21.3A1.3 1.3 0 0 1 20 34.300000000000004V34.3A1.3 1.3 0 0 1 21.3 33ZM21.3 39H29.7A1.3 1.3 0 0 1 31 40.3V40.300000000000004A1.3 1.3 0 0 1 29.7 41.6H21.3A1.3 1.3 0 0 1 20 40.300000000000004V40.3A1.3 1.3 0 0 1 21.3 39Z" fill="url(#g)" fill-rule="evenodd"/><path d="M32 18L41 27H34.5A2.5 2.5 0 0 1 32 24.5Z" fill="url(#g)" fill-opacity="0.45" fill-rule="evenodd"/></svg></svg>',
|
|
20
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Empreinte de chaque icône d'e-mail (`email-icons/<mot>.png`), ajoutée à son adresse : un client
|
|
23
|
+
* mail qui garde l'ancienne image en cache (proxy d'images de Gmail) charge la nouvelle.
|
|
24
|
+
*/
|
|
25
|
+
export const EMAIL_ICON_VERSION = {
|
|
26
|
+
account: "2263211d",
|
|
27
|
+
host: "ceb0451c",
|
|
28
|
+
mail: "5c18df41",
|
|
29
|
+
portal: "e18dd184",
|
|
30
|
+
status: "3f91af62",
|
|
31
|
+
analytics: "be693909",
|
|
32
|
+
docs: "4f1fb8a0",
|
|
33
|
+
};
|
|
21
34
|
/** SVG du H « Double », la marque de la H Collection, sans fond (`family/hcollection-icon(-dark).svg`). */
|
|
22
35
|
export const FAMILY_MARK_SVG = {
|
|
23
36
|
light: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="9 9 46 46"><defs><linearGradient id="hl" x1="0" y1="11.5" x2="0" y2="47.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2c78fd"/><stop offset="1" stop-color="#7343fe"/></linearGradient><linearGradient id="hr" x1="0" y1="11.5" x2="0" y2="47.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fe5292"/><stop offset="1" stop-color="#ff9a46"/></linearGradient><linearGradient id="hf" x1="24.5" y1="0" x2="34.5" y2="0" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#000"/></linearGradient><mask id="hm" maskUnits="userSpaceOnUse" x="0" y="0" width="64" height="64"><rect x="24" y="24" width="11" height="11" fill="url(#hf)"/></mask><g id="h"><path d="M24 25H35V34H24Z" fill="url(#hr)"/><path d="M24 25H35V34H24Z" fill="url(#hl)" mask="url(#hm)"/><path d="M17.5 11.5H21.5A3 3 0 0 1 24.5 14.5V44.5A3 3 0 0 1 21.5 47.5H17.5A3 3 0 0 1 14.5 44.5V14.5A3 3 0 0 1 17.5 11.5Z" fill="url(#hl)"/><path d="M37.5 11.5H41.5A3 3 0 0 1 44.5 14.5V44.5A3 3 0 0 1 41.5 47.5H37.5A3 3 0 0 1 34.5 44.5V14.5A3 3 0 0 1 37.5 11.5Z" fill="url(#hr)"/></g></defs><use href="#h" transform="translate(6 6)" opacity="0.35"/><use href="#h"/></svg>',
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Le choix du thème en trois icônes dans une pilule (Système, Clair, Sombre), à poser dans un
|
|
3
|
+
* menu : chaque choix est une entrée du menu, qui ne se ferme pas. Flèches gauche et droite
|
|
4
|
+
* passent d'un choix à l'autre et l'appliquent ; le thème vient de `ThemeProvider`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function ThemeSwitch({ className }: {
|
|
7
|
+
className?: string;
|
|
8
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import ComputerIcon from "@hugeicons/core-free-icons/ComputerIcon";
|
|
3
|
+
import Moon02Icon from "@hugeicons/core-free-icons/Moon02Icon";
|
|
4
|
+
import Sun03Icon from "@hugeicons/core-free-icons/Sun03Icon";
|
|
5
|
+
import { HugeiconsIcon } from "@hugeicons/react";
|
|
6
|
+
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
|
|
7
|
+
import { useRef } from "react";
|
|
8
|
+
import { cn } from "./cn.js";
|
|
9
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
|
|
10
|
+
import { useTheme } from "./theme.js";
|
|
11
|
+
const CHOICES = [
|
|
12
|
+
{ value: "system", label: "Système", icon: ComputerIcon },
|
|
13
|
+
{ value: "light", label: "Clair", icon: Sun03Icon },
|
|
14
|
+
{ value: "dark", label: "Sombre", icon: Moon02Icon },
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* Le choix du thème en trois icônes dans une pilule (Système, Clair, Sombre), à poser dans un
|
|
18
|
+
* menu : chaque choix est une entrée du menu, qui ne se ferme pas. Flèches gauche et droite
|
|
19
|
+
* passent d'un choix à l'autre et l'appliquent ; le thème vient de `ThemeProvider`.
|
|
20
|
+
*/
|
|
21
|
+
export function ThemeSwitch({ className }) {
|
|
22
|
+
const { theme, setTheme } = useTheme();
|
|
23
|
+
const group = useRef(null);
|
|
24
|
+
function onKeyDown(event) {
|
|
25
|
+
const step = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
|
|
26
|
+
if (step === 0)
|
|
27
|
+
return;
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
event.stopPropagation();
|
|
30
|
+
const index = CHOICES.findIndex((choice) => choice.value === theme);
|
|
31
|
+
const next = CHOICES[(index + step + CHOICES.length) % CHOICES.length];
|
|
32
|
+
if (!next)
|
|
33
|
+
return;
|
|
34
|
+
setTheme(next.value);
|
|
35
|
+
group.current?.querySelector(`[data-theme-choice="${next.value}"]`)?.focus();
|
|
36
|
+
}
|
|
37
|
+
const current = CHOICES.find((choice) => choice.value === theme);
|
|
38
|
+
return (_jsxs("div", { className: cn("flex items-center gap-2.5 px-3 py-[3px] text-sm font-medium max-lg:min-h-11 coarse:min-h-11", className), children: [_jsx(HugeiconsIcon, { icon: current?.icon ?? ComputerIcon, "aria-hidden": true, className: "size-4 shrink-0" }), _jsx("span", { className: "flex-1", children: "Th\u00E8me" }), _jsx("div", { ref: group, role: "radiogroup", "aria-label": "Th\u00E8me", tabIndex: -1, onKeyDown: onKeyDown, className: "flex w-fit items-center gap-0.5 rounded-full border border-border p-0.5", children: CHOICES.map(({ value, label, icon }) => {
|
|
39
|
+
const active = theme === value;
|
|
40
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx(MenuPrimitive.Item, { closeOnClick: false, role: "radio", "aria-checked": active, "aria-label": label, "data-theme-choice": value, onClick: () => setTheme(value), className: cn("touch-target grid size-6 cursor-default place-items-center rounded-full outline-none", "transition-colors duration-(--duration-fast) ease-out focus-visible:ring-2 focus-visible:ring-ring/50", active ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground") }), children: _jsx(HugeiconsIcon, { icon: icon, "aria-hidden": true, className: "size-3.5" }) }), _jsx(TooltipContent, { children: label })] }, value));
|
|
41
|
+
}) })] }));
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hgdc/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Interface de la H Collection : jetons de la DA, thèmes, polices et composants.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://github.com/hugogebel-boop/hgdc-kit/tree/main/packages/ui#readme",
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
"email-icons",
|
|
21
21
|
"family"
|
|
22
22
|
],
|
|
23
|
+
"bin": {
|
|
24
|
+
"hgdc-brand": "./dist/cli.js"
|
|
25
|
+
},
|
|
23
26
|
"sideEffects": [
|
|
24
27
|
"*.css"
|
|
25
28
|
],
|
|
@@ -34,7 +37,7 @@
|
|
|
34
37
|
}
|
|
35
38
|
},
|
|
36
39
|
"scripts": {
|
|
37
|
-
"build": "tsc -p tsconfig.build.json"
|
|
40
|
+
"build": "tsc -p tsconfig.build.json && tsc -p tsconfig.cli.json"
|
|
38
41
|
},
|
|
39
42
|
"dependencies": {
|
|
40
43
|
"@base-ui/react": "^1.9.0",
|