@nxgt/mail-ui 0.1.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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +311 -0
  3. package/components/alert.vue +75 -0
  4. package/components/avatar-fallback.vue +21 -0
  5. package/components/avatar-group.vue +44 -0
  6. package/components/avatar-image.vue +20 -0
  7. package/components/avatar.vue +29 -0
  8. package/components/badge.vue +47 -0
  9. package/components/banner.vue +57 -0
  10. package/components/breakdown-card.vue +27 -0
  11. package/components/card-content.vue +16 -0
  12. package/components/card-description.vue +16 -0
  13. package/components/card-footer.vue +16 -0
  14. package/components/card-header.vue +33 -0
  15. package/components/card-title.vue +20 -0
  16. package/components/card.vue +30 -0
  17. package/components/chip.vue +41 -0
  18. package/components/code.vue +26 -0
  19. package/components/compare-card.vue +63 -0
  20. package/components/description.vue +24 -0
  21. package/components/entity-header.vue +48 -0
  22. package/components/goal-card.vue +33 -0
  23. package/components/hero.vue +36 -0
  24. package/components/layout.vue +85 -0
  25. package/components/link.vue +19 -0
  26. package/components/list-tile.vue +80 -0
  27. package/components/nx-button.vue +91 -0
  28. package/components/progress.vue +66 -0
  29. package/components/ratio-card.vue +36 -0
  30. package/components/see-also.vue +53 -0
  31. package/components/separator.vue +27 -0
  32. package/components/stat-card.vue +43 -0
  33. package/components/status-indicator.vue +35 -0
  34. package/components/steps-item.vue +48 -0
  35. package/components/steps.vue +31 -0
  36. package/components/summary-data.vue +45 -0
  37. package/components/table-body.vue +11 -0
  38. package/components/table-caption.vue +16 -0
  39. package/components/table-cell.vue +28 -0
  40. package/components/table-empty.vue +23 -0
  41. package/components/table-footer.vue +11 -0
  42. package/components/table-head.vue +25 -0
  43. package/components/table-header.vue +11 -0
  44. package/components/table-row.vue +15 -0
  45. package/components/table.vue +25 -0
  46. package/components/timeline.vue +70 -0
  47. package/components/typography.vue +64 -0
  48. package/components/ui.ts +166 -0
  49. package/dist/catalogues.d.ts +56 -0
  50. package/dist/catalogues.d.ts.map +1 -0
  51. package/dist/index.d.ts +27 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +214 -0
  54. package/dist/index.js.map +13 -0
  55. package/dist/packaged.d.ts +27 -0
  56. package/dist/packaged.d.ts.map +1 -0
  57. package/dist/plugin.d.ts +55 -0
  58. package/dist/plugin.d.ts.map +1 -0
  59. package/dist/theme.d.ts +11 -0
  60. package/dist/theme.d.ts.map +1 -0
  61. package/dist/vue.d.ts +9 -0
  62. package/dist/vue.d.ts.map +1 -0
  63. package/docs/README.md +15 -0
  64. package/docs/guide/components.md +984 -0
  65. package/docs/guide/messages.md +156 -0
  66. package/docs/guide/plugin.md +365 -0
  67. package/docs/guide/theme.md +153 -0
  68. package/docs/roadmap.md +97 -0
  69. package/docs/troubleshooting.md +519 -0
  70. package/package.json +69 -0
  71. package/theme.css +292 -0
@@ -0,0 +1,156 @@
1
+ # Shared messages
2
+
3
+ This page is for the messages `@nxgt/mail-ui` brings to `@nxgt/mail-i18n`:
4
+ what they are, how your catalogues override them, and what a project in
5
+ another locale writes.
6
+
7
+ ```ts
8
+ // maizzle.config.ts
9
+ import { defineMailConfig } from '@nxgt/mail-config';
10
+ import { i18n } from '@nxgt/mail-i18n';
11
+ import { ui, uiCatalogues } from '@nxgt/mail-ui';
12
+
13
+ export default defineMailConfig({
14
+ plugins: [
15
+ ui({ brand: { name: 'Acme', url: 'https://acme.example' } }),
16
+ i18n({ locales: ['en', 'fr'], catalogues: [uiCatalogues] }),
17
+ ],
18
+ });
19
+ ```
20
+
21
+ ```vue
22
+ <!-- emails/welcome.vue -->
23
+ <template>
24
+ <NxLayout>
25
+ <NxTypography>{{ t('common.greeting', { name: placeholder('name') }) }}</NxTypography>
26
+ <NxTypography>{{ t('welcome.body') }}</NxTypography>
27
+ <NxTypography variant="caption">{{ t('common.footer.ignore') }}</NxTypography>
28
+ </NxLayout>
29
+ </template>
30
+ ```
31
+
32
+ Your `locales/en.json` and `locales/fr.json` hold `welcome.subject` and
33
+ `welcome.body`; the `common` keys come from `uiCatalogues`. The English build
34
+ starts with `Hello {{ name }},` and the footer says `You received this e-mail
35
+ because you have an account with Acme.`
36
+
37
+ ## The messages
38
+
39
+ | Key | `en` | `fr` | Arguments |
40
+ | --- | --- | --- | --- |
41
+ | `common.greeting` | `Hello {name},` | `Bonjour {name},` | `name` |
42
+ | `common.footer.why` | `You received this e-mail because you have an account with {brand}.` | `Vous recevez cet e-mail parce que vous avez un compte chez {brand}.` | `brand` |
43
+ | `common.footer.ignore` | `If you did not ask for this, you can ignore this e-mail.` | `Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer cet e-mail.` | — |
44
+ | `common.avatarGroup.more` | `{count, plural, other {# more}}` | `{count, plural, one {# autre} other {# autres}}` | `count`, a number |
45
+ | `common.timeline.empty` | `No activity yet` | `Aucune activité pour le moment` | — |
46
+ | `common.metrics.ofTarget` | `of {target}` | `sur {target}` | `target` |
47
+ | `common.metrics.thisPeriod` | `This period` | `Cette période` | — |
48
+ | `common.metrics.lastPeriod` | `Last period` | `Période précédente` | — |
49
+ | `common.seeAlso` | `See also` | `Voir aussi` | — |
50
+
51
+ ```ts
52
+ import type { Catalogues } from '@nxgt/mail-i18n';
53
+ import { uiCatalogues } from '@nxgt/mail-ui';
54
+
55
+ const shared: Catalogues = uiCatalogues; // { en: { common: {…} }, fr: { common: {…} } }
56
+ ```
57
+
58
+ `common.footer.why` is the one `<NxLayout>` writes itself, in its footer,
59
+ with the brand's name; `common.avatarGroup.more` is the label `<NxAvatarGroup>`
60
+ gives its `+N`, for a screen reader; `common.timeline.empty` is what
61
+ `<NxTimeline>` writes for no events, unless given `empty`;
62
+ `common.metrics.ofTarget` is `<NxGoalCard>`'s `of 24`, with its `target`;
63
+ `common.metrics.thisPeriod` and `common.metrics.lastPeriod` label
64
+ `<NxCompareCard>`'s two boxes, unless given their `label`; `common.seeAlso`
65
+ heads `<NxSeeAlso>`, unless given `label`. The other two, `common.greeting`
66
+ and `common.footer.ignore`, are for your templates.
67
+
68
+ ## `<NxLayout>` needs `common.footer.why`
69
+
70
+ Once `i18n()` is in the plugins, `<NxLayout>` calls
71
+ `t('common.footer.why', { brand })`. Without `uiCatalogues`, and without the
72
+ key in your catalogues, the build fails:
73
+
74
+ ```text
75
+ Error: i18n: fr: welcome calls t('common.footer.why'), which is not a key of the catalogues
76
+ ```
77
+
78
+ Pass `catalogues: [uiCatalogues]`, or write the key yourself. The layout
79
+ formats it even when you fill its `footer` slot, so the key is needed either
80
+ way.
81
+
82
+ Without `@nxgt/mail-i18n`, `<NxLayout>` calls nothing and the footer shows the
83
+ brand's name alone.
84
+
85
+ ## Overriding a message
86
+
87
+ Your catalogues are merged **over** `uiCatalogues`, key by key: write the
88
+ keys you want to change, and keep the others.
89
+
90
+ ```json
91
+ // locales/en.json
92
+ {
93
+ "common": { "greeting": "Hi {name}," },
94
+ "welcome": { "subject": "Welcome to Acme, {name}", "body": "Your account is ready." }
95
+ }
96
+ ```
97
+
98
+ ```json
99
+ // locales/fr.json — no common: the nine French messages are uiCatalogues'
100
+ {
101
+ "welcome": { "subject": "Bienvenue chez Acme, {name}", "body": "Votre compte est prêt." }
102
+ }
103
+ ```
104
+
105
+ The English build writes `Hi {{ name }},`, the French one
106
+ `Bonjour {{ name }},`; both keep the eight other `common` messages from the
107
+ package. The merge rules — several sources, a
108
+ message replacing a group — are in `@nxgt/mail-i18n`'s
109
+ [Catalogues](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-i18n/docs/guide/catalogues.md#catalogues-from-a-package).
110
+
111
+ The merged catalogues are checked as your own are, against the fallback
112
+ locale: an override in `fr` may leave `{name}` out, but not use an argument
113
+ the `en` message does not declare.
114
+
115
+ ## Another locale
116
+
117
+ `uiCatalogues` has `en` and `fr`. For any other locale, your catalogue writes
118
+ the nine `common` keys, or the build fails on the first one missing:
119
+
120
+ ```text
121
+ Error: i18n: de: common.footer.ignore is missing — en, the fallback locale, has it
122
+ ```
123
+
124
+ ```json
125
+ // locales/de.json
126
+ {
127
+ "common": {
128
+ "greeting": "Hallo {name},",
129
+ "footer": {
130
+ "why": "Sie erhalten diese E-Mail, weil Sie ein Konto bei {brand} haben.",
131
+ "ignore": "Wenn Sie dies nicht angefordert haben, können Sie diese E-Mail ignorieren."
132
+ },
133
+ "avatarGroup": { "more": "{count, plural, other {# weitere}}" },
134
+ "timeline": { "empty": "Noch keine Aktivität" },
135
+ "metrics": {
136
+ "ofTarget": "von {target}",
137
+ "thisPeriod": "Dieser Zeitraum",
138
+ "lastPeriod": "Vorheriger Zeitraum"
139
+ },
140
+ "seeAlso": "Siehe auch"
141
+ },
142
+ "welcome": { "subject": "Willkommen bei Acme, {name}", "body": "Ihr Konto ist bereit." }
143
+ }
144
+ ```
145
+
146
+ `<NxLayout>` passes `{brand}` to `common.footer.why`, the brand's name, and
147
+ `<NxGoalCard>` `{target}` to `common.metrics.ofTarget`. A translation may
148
+ leave one out; it cannot add another argument.
149
+
150
+ ## See also
151
+
152
+ - [Components](components.md#nxlayout) — the layout's footer slot, and the
153
+ components that write a shared message.
154
+ - `@nxgt/mail-i18n`'s
155
+ [Templates](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-i18n/docs/guide/templates.md)
156
+ — `t` and `placeholder`.
@@ -0,0 +1,365 @@
1
+ # The plugin
2
+
3
+ This page is for adding `ui({ brand, theme })` to a project: what it gives
4
+ every template, the options it checks, and how a project replaces one of its
5
+ components or its layout.
6
+
7
+ ```ts
8
+ // maizzle.config.ts
9
+ import { defineMailConfig } from '@nxgt/mail-config';
10
+ import { ui } from '@nxgt/mail-ui';
11
+
12
+ export default defineMailConfig({
13
+ plugins: [ui({ brand: { name: 'Acme', url: 'https://acme.example' } })],
14
+ });
15
+ ```
16
+
17
+ ```vue
18
+ <!-- emails/welcome.vue -->
19
+ <template>
20
+ <NxLayout preheader="Your account is ready">
21
+ <NxTypography variant="headline-small">Welcome to {{ brand.name }}</NxTypography>
22
+ <NxButton href="https://acme.example/start">Get started</NxButton>
23
+ </NxLayout>
24
+ </template>
25
+ ```
26
+
27
+ `maizzle build` writes `dist/welcome.html`: `Acme` at the top, linked to
28
+ `https://acme.example`, the heading and the button on a white card, and
29
+ `Acme` again in the footer.
30
+
31
+ ## The signature
32
+
33
+ ```ts
34
+ import type { MailPlugin } from '@nxgt/mail-config';
35
+
36
+ interface Brand {
37
+ readonly name: string;
38
+ readonly url?: string;
39
+ readonly logo?: {
40
+ readonly src: string;
41
+ readonly width?: number; // pixels, default 120
42
+ readonly alt?: string; // default the brand's name
43
+ };
44
+ }
45
+
46
+ interface UiOptions {
47
+ readonly brand: Brand;
48
+ readonly theme?: Readonly<Record<string, string>>;
49
+ }
50
+
51
+ function ui(options: UiOptions): MailPlugin;
52
+ ```
53
+
54
+ The plugin it answers, named `ui`, does four things:
55
+
56
+ - registers every component of the package's `components/` folder under the
57
+ prefix `Nx` (`card-header.vue` is `<NxCardHeader>`); Maizzle's own stay
58
+ available (`<Button>`, `<Spacer>`);
59
+ - gives every template `brand`, the brand as passed;
60
+ - provides the brand and the theme's CSS to the components, under
61
+ [`UI_CONTEXT`](#your-own-layout--ui_context);
62
+ - resolves the tags of a `.vue` file installed in `node_modules` — its own
63
+ components, and a package's templates such as `@nxgt/mail-presets`' — see
64
+ [Components from a package](#components-from-a-package).
65
+
66
+ It sets no build event, and no list but the three `defineMailConfig` joins
67
+ (`components.source`, `vite.plugins`, `vue.plugins`), so it drops nothing
68
+ another plugin or your config sets. See
69
+ [`@nxgt/mail-config`](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-config/docs/guide/config.md)
70
+ for how plugins merge.
71
+
72
+ ## Options
73
+
74
+ | Option | Type | Default | Effect |
75
+ | --- | --- | --- | --- |
76
+ | `brand` | `Brand` | — (required) | Who sends the e-mail: `<NxLayout>`'s header and footer, and `brand` in templates |
77
+ | `brand.name` | `string` | — (required) | The header's text when there is no logo; the footer's, always |
78
+ | `brand.url` | `string` | none | Where the header and the footer's name link to. Without it, neither is a link |
79
+ | `brand.logo.src` | `string` | — (required in `logo`) | The header's image, instead of the name |
80
+ | `brand.logo.width` | `number` | `120` | The image's `width` attribute, in pixels |
81
+ | `brand.logo.alt` | `string` | `brand.name` | The image's `alt` |
82
+ | `theme` | `Record<string, string>` | `{}` | Tokens of `theme.css` to override — see [The theme](theme.md) |
83
+
84
+ ### `brand.url` and `brand.logo.src` are absolute
85
+
86
+ ```ts
87
+ ui({
88
+ brand: {
89
+ name: 'Acme',
90
+ url: 'https://acme.example',
91
+ logo: { src: 'https://acme.example/logo.png', width: 96 },
92
+ },
93
+ });
94
+ ```
95
+
96
+ A mail client opens the e-mail far from your site: it resolves no relative
97
+ URL, and `/logo.png` would show nothing. Both are refused unless they start
98
+ with `http://` or `https://`. The header then shows:
99
+
100
+ ```html
101
+ <a href="https://acme.example" …><img src="https://acme.example/logo.png" width="96" alt="Acme" …></a>
102
+ ```
103
+
104
+ ### The brand is fixed when `ui()` is called
105
+
106
+ `ui()` keeps a frozen copy of `brand`: changing the object you passed, after
107
+ the call, changes nothing in the e-mails.
108
+
109
+ ## `brand` in templates
110
+
111
+ ```vue
112
+ <template>
113
+ <NxTypography>Thanks for joining {{ brand.name }}.</NxTypography>
114
+ <NxLink v-if="brand.url" :href="`${brand.url}/help`">Help</NxLink>
115
+ </template>
116
+ ```
117
+
118
+ `brand` is a Vue global property, read-only. It is known at build time, so a
119
+ template may branch on it, unlike a placeholder.
120
+
121
+ ### Typed in the editor and in CI
122
+
123
+ `ui()` writes `.maizzle/nxgt-mail-ui.d.ts` each time the config loads —
124
+ `maizzle prepare` (the starter's `postinstall`), `maizzle serve`,
125
+ `maizzle build` — only when its text changed, and only on the main thread of
126
+ a parallel build:
127
+
128
+ ```ts
129
+ // .maizzle/nxgt-mail-ui.d.ts, generated
130
+ // Generated by @nxgt/mail-ui each time the config loads. Never edited, never committed:
131
+ // it gives the templates `brand`.
132
+ import type {} from '@nxgt/mail-ui';
133
+ ```
134
+
135
+ That import loads the package's declaration of `brand` on Vue's template
136
+ properties:
137
+
138
+ ```ts
139
+ declare module 'vue' {
140
+ interface ComponentCustomProperties {
141
+ readonly brand: Brand;
142
+ }
143
+ }
144
+ ```
145
+
146
+ The file is needed because the starter's `tsconfig.json` includes `**/*.vue`
147
+ and `.maizzle/*.d.ts`, not `maizzle.config.ts`: nothing the config imports
148
+ reaches the editor otherwise. Keep that include, and run `maizzle prepare`
149
+ once after installing:
150
+
151
+ ```jsonc
152
+ // tsconfig.json — the official starter's
153
+ { "include": ["**/*.vue", ".maizzle/*.d.ts"] }
154
+ ```
155
+
156
+ ```jsonc
157
+ // package.json
158
+ { "scripts": { "postinstall": "maizzle prepare", "typecheck": "vue-tsc --noEmit" } }
159
+ ```
160
+
161
+ Vue's language tools (the **Vue - Official** extension, `vue-tsc`) then check
162
+ `brand` in every template:
163
+
164
+ ```vue
165
+ <template>
166
+ <NxTypography>Welcome to {{ brand.nam }}</NxTypography>
167
+ <!-- Property 'nam' does not exist on type 'Brand'. Did you mean 'name'? -->
168
+ </template>
169
+ ```
170
+
171
+ `.maizzle/` is in the starter's `.gitignore`, and the file is written under
172
+ the directory `maizzle` runs in — run it from the project root. The keys of
173
+ `t` are typed by `@nxgt/mail-i18n`'s own generated file: see its guide,
174
+ [Editor and type checking](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-i18n/docs/guide/editor.md).
175
+
176
+ A project that sets Maizzle's `root`, or a Laravel project (whose types
177
+ Maizzle writes to `resources/js/types/maizzle`), must include the
178
+ `.maizzle/*.d.ts` of the folder `maizzle` runs in itself. If the editor says
179
+ `Property 'brand' does not exist`, see
180
+ [the troubleshooting entry](../troubleshooting.md#the-editor-says-property-brand-does-not-exist-in-a-template).
181
+ If Biome is the editor's linter, set `html.experimentalFullSupportEnabled` in
182
+ `biome.json`, or it reads a template without `<script>` as JavaScript once you
183
+ edit it — see
184
+ [Biome reports `parse` errors](../troubleshooting.md#biome-reports-parse-errors-in-a-template-as-soon-as-you-edit-it).
185
+
186
+ ## Replacing a component
187
+
188
+ A file in your project's `components/` named as one of our tags,
189
+ `nx-badge.vue` for `<NxBadge>`, replaces it in every template:
190
+
191
+ ```vue
192
+ <!-- components/nx-badge.vue -->
193
+ <template>
194
+ <span class="rounded-sm bg-primary px-2 text-xs text-primary-foreground"><slot /></span>
195
+ </template>
196
+ ```
197
+
198
+ The theme's tokens (`bg-primary`, `rounded-sm`) work in it, since it renders
199
+ inside `<NxLayout>`. To start from ours, copy it from the package — its
200
+ folder is `COMPONENTS_DIR`:
201
+
202
+ ```ts
203
+ import { COMPONENTS_DIR } from '@nxgt/mail-ui';
204
+
205
+ console.log(COMPONENTS_DIR); // /…/node_modules/@nxgt/mail-ui/components
206
+ ```
207
+
208
+ Ours are named without the prefix, which `ui()` adds: `badge.vue` is
209
+ `<NxBadge>` (`nx-button.vue` keeps it, being built on Maizzle's `<Button>`).
210
+ Your `components/` has no prefix, so **rename the copy with the tag's whole
211
+ name**, `nx-badge.vue`: a `components/badge.vue` is `<Badge>`, a component of
212
+ its own, and replaces nothing.
213
+
214
+ A copied component imports `./ui` for its shared types; copy `ui.ts` from the
215
+ same folder beside it, or inline what it uses.
216
+
217
+ ## Components from a package
218
+
219
+ Maizzle finds the component behind a tag (`<NxButton>`, `<Container>`) with
220
+ `unplugin-vue-components`, which skips every file under `node_modules`. Left
221
+ alone, our components — installed from npm — and a package's templates, as
222
+ `@nxgt/mail-presets` ships them, would render as an empty document, and the
223
+ build would pass.
224
+
225
+ `ui()` adds its own resolver to Maizzle's Vite config for those files: a
226
+ `.vue` file under `node_modules`, Maizzle's own excepted. It looks a tag up in
227
+ this order:
228
+
229
+ 1. your project's `components/`: the file Maizzle names as the tag,
230
+ `nx-badge.vue` or `NxBadge.vue` for `<NxBadge>` — so a component you
231
+ replace is replaced in a package's templates too;
232
+ 2. ours, in `COMPONENTS_DIR`, named without the prefix (`badge.vue`);
233
+ 3. Maizzle's built-ins (`Container`, `Spacer`, `Button`, …).
234
+
235
+ Only the top level of your `components/` counts there: a component in a
236
+ subfolder (`components/brand/logo.vue`, `<BrandLogo>`) or in a
237
+ `components.source` folder is found in your own templates, not inside an
238
+ installed one. Put a component that replaces ours at the top of
239
+ `components/`. Under `maizzle serve`, restart after adding one.
240
+
241
+ Nothing to write for it: listing `ui()` is enough, and a package's templates
242
+ need no import of their components.
243
+
244
+ ```ts
245
+ // maizzle.config.ts
246
+ import { defineMailConfig } from '@nxgt/mail-config';
247
+ import { i18n } from '@nxgt/mail-i18n';
248
+ import { presets } from '@nxgt/mail-presets';
249
+ import { ui, uiCatalogues } from '@nxgt/mail-ui';
250
+
251
+ const mails = presets();
252
+
253
+ export default defineMailConfig({
254
+ plugins: [
255
+ ui({ brand: { name: 'Acme' } }), // resolves <NxLayout> in the installed templates
256
+ i18n({
257
+ locales: ['en', 'fr'],
258
+ catalogues: [uiCatalogues, mails.catalogues],
259
+ templates: [mails.templates],
260
+ }),
261
+ ],
262
+ });
263
+ ```
264
+
265
+ `ui()` brings `unplugin-vue-components` as a dependency — the one Maizzle
266
+ uses — and finds Maizzle's built-ins beside the package, up through each
267
+ `node_modules`; without `@maizzle/framework` installed, the config fails to
268
+ load with `ui: @maizzle/framework is not installed beside @nxgt/mail-ui`.
269
+
270
+ With `@nxgt/mail-i18n`, an e-mail that still renders empty — a tag none of
271
+ the three places has — fails the build:
272
+ `i18n: en/welcome.html is empty — a tag of its template resolved to no
273
+ component; list the plugin that brings it, as ui()`.
274
+
275
+ ## Your own layout — `UI_CONTEXT`
276
+
277
+ Replace `<NxLayout>` the same way, with `components/nx-layout.vue`. Read the
278
+ brand and the theme's CSS from `UI_CONTEXT`, so `ui({ theme })` still applies
279
+ to every component inside:
280
+
281
+ ```vue
282
+ <!-- components/nx-layout.vue -->
283
+ <script setup lang="ts">
284
+ import { type UiContext, UI_CONTEXT } from '@nxgt/mail-ui';
285
+ import { inject } from 'vue';
286
+
287
+ const { brand, css } = inject(UI_CONTEXT) as UiContext;
288
+ const style = `@import "@maizzle/tailwindcss";\n${css}`;
289
+ </script>
290
+
291
+ <template>
292
+ <Html lang="en">
293
+ <Head>
294
+ <meta name="color-scheme" content="light">
295
+ <style v-html="style"></style>
296
+ </Head>
297
+ <Body class="bg-background font-sans">
298
+ <Container class="px-6 py-8">
299
+ <p class="m-0 mb-6 text-lg font-semibold text-primary">{{ brand.name }}</p>
300
+ <slot />
301
+ </Container>
302
+ </Body>
303
+ </Html>
304
+ </template>
305
+ ```
306
+
307
+ ```ts
308
+ const UI_CONTEXT = 'nxgt:mail-ui';
309
+
310
+ interface UiContext {
311
+ readonly brand: Brand;
312
+ /** theme.css, then an @theme block of the overrides. */
313
+ readonly css: string;
314
+ }
315
+ ```
316
+
317
+ The theme is not a stylesheet link: a mail client loads none. Its tokens go
318
+ in the layout's `<style>` under the Tailwind import, and Maizzle inlines the
319
+ result.
320
+
321
+ ## Errors
322
+
323
+ `ui()` throws a bare `TypeError` when `maizzle.config.ts` loads:
324
+
325
+ | Message | Cause |
326
+ | --- | --- |
327
+ | `ui: options must be an object, as { brand: { name: 'Acme' } }` | `ui()` called with nothing, or not an object |
328
+ | `ui: brand must be an object, as { name: 'Acme', url: 'https://acme.example' }` | No `brand`, or `brand: 'Acme'` |
329
+ | `ui: brand.name must be the name the e-mails show` | No `name`, or a blank one |
330
+ | `ui: brand.url must be an absolute http(s) URL` | `url: '/home'`, `url: 'acme.example'` |
331
+ | `ui: brand.logo.src must be an absolute http(s) URL — a mail client loads nothing relative` | `logo: 'logo.png'`, or `logo: { src: 'logo.png' }` |
332
+ | `ui: brand.logo.width must be a width in pixels` | `0`, `1.5`, `'96'` |
333
+ | `ui: brand.logo.alt must be a string` | `alt: 1` |
334
+ | `ui: theme must be an object of tokens, as { 'color-primary': '#0f766e' }` | `theme: ['#0f766e']` |
335
+ | `ui: theme.color-primay is not a token of the theme — name one of theme.css without its --, as color-primary` | A misspelled token, or one written with its `--` |
336
+ | `ui: theme.color-primary must be a CSS value, as #0f766e or 8px` | An empty value, a number, or one holding `;`, `{`, `}`, `<`, `>`, a quote, a backslash, a CSS comment (`/*`, `*/`) or a line break |
337
+
338
+ ```ts
339
+ ui({ brand: { name: 'Acme', url: '/home' } });
340
+ // TypeError: ui: brand.url must be an absolute http(s) URL
341
+ ```
342
+
343
+ One plain `Error`, when `ui()` is called: `@maizzle/framework`, a required
344
+ peer, could not be found up from the package.
345
+
346
+ ```text
347
+ Error: ui: @maizzle/framework is not installed beside @nxgt/mail-ui
348
+ ```
349
+
350
+ A component rendered without the plugin fails the build instead, naming
351
+ itself:
352
+
353
+ ```text
354
+ Error: NxLayout: ui() is not in the plugins of defineMailConfig
355
+ ```
356
+
357
+ It happens when the components are registered by hand from
358
+ `COMPONENTS_DIR` rather than through `ui()`. List `ui()` in the project's
359
+ `plugins`.
360
+
361
+ ## See also
362
+
363
+ - [Components](components.md) — every component, its props and its slots.
364
+ - [The theme](theme.md) — the tokens `theme` overrides.
365
+ - [Shared messages](messages.md) — the footer's text with `@nxgt/mail-i18n`.
@@ -0,0 +1,153 @@
1
+ # The theme
2
+
3
+ This page is for changing how the components look: the tokens of
4
+ `theme.css`, how a colour reaches the built HTML, and what an override
5
+ changes.
6
+
7
+ ```ts
8
+ // maizzle.config.ts
9
+ import { defineMailConfig } from '@nxgt/mail-config';
10
+ import { ui } from '@nxgt/mail-ui';
11
+
12
+ export default defineMailConfig({
13
+ plugins: [
14
+ ui({
15
+ brand: { name: 'Acme' },
16
+ theme: { 'color-primary': '#0f766e', 'radius-xl': '8px' },
17
+ }),
18
+ ],
19
+ });
20
+ ```
21
+
22
+ Every `<NxButton>` is then `#0f766e`, every tonal button and the page behind
23
+ the card are tints of it, and the card's corners are 8px.
24
+
25
+ ## Where the theme comes from
26
+
27
+ `theme.css` is `@nxgt/material-vue`'s light theme, as Tailwind 4 `@theme`
28
+ tokens. `<NxLayout>` writes it in its `<style>`, under
29
+ `@import "@maizzle/tailwindcss"`, then an `@theme` block of your overrides,
30
+ which wins. So any Tailwind class a template writes — `bg-primary`,
31
+ `text-muted-foreground`, `rounded-lg` — uses the theme, in the components and
32
+ in your own markup inside `<NxLayout>`.
33
+
34
+ The colours are material-vue's, in `oklch`. Maizzle writes each as a hex
35
+ value in the built HTML, followed by a `lab()` one for the clients that read
36
+ it, and leaves no `var()` and no `oklch()` for a client to resolve:
37
+
38
+ ```html
39
+ <p style="color: #020918; color: lab(2.35721% .367686 -8.51797)">…</p>
40
+ ```
41
+
42
+ ## Tokens
43
+
44
+ Name a token in `theme` without its `--`: `--color-primary` is
45
+ `'color-primary'`.
46
+
47
+ | Token | Default | Used for |
48
+ | --- | --- | --- |
49
+ | `radius-sm`, `radius-md`, `radius-lg`, `radius-xl` | `6px`, `8px`, `10px`, `14px` | `rounded-*`: `NxBanner` (`md`), `NxCode` (`lg`), the card (`xl`) |
50
+ | `color-background` | white | What every tint is mixed over |
51
+ | `color-foreground` | near black | Text |
52
+ | `color-card`, `color-card-foreground` | white, near black | The layout's card, `NxCard` |
53
+ | `color-primary`, `color-primary-foreground` | indigo, near white | The default colour of buttons, badges, alerts |
54
+ | `color-secondary`, `color-secondary-foreground` | blue, near white | `color="secondary"` |
55
+ | `color-muted`, `color-muted-foreground` | light grey, grey | `NxCode`'s background, a table's footer, an avatar's initials; descriptions, captions, the footer |
56
+ | `color-accent`, `color-accent-foreground` | light grey, near black | `bg-accent` in your markup |
57
+ | `color-error`, `color-success`, `color-info`, `color-warning` | red, teal, blue, orange | The status colours |
58
+ | `color-error-foreground`, … `color-warning-foreground` | near white | Text on a status colour |
59
+ | `color-border` | light grey | Card, separator, table and outlined borders |
60
+ | `color-paper` | 5% primary over background | The page behind the layout's card |
61
+ | `color-<colour>-5`, `-10`, `-15`, `-20`, `-25`, `-40`, `-50` | the colour mixed over background | Tints — see below |
62
+
63
+ The exact values are in the file itself:
64
+
65
+ ```ts
66
+ import { readFileSync } from 'node:fs';
67
+ import { THEME_FILE } from '@nxgt/mail-ui';
68
+
69
+ const css = readFileSync(THEME_FILE, 'utf8'); // @theme { --radius-sm: 6px; … }
70
+ ```
71
+
72
+ A layout of your own that does not read [`UI_CONTEXT`](plugin.md#your-own-layout--ui_context)
73
+ can import the file by its subpath — without the `theme` overrides, which
74
+ only `UI_CONTEXT` carries:
75
+
76
+ ```vue
77
+ <template>
78
+ <Html>
79
+ <Head>
80
+ <style>
81
+ @import "@maizzle/tailwindcss";
82
+ @import "@nxgt/mail-ui/theme.css";
83
+ </style>
84
+ </Head>
85
+ <Body><p class="bg-primary-15 text-primary">…</p></Body>
86
+ </Html>
87
+ </template>
88
+ ```
89
+
90
+ ## Tints, where material-vue uses an alpha
91
+
92
+ material-vue writes a translucent colour: `bg-primary/15`, `border-error/50`.
93
+ A mail client cannot be trusted with an alpha, so the theme declares each tint
94
+ as a plain colour: `bg-primary-15` is the primary colour mixed at 15% over the
95
+ background, in sRGB — the colour a browser shows for the alpha — and written
96
+ as hex in the built HTML.
97
+
98
+ | Tint | Used by |
99
+ | --- | --- |
100
+ | `-5` | `NxAlert`'s background; `NxListTile`'s; `NxHero`'s; `paper` |
101
+ | `-10` | `NxBanner`'s background; a selected `NxListTile size="sm"` |
102
+ | `-15` | `NxButton` and `NxChip variant="tonal"`; a selected `NxListTile`; an `NxTimeline` marker's ground; `NxRatioCard`'s track |
103
+ | `-20` | `NxProgress`'s track |
104
+ | `-25` | `NxStepsItem`'s circle |
105
+ | `-40` | `NxBanner`'s border; `NxSummaryData`'s lines; a selected `NxListTile`'s border; an `NxTimeline` marker's border |
106
+ | `-50` | `NxButton` and `NxChip variant="outlined"`'s border |
107
+
108
+ They exist for `primary`, `secondary`, `info`, `success`, `warning`, `error`
109
+ and `foreground`, so your own markup can use them too:
110
+
111
+ ```vue
112
+ <template>
113
+ <p class="m-0 rounded-md bg-success-10 p-3 text-success">Your payment went through.</p>
114
+ </template>
115
+ ```
116
+
117
+ ## Overriding a token
118
+
119
+ ```ts
120
+ ui({ brand: { name: 'Acme' }, theme: { 'color-primary': '#0f766e' } });
121
+ ```
122
+
123
+ - **A tint follows its colour.** Each is mixed from the colour's token, so
124
+ `color-primary` above makes the tonal button `#dbeae9` and `paper`
125
+ `#f3f8f8`, with no other token to change.
126
+ - **A tint can be overridden alone**: `{ 'color-primary-15': '#e0f2f1' }`.
127
+ - **Any CSS colour works**, `oklch()` included: Maizzle turns it into hex,
128
+ as it does the theme's own.
129
+ - **The value is trimmed**, and refused if it is empty or holds `;`, `{`,
130
+ `}`, `<`, `>`, a quote, a backslash, a CSS comment (`/*`, `*/`) or a line
131
+ break: it must stay one declaration.
132
+ - **Only tokens of `theme.css`.** A name it does not declare is refused, so a
133
+ misspelling fails when the config loads rather than being ignored.
134
+
135
+ ```ts
136
+ ui({ brand: { name: 'Acme' }, theme: { 'color-primay': '#0f766e' } });
137
+ // TypeError: ui: theme.color-primay is not a token of the theme — name one of theme.css without its --, as color-primary
138
+ ```
139
+
140
+ The full list of errors is in [The plugin](plugin.md#errors).
141
+
142
+ ## Light only
143
+
144
+ The layout declares `<meta name="color-scheme" content="light">` and
145
+ `supported-color-schemes` `light`, and the theme has no dark tokens. A client
146
+ in dark mode may still invert the colours on its own; the e-mail asks it not
147
+ to, and does not ship a second palette.
148
+
149
+ ## See also
150
+
151
+ - [Components](components.md) — which tokens each component uses.
152
+ - [The plugin](plugin.md) — `theme` among the other options, and your own
153
+ layout on the same CSS.