@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.
- package/LICENSE +21 -0
- package/README.md +311 -0
- package/components/alert.vue +75 -0
- package/components/avatar-fallback.vue +21 -0
- package/components/avatar-group.vue +44 -0
- package/components/avatar-image.vue +20 -0
- package/components/avatar.vue +29 -0
- package/components/badge.vue +47 -0
- package/components/banner.vue +57 -0
- package/components/breakdown-card.vue +27 -0
- package/components/card-content.vue +16 -0
- package/components/card-description.vue +16 -0
- package/components/card-footer.vue +16 -0
- package/components/card-header.vue +33 -0
- package/components/card-title.vue +20 -0
- package/components/card.vue +30 -0
- package/components/chip.vue +41 -0
- package/components/code.vue +26 -0
- package/components/compare-card.vue +63 -0
- package/components/description.vue +24 -0
- package/components/entity-header.vue +48 -0
- package/components/goal-card.vue +33 -0
- package/components/hero.vue +36 -0
- package/components/layout.vue +85 -0
- package/components/link.vue +19 -0
- package/components/list-tile.vue +80 -0
- package/components/nx-button.vue +91 -0
- package/components/progress.vue +66 -0
- package/components/ratio-card.vue +36 -0
- package/components/see-also.vue +53 -0
- package/components/separator.vue +27 -0
- package/components/stat-card.vue +43 -0
- package/components/status-indicator.vue +35 -0
- package/components/steps-item.vue +48 -0
- package/components/steps.vue +31 -0
- package/components/summary-data.vue +45 -0
- package/components/table-body.vue +11 -0
- package/components/table-caption.vue +16 -0
- package/components/table-cell.vue +28 -0
- package/components/table-empty.vue +23 -0
- package/components/table-footer.vue +11 -0
- package/components/table-head.vue +25 -0
- package/components/table-header.vue +11 -0
- package/components/table-row.vue +15 -0
- package/components/table.vue +25 -0
- package/components/timeline.vue +70 -0
- package/components/typography.vue +64 -0
- package/components/ui.ts +166 -0
- package/dist/catalogues.d.ts +56 -0
- package/dist/catalogues.d.ts.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +214 -0
- package/dist/index.js.map +13 -0
- package/dist/packaged.d.ts +27 -0
- package/dist/packaged.d.ts.map +1 -0
- package/dist/plugin.d.ts +55 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/theme.d.ts +11 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/vue.d.ts +9 -0
- package/dist/vue.d.ts.map +1 -0
- package/docs/README.md +15 -0
- package/docs/guide/components.md +984 -0
- package/docs/guide/messages.md +156 -0
- package/docs/guide/plugin.md +365 -0
- package/docs/guide/theme.md +153 -0
- package/docs/roadmap.md +97 -0
- package/docs/troubleshooting.md +519 -0
- package/package.json +69 -0
- 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.
|