@hgdc/ui 0.5.1 → 0.7.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 +34 -1
- package/dist/access-request.d.ts +50 -0
- package/dist/access-request.js +84 -0
- package/dist/email-palette.d.ts +45 -0
- package/dist/email-palette.js +71 -0
- package/dist/email.d.ts +71 -0
- package/dist/email.js +173 -0
- package/dist/products.d.ts +6 -0
- package/dist/products.js +11 -1
- package/email-icons/account.png +0 -0
- package/email-icons/analytics.png +0 -0
- package/email-icons/docs.png +0 -0
- package/email-icons/host.png +0 -0
- package/email-icons/mail.png +0 -0
- package/email-icons/portal.png +0 -0
- package/email-icons/status.png +0 -0
- package/package.json +14 -3
package/README.md
CHANGED
|
@@ -99,6 +99,8 @@ Filtres, tri et page vivent dans l'URL du produit : les composants sont contrôl
|
|
|
99
99
|
| `money-field` | `<MoneyField value={centimes} onChange={…} />` : CHF en centimes, « 1’250.50 », accepte « 12.– » ; `formatChf`, `parseMoney` |
|
|
100
100
|
| `address-fields` | `<AddressFields value={address} onChange={…} errors={validatePostalAddress(address)} />` : rue, numéro, NPA, localité, pays (facture QR) |
|
|
101
101
|
|
|
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.
|
|
103
|
+
|
|
102
104
|
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.
|
|
103
105
|
|
|
104
106
|
## Contrôles et surfaces flottantes
|
|
@@ -111,6 +113,37 @@ Les champs de date sont ceux du navigateur, sans calendrier maison (ADR 0006). L
|
|
|
111
113
|
| `tooltip` | `<TooltipProvider>` autour de l'app, puis `<Tooltip><TooltipTrigger aria-label="Copier">…</TooltipTrigger><TooltipContent>Copier</TooltipContent></Tooltip>` |
|
|
112
114
|
| `popover` | `<Popover><PopoverTrigger>Aide</PopoverTrigger><PopoverContent><PopoverTitle>…</PopoverTitle></PopoverContent></Popover>` |
|
|
113
115
|
|
|
116
|
+
## E-mails
|
|
117
|
+
|
|
118
|
+
`@hgdc/ui/email` : le modèle commun des e-mails transactionnels, rendu par React Email (`bun add --exact react-email@6.11.1`, version de la stack). Rendu côté serveur, dans une action Convex `"use node"`.
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import { EmailButton, EmailHeading, EmailLayout, EmailText, renderEmail } from "@hgdc/ui/email"
|
|
122
|
+
|
|
123
|
+
const { html, text } = await renderEmail(
|
|
124
|
+
<EmailLayout product="Portal" preview="Claire t'invite à rejoindre Atelier Dupont.">
|
|
125
|
+
<EmailHeading>Claire t'invite dans Atelier Dupont</EmailHeading>
|
|
126
|
+
<EmailText>Tu pourras suivre le projet, signer et payer au même endroit.</EmailText>
|
|
127
|
+
<EmailButton href={url}>Voir l'invitation</EmailButton>
|
|
128
|
+
</EmailLayout>,
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
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`.
|
|
146
|
+
|
|
114
147
|
## Règles de la DA
|
|
115
148
|
|
|
116
149
|
- Chrome monochrome, un seul accent violet : focus, lien, un geste principal par écran. « Vous êtes ici » (barre, onglets) reste en teinte.
|
|
@@ -120,6 +153,6 @@ Les champs de date sont ceux du navigateur, sans calendrier maison (ADR 0006). L
|
|
|
120
153
|
- Graphiques : `--chart-1` à `--chart-6` dans cet ordre, validées pour le daltonisme dans les deux thèmes ; au-delà de six séries, regrouper en « Autres ».
|
|
121
154
|
- Référence de l'interface : Host. Couleur d'un client : `--brand-accent`.
|
|
122
155
|
|
|
123
|
-
Mode d'emploi complet : [Adopter le kit](https://github.com/hugogebel-boop/hgdc-kit/blob/main/docs/guides/adopter-le-kit.md#7-interface). Décisions : [ADR 0005](https://github.com/hugogebel-boop/hgdc-kit/blob/main/docs/adr/0005-ui-css-brute-et-sous-chemins.md), [ADR 0006](https://github.com/hugogebel-boop/hgdc-kit/blob/main/docs/adr/0006-ui-composants-des-consoles.md).
|
|
156
|
+
Mode d'emploi complet : [Adopter le kit](https://github.com/hugogebel-boop/hgdc-kit/blob/main/docs/guides/adopter-le-kit.md#7-interface). Décisions : [ADR 0005](https://github.com/hugogebel-boop/hgdc-kit/blob/main/docs/adr/0005-ui-css-brute-et-sous-chemins.md), [ADR 0006](https://github.com/hugogebel-boop/hgdc-kit/blob/main/docs/adr/0006-ui-composants-des-consoles.md), [ADR 0007](https://github.com/hugogebel-boop/hgdc-kit/blob/main/docs/adr/0007-ui-modele-des-e-mails.md).
|
|
124
157
|
|
|
125
158
|
Tous droits réservés, Hugo Gebel Développement Conseil.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Demander un accès à un produit de la famille, sans compte : les comptes HGDC s'ouvrent sur
|
|
4
|
+
* invitation. Le formulaire poste en JSON vers un formulaire HGDC Mail Forms (`POST /f/<id>`,
|
|
5
|
+
* réponse `{ ok }` ou `{ ok: false, error: { code }, errors }`) ; l'équipe reçoit l'avis et
|
|
6
|
+
* invite depuis HGDC Account. Le texte est au tutoiement, comme les pages publiques de la famille.
|
|
7
|
+
*/
|
|
8
|
+
export type AccessRequestValues = {
|
|
9
|
+
prenom: string;
|
|
10
|
+
nom: string;
|
|
11
|
+
entreprise: string;
|
|
12
|
+
email: string;
|
|
13
|
+
message: string;
|
|
14
|
+
};
|
|
15
|
+
export declare const EMPTY_ACCESS_REQUEST: AccessRequestValues;
|
|
16
|
+
/** Ce qu'il faut de `fetch` : une adresse et une requête. */
|
|
17
|
+
export type AccessRequestFetcher = (url: string, init: RequestInit) => Promise<Response>;
|
|
18
|
+
export type AccessRequestResult = {
|
|
19
|
+
kind: "sent";
|
|
20
|
+
} | {
|
|
21
|
+
kind: "invalid";
|
|
22
|
+
errors: Record<string, string>;
|
|
23
|
+
} | {
|
|
24
|
+
kind: "failed";
|
|
25
|
+
message: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Envoie la demande et traduit la réponse. Fonction pure à part du composant : un produit peut
|
|
29
|
+
* l'appeler depuis son propre formulaire. `trap` est le piège à robots (`_gotcha`), vide pour un humain.
|
|
30
|
+
*/
|
|
31
|
+
export declare function submitAccessRequest(endpoint: string, values: AccessRequestValues, product: string, { trap, fetcher }?: {
|
|
32
|
+
trap?: string;
|
|
33
|
+
fetcher?: AccessRequestFetcher;
|
|
34
|
+
}): Promise<AccessRequestResult>;
|
|
35
|
+
export declare function AccessRequest({ endpoint, product, title, description, messageLabel, signInHref, signInRender, homeHref, homeRender, fetcher, }: {
|
|
36
|
+
/** Adresse d'envoi du formulaire Forms : `https://<deploiement>.convex.site/f/<id>`. */
|
|
37
|
+
endpoint: string;
|
|
38
|
+
/** Nom affiché et valeur du champ caché `produit` : « HGDC Mail ». */
|
|
39
|
+
product: string;
|
|
40
|
+
title?: string;
|
|
41
|
+
description?: ReactNode;
|
|
42
|
+
messageLabel?: string;
|
|
43
|
+
signInHref?: string;
|
|
44
|
+
/** Lien du routeur du produit vers la connexion : `signInRender={<Link to="/sign-in" />}`. */
|
|
45
|
+
signInRender?: ReactElement;
|
|
46
|
+
homeHref?: string;
|
|
47
|
+
homeRender?: ReactElement;
|
|
48
|
+
/** Pour les tests. */
|
|
49
|
+
fetcher?: AccessRequestFetcher;
|
|
50
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
3
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
4
|
+
import CheckmarkCircle02Icon from "@hugeicons/core-free-icons/CheckmarkCircle02Icon";
|
|
5
|
+
import { HugeiconsIcon } from "@hugeicons/react";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { Alert } from "./alert.js";
|
|
8
|
+
import { Button, buttonVariants } from "./button.js";
|
|
9
|
+
import { Field, FieldError, FieldLabel } from "./field.js";
|
|
10
|
+
import { Input } from "./input.js";
|
|
11
|
+
import { Textarea } from "./textarea.js";
|
|
12
|
+
export const EMPTY_ACCESS_REQUEST = { prenom: "", nom: "", entreprise: "", email: "", message: "" };
|
|
13
|
+
/** Les refus de l'endpoint, dits au tutoiement. */
|
|
14
|
+
const FAILURES = {
|
|
15
|
+
rate_limited: "Trop d'envois en peu de temps. Réessaie dans une minute.",
|
|
16
|
+
origin_not_allowed: "Ce formulaire ne s'envoie que depuis le site de HGDC.",
|
|
17
|
+
form_closed: "Les demandes d'accès sont fermées pour le moment. Réessaie plus tard.",
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Envoie la demande et traduit la réponse. Fonction pure à part du composant : un produit peut
|
|
21
|
+
* l'appeler depuis son propre formulaire. `trap` est le piège à robots (`_gotcha`), vide pour un humain.
|
|
22
|
+
*/
|
|
23
|
+
export async function submitAccessRequest(endpoint, values, product, { trap = "", fetcher = (url, init) => fetch(url, init) } = {}) {
|
|
24
|
+
let result;
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetcher(endpoint, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
29
|
+
body: JSON.stringify({ ...values, produit: product, _gotcha: trap }),
|
|
30
|
+
});
|
|
31
|
+
result = (await response.json());
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return { kind: "failed", message: "Impossible d'envoyer la demande. Vérifie ta connexion et réessaie." };
|
|
35
|
+
}
|
|
36
|
+
if (result.ok)
|
|
37
|
+
return { kind: "sent" };
|
|
38
|
+
if (result.errors && Object.keys(result.errors).length)
|
|
39
|
+
return { kind: "invalid", errors: result.errors };
|
|
40
|
+
return {
|
|
41
|
+
kind: "failed",
|
|
42
|
+
message: FAILURES[result.error?.code ?? ""] ?? "La demande n'est pas partie. Réessaie dans un instant.",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function LinkSlot({ href, render, className, children, }) {
|
|
46
|
+
return useRender({
|
|
47
|
+
defaultTagName: "a",
|
|
48
|
+
render,
|
|
49
|
+
props: mergeProps({ className, children }, render ? {} : { href }),
|
|
50
|
+
});
|
|
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, }) {
|
|
53
|
+
const [values, setValues] = useState(EMPTY_ACCESS_REQUEST);
|
|
54
|
+
const [trap, setTrap] = useState("");
|
|
55
|
+
const [pending, setPending] = useState(false);
|
|
56
|
+
const [fieldErrors, setFieldErrors] = useState({});
|
|
57
|
+
const [failure, setFailure] = useState(null);
|
|
58
|
+
const [sentTo, setSentTo] = useState(null);
|
|
59
|
+
const set = (key) => (event) => {
|
|
60
|
+
setValues((current) => ({ ...current, [key]: event.target.value }));
|
|
61
|
+
setFieldErrors(({ [key]: _gone, ...rest }) => rest);
|
|
62
|
+
};
|
|
63
|
+
async function submit(event) {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
setPending(true);
|
|
66
|
+
setFailure(null);
|
|
67
|
+
setFieldErrors({});
|
|
68
|
+
const result = await submitAccessRequest(endpoint, values, product, { trap, ...(fetcher ? { fetcher } : {}) });
|
|
69
|
+
if (result.kind === "sent")
|
|
70
|
+
setSentTo(values.email.trim());
|
|
71
|
+
else if (result.kind === "invalid")
|
|
72
|
+
setFieldErrors(result.errors);
|
|
73
|
+
else
|
|
74
|
+
setFailure(result.message);
|
|
75
|
+
setPending(false);
|
|
76
|
+
}
|
|
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"));
|
|
79
|
+
}
|
|
80
|
+
const invalid = (key) => (fieldErrors[key] ? true : undefined);
|
|
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
|
+
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"));
|
|
84
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Couleurs des e-mails, en hex : les clients mail ne lisent ni oklch ni les variables CSS. Chaque
|
|
3
|
+
* valeur est la traduction d'un jeton de `styles.css` (même nom), en deux thèmes.
|
|
4
|
+
*
|
|
5
|
+
* Blanc et noir purs sont évités (`#fffffe`, `#0a0a0a`) : Gmail et Outlook inversent les couleurs
|
|
6
|
+
* en mode sombre, et une valeur légèrement décalée y résiste mieux.
|
|
7
|
+
*/
|
|
8
|
+
export type EmailTheme = {
|
|
9
|
+
/** Fond autour de la carte (`muted`). */
|
|
10
|
+
ground: string;
|
|
11
|
+
card: string;
|
|
12
|
+
ink: string;
|
|
13
|
+
/** Texte courant, un cran sous l'encre. */
|
|
14
|
+
body: string;
|
|
15
|
+
/** Texte secondaire, pied de page. */
|
|
16
|
+
muted: string;
|
|
17
|
+
border: string;
|
|
18
|
+
/** Fond du code et des détails. */
|
|
19
|
+
wash: string;
|
|
20
|
+
};
|
|
21
|
+
export declare const EMAIL_LIGHT: EmailTheme;
|
|
22
|
+
export declare const EMAIL_DARK: EmailTheme;
|
|
23
|
+
/** Accent d'un produit : `--accent` en clair et en sombre. */
|
|
24
|
+
export type EmailAccent = {
|
|
25
|
+
light: string;
|
|
26
|
+
dark: string;
|
|
27
|
+
};
|
|
28
|
+
/** L'accent violet de la DA, `oklch(0.55 0.25 296)` et `oklch(0.74 0.19 296)`. */
|
|
29
|
+
export declare const HGDC_EMAIL_ACCENT: EmailAccent;
|
|
30
|
+
export type EmailTone = "neutral" | "info" | "success" | "warning" | "danger";
|
|
31
|
+
/** Encre, lavis et filet d'un encadré, par ton (jetons `--info`, `--status-*`, `--destructive`). */
|
|
32
|
+
export type ToneColors = {
|
|
33
|
+
ink: string;
|
|
34
|
+
wash: string;
|
|
35
|
+
bar: string;
|
|
36
|
+
};
|
|
37
|
+
export declare const EMAIL_TONES: Readonly<Record<EmailTone, {
|
|
38
|
+
light: ToneColors;
|
|
39
|
+
dark: ToneColors;
|
|
40
|
+
}>>;
|
|
41
|
+
export declare function isHexColor(value: string): boolean;
|
|
42
|
+
/** Rapport de contraste WCAG 2 entre deux couleurs hex, de 1 à 21. */
|
|
43
|
+
export declare function contrastRatio(a: string, b: string): number;
|
|
44
|
+
/** Le texte le plus lisible sur un fond : quasi blanc ou quasi noir. */
|
|
45
|
+
export declare function readableOn(background: string): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Couleurs des e-mails, en hex : les clients mail ne lisent ni oklch ni les variables CSS. Chaque
|
|
3
|
+
* valeur est la traduction d'un jeton de `styles.css` (même nom), en deux thèmes.
|
|
4
|
+
*
|
|
5
|
+
* Blanc et noir purs sont évités (`#fffffe`, `#0a0a0a`) : Gmail et Outlook inversent les couleurs
|
|
6
|
+
* en mode sombre, et une valeur légèrement décalée y résiste mieux.
|
|
7
|
+
*/
|
|
8
|
+
export const EMAIL_LIGHT = {
|
|
9
|
+
ground: "#f5f5f5",
|
|
10
|
+
card: "#fffffe",
|
|
11
|
+
ink: "#0a0a0a",
|
|
12
|
+
body: "#404040",
|
|
13
|
+
muted: "#696969",
|
|
14
|
+
border: "#e5e5e5",
|
|
15
|
+
wash: "#f5f5f5",
|
|
16
|
+
};
|
|
17
|
+
export const EMAIL_DARK = {
|
|
18
|
+
ground: "#0a0a0a",
|
|
19
|
+
card: "#171717",
|
|
20
|
+
ink: "#fafafa",
|
|
21
|
+
body: "#d4d4d4",
|
|
22
|
+
muted: "#a1a1a1",
|
|
23
|
+
border: "#262626",
|
|
24
|
+
wash: "#262626",
|
|
25
|
+
};
|
|
26
|
+
/** L'accent violet de la DA, `oklch(0.55 0.25 296)` et `oklch(0.74 0.19 296)`. */
|
|
27
|
+
export const HGDC_EMAIL_ACCENT = { light: "#8538ee", dark: "#b78dff" };
|
|
28
|
+
export const EMAIL_TONES = {
|
|
29
|
+
neutral: {
|
|
30
|
+
light: { ink: "#404040", wash: "#f5f5f5", bar: "#a1a1a1" },
|
|
31
|
+
dark: { ink: "#d4d4d4", wash: "#262626", bar: "#737373" },
|
|
32
|
+
},
|
|
33
|
+
info: {
|
|
34
|
+
light: { ink: "#005aba", wash: "#edf6ff", bar: "#2584f5" },
|
|
35
|
+
dark: { ink: "#83baff", wash: "#172537", bar: "#569fff" },
|
|
36
|
+
},
|
|
37
|
+
success: {
|
|
38
|
+
light: { ink: "#006836", wash: "#e6fcec", bar: "#0a924b" },
|
|
39
|
+
dark: { ink: "#6cd092", wash: "#132a1b", bar: "#46b86e" },
|
|
40
|
+
},
|
|
41
|
+
warning: {
|
|
42
|
+
light: { ink: "#8e5400", wash: "#fff4d7", bar: "#b67700" },
|
|
43
|
+
dark: { ink: "#edbb64", wash: "#32230e", bar: "#edbb64" },
|
|
44
|
+
},
|
|
45
|
+
danger: {
|
|
46
|
+
light: { ink: "#b7191c", wash: "#fff0ee", bar: "#e7000b" },
|
|
47
|
+
dark: { ink: "#ff8482", wash: "#391a17", bar: "#ff6467" },
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
51
|
+
export function isHexColor(value) {
|
|
52
|
+
return HEX.test(value);
|
|
53
|
+
}
|
|
54
|
+
function channels(hex) {
|
|
55
|
+
const full = hex.length === 4 ? hex.replace(/[0-9a-f]/gi, (c) => c + c) : hex;
|
|
56
|
+
return [1, 3, 5].map((i) => Number.parseInt(full.slice(i, i + 2), 16) / 255);
|
|
57
|
+
}
|
|
58
|
+
/** Luminance relative WCAG 2. */
|
|
59
|
+
function luminance(hex) {
|
|
60
|
+
const [r, g, b] = channels(hex).map((c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
|
|
61
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
62
|
+
}
|
|
63
|
+
/** Rapport de contraste WCAG 2 entre deux couleurs hex, de 1 à 21. */
|
|
64
|
+
export function contrastRatio(a, b) {
|
|
65
|
+
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
|
66
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
67
|
+
}
|
|
68
|
+
/** Le texte le plus lisible sur un fond : quasi blanc ou quasi noir. */
|
|
69
|
+
export function readableOn(background) {
|
|
70
|
+
return contrastRatio(background, "#fffffe") >= contrastRatio(background, "#0a0a0a") ? "#fffffe" : "#0a0a0a";
|
|
71
|
+
}
|
package/dist/email.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import { type EmailAccent, type EmailTone } from "./email-palette.ts";
|
|
3
|
+
import { type HgdcCatalogProduct } from "./products.ts";
|
|
4
|
+
/**
|
|
5
|
+
* Modèle des e-mails transactionnels de la famille : une mise en page (`EmailLayout`) et des blocs,
|
|
6
|
+
* rendus par React Email en HTML pour clients mail (tableaux, styles en ligne, 600 px) et en texte.
|
|
7
|
+
* Le rendu se fait côté serveur (action Convex `"use node"`, route serveur) : `renderEmail`.
|
|
8
|
+
*/
|
|
9
|
+
/** Où hgdc.ch sert les icônes des e-mails (`packages/ui/email-icons/`). */
|
|
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;
|
|
13
|
+
export type EmailLayoutProps = {
|
|
14
|
+
/** Produit qui écrit : son mot (« Status ») ou son entrée du catalogue. */
|
|
15
|
+
product: HgdcCatalogProduct | string;
|
|
16
|
+
/** Aperçu affiché après l'objet dans la boîte de réception, 90 caractères utiles. */
|
|
17
|
+
preview: string;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
/** Nom affiché en tête, à la place de celui du produit (page de statut d'un client). */
|
|
20
|
+
name?: string | undefined;
|
|
21
|
+
/** Icône en tête, PNG en https ; à la place de celle du produit. */
|
|
22
|
+
iconSrc?: string | undefined;
|
|
23
|
+
/** Accent en hex à la place de celui du catalogue (couleur d'un client). */
|
|
24
|
+
accent?: EmailAccent | string | undefined;
|
|
25
|
+
/** Ligne au-dessus du pied : pourquoi la personne reçoit l'e-mail, lien de désabonnement. */
|
|
26
|
+
footer?: ReactNode;
|
|
27
|
+
};
|
|
28
|
+
export declare function EmailLayout({ product: ref, preview, children, name, iconSrc, accent, footer, }: EmailLayoutProps): ReactElement;
|
|
29
|
+
export declare function EmailHeading({ children }: {
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
}): ReactElement;
|
|
32
|
+
/** Paragraphe. `small` : note secondaire (validité d'un code, « si tu n'as rien demandé »). */
|
|
33
|
+
export declare function EmailText({ children, small }: {
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
small?: boolean;
|
|
36
|
+
}): ReactElement;
|
|
37
|
+
/** Lien dans le texte, à la couleur d'accent. */
|
|
38
|
+
export declare function EmailLink({ href, children }: {
|
|
39
|
+
href: string;
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
}): ReactElement;
|
|
42
|
+
/** Bouton d'action : un lien direct vers `href`, tel quel, sans redirection de suivi. */
|
|
43
|
+
export declare function EmailButton({ href, children }: {
|
|
44
|
+
href: string;
|
|
45
|
+
children: ReactNode;
|
|
46
|
+
}): ReactElement;
|
|
47
|
+
/** Code à usage unique : chiffres à chasse fixe, coupés en deux groupes à l'œil mais copiés d'un bloc. */
|
|
48
|
+
export declare function EmailCode({ code }: {
|
|
49
|
+
code: string;
|
|
50
|
+
}): ReactElement;
|
|
51
|
+
/** Encadré : une information à ne pas manquer. Le titre porte le sens, jamais la couleur seule. */
|
|
52
|
+
export declare function EmailCallout({ tone, title, children, }: {
|
|
53
|
+
tone?: EmailTone;
|
|
54
|
+
title?: ReactNode;
|
|
55
|
+
children: ReactNode;
|
|
56
|
+
}): ReactElement;
|
|
57
|
+
export declare function EmailDivider(): ReactElement;
|
|
58
|
+
export type EmailDetail = {
|
|
59
|
+
label: string;
|
|
60
|
+
value: ReactNode;
|
|
61
|
+
};
|
|
62
|
+
/** Liste clé / valeur : site, heure, durée, organisation. */
|
|
63
|
+
export declare function EmailDetails({ items }: {
|
|
64
|
+
items: ReadonlyArray<EmailDetail>;
|
|
65
|
+
}): ReactElement;
|
|
66
|
+
export type RenderedEmail = {
|
|
67
|
+
html: string;
|
|
68
|
+
text: string;
|
|
69
|
+
};
|
|
70
|
+
/** HTML pour les clients mail et version texte, tirée du même rendu. */
|
|
71
|
+
export declare function renderEmail(element: ReactElement): Promise<RenderedEmail>;
|
package/dist/email.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
import { Body, Button, Column, Container, Head, Heading, Hr, Html, Img, Link, Preview, Row, Section, Text, render, toPlainText, } from "react-email";
|
|
4
|
+
import { EMAIL_DARK, EMAIL_LIGHT, EMAIL_TONES, HGDC_EMAIL_ACCENT, isHexColor, readableOn, } from "./email-palette.js";
|
|
5
|
+
import { HGDC_ACCOUNT, HGDC_PRODUCTS } from "./products.js";
|
|
6
|
+
/**
|
|
7
|
+
* Modèle des e-mails transactionnels de la famille : une mise en page (`EmailLayout`) et des blocs,
|
|
8
|
+
* rendus par React Email en HTML pour clients mail (tableaux, styles en ligne, 600 px) et en texte.
|
|
9
|
+
* Le rendu se fait côté serveur (action Convex `"use node"`, route serveur) : `renderEmail`.
|
|
10
|
+
*/
|
|
11
|
+
/** Où hgdc.ch sert les icônes des e-mails (`packages/ui/email-icons/`). */
|
|
12
|
+
export const EMAIL_ICON_BASE = "https://hgdc.ch/email/";
|
|
13
|
+
const MAIL_HREF = "https://mail.hgdc.ch";
|
|
14
|
+
const LEGAL = "Hugo Gebel Développement Conseil, Lausanne";
|
|
15
|
+
// Pas de police web : celle du système, Inter ou Geist quand elles sont installées.
|
|
16
|
+
const FONT = 'Geist, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
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
|
+
function findProduct(product) {
|
|
23
|
+
if (typeof product !== "string")
|
|
24
|
+
return product;
|
|
25
|
+
const found = [HGDC_ACCOUNT, ...HGDC_PRODUCTS].find((item) => item.word.toLowerCase() === product.toLowerCase());
|
|
26
|
+
if (!found)
|
|
27
|
+
throw new Error(`@hgdc/ui/email : produit inconnu « ${product} »`);
|
|
28
|
+
return found;
|
|
29
|
+
}
|
|
30
|
+
function resolveAccent(accent, product) {
|
|
31
|
+
const value = typeof accent === "string" ? { light: accent, dark: accent } : (accent ?? product.accent ?? HGDC_EMAIL_ACCENT);
|
|
32
|
+
if (!isHexColor(value.light) || !isHexColor(value.dark))
|
|
33
|
+
throw new Error("@hgdc/ui/email : accent en hex attendu (#8538ee)");
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
/** Seuls les liens absolus http(s) et mailto partent dans un e-mail ; ils ne sont jamais réécrits. */
|
|
37
|
+
function assertLink(href) {
|
|
38
|
+
if (!/^(?:https?:\/\/|mailto:)/i.test(href))
|
|
39
|
+
throw new Error(`@hgdc/ui/email : lien absolu attendu, reçu « ${href} »`);
|
|
40
|
+
return href;
|
|
41
|
+
}
|
|
42
|
+
const AccentContext = createContext(HGDC_EMAIL_ACCENT);
|
|
43
|
+
/**
|
|
44
|
+
* Mode sombre : les clients qui le gèrent (Apple Mail, Outlook macOS et iOS, Outlook.com par
|
|
45
|
+
* `data-ogsc`/`data-ogsb`) appliquent ces classes ; les autres gardent les couleurs en ligne, claires.
|
|
46
|
+
*/
|
|
47
|
+
function darkStyles(accent) {
|
|
48
|
+
const d = EMAIL_DARK;
|
|
49
|
+
const rules = [
|
|
50
|
+
`body.hg-ground, body.hg-ground > table > tbody > tr > td { background-color: ${d.ground} !important; }`,
|
|
51
|
+
`.hg-card { background-color: ${d.card} !important; border-color: ${d.border} !important; }`,
|
|
52
|
+
`.hg-ink { color: ${d.ink} !important; }`,
|
|
53
|
+
`.hg-body { color: ${d.body} !important; }`,
|
|
54
|
+
`.hg-muted, .hg-muted a { color: ${d.muted} !important; }`,
|
|
55
|
+
`.hg-wash { background-color: ${d.wash} !important; }`,
|
|
56
|
+
`.hg-rule { border-color: ${d.border} !important; }`,
|
|
57
|
+
`.hg-link { color: ${accent.dark} !important; }`,
|
|
58
|
+
`.hg-button { background-color: ${accent.dark} !important; color: ${readableOn(accent.dark)} !important; }`,
|
|
59
|
+
...Object.entries(EMAIL_TONES).flatMap(([tone, { dark }]) => [
|
|
60
|
+
`.hg-callout-${tone} { background-color: ${dark.wash} !important; border-color: ${dark.bar} !important; }`,
|
|
61
|
+
`.hg-tone-${tone} { color: ${dark.ink} !important; }`,
|
|
62
|
+
]),
|
|
63
|
+
];
|
|
64
|
+
const outlook = rules.flatMap((rule) => {
|
|
65
|
+
const [selectors = "", declarations = ""] = rule.split(" {");
|
|
66
|
+
const scoped = selectors.split(", ").filter((s) => !s.startsWith("body"));
|
|
67
|
+
return scoped.length
|
|
68
|
+
? [`${scoped.map((s) => `[data-ogsc] ${s}, [data-ogsb] ${s}`).join(", ")} {${declarations}`]
|
|
69
|
+
: [];
|
|
70
|
+
});
|
|
71
|
+
return [
|
|
72
|
+
":root { color-scheme: light dark; supported-color-schemes: light dark; }",
|
|
73
|
+
`@media (prefers-color-scheme: dark) { ${rules.join(" ")} }`,
|
|
74
|
+
...outlook,
|
|
75
|
+
"@media only screen and (max-width: 480px) { .hg-pad { padding: 24px 20px !important; } .hg-heading { font-size: 20px !important; line-height: 26px !important; } }",
|
|
76
|
+
].join("\n");
|
|
77
|
+
}
|
|
78
|
+
export function EmailLayout({ product: ref, preview, children, name, iconSrc, accent, footer, }) {
|
|
79
|
+
const product = findProduct(ref);
|
|
80
|
+
const colors = resolveAccent(accent, product);
|
|
81
|
+
const l = EMAIL_LIGHT;
|
|
82
|
+
const muted = { color: l.muted, fontSize: 12, lineHeight: "18px", margin: 0 };
|
|
83
|
+
return (_jsxs(Html, { lang: "fr", dir: "ltr", children: [_jsxs(Head, { children: [_jsx("meta", { name: "color-scheme", content: "light dark" }), _jsx("meta", { name: "supported-color-schemes", content: "light dark" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("style", { dangerouslySetInnerHTML: { __html: darkStyles(colors) } })] }), _jsx(Preview, { children: preview }), _jsx(Body, { lang: "fr", className: "hg-ground", style: { backgroundColor: l.ground, fontFamily: FONT, margin: 0, padding: "32px 12px" }, children: _jsx(AccentContext, { value: colors, children: _jsxs(Container, { style: { width: "100%", maxWidth: 600 }, children: [_jsx(Section, { style: { padding: "0 4px 16px" }, children: _jsxs(Row, { children: [_jsx(Column, { style: { width: 40, verticalAlign: "middle" }, children: _jsx(Img, { src: assertLink(iconSrc ?? emailIconUrl(product.word)), width: 32, height: 32, alt: "", style: { display: "block", borderRadius: 16 } }) }), _jsx(Column, { style: { verticalAlign: "middle" }, children: _jsx(Text, { className: "hg-ink", style: { color: l.ink, fontSize: 15, fontWeight: 600, lineHeight: "20px", margin: 0 }, children: name ?? product.name }) })] }) }), _jsx(Section, { className: "hg-card", tdClassName: "hg-pad", style: {
|
|
84
|
+
backgroundColor: l.card,
|
|
85
|
+
border: `1px solid ${l.border}`,
|
|
86
|
+
borderRadius: 16,
|
|
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 })] })] }) }) })] }));
|
|
89
|
+
}
|
|
90
|
+
export function EmailHeading({ children }) {
|
|
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 }));
|
|
92
|
+
}
|
|
93
|
+
/** Paragraphe. `small` : note secondaire (validité d'un code, « si tu n'as rien demandé »). */
|
|
94
|
+
export function EmailText({ children, small = false }) {
|
|
95
|
+
return (_jsx(Text, { className: small ? "hg-muted" : "hg-body", style: {
|
|
96
|
+
color: small ? EMAIL_LIGHT.muted : EMAIL_LIGHT.body,
|
|
97
|
+
fontSize: small ? 13 : 15,
|
|
98
|
+
lineHeight: small ? "20px" : "24px",
|
|
99
|
+
margin: "0 0 16px",
|
|
100
|
+
}, children: children }));
|
|
101
|
+
}
|
|
102
|
+
/** Lien dans le texte, à la couleur d'accent. */
|
|
103
|
+
export function EmailLink({ href, children }) {
|
|
104
|
+
const accent = useContext(AccentContext);
|
|
105
|
+
return (_jsx(Link, { href: assertLink(href), className: "hg-link", style: { color: accent.light, textDecoration: "underline" }, children: children }));
|
|
106
|
+
}
|
|
107
|
+
/** Bouton d'action : un lien direct vers `href`, tel quel, sans redirection de suivi. */
|
|
108
|
+
export function EmailButton({ href, children }) {
|
|
109
|
+
const accent = useContext(AccentContext);
|
|
110
|
+
return (_jsx(Section, { style: { margin: "8px 0 24px" }, children: _jsx(Button, { href: assertLink(href), className: "hg-button", style: {
|
|
111
|
+
backgroundColor: accent.light,
|
|
112
|
+
borderRadius: 999,
|
|
113
|
+
color: readableOn(accent.light),
|
|
114
|
+
fontSize: 15,
|
|
115
|
+
fontWeight: 600,
|
|
116
|
+
padding: "12px 24px",
|
|
117
|
+
}, children: children }) }));
|
|
118
|
+
}
|
|
119
|
+
/** Code à usage unique : chiffres à chasse fixe, coupés en deux groupes à l'œil mais copiés d'un bloc. */
|
|
120
|
+
export function EmailCode({ code }) {
|
|
121
|
+
const half = code.length >= 6 && code.length % 2 === 0 ? code.length / 2 : code.length;
|
|
122
|
+
const groups = [code.slice(0, half), code.slice(half)].filter(Boolean);
|
|
123
|
+
return (_jsx(Section, { className: "hg-wash", style: {
|
|
124
|
+
backgroundColor: EMAIL_LIGHT.wash,
|
|
125
|
+
borderRadius: 12,
|
|
126
|
+
margin: "8px 0 24px",
|
|
127
|
+
padding: "18px 16px",
|
|
128
|
+
textAlign: "center",
|
|
129
|
+
}, children: _jsx(Text, { className: "hg-ink", style: {
|
|
130
|
+
color: EMAIL_LIGHT.ink,
|
|
131
|
+
fontFamily: MONO,
|
|
132
|
+
fontSize: 32,
|
|
133
|
+
fontWeight: 600,
|
|
134
|
+
letterSpacing: 6,
|
|
135
|
+
lineHeight: "40px",
|
|
136
|
+
margin: 0,
|
|
137
|
+
}, children: groups.map((group, i) => (_jsx("span", { style: i ? { marginLeft: 14 } : undefined, children: group }, i))) }) }));
|
|
138
|
+
}
|
|
139
|
+
/** Encadré : une information à ne pas manquer. Le titre porte le sens, jamais la couleur seule. */
|
|
140
|
+
export function EmailCallout({ tone = "neutral", title, children, }) {
|
|
141
|
+
const colors = EMAIL_TONES[tone].light;
|
|
142
|
+
return (_jsxs(Section, { className: `hg-callout-${tone}`, style: {
|
|
143
|
+
backgroundColor: colors.wash,
|
|
144
|
+
borderLeft: `3px solid ${colors.bar}`,
|
|
145
|
+
borderRadius: 8,
|
|
146
|
+
margin: "4px 0 20px",
|
|
147
|
+
padding: "14px 16px",
|
|
148
|
+
}, children: [title ? (_jsx(Text, { className: `hg-tone-${tone}`, style: { color: colors.ink, fontSize: 14, fontWeight: 600, lineHeight: "20px", margin: "0 0 4px" }, children: title })) : null, _jsx(Text, { className: "hg-body", style: { color: EMAIL_LIGHT.body, fontSize: 14, lineHeight: "22px", margin: 0 }, children: children })] }));
|
|
149
|
+
}
|
|
150
|
+
export function EmailDivider() {
|
|
151
|
+
return (_jsx(Hr, { className: "hg-rule", style: { border: "none", borderTop: `1px solid ${EMAIL_LIGHT.border}`, margin: "24px 0" } }));
|
|
152
|
+
}
|
|
153
|
+
/** Liste clé / valeur : site, heure, durée, organisation. */
|
|
154
|
+
export function EmailDetails({ items }) {
|
|
155
|
+
return (_jsx(Section, { style: { margin: "4px 0 20px" }, children: items.map((item, i) => {
|
|
156
|
+
const cell = {
|
|
157
|
+
borderTop: i ? `1px solid ${EMAIL_LIGHT.border}` : undefined,
|
|
158
|
+
padding: "9px 0",
|
|
159
|
+
verticalAlign: "top",
|
|
160
|
+
};
|
|
161
|
+
return (_jsxs(Row, { className: "hg-detail", children: [_jsx(Column, { className: i ? "hg-rule" : undefined, style: { ...cell, width: "38%", paddingRight: 12 }, children: _jsx(Text, { className: "hg-muted", style: { color: EMAIL_LIGHT.muted, fontSize: 13, lineHeight: "20px", margin: 0 }, children: item.label }) }), _jsx(Column, { className: i ? "hg-rule" : undefined, style: cell, children: _jsx(Text, { className: "hg-ink", style: { color: EMAIL_LIGHT.ink, fontSize: 14, lineHeight: "20px", margin: 0 }, children: item.value }) })] }, item.label));
|
|
162
|
+
}) }));
|
|
163
|
+
}
|
|
164
|
+
const TEXT_SELECTORS = [
|
|
165
|
+
{ selector: "h1", options: { uppercase: false } },
|
|
166
|
+
// Une ligne par détail, « Libellé valeur », plutôt que deux paragraphes.
|
|
167
|
+
{ selector: "table.hg-detail", format: "dataTable" },
|
|
168
|
+
];
|
|
169
|
+
/** HTML pour les clients mail et version texte, tirée du même rendu. */
|
|
170
|
+
export async function renderEmail(element) {
|
|
171
|
+
const html = await render(element);
|
|
172
|
+
return { html, text: toPlainText(html, { selectors: TEXT_SELECTORS }).trim() };
|
|
173
|
+
}
|
package/dist/products.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IconSvgElement } from "@hugeicons/react";
|
|
2
|
+
import { type EmailAccent } from "./email-palette.ts";
|
|
2
3
|
/** Où en est un produit : seul ce qui répond s'ouvre depuis les menus. */
|
|
3
4
|
export type ProductStage = "live" | "preview" | "soon";
|
|
4
5
|
export declare const PRODUCT_STAGE_LABEL: Readonly<Record<ProductStage, string>>;
|
|
@@ -15,6 +16,11 @@ export type HgdcCatalogProduct = {
|
|
|
15
16
|
pitch: string;
|
|
16
17
|
icon: IconSvgElement;
|
|
17
18
|
stage: ProductStage;
|
|
19
|
+
/**
|
|
20
|
+
* Accent du produit en hex, pour ce qui ne lit pas les jetons CSS (e-mails). Absent : l'accent
|
|
21
|
+
* violet de la DA. Tous les produits le partagent aujourd'hui ; un écart se décide ici.
|
|
22
|
+
*/
|
|
23
|
+
accent?: EmailAccent | undefined;
|
|
18
24
|
};
|
|
19
25
|
/** HGDC Account : le compte commun (profil, sécurité, organisations), hors du catalogue des produits. */
|
|
20
26
|
export declare const HGDC_ACCOUNT: HgdcCatalogProduct;
|
package/dist/products.js
CHANGED
|
@@ -5,6 +5,7 @@ import Briefcase01Icon from "@hugeicons/core-free-icons/Briefcase01Icon";
|
|
|
5
5
|
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
|
+
import { HGDC_EMAIL_ACCENT } from "./email-palette.js";
|
|
8
9
|
export const PRODUCT_STAGE_LABEL = {
|
|
9
10
|
live: "En service",
|
|
10
11
|
preview: "Préversion",
|
|
@@ -12,7 +13,16 @@ export const PRODUCT_STAGE_LABEL = {
|
|
|
12
13
|
};
|
|
13
14
|
function product(word, pitch, icon, stage) {
|
|
14
15
|
const subdomain = `${word.toLowerCase()}.hgdc.ch`;
|
|
15
|
-
return {
|
|
16
|
+
return {
|
|
17
|
+
word,
|
|
18
|
+
name: `HGDC ${word}`,
|
|
19
|
+
subdomain,
|
|
20
|
+
href: `https://${subdomain}`,
|
|
21
|
+
pitch,
|
|
22
|
+
icon,
|
|
23
|
+
stage,
|
|
24
|
+
accent: HGDC_EMAIL_ACCENT,
|
|
25
|
+
};
|
|
16
26
|
}
|
|
17
27
|
/** HGDC Account : le compte commun (profil, sécurité, organisations), hors du catalogue des produits. */
|
|
18
28
|
export const HGDC_ACCOUNT = product("Account", "Ton compte HGDC : profil, sécurité et organisations, pour tous les produits.", UserCircleIcon, "live");
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hgdc/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
19
|
-
"styles.css"
|
|
19
|
+
"styles.css",
|
|
20
|
+
"email-icons"
|
|
20
21
|
],
|
|
21
22
|
"sideEffects": [
|
|
22
23
|
"*.css"
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
"exports": {
|
|
25
26
|
"./styles.css": "./styles.css",
|
|
26
27
|
"./package.json": "./package.json",
|
|
28
|
+
"./email-icons/*": "./email-icons/*",
|
|
27
29
|
"./*": {
|
|
28
30
|
"types": "./dist/*.d.ts",
|
|
29
31
|
"default": "./dist/*.js"
|
|
@@ -48,6 +50,15 @@
|
|
|
48
50
|
"peerDependencies": {
|
|
49
51
|
"react": "^19.3.0",
|
|
50
52
|
"react-dom": "^19.3.0",
|
|
51
|
-
"tailwindcss": "^4.3.3"
|
|
53
|
+
"tailwindcss": "^4.3.3",
|
|
54
|
+
"react-email": "^6.11.1"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"react-email": {
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"react-email": "6.11.1"
|
|
52
63
|
}
|
|
53
64
|
}
|