@hgdc/ui 0.5.0 → 0.6.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 +32 -1
- 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 +15 -5
- 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
|
@@ -111,6 +111,37 @@ Les champs de date sont ceux du navigateur, sans calendrier maison (ADR 0006). L
|
|
|
111
111
|
| `tooltip` | `<TooltipProvider>` autour de l'app, puis `<Tooltip><TooltipTrigger aria-label="Copier">…</TooltipTrigger><TooltipContent>Copier</TooltipContent></Tooltip>` |
|
|
112
112
|
| `popover` | `<Popover><PopoverTrigger>Aide</PopoverTrigger><PopoverContent><PopoverTitle>…</PopoverTitle></PopoverContent></Popover>` |
|
|
113
113
|
|
|
114
|
+
## E-mails
|
|
115
|
+
|
|
116
|
+
`@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"`.
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
import { EmailButton, EmailHeading, EmailLayout, EmailText, renderEmail } from "@hgdc/ui/email"
|
|
120
|
+
|
|
121
|
+
const { html, text } = await renderEmail(
|
|
122
|
+
<EmailLayout product="Portal" preview="Claire t'invite à rejoindre Atelier Dupont.">
|
|
123
|
+
<EmailHeading>Claire t'invite dans Atelier Dupont</EmailHeading>
|
|
124
|
+
<EmailText>Tu pourras suivre le projet, signer et payer au même endroit.</EmailText>
|
|
125
|
+
<EmailButton href={url}>Voir l'invitation</EmailButton>
|
|
126
|
+
</EmailLayout>,
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
| Bloc | Rôle |
|
|
131
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
132
|
+
| `EmailLayout` | Icône et nom du produit, carte, pied « Propulsé par HGDC Mail » et mentions légales ; `footer` (désabonnement), `name`, `iconSrc`, `accent` |
|
|
133
|
+
| `EmailHeading` | Titre |
|
|
134
|
+
| `EmailText` | Paragraphe ; `small` pour une note secondaire |
|
|
135
|
+
| `EmailButton` | Lien direct, jamais réécrit ; absolu (`https:`, `mailto:`) ou erreur |
|
|
136
|
+
| `EmailLink` | Lien dans le texte |
|
|
137
|
+
| `EmailCode` | Code à usage unique, en deux groupes à l'œil, d'un bloc au copier |
|
|
138
|
+
| `EmailCallout` | Encadré `neutral`, `info`, `success`, `warning`, `danger`, avec titre |
|
|
139
|
+
| `EmailDivider` | Séparateur |
|
|
140
|
+
| `EmailDetails` | Liste clé / valeur |
|
|
141
|
+
| `renderEmail` | `{ html, text }` : HTML pour clients mail, texte tiré du même rendu |
|
|
142
|
+
|
|
143
|
+
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`.
|
|
144
|
+
|
|
114
145
|
## Règles de la DA
|
|
115
146
|
|
|
116
147
|
- Chrome monochrome, un seul accent violet : focus, lien, un geste principal par écran. « Vous êtes ici » (barre, onglets) reste en teinte.
|
|
@@ -120,6 +151,6 @@ Les champs de date sont ceux du navigateur, sans calendrier maison (ADR 0006). L
|
|
|
120
151
|
- 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
152
|
- Référence de l'interface : Host. Couleur d'un client : `--brand-accent`.
|
|
122
153
|
|
|
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).
|
|
154
|
+
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
155
|
|
|
125
156
|
Tous droits réservés, Hugo Gebel Développement Conseil.
|
|
@@ -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");
|
|
@@ -20,10 +30,10 @@ export const HGDC_ACCOUNT = product("Account", "Ton compte HGDC : profil, sécur
|
|
|
20
30
|
export const HGDC_PRODUCTS = [
|
|
21
31
|
product("Host", "Ton site en ligne, déployé à chaque modification.", Rocket01Icon, "preview"),
|
|
22
32
|
product("Mail", "Tes e-mails, newsletters et formulaires, envoyés depuis ton adresse.", Mail01Icon, "live"),
|
|
23
|
-
product("Portal", "Un espace partagé où ton client suit son projet, signe et paie.", Briefcase01Icon, "
|
|
24
|
-
product("Status", "La surveillance de tes sites et ta page de statut.", Activity01Icon, "
|
|
25
|
-
product("Analytics", "Tes visiteurs mesurés, sans cookies ni bannière.", Analytics01Icon, "
|
|
26
|
-
product("Docs", "Ta documentation en ligne, avec un assistant qui y répond.", BookOpen01Icon, "
|
|
33
|
+
product("Portal", "Un espace partagé où ton client suit son projet, signe et paie.", Briefcase01Icon, "live"),
|
|
34
|
+
product("Status", "La surveillance de tes sites et ta page de statut.", Activity01Icon, "live"),
|
|
35
|
+
product("Analytics", "Tes visiteurs mesurés, sans cookies ni bannière.", Analytics01Icon, "live"),
|
|
36
|
+
product("Docs", "Ta documentation en ligne, avec un assistant qui y répond.", BookOpen01Icon, "live"),
|
|
27
37
|
];
|
|
28
38
|
/** Vrai si le produit répond déjà (en service ou en préversion). */
|
|
29
39
|
export function isProductOpen(item) {
|
|
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.6.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
|
}
|