@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,984 @@
1
+ # Components
2
+
3
+ This page is for writing templates with the `Nx*` components: each one's
4
+ props, defaults and slots, and the `@nxgt/material-vue` component it mirrors.
5
+
6
+ ```vue
7
+ <!-- emails/sign-in-code.vue -->
8
+ <template>
9
+ <NxLayout preheader="Your sign-in code">
10
+ <NxTypography variant="headline-small">Your sign-in code</NxTypography>
11
+ <NxTypography>Enter this code to sign in to {{ brand.name }}.</NxTypography>
12
+ <NxCode>493 812</NxCode>
13
+ <NxTypography variant="caption">It expires in 15 minutes.</NxTypography>
14
+ </NxLayout>
15
+ </template>
16
+ ```
17
+
18
+ <img src="https://raw.githubusercontent.com/softistx/nxgt-mail/refs/tags/@nxgt/mail-ui@0.1.0/packages/mail-ui/previews/components-en.png" width="420" alt="An e-mail using the first Nx components: layout, typography, code, buttons, separator, card with badge, summary data and status, alert, banner, link">
19
+
20
+ The components from `NxLayout` to `NxCode`, in one e-mail
21
+ ([its template](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-ui/test/fixture/emails/welcome.vue)).
22
+
23
+ <img src="https://raw.githubusercontent.com/softistx/nxgt-mail/refs/tags/@nxgt/mail-ui@0.1.0/packages/mail-ui/previews/data-components.png" width="420" alt="An e-mail using the data components: a table with a footer and caption, an empty table, descriptions, list tiles with an avatar and a chip, chips, an avatar group and an avatar">
24
+
25
+ The components from `NxTable` to `NxAvatar`
26
+ ([their template](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-ui/test/fixture/emails/gallery.vue)).
27
+
28
+ <img src="https://raw.githubusercontent.com/softistx/nxgt-mail/refs/tags/@nxgt/mail-ui@0.1.0/packages/mail-ui/previews/sequence-components.png" width="420" alt="An e-mail using the sequence components: three progress bars, three numbered steps joined by a line, a timeline of three toned events, and an empty timeline's text">
29
+
30
+ `NxProgress`, `NxSteps` and `NxTimeline`
31
+ ([their template](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-ui/test/fixture/emails/sequence.vue)).
32
+
33
+ <img src="https://raw.githubusercontent.com/softistx/nxgt-mail/refs/tags/@nxgt/mail-ui@0.1.0/packages/mail-ui/previews/summary-components.png" width="420" alt="An e-mail using the summary components: a hero with an eyebrow and a button, an entity header with an icon, a status badge and a link, three stat cards with toned deltas, a goal card, a ratio card, a compare card, a breakdown card with three bars, and a see-also list of two links">
34
+
35
+ The components from `NxHero` to `NxSeeAlso`
36
+ ([their template](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-ui/test/fixture/emails/summary.vue)).
37
+
38
+ Every component is registered by [`ui()`](plugin.md) and used with no import.
39
+ They keep material-vue's names (with the `Nx` prefix), its props and its
40
+ values, and render with tables and inlined styles, which every mail client
41
+ reads. They need no JavaScript and no web font.
42
+
43
+ ## What every component shares
44
+
45
+ - **`class` is merged, not appended.** A class you pass wins over the
46
+ component's own for the same property (Tailwind Merge):
47
+ `<NxButton class="rounded-md">` replaces its `rounded-full`. Other
48
+ attributes (`style`, `id`, `data-*`) go to the same element as `class`.
49
+ - **Block components end with space below.** `NxTypography`, `NxAlert`,
50
+ `NxBanner`, `NxCard`, `NxCode`, `NxSummaryData`, `NxTable`,
51
+ `NxDescription`, `NxAvatarGroup`, `NxProgress`, `NxSteps`, `NxTimeline`,
52
+ `NxHero`, `NxEntityHeader`, the metric cards (`NxStatCard` to
53
+ `NxBreakdownCard`) and `NxSeeAlso` carry `mb-4`, `NxListTile` `mb-2`. On
54
+ `NxTypography`, `NxSummaryData`, `NxTable`, `NxDescription`,
55
+ `NxAvatarGroup`, `NxProgress`, `NxSteps`, `NxTimeline` and `NxSeeAlso`,
56
+ `class="mb-0"` removes it. `NxAlert`, `NxBanner`, `NxCard`, `NxCode`,
57
+ `NxListTile`, `NxHero`, `NxEntityHeader` and the metric cards put their
58
+ `class` on the box inside, and keep their 16px below: to change it, replace
59
+ the component with your own
60
+ (see [Replacing a component](plugin.md#replacing-a-component)).
61
+ - **Vertical space is Maizzle's `<Spacer>`**, which Outlook respects:
62
+
63
+ ```vue
64
+ <Spacer height="24px" />
65
+ ```
66
+
67
+ - **Icons are slots.** An e-mail has no icon font: pass an `<img>` with an
68
+ absolute URL, or a character, where a component has an `icon` slot.
69
+ - **A placeholder passes through.** `:href="placeholder('link')"` or
70
+ `{{ placeholder('code') }}` from `@nxgt/mail-i18n` is written as is; no
71
+ component branches on a value only known at send time.
72
+
73
+ ## Summary
74
+
75
+ | Component | Mirrors | Props (default) | Slots |
76
+ | --- | --- | --- | --- |
77
+ | [`NxLayout`](#nxlayout) | the app shell | `lang`, `preheader`, `width` (`600`) | default, `footer` |
78
+ | [`NxTypography`](#nxtypography) | `Typography` | `variant` (`'normal'`), `as` | default |
79
+ | [`NxButton`](#nxbutton) | `Button` | `href` (required), `variant` (`'filled'`), `color` (`'primary'`), `size` (`'default'`), `align` | default |
80
+ | [`NxLink`](#nxlink) | a text link | `href` (required) | default |
81
+ | [`NxSeparator`](#nxseparator) | `Separator` | — | — |
82
+ | [`NxCard`](#nxcard-and-its-parts) | `Card` | — | default: its parts |
83
+ | `NxCardHeader` | `CardHeader` | `title`, `description` | default, `action` |
84
+ | `NxCardTitle`, `NxCardDescription` | `CardTitle`, `CardDescription` | — | default |
85
+ | `NxCardContent`, `NxCardFooter` | `CardContent`, `CardFooter` | — | default |
86
+ | [`NxBadge`](#nxbadge) | `Badge` | `variant` (`'default'`) | default |
87
+ | [`NxAlert`](#nxalert) | `Alert` | `variant` (`'primary'`), `title`, `description` | default, `icon`, `title`, `description` |
88
+ | [`NxBanner`](#nxbanner) | `Banner` | `tone` (`'info'`), `title`, `description` | default, `icon`, `action` |
89
+ | [`NxStatusIndicator`](#nxstatusindicator) | `StatusIndicator` | `tone` (`'neutral'`) | default |
90
+ | [`NxSummaryData`](#nxsummarydata) | `SummaryData` | `data` (`[]`), `inline` (`false`), `showEmpty` (`false`) | — |
91
+ | [`NxCode`](#nxcode) | — (e-mail's own) | — | default |
92
+ | [`NxTable`](#nxtable-and-its-parts) | `Table` | — | default: its parts |
93
+ | `NxTableHeader`, `NxTableBody`, `NxTableFooter` | `TableHeader`, `TableBody`, `TableFooter` | — | default: `NxTableRow`s |
94
+ | `NxTableRow`, `NxTableHead`, `NxTableCell`, `NxTableCaption` | `TableRow`, `TableHead`, `TableCell`, `TableCaption` | — | default |
95
+ | `NxTableEmpty` | `TableEmpty` | `colspan` (`1`) | default |
96
+ | [`NxDescription`](#nxdescription) | `Description` | `label` (required), `value` | default |
97
+ | [`NxListTile`](#nxlisttile) | `ListTile` | `title` (required), `subtitle`, `href`, `selected` (`false`), `disabled` (`false`), `size` (`'md'`) | `leading`, `trailing` |
98
+ | [`NxChip`](#nxchip) | `Chip` | `label`, `variant` (`'outlined'`), `color` (`'primary'`), `active` (`false`) | default, `avatar`, `leading`, `trailing` |
99
+ | [`NxAvatar`](#nxavatar-and-nxavatargroup) | `Avatar` | `size` (`32`, or its group's) | default: `NxAvatarImage` or `NxAvatarFallback` |
100
+ | `NxAvatarImage` | `AvatarImage` | `src` (required), `alt` | — |
101
+ | `NxAvatarFallback` | `AvatarFallback` | — | default |
102
+ | `NxAvatarGroup` | `AvatarGroup` | `max`, `size` (`'md'`) | default: `NxAvatar`s |
103
+ | [`NxProgress`](#nxprogress) | `Progress` | `modelValue` (`0`), `max` (`100`), `height` (`8`) | — |
104
+ | [`NxSteps`](#nxsteps-and-nxstepsitem) | `Steps` | — | default: `NxStepsItem`s |
105
+ | `NxStepsItem` | `StepsItem` | `title`, `index` (its place), `last` (set by `NxSteps`) | default, `index` |
106
+ | [`NxTimeline`](#nxtimeline) | `Timeline` | `items` (required), `empty` (a shared message) | — |
107
+ | [`NxHero`](#nxhero) | `Hero` | `title` (required), `eyebrow`, `description` | default, `actions` |
108
+ | [`NxEntityHeader`](#nxentityheader) | `EntityHeader` | `title` (required), `metadata` (`[]`) | default, `icon`, `status`, `actions` |
109
+ | [`NxStatCard`](#nxstatcard) | `StatCard` | `label` (required), `value`, `hint`, `delta`, `deltaTone` (by the delta's sign) | `icon` |
110
+ | [`NxGoalCard`](#nxgoalcard) | `GoalCard` | `label`, `value`, `target` (required), `unit` | — |
111
+ | [`NxRatioCard`](#nxratiocard) | `RatioCard` | `label`, `left`, `right`, `percent` (required) | — |
112
+ | [`NxCompareCard`](#nxcomparecard) | `CompareCard` | `label`, `current`, `previous` (required), `delta` | — |
113
+ | [`NxBreakdownCard`](#nxbreakdowncard) | `BreakdownCard` | `label`, `items` (required) | — |
114
+ | [`NxSeeAlso`](#nxseealso) | `SeeAlso` | `items` (required), `label` (a shared message) | — |
115
+
116
+ ## NxLayout
117
+
118
+ The page of an e-mail: the brand's logo or name at the top, the content on a
119
+ card, and a footer. Every other component goes inside it: it holds the theme
120
+ in its `<style>`.
121
+
122
+ ```vue
123
+ <template>
124
+ <NxLayout preheader="Your account is ready" :width="560">
125
+ <NxTypography>Welcome aboard.</NxTypography>
126
+ <template #footer>
127
+ <p class="m-0 mb-2">You are receiving this because you signed up.</p>
128
+ </template>
129
+ </NxLayout>
130
+ </template>
131
+ ```
132
+
133
+ | Prop | Type | Default | Effect |
134
+ | --- | --- | --- | --- |
135
+ | `lang` | `string` | the template's `locale` with `@nxgt/mail-i18n`, else `'en'` | `<html lang>` |
136
+ | `preheader` | `string` | none | The text a client shows beside the subject, before the e-mail is opened (Maizzle's `<Preheader>`) |
137
+ | `width` | `number` | `600` | The card's maximum width, in pixels — and the fixed width of the table Outlook on Windows draws, since it ignores a maximum width |
138
+
139
+ | Slot | Default content |
140
+ | --- | --- |
141
+ | default | — the e-mail's content, on the card |
142
+ | `footer` | With `@nxgt/mail-i18n` listed: `t('common.footer.why', { brand: brand.name })`, as `You received this e-mail because you have an account with Acme.` Without it: nothing |
143
+
144
+ Under the footer slot, the brand's name is always written, linked to
145
+ `brand.url` when there is one.
146
+
147
+ The page behind the card is `paper` (5% of the primary colour over the
148
+ background), the card `card` with a `border` line and `rounded-xl` corners.
149
+ The layout declares `<meta name="color-scheme" content="light">`: there is no
150
+ dark version — see [The theme](theme.md#light-only).
151
+
152
+ It is built on Maizzle's `<Html>`, `<Head>`, `<Body>`, `<Preheader>` and
153
+ `<Container>`, so Maizzle's Outlook and client resets apply, and the
154
+ `<Container>` writes the fixed-width table that holds the card at `width` in
155
+ Outlook.
156
+
157
+ ## NxTypography
158
+
159
+ Text, on the tag an e-mail reads it as.
160
+
161
+ ```vue
162
+ <template>
163
+ <NxTypography variant="headline-small">Reset your password</NxTypography>
164
+ <NxTypography>Someone asked to reset the password of your account.</NxTypography>
165
+ <NxTypography variant="caption">If it was not you, ignore this e-mail.</NxTypography>
166
+ </template>
167
+ ```
168
+
169
+ | Prop | Type | Default | Effect |
170
+ | --- | --- | --- | --- |
171
+ | `variant` | see below | `'normal'` | Size and weight |
172
+ | `as` | `string` | by `variant` | The tag: `h1`, `p`, `span`… |
173
+
174
+ | `variant` | Style | Tag |
175
+ | --- | --- | --- |
176
+ | `normal`, `body-medium` | `text-sm` | `p` |
177
+ | `body-small` | `text-xs` | `p` |
178
+ | `caption` | `text-[10px]`, muted | `p` |
179
+ | `headline-large` | `text-4xl`, semibold | `h1` |
180
+ | `headline-medium` | `text-3xl`, semibold | `h1` |
181
+ | `headline-small` | `text-2xl`, semibold | `h1` |
182
+ | `title-large` | `text-xl`, semibold | `h2` |
183
+ | `title-medium` | `text-lg`, semibold | `h3` |
184
+ | `title-small` | `text-base`, semibold | `h4` |
185
+
186
+ All are `text-foreground` with `m-0 mb-4`, but `caption`, which is
187
+ `text-muted-foreground`.
188
+
189
+ ## NxButton
190
+
191
+ material-vue's `Button`, as a link: its variants, colours and sizes, on
192
+ Maizzle's `<Button>`, which pads it for Outlook.
193
+
194
+ ```vue
195
+ <template>
196
+ <NxButton :href="placeholder('link')">Confirm my address</NxButton>
197
+ <NxButton href="https://acme.example/billing" variant="tonal" size="sm">See my invoice</NxButton>
198
+ <NxButton href="https://acme.example/account/delete" variant="outlined" color="error">Delete my account</NxButton>
199
+ </template>
200
+ ```
201
+
202
+ | Prop | Type | Default | Effect |
203
+ | --- | --- | --- | --- |
204
+ | `href` | `string` | — (required) | Where it goes. A placeholder is fine |
205
+ | `variant` | `'filled' \| 'tonal' \| 'outlined' \| 'ghost' \| 'link'` | `'filled'` | See below |
206
+ | `color` | `'primary' \| 'secondary' \| 'info' \| 'success' \| 'warning' \| 'error' \| 'default'` | `'primary'` | `default` is the foreground colour |
207
+ | `size` | `'xs' \| 'sm' \| 'default' \| 'lg'` | `'default'` | Padding and text size |
208
+ | `align` | `'left' \| 'center' \| 'right'` | none | Aligns the button in its row |
209
+
210
+ | `variant` | Looks like |
211
+ | --- | --- |
212
+ | `filled` | The colour, with its `-foreground` text (`default`: foreground on background) |
213
+ | `tonal` | The colour at 15% over the background, with the colour as text |
214
+ | `outlined` | Transparent, a 1px border of the colour at 50%, the colour as text |
215
+ | `ghost` | Transparent, foreground text; `color` is ignored |
216
+ | `link` | No padding, no background: the colour as text, not underlined |
217
+
218
+ | `size` | Padding | Text |
219
+ | --- | --- | --- |
220
+ | `xs` | `px-2 py-1` | `text-xs` |
221
+ | `sm` | `px-3 py-2` | `text-sm` |
222
+ | `default` | `px-4 py-2.5` | `text-sm` |
223
+ | `lg` | `px-6 py-3` | `text-sm` |
224
+
225
+ Every variant but `link` is `rounded-full`, as in material-vue. The Outlook
226
+ padding for each size is set for you.
227
+
228
+ ## NxLink
229
+
230
+ A link in running text, in material-vue's link colour (`info`), underlined —
231
+ a phone has no hover to reveal it.
232
+
233
+ ```vue
234
+ <template>
235
+ <NxTypography>
236
+ Questions? Read the <NxLink href="https://acme.example/help">help centre</NxLink>.
237
+ </NxTypography>
238
+ </template>
239
+ ```
240
+
241
+ | Prop | Type | Default | Effect |
242
+ | --- | --- | --- | --- |
243
+ | `href` | `string` | — (required) | Where it goes |
244
+
245
+ ## NxSeparator
246
+
247
+ material-vue's horizontal `Separator`: a 1px line in the `border` colour,
248
+ with `py-6` around it. The space is padding, which Outlook keeps where it
249
+ drops a margin.
250
+
251
+ ```vue
252
+ <template>
253
+ <NxSeparator />
254
+ <NxSeparator class="py-2" />
255
+ </template>
256
+ ```
257
+
258
+ No props, no slot.
259
+
260
+ ## NxCard and its parts
261
+
262
+ material-vue's `Card`: a bordered, rounded box, `bg-card`. Its parts are its
263
+ rows, so **only its parts go directly inside it**: `NxCardHeader`,
264
+ `NxCardContent`, `NxCardFooter`.
265
+
266
+ ```vue
267
+ <template>
268
+ <NxCard>
269
+ <NxCardHeader title="Pro plan" description="Billed monthly">
270
+ <template #action><NxBadge variant="success">Active</NxBadge></template>
271
+ </NxCardHeader>
272
+ <NxCardContent>
273
+ <NxSummaryData :data="[{ label: 'Seats', value: 3 }, { label: 'Next invoice', value: '1 November' }]" />
274
+ </NxCardContent>
275
+ <NxCardFooter>
276
+ <NxButton href="https://acme.example/billing" variant="tonal" size="sm">Manage billing</NxButton>
277
+ </NxCardFooter>
278
+ </NxCard>
279
+ </template>
280
+ ```
281
+
282
+ | Part | Props | Slots | Renders |
283
+ | --- | --- | --- | --- |
284
+ | `NxCard` | — | default: its parts | The box, `py-6` |
285
+ | `NxCardHeader` | `title?: string`, `description?: string` | default (under the description), `action` (on the right) | A row, `px-6 pb-6` |
286
+ | `NxCardTitle` | — | default | `<h3>`, `text-base` semibold |
287
+ | `NxCardDescription` | — | default | `<p>`, `text-sm` muted |
288
+ | `NxCardContent` | — | default | A row, `px-6` |
289
+ | `NxCardFooter` | — | default | A row, `px-6 pt-6` |
290
+
291
+ `title` and `description` are shorthands for `NxCardTitle` and
292
+ `NxCardDescription`; use the parts in the default slot when the title needs
293
+ markup:
294
+
295
+ ```vue
296
+ <template>
297
+ <NxCardHeader>
298
+ <NxCardTitle>Order <strong>#{{ placeholder('orderNumber') }}</strong></NxCardTitle>
299
+ <NxCardDescription>Shipped today</NxCardDescription>
300
+ </NxCardHeader>
301
+ </template>
302
+ ```
303
+
304
+ ## NxBadge
305
+
306
+ material-vue's `Badge`: a small pill of text, inline.
307
+
308
+ ```vue
309
+ <template>
310
+ <NxTypography>Your plan is <NxBadge variant="success">Active</NxBadge></NxTypography>
311
+ </template>
312
+ ```
313
+
314
+ | Prop | Type | Default |
315
+ | --- | --- | --- |
316
+ | `variant` | `'default' \| 'secondary' \| 'destructive' \| 'error' \| 'success' \| 'info' \| 'warning' \| 'outline' \| 'outlined'` | `'default'` |
317
+
318
+ `default` is the primary colour. Each colour variant is filled with its
319
+ `-foreground` text; `destructive` is `error` with white text; `outline` and
320
+ `outlined` are a `border` line with foreground text.
321
+
322
+ ## NxAlert
323
+
324
+ material-vue's `Alert`: a block tinted at 5% of its colour, with an 8px bar of
325
+ the colour on its left.
326
+
327
+ ```vue
328
+ <template>
329
+ <NxAlert variant="warning" title="Heads up" description="The code expires in 15 minutes." />
330
+ <NxAlert variant="error">
331
+ <template #icon><img src="https://acme.example/icons/alert.png" width="16" alt=""></template>
332
+ <template #title>Payment failed</template>
333
+ We could not charge your card ending in {{ placeholder('last4') }}.
334
+ </NxAlert>
335
+ </template>
336
+ ```
337
+
338
+ | Prop | Type | Default | Effect |
339
+ | --- | --- | --- | --- |
340
+ | `variant` | `'primary' \| 'secondary' \| 'error' \| 'success' \| 'info' \| 'warning' \| 'foreground'` | `'primary'` | The colour. `foreground` is a foreground bar on the `muted` background |
341
+ | `title` | `string` | none | Bold, first line |
342
+ | `description` | `string` | none | Muted, under the title |
343
+
344
+ | Slot | Replaces |
345
+ | --- | --- |
346
+ | `icon` | Nothing: a 24px column on the left, shown only when given, in the variant's colour (`text-<variant>`), so a character icon takes it |
347
+ | `title`, `description` | The prop of the same name |
348
+ | default | — written under the description |
349
+
350
+ ## NxBanner
351
+
352
+ material-vue's `Banner`: a status in a rounded box tinted at 10% of its tone,
353
+ with a border at 40%, and an optional action on the right.
354
+
355
+ ```vue
356
+ <template>
357
+ <NxBanner tone="info" title="New sign-in" description="From Firefox on Linux.">
358
+ <template #action>
359
+ <NxLink href="https://acme.example/security">Review</NxLink>
360
+ </template>
361
+ </NxBanner>
362
+ </template>
363
+ ```
364
+
365
+ | Prop | Type | Default | Effect |
366
+ | --- | --- | --- | --- |
367
+ | `tone` | `'info' \| 'success' \| 'warning' \| 'error'` | `'info'` | The colour of the box, and of the icon column's text |
368
+ | `title` | `string` | none | Semibold, first line |
369
+ | `description` | `string` | none | Muted, under the title |
370
+
371
+ | Slot | Effect |
372
+ | --- | --- |
373
+ | `icon` | A column on the left, in the tone's colour (a character takes it) |
374
+ | `action` | A column on the right, aligned to the middle |
375
+ | default | Written under the description |
376
+
377
+ ## NxStatusIndicator
378
+
379
+ material-vue's `StatusIndicator`: a coloured dot before a label, inline. The
380
+ dot is a character (`●`), which every client sizes, where an empty `<span>`
381
+ would be dropped.
382
+
383
+ ```vue
384
+ <template>
385
+ <NxTypography>Invoice: <NxStatusIndicator tone="success">Paid</NxStatusIndicator></NxTypography>
386
+ </template>
387
+ ```
388
+
389
+ | Prop | Type | Default | Effect |
390
+ | --- | --- | --- | --- |
391
+ | `tone` | `'neutral' \| 'primary' \| 'success' \| 'info' \| 'warning' \| 'error'` | `'neutral'` | The dot's colour; `neutral` is muted |
392
+
393
+ ## NxSummaryData
394
+
395
+ material-vue's `SummaryData`: labels and their values, one row each with a
396
+ line under it, or on one line with `inline`.
397
+
398
+ ```vue
399
+ <template>
400
+ <NxSummaryData
401
+ :data="[
402
+ { label: 'Order', value: placeholder('orderNumber') },
403
+ { label: 'Items', value: 3 },
404
+ { label: 'Discount', value: 0 },
405
+ { label: 'Coupon', value: null },
406
+ ]"
407
+ />
408
+ <NxSummaryData inline :data="[{ label: 'Plan', value: 'Pro' }, { label: 'Seats', value: 3 }]" />
409
+ </template>
410
+ ```
411
+
412
+ | Prop | Type | Default | Effect |
413
+ | --- | --- | --- | --- |
414
+ | `data` | `{ label: string; value?: string \| number \| null }[]` | `[]` | The rows, in order |
415
+ | `inline` | `boolean` | `false` | One paragraph, `Plan: Pro Seats: 3`, rather than a table |
416
+ | `showEmpty` | `boolean` | `false` | Keeps a row whose value is empty |
417
+
418
+ A row without a value (`undefined`, `null`, `''`) is left out, unless
419
+ `showEmpty`; `0` is a value and stays. Above, `Coupon` is left out and
420
+ `Discount` shows `0`. A placeholder is a value: its row is always written.
421
+
422
+ ## NxCode
423
+
424
+ A code the reader types in, such as a one-time code: large, spaced, in
425
+ monospace, centred on the `muted` background. It has no material-vue
426
+ counterpart.
427
+
428
+ ```vue
429
+ <template>
430
+ <NxCode>{{ placeholder('code') }}</NxCode>
431
+ </template>
432
+ ```
433
+
434
+ No props; the code is the default slot.
435
+
436
+ ## NxTable and its parts
437
+
438
+ material-vue's `Table`: rows ruled by a line under each, a header in
439
+ `font-medium`, and a footer on the `muted` background with a line above it.
440
+ The parts are the HTML table's, so they nest as it does.
441
+
442
+ ```vue
443
+ <template>
444
+ <NxTable>
445
+ <NxTableCaption>Prices include VAT.</NxTableCaption>
446
+ <NxTableHeader>
447
+ <NxTableRow>
448
+ <NxTableHead>Item</NxTableHead>
449
+ <NxTableHead class="text-right">Amount</NxTableHead>
450
+ </NxTableRow>
451
+ </NxTableHeader>
452
+ <NxTableBody>
453
+ <NxTableRow>
454
+ <NxTableCell>Pro plan</NxTableCell>
455
+ <NxTableCell class="text-right">{{ placeholder('amount') }}</NxTableCell>
456
+ </NxTableRow>
457
+ </NxTableBody>
458
+ <NxTableFooter>
459
+ <NxTableRow>
460
+ <NxTableCell>Total</NxTableCell>
461
+ <NxTableCell class="text-right">{{ placeholder('total') }}</NxTableCell>
462
+ </NxTableRow>
463
+ </NxTableFooter>
464
+ </NxTable>
465
+ </template>
466
+ ```
467
+
468
+ | Part | Props | Renders |
469
+ | --- | --- | --- |
470
+ | `NxTable` | — | `<table role="table">`, full width, `text-sm` |
471
+ | `NxTableHeader`, `NxTableBody`, `NxTableFooter` | — | `<thead>`, `<tbody>`, `<tfoot>` |
472
+ | `NxTableRow` | — | `<tr>` |
473
+ | `NxTableHead` | — | `<th>`, `h-10 px-2`, left-aligned, `font-medium`, a line under it; in `NxTableFooter`, a line above it instead, `bg-muted` |
474
+ | `NxTableCell` | — | `<td>`, `p-2`, a line under it; in `NxTableFooter`, a line above it instead, `bg-muted`, `font-medium` |
475
+ | `NxTableCaption` | — | `<caption>`, under the table, `text-sm` muted |
476
+ | `NxTableEmpty` | `colspan?: number` (`1`) | A row of one cell across `colspan` columns, centred, `py-10` — the text for a table with no rows. Its `class` goes on the cell |
477
+
478
+ A cell's text wraps, where material-vue's does not: a long value would
479
+ otherwise widen the e-mail past a phone's screen. `NxTableHead` keeps
480
+ `whitespace-nowrap`. Align a column with `class="text-right"` on its head and
481
+ its cells.
482
+
483
+ `NxTable` carries `role="table"`: Maizzle marks every other table
484
+ `role="none"`, a layout, and a screen reader then reads its rows as plain text.
485
+ The caption is placed under the table by `align="bottom"` as well as
486
+ `caption-side`, which some clients drop.
487
+
488
+ ## NxDescription
489
+
490
+ material-vue's `Description`: a label in `font-semibold`, and its value under
491
+ it, muted.
492
+
493
+ ```vue
494
+ <template>
495
+ <NxDescription label="Billing period" value="September 2026" />
496
+ <NxDescription label="Credits left" :value="0" />
497
+ <NxDescription label="Coupon" :value="null" />
498
+ </template>
499
+ ```
500
+
501
+ | Prop | Type | Default | Effect |
502
+ | --- | --- | --- | --- |
503
+ | `label` | `string` | required | The first line |
504
+ | `value` | `string \| number \| null` | none | The second line |
505
+
506
+ Like a row of `NxSummaryData`, a description without a value (`undefined`,
507
+ `null`, `''`) is left out entirely, and `0` is a value: above, `Coupon` is
508
+ not written. The default slot is written under the value.
509
+
510
+ ## NxListTile
511
+
512
+ material-vue's `ListTile`: a title, a subtitle under it, and `leading` and
513
+ `trailing` slots on its sides, on a rounded box tinted at 5% of the primary
514
+ colour.
515
+
516
+ ```vue
517
+ <template>
518
+ <NxListTile title="Ada Lovelace" subtitle="Owner" href="https://acme.example/team/ada" selected>
519
+ <template #leading>
520
+ <NxAvatar><NxAvatarFallback>AL</NxAvatarFallback></NxAvatar>
521
+ </template>
522
+ <template #trailing><NxChip variant="tonal" color="success">Active</NxChip></template>
523
+ </NxListTile>
524
+ <NxListTile title="Grace Hopper" subtitle="Invited" size="sm" disabled />
525
+ </template>
526
+ ```
527
+
528
+ | Prop | Type | Default | Effect |
529
+ | --- | --- | --- | --- |
530
+ | `title` | `string` | required | `text-sm` semibold (`md`) or `text-[13px]` medium (`sm`) |
531
+ | `subtitle` | `string` | none | Muted, under the title |
532
+ | `href` | `string` | none | Makes the title a link |
533
+ | `selected` | `boolean` | `false` | A border at 40% of the primary colour, on a 15% tint (`md`) or a 10% one (`sm`) |
534
+ | `disabled` | `boolean` | `false` | A muted title, and no link even with `href` |
535
+ | `size` | `'sm' \| 'md'` | `'md'` | `md` is `px-4 py-2` on the tint; `sm` is `px-2 py-1.5` with no tint until selected |
536
+
537
+ material-vue's ring is a border here: a mail client draws no `box-shadow`.
538
+ Only the title is a link, not the whole tile: a mail client cannot make a
539
+ table cell clickable.
540
+
541
+ ## NxChip
542
+
543
+ material-vue's `Chip`, static: a rounded label, with the colours of
544
+ [`NxButton`](#nxbutton).
545
+
546
+ ```vue
547
+ <template>
548
+ <NxTypography>
549
+ <NxChip label="Design" />
550
+ <NxChip label="Selected" active />
551
+ <NxChip variant="tonal" color="success">Paid</NxChip>
552
+ </NxTypography>
553
+ </template>
554
+ ```
555
+
556
+ | Prop | Type | Default | Effect |
557
+ | --- | --- | --- | --- |
558
+ | `label` | `string` | none | The text, when the default slot is empty |
559
+ | `variant` | `'filled' \| 'tonal' \| 'outlined' \| 'ghost' \| 'link'` | `'outlined'` | As `NxButton`'s |
560
+ | `color` | `'default' \| 'primary' \| 'secondary' \| 'error' \| 'success' \| 'info' \| 'warning'` | `'primary'` | As `NxButton`'s |
561
+ | `active` | `boolean` | `false` | Makes it `filled`, whatever its `variant` |
562
+
563
+ | Slot | Effect |
564
+ | --- | --- |
565
+ | default | The text, in place of `label` |
566
+ | `avatar` | Before the text; an `NxAvatar` in it is 20px |
567
+ | `leading` | Before the text, when there is no `avatar` |
568
+ | `trailing` | After the text |
569
+
570
+ An e-mail runs no script: a chip has no dismiss button and no click. It is
571
+ inline; put several in an `NxTypography` to give them space below. Unlike
572
+ material-vue's, the avatar is not pulled into the chip's padding: Gmail drops
573
+ a negative margin.
574
+
575
+ ## NxAvatar and NxAvatarGroup
576
+
577
+ material-vue's `Avatar`: a round picture, or initials on the `muted`
578
+ background when there is none.
579
+
580
+ ```vue
581
+ <template>
582
+ <NxAvatar :size="48"><NxAvatarImage src="https://acme.example/ada.png" alt="Ada" /></NxAvatar>
583
+ <NxAvatarGroup :max="3" size="lg">
584
+ <NxAvatar><NxAvatarImage src="https://acme.example/ada.png" alt="Ada" /></NxAvatar>
585
+ <NxAvatar><NxAvatarFallback>GH</NxAvatarFallback></NxAvatar>
586
+ <NxAvatar><NxAvatarFallback>AT</NxAvatarFallback></NxAvatar>
587
+ <NxAvatar><NxAvatarFallback>KJ</NxAvatarFallback></NxAvatar>
588
+ </NxAvatarGroup>
589
+ </template>
590
+ ```
591
+
592
+ | Part | Props | Renders |
593
+ | --- | --- | --- |
594
+ | `NxAvatar` | `size?: number`: pixels; its group's, else `32` | A round, inline box of that size |
595
+ | `NxAvatarImage` | `src: string` (required), `alt?: string` (`''`, a decorative picture) | The `<img>`, with `width` and `height` set to the avatar's size |
596
+ | `NxAvatarFallback` | — | Its text (initials), `text-[12px]`, centred on `bg-muted` |
597
+ | `NxAvatarGroup` | `max?: number`, `size?: 'sm' \| 'md' \| 'lg'` (`'md'`) | Its avatars in a row, each ringed with the background, sized 24, 32 or 40px; past `max`, one more reading `+N`. Without `max`, or with one under 1, all of them |
598
+
599
+ Choose `NxAvatarImage` or `NxAvatarFallback`: an e-mail cannot fall back on a
600
+ picture that fails to load, so there is no switching between them. Give the
601
+ image an absolute URL, and a square picture. A group's avatars sit side by
602
+ side, 4px apart, rather than overlapping as in material-vue: Gmail drops the
603
+ negative margin that stacks them. Avatars from a `v-for` count one by one.
604
+
605
+ The `+N` is labelled for a screen reader with the shared message
606
+ `common.avatarGroup.more` (`2 more`, `2 autres`) when `@nxgt/mail-i18n` is
607
+ listed, and in English otherwise; see [Shared messages](messages.md).
608
+
609
+ Outlook on Windows ignores the width and height of an inline box: there,
610
+ `NxAvatarFallback`'s initials show on a grey strip rather than in a circle. An
611
+ `NxAvatarImage` keeps its size everywhere, from its `width` and `height`.
612
+
613
+ ## NxProgress
614
+
615
+ material-vue's `Progress`, still: a bar filled to `modelValue` out of `max`,
616
+ on a rounded track at 20% of the primary colour.
617
+
618
+ ```vue
619
+ <template>
620
+ <NxTypography>2 of 3 steps done</NxTypography>
621
+ <NxProgress :model-value="2" :max="3" />
622
+ </template>
623
+ ```
624
+
625
+ | Prop | Type | Default | Effect |
626
+ | --- | --- | --- | --- |
627
+ | `modelValue` | `number` | `0` | How much is done — material-vue's name, passed one way: an e-mail has no `v-model` |
628
+ | `max` | `number` | `100` | What `modelValue` is out of |
629
+ | `height` | `number` | `8` | The bar's height in pixels, above 0; an addition, where material-vue sets a class such as `h-1.5` |
630
+
631
+ The fill is `modelValue / max`, rounded to a whole percent and kept between 0 and
632
+ 100. It is a table cell of that width, which every client draws: material-vue's
633
+ `transform` and pulse are not. The table carries `role="progressbar"` and the
634
+ `aria-value*` attributes.
635
+
636
+ The share is computed when the e-mail is built, so each prop is a number,
637
+ never a placeholder: one that is not
638
+ [fails the build](../troubleshooting.md#nxprogress-modelvalue-must-be-a-number-known-when-the-e-mail-is-built--a-placeholder-is-filled-only-when-it-is-sent).
639
+ A share that differs per recipient is written as text. The bar is left out of
640
+ the plain-text version, where it says nothing.
641
+
642
+ ## NxSteps and NxStepsItem
643
+
644
+ material-vue's `Steps`: numbered circles one under the other, each with a
645
+ title and its text, joined by a line.
646
+
647
+ ```vue
648
+ <template>
649
+ <NxSteps>
650
+ <NxStepsItem title="Create your account">Done on 1 September.</NxStepsItem>
651
+ <NxStepsItem title="Invite your team">Add the people who work with you.</NxStepsItem>
652
+ <NxStepsItem title="Connect your bank" />
653
+ </NxSteps>
654
+ </template>
655
+ ```
656
+
657
+ | Part | Props | Slots | Renders |
658
+ | --- | --- | --- | --- |
659
+ | `NxSteps` | — | default: `NxStepsItem`s | A table of its items, `mb-4`; numbers them 1, 2, 3… |
660
+ | `NxStepsItem` | `title?: string`, `index?: number` (its place in `NxSteps`), `last?: boolean` (set by `NxSteps`) | default (the text), `index` (in the circle, in place of the number) | A 32px circle, `border-primary-25`, the number in `text-primary`; the title `text-base` semibold; the text `text-sm` muted; a line down to the next item |
661
+
662
+ **Only `NxStepsItem`s go directly inside `NxSteps`, and an `NxStepsItem` only
663
+ inside `NxSteps`**, as a `v-for` or one by one: `NxSteps` numbers them and
664
+ tells each whether it is the `last`, which draws no line under it. An item is
665
+ table rows: alone, it is broken HTML with an empty circle.
666
+ An `index` you give wins over the item's place. The item's `class` goes on its
667
+ text's cell.
668
+
669
+ The line is the border of a cell in the same row as the text, so it runs as far
670
+ down as the text in every client; Outlook on Windows draws the circle square.
671
+
672
+ ## NxTimeline
673
+
674
+ material-vue's `Timeline`: events one under the other, each a toned marker on
675
+ a line, its title, a time on the right, and a description.
676
+
677
+ ```vue
678
+ <template>
679
+ <NxTimeline
680
+ :items="[
681
+ { id: 'sign-in', title: 'Signed in', description: 'Firefox on Linux', timestampLabel: placeholder('time'), tone: 'success' },
682
+ { id: 'password', title: 'Password changed', tone: 'warning' },
683
+ { id: 'created', title: 'Account created' },
684
+ ]"
685
+ />
686
+ <NxTimeline :items="[]" empty="Nothing this week" />
687
+ </template>
688
+ ```
689
+
690
+ | Prop | Type | Default | Effect |
691
+ | --- | --- | --- | --- |
692
+ | `items` | `{ id: string; title: string; description?: string; timestampLabel?: string; tone?: TimelineTone }[]` | required | The events, in order; each `id` unique |
693
+ | `empty` | `string` | the shared message `common.timeline.empty` (`No activity yet`); without `@nxgt/mail-i18n`, `No activity yet` in every language | The text shown, centred and muted, when `items` is empty |
694
+
695
+ `TimelineTone` is `'default' | 'primary' | 'success' | 'info' | 'warning' | 'error'`:
696
+ the marker's border at 40% of the tone, its ground at 15%, and its dot in the
697
+ tone; `default` is `border`, `muted` and muted text.
698
+
699
+ An e-mail is built before it is sent, so there is no `timestamp` turned into
700
+ "2 hours ago" as in material-vue: that would be the time of the build. Write
701
+ the time as `timestampLabel`, most often a placeholder filled at send time.
702
+ There is no `loading` either: an e-mail does not load.
703
+
704
+ ## NxHero
705
+
706
+ material-vue's `Hero`: an eyebrow, a large title, a description and actions,
707
+ in a rounded, bordered box. Its gradient and blurred shapes are a plain ground
708
+ at 5% of the primary colour: a mail client draws neither reliably.
709
+
710
+ ```vue
711
+ <template>
712
+ <NxHero eyebrow="September" title="Your month at Acme" description="What your team did, and what is next.">
713
+ <template #actions><NxButton href="https://acme.example/report">Open the report</NxButton></template>
714
+ </NxHero>
715
+ </template>
716
+ ```
717
+
718
+ | Prop | Type | Default | Effect |
719
+ | --- | --- | --- | --- |
720
+ | `title` | `string` | required | `<h1>`, `text-3xl` semibold |
721
+ | `eyebrow` | `string` | none | Above the title: `text-xs`, semibold, uppercase, in `text-primary` |
722
+ | `description` | `string` | none | Under the title, `text-base` muted |
723
+
724
+ | Slot | Effect |
725
+ | --- | --- |
726
+ | `actions` | Under the description, `mt-6`: an `NxButton` or two |
727
+ | default | Under the actions, `mt-8` |
728
+
729
+ The box is `rounded-2xl`, a `border` line, `px-8 py-10`; its `class` goes on
730
+ it.
731
+
732
+ ## NxEntityHeader
733
+
734
+ material-vue's `EntityHeader`: what an e-mail is about — an icon, a title with
735
+ its status, its details on one line, and actions on the right. Its shadow is a
736
+ border here.
737
+
738
+ ```vue
739
+ <template>
740
+ <NxEntityHeader title="Acme Labs" :metadata="[{ label: 'Plan', value: 'Pro' }, { label: 'Seats', value: 12 }]">
741
+ <template #icon>&#127970;</template>
742
+ <template #status><NxBadge variant="success">Active</NxBadge></template>
743
+ <template #actions><NxLink href="https://acme.example/settings">Settings</NxLink></template>
744
+ </NxEntityHeader>
745
+ </template>
746
+ ```
747
+
748
+ | Prop | Type | Default | Effect |
749
+ | --- | --- | --- | --- |
750
+ | `title` | `string` | required | `<h2>`, `text-lg` semibold |
751
+ | `metadata` | `{ label: string; value?: string \| number \| null }[]` | `[]` | Under the title, as an `inline` [`NxSummaryData`](#nxsummarydata): `Plan: Pro Seats: 12`. A row without a value is left out, as there |
752
+
753
+ | Slot | Effect |
754
+ | --- | --- |
755
+ | `icon` | A column on the left, `text-3xl`: a character or an `<img>` |
756
+ | `status` | After the title, on its line: an `NxBadge` or an `NxStatusIndicator` |
757
+ | `actions` | A column on the right, aligned to the middle |
758
+ | default | Under the title and its details |
759
+
760
+ The box is `rounded`, a `border` line, `bg-background`, `p-4`; its `class`
761
+ goes on it.
762
+
763
+ ## The metric cards
764
+
765
+ `NxStatCard`, `NxGoalCard`, `NxRatioCard`, `NxCompareCard` and
766
+ `NxBreakdownCard` are material-vue's metric cards: each is an
767
+ [`NxCard`](#nxcard-and-its-parts) with a `label` at its top, one under the
768
+ other, full width. Their `class` goes on the card's box.
769
+
770
+ They have no `loading` state: an e-mail does not load. The figures they
771
+ **write** — a `value`, a `delta` — may be placeholders; the numbers they
772
+ **draw** as an [`NxProgress`](#nxprogress) — `NxGoalCard`'s `value` and
773
+ `target`, `NxRatioCard`'s `percent`, each `percent` of `NxBreakdownCard` —
774
+ are known when the e-mail is built: a placeholder there
775
+ [fails the build](../troubleshooting.md#nxprogress-modelvalue-must-be-a-number-known-when-the-e-mail-is-built--a-placeholder-is-filled-only-when-it-is-sent).
776
+
777
+ ### A delta and its arrow
778
+
779
+ `NxStatCard` and `NxCompareCard` write a `delta` as material-vue's
780
+ `formatStatDelta` does, with an arrow before it that follows its tone, as
781
+ material-vue's `TrendingUp`, `TrendingDown` and `Minus` icons do — a
782
+ character here, since an e-mail has no icon font:
783
+
784
+ | `delta` | Written | Tone (without `deltaTone`) |
785
+ | --- | --- | --- |
786
+ | `12` | `▲ +12` | `up`: `text-success` |
787
+ | `-3` | `▼ -3` | `down`: `text-error` |
788
+ | `0` | `– 0` | `neutral`: muted |
789
+ | `'flat'`, any string | `– flat`, as given | `neutral`: muted |
790
+
791
+ The arrow is `aria-hidden`, and left out of the plain-text version: there,
792
+ `▲ +12` reads `+12`.
793
+
794
+ ## NxStatCard
795
+
796
+ material-vue's `StatCard`: a label, a figure, and under it a delta and a hint.
797
+
798
+ ```vue
799
+ <template>
800
+ <NxStatCard label="Revenue" value="$12,400" :delta="12" hint="vs last month" />
801
+ <NxStatCard label="Refunds" value="3" :delta="-2" delta-tone="up" />
802
+ <NxStatCard label="Churn" value="0.4%" delta="flat" />
803
+ </template>
804
+ ```
805
+
806
+ | Prop | Type | Default | Effect |
807
+ | --- | --- | --- | --- |
808
+ | `label` | `string` | required | The first line, `text-sm` muted |
809
+ | `value` | `string \| number` | none | The figure, `text-2xl` semibold. A placeholder is fine |
810
+ | `delta` | `number \| string` | none | Under the figure, toned, with its arrow — see [A delta and its arrow](#a-delta-and-its-arrow). `''` writes none |
811
+ | `deltaTone` | `'up' \| 'down' \| 'neutral'` | by the sign; `neutral` for a string | Overrides the tone, and so the arrow and colour |
812
+ | `hint` | `string` | none | After the delta, `text-xs` muted |
813
+
814
+ | Slot | Effect |
815
+ | --- | --- |
816
+ | `icon` | On the right of the label, muted: a character or an `<img>` |
817
+
818
+ `deltaTone` is for a figure whose fall is good: above, `Refunds` is `▲ -2` in
819
+ `text-success`. A delta known only when the e-mail is sent is a string —
820
+ `:delta="placeholder('delta')"` — and neutral, unless `delta-tone` says
821
+ otherwise: no component branches on a placeholder.
822
+
823
+ ## NxGoalCard
824
+
825
+ material-vue's `GoalCard`: a figure against its target, `of {target}`, and a
826
+ thin bar of the share reached.
827
+
828
+ ```vue
829
+ <template>
830
+ <NxGoalCard label="Signed contracts" :value="18" :target="24" />
831
+ <NxGoalCard label="Storage used" :value="42" :target="100" unit=" GB" />
832
+ </template>
833
+ ```
834
+
835
+ | Prop | Type | Default | Effect |
836
+ | --- | --- | --- | --- |
837
+ | `label` | `string` | required | The first line, `text-sm` muted |
838
+ | `value` | `number` | required | The figure, `text-2xl` semibold, and the bar's `modelValue` |
839
+ | `target` | `number` | required | Written after the figure, and the bar's `max` |
840
+ | `unit` | `string` | none | Written right after `value`, with no space: `unit="%"` gives `18%`. Not after the target |
841
+
842
+ `of 24` is the shared message `common.metrics.ofTarget` (`of {target}`,
843
+ `sur {target}`) when `@nxgt/mail-i18n` is listed, and English otherwise; see
844
+ [Shared messages](messages.md). The bar is an `NxProgress` of `height` 6: a
845
+ `value` past its `target` fills it, and both are numbers known at build time.
846
+
847
+ ## NxRatioCard
848
+
849
+ material-vue's `RatioCard`: two figures side by side, and a bar of the left
850
+ one's share, on a track at 15% of the primary colour.
851
+
852
+ ```vue
853
+ <template>
854
+ <NxRatioCard label="Plans" :left="{ label: 'Pro', value: '72%' }" :right="{ label: 'Free', value: '28%' }" :percent="72" />
855
+ </template>
856
+ ```
857
+
858
+ | Prop | Type | Default | Effect |
859
+ | --- | --- | --- | --- |
860
+ | `label` | `string` | required | The first line, `text-sm` muted |
861
+ | `left` | `{ label: string; value: string }` | required | On the left: its label small and uppercase, its value `text-lg` semibold |
862
+ | `right` | `{ label: string; value: string }` | required | On the right, the same, its value muted |
863
+ | `percent` | `number` | required | The bar's fill, out of 100: the left one's share |
864
+
865
+ The values are written as given, and `percent` is not computed from them:
866
+ pass both. `percent` is a number known at build time; the values may be
867
+ placeholders.
868
+
869
+ ## NxCompareCard
870
+
871
+ material-vue's `CompareCard`: this period's figure beside the last one's, in
872
+ two boxes, and the delta between them.
873
+
874
+ ```vue
875
+ <template>
876
+ <NxCompareCard label="Sign-ups" :current="{ value: '340' }" :previous="{ value: '298' }" :delta="42" />
877
+ <NxCompareCard label="Orders" :current="{ value: '51', label: 'October' }" :previous="{ value: '63', label: 'September' }" :delta="-12" />
878
+ </template>
879
+ ```
880
+
881
+ | Prop | Type | Default | Effect |
882
+ | --- | --- | --- | --- |
883
+ | `label` | `string` | required | The first line, `text-sm` muted |
884
+ | `current` | `{ value: string; label?: string }` | required | The left box, `text-2xl` semibold. `label` defaults to the shared message `common.metrics.thisPeriod` (`This period`) |
885
+ | `previous` | `{ value: string; label?: string }` | required | The right box, muted. `label` defaults to `common.metrics.lastPeriod` (`Last period`) |
886
+ | `delta` | `number` | none | On the right of the label, toned by its sign, with its arrow — see [A delta and its arrow](#a-delta-and-its-arrow) |
887
+
888
+ Without `@nxgt/mail-i18n`, the two default labels are in English. The delta's
889
+ tone is its sign's: there is no `deltaTone` here.
890
+
891
+ ## NxBreakdownCard
892
+
893
+ material-vue's `BreakdownCard`: the parts of a whole, each a label, its value,
894
+ and a thin bar of its share.
895
+
896
+ ```vue
897
+ <template>
898
+ <NxBreakdownCard
899
+ label="Traffic"
900
+ :items="[
901
+ { label: 'Search', percent: 54.4 },
902
+ { label: 'Direct', value: '1,204', percent: 30 },
903
+ { label: 'Social', percent: 15.6 },
904
+ ]"
905
+ />
906
+ </template>
907
+ ```
908
+
909
+ | Prop | Type | Default | Effect |
910
+ | --- | --- | --- | --- |
911
+ | `label` | `string` | required | The first line, `text-sm` muted |
912
+ | `items` | `{ label: string; value?: string; percent: number }[]` | required | The parts, in order; each `label` unique |
913
+
914
+ Each part writes its `value`, or its share rounded to a whole percent when it
915
+ has none: above, `54%`, `1,204` and `16%`. Its bar is an `NxProgress` of
916
+ `height` 6 filled to `percent`, a number known at build time.
917
+
918
+ ## NxSeeAlso
919
+
920
+ material-vue's `SeeAlso`: a line, a small uppercase label, and links one per
921
+ row, each with the `↗` material-vue gives an external link — every link of an
922
+ e-mail opens a browser.
923
+
924
+ ```vue
925
+ <template>
926
+ <NxSeeAlso
927
+ :items="[
928
+ { title: 'Billing', href: 'https://acme.example/billing' },
929
+ { id: 'team', title: 'Your team', href: 'https://acme.example/team' },
930
+ ]"
931
+ />
932
+ </template>
933
+ ```
934
+
935
+ | Prop | Type | Default | Effect |
936
+ | --- | --- | --- | --- |
937
+ | `items` | `{ id?: string; title: string; href: string }[]` | required | The links, in order; each `id`, or else each `href`, unique |
938
+ | `label` | `string` | the shared message `common.seeAlso` (`See also`); without `@nxgt/mail-i18n`, `See also` in every language | The label over the links |
939
+
940
+ With no `items`, it writes nothing: no line, no label. The `↗` is
941
+ `aria-hidden` and left out of the plain-text version. Its `class` is merged
942
+ on the block, `mt-10 pt-8 mb-4` with a line above.
943
+
944
+ ## A complete template
945
+
946
+ With `@nxgt/mail-i18n` and [`uiCatalogues`](messages.md), in two locales:
947
+
948
+ ```json
949
+ // locales/en.json — locales/fr.json has the same keys
950
+ {
951
+ "paymentFailed": {
952
+ "subject": "Your payment did not go through",
953
+ "title": "Your payment did not go through",
954
+ "body": "We could not charge your card for the {plan} plan.",
955
+ "action": "Update my card"
956
+ }
957
+ }
958
+ ```
959
+
960
+ ```vue
961
+ <!-- emails/payment-failed.vue -->
962
+ <template>
963
+ <NxLayout :preheader="t('paymentFailed.body', { plan: 'Pro' })">
964
+ <NxTypography variant="headline-small">{{ t('paymentFailed.title') }}</NxTypography>
965
+ <NxTypography>{{ t('common.greeting', { name: placeholder('name') }) }}</NxTypography>
966
+ <NxAlert variant="error" :description="t('paymentFailed.body', { plan: 'Pro' })" />
967
+ <NxSummaryData :data="[{ label: 'Invoice', value: placeholder('invoiceNumber') }, { label: 'Amount', value: placeholder('amount') }]" />
968
+ <NxButton :href="placeholder('link')" align="center">{{ t('paymentFailed.action') }}</NxButton>
969
+ <Spacer height="16px" />
970
+ <NxTypography variant="caption">{{ t('common.footer.ignore') }}</NxTypography>
971
+ </NxLayout>
972
+ </template>
973
+ ```
974
+
975
+ `maizzle build` writes `dist/en/payment-failed.html` and
976
+ `dist/fr/payment-failed.html`, each with `{{ name }}`, `{{ invoiceNumber }}`,
977
+ `{{ amount }}` and `{{ link }}` left for the code that sends.
978
+
979
+ ## See also
980
+
981
+ - [The plugin](plugin.md) — replacing one of these components with your own.
982
+ - [The theme](theme.md) — the colours and radii these classes use.
983
+ - [Shared messages](messages.md) — `common.greeting`, the footer's text, and
984
+ the words the components write themselves.