@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Steve Tsala
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# @nxgt/mail-ui
|
|
2
|
+
|
|
3
|
+
E-mail components for a normal [Maizzle](https://maizzle.com) 6 project, in
|
|
4
|
+
the style of `@nxgt/material-vue`: `<NxLayout>`, `<NxButton>`, `<NxCard>`,
|
|
5
|
+
`<NxAlert>`, `<NxTable>`, `<NxTimeline>`, `<NxStatCard>`… with its variants,
|
|
6
|
+
colours and tokens, rendered with tables and
|
|
7
|
+
inlined styles. One plugin gives every template the components, your brand
|
|
8
|
+
and the theme; a second export gives `@nxgt/mail-i18n` the messages they share.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
// maizzle.config.ts
|
|
12
|
+
import { defineMailConfig } from '@nxgt/mail-config';
|
|
13
|
+
import { i18n } from '@nxgt/mail-i18n';
|
|
14
|
+
import { ui, uiCatalogues } from '@nxgt/mail-ui';
|
|
15
|
+
|
|
16
|
+
export default defineMailConfig({
|
|
17
|
+
plugins: [
|
|
18
|
+
ui({ brand: { name: 'Acme', url: 'https://acme.example' } }),
|
|
19
|
+
i18n({ locales: ['en', 'fr'], catalogues: [uiCatalogues] }),
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Your project stays a Maizzle project: `emails/`, `components/`, `public/`,
|
|
25
|
+
`maizzle serve`, `maizzle build`. Maizzle's own components (`<Button>`,
|
|
26
|
+
`<Spacer>`) stay available; ours carry the `Nx` prefix and never shadow them.
|
|
27
|
+
|
|
28
|
+
<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">
|
|
29
|
+
|
|
30
|
+
The components from `NxLayout` to `NxCode` in one e-mail, with the brand
|
|
31
|
+
`Acme` and the default theme —
|
|
32
|
+
[in French](https://raw.githubusercontent.com/softistx/nxgt-mail/refs/tags/@nxgt/mail-ui@0.1.0/packages/mail-ui/previews/components-fr.png).
|
|
33
|
+
|
|
34
|
+
> **0.x.** A minor version may still change the surface; the changelog says how.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
bun add @nxgt/mail-ui @nxgt/mail-config @maizzle/framework @maizzle/tailwindcss vue
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Peers, all required:
|
|
43
|
+
|
|
44
|
+
- `@maizzle/framework` (`^6.1.7`) — Maizzle itself.
|
|
45
|
+
- `@nxgt/mail-config` — `ui()` is a plugin for its `defineMailConfig`. It
|
|
46
|
+
needs `@maizzle/tailwindcss` (`^1.5.6`) **as a direct dependency of your
|
|
47
|
+
project**; see [Setup](#setup).
|
|
48
|
+
- `vue` (`^3.5`) — the components are Vue single-file components, and the
|
|
49
|
+
package types `brand` on Vue's template properties.
|
|
50
|
+
- `typescript` (6). Bundler resolution (`"moduleResolution": "bundler"`) is
|
|
51
|
+
what is supported and tested; `nodenext` is out of contract.
|
|
52
|
+
|
|
53
|
+
`@nxgt/mail-i18n` is not a peer: add it (`bun add @nxgt/mail-i18n`) to
|
|
54
|
+
translate your e-mails and to use `uiCatalogues`.
|
|
55
|
+
|
|
56
|
+
One dependency comes with the package: `unplugin-vue-components`, which
|
|
57
|
+
Maizzle already uses. Maizzle resolves no tag in a `.vue` file under
|
|
58
|
+
`node_modules`, so `ui()` resolves them itself for the files a package ships —
|
|
59
|
+
ours, and a package's templates such as `@nxgt/mail-presets`'. See
|
|
60
|
+
[Components from a package](docs/guide/plugin.md#components-from-a-package).
|
|
61
|
+
|
|
62
|
+
## Setup
|
|
63
|
+
|
|
64
|
+
```css
|
|
65
|
+
/* what <NxLayout> writes in its <style> — you do not write it */
|
|
66
|
+
@import "@maizzle/tailwindcss";
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
That import is resolved from your project. Under an isolated install (Bun
|
|
70
|
+
workspaces, pnpm) it fails **silently** unless `@maizzle/tailwindcss` is in
|
|
71
|
+
your own `package.json`, as the `bun add` above makes it: the build succeeds,
|
|
72
|
+
and no style is generated.
|
|
73
|
+
|
|
74
|
+
```jsonc
|
|
75
|
+
// tsconfig.json — the official starter's include; keep .maizzle/*.d.ts in it
|
|
76
|
+
{ "include": ["**/*.vue", ".maizzle/*.d.ts"] }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```jsonc
|
|
80
|
+
// package.json — the starter's postinstall
|
|
81
|
+
{ "scripts": { "postinstall": "maizzle prepare" } }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Each time the config loads (`maizzle prepare`, `serve`, `build`), `ui()`
|
|
85
|
+
writes `.maizzle/nxgt-mail-ui.d.ts`, which loads the type of `brand` for the
|
|
86
|
+
templates. The starter's `tsconfig.json` does not include
|
|
87
|
+
`maizzle.config.ts`, so this file is how the editor learns it. `.maizzle/` is
|
|
88
|
+
in the starter's `.gitignore`. See
|
|
89
|
+
[Editor and type checking](#editor-and-type-checking).
|
|
90
|
+
|
|
91
|
+
## Exports
|
|
92
|
+
|
|
93
|
+
| Export | What it is |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `ui(options)` | The plugin, for `defineMailConfig({ plugins })`: the `Nx*` components, `brand` in every template, the theme |
|
|
96
|
+
| `uiCatalogues` | The shared messages in `en` and `fr`, for `i18n({ catalogues: [uiCatalogues] })` |
|
|
97
|
+
| `UI_CONTEXT` | `'nxgt:mail-ui'`, the Vue `provide` key a component reads the brand and the theme's CSS from |
|
|
98
|
+
| `COMPONENTS_DIR` | The absolute path of the package's `components/` folder |
|
|
99
|
+
| `THEME_FILE` | The absolute path of the package's `theme.css` |
|
|
100
|
+
| `Brand`, `UiOptions`, `UiContext` | The brand, the plugin's options, and what `UI_CONTEXT` provides |
|
|
101
|
+
|
|
102
|
+
| Subpath | What it is |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `@nxgt/mail-ui` | The exports above |
|
|
105
|
+
| `@nxgt/mail-ui/theme.css` | The theme: `@theme` tokens for Tailwind 4 |
|
|
106
|
+
|
|
107
|
+
## Usage
|
|
108
|
+
|
|
109
|
+
### A template
|
|
110
|
+
|
|
111
|
+
```vue
|
|
112
|
+
<!-- emails/verify-email.vue -->
|
|
113
|
+
<template>
|
|
114
|
+
<NxLayout :preheader="t('verifyEmail.title')">
|
|
115
|
+
<NxTypography variant="headline-small">{{ t('verifyEmail.title') }}</NxTypography>
|
|
116
|
+
<NxTypography>{{ t('common.greeting', { name: placeholder('name') }) }}</NxTypography>
|
|
117
|
+
<NxButton :href="placeholder('link')">{{ t('verifyEmail.action') }}</NxButton>
|
|
118
|
+
<Spacer height="24px" />
|
|
119
|
+
<NxAlert variant="warning" title="The link expires in 15 minutes." />
|
|
120
|
+
<NxTypography variant="caption">{{ t('common.footer.ignore') }}</NxTypography>
|
|
121
|
+
</NxLayout>
|
|
122
|
+
</template>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`<NxLayout>` is the page: the brand's logo or name at the top, the content on
|
|
126
|
+
a card, and a footer that says why the e-mail came (`common.footer.why`) and
|
|
127
|
+
links the brand. Every other component goes inside it. `t` and `placeholder`
|
|
128
|
+
come from `@nxgt/mail-i18n`; without it, write the text directly. See
|
|
129
|
+
[Components](docs/guide/components.md) for every component, its props and
|
|
130
|
+
its slots.
|
|
131
|
+
|
|
132
|
+
### The plugin — `ui({ brand, theme })`
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { ui } from '@nxgt/mail-ui';
|
|
136
|
+
|
|
137
|
+
ui({
|
|
138
|
+
brand: {
|
|
139
|
+
name: 'Acme',
|
|
140
|
+
url: 'https://acme.example',
|
|
141
|
+
logo: { src: 'https://acme.example/logo.png', width: 96, alt: 'Acme' },
|
|
142
|
+
},
|
|
143
|
+
theme: { 'color-primary': '#0f766e', 'radius-xl': '8px' },
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
| Option | Type | Default | Effect |
|
|
148
|
+
| --- | --- | --- | --- |
|
|
149
|
+
| `brand.name` | `string` | — (required) | Shown in the header without a logo, and in the footer always |
|
|
150
|
+
| `brand.url` | `string` | none | An absolute `http(s)` URL the header and the footer link to |
|
|
151
|
+
| `brand.logo` | `{ src, width?, alt? }` | none | The header's image: `src` an absolute `http(s)` URL, `width` in pixels (120), `alt` (the brand's name) |
|
|
152
|
+
| `theme` | `Record<string, string>` | `{}` | Tokens of `theme.css` to override, named without their `--` |
|
|
153
|
+
|
|
154
|
+
A wrong option is a bare `TypeError` when the config loads:
|
|
155
|
+
`ui: brand.logo.src must be an absolute http(s) URL — a mail client loads
|
|
156
|
+
nothing relative`. Every template also gets `brand`: `{{ brand.name }}`. See
|
|
157
|
+
[The plugin](docs/guide/plugin.md).
|
|
158
|
+
|
|
159
|
+
### The theme
|
|
160
|
+
|
|
161
|
+
`theme.css` holds material-vue's light tokens. Maizzle turns each colour into
|
|
162
|
+
a hex value in the built HTML, and material-vue's translucent `bg-primary/15`
|
|
163
|
+
is `bg-primary-15` here, a plain colour, because a mail client drops an
|
|
164
|
+
alpha. Override a colour, and its tints follow:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
ui({ brand: { name: 'Acme' }, theme: { 'color-primary': '#0f766e' } });
|
|
168
|
+
// <NxButton> is #0f766e; <NxButton variant="tonal"> is #dbeae9, 15% of it over white
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
See [The theme](docs/guide/theme.md) for every token.
|
|
172
|
+
|
|
173
|
+
### The shared messages — `uiCatalogues`
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
i18n({ locales: ['en', 'fr'], catalogues: [uiCatalogues] });
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`common.greeting` (`Hello {name},`), `common.footer.why`,
|
|
180
|
+
`common.footer.ignore`, `common.avatarGroup.more`, `common.timeline.empty`,
|
|
181
|
+
`common.metrics.ofTarget`, `common.metrics.thisPeriod`,
|
|
182
|
+
`common.metrics.lastPeriod` and `common.seeAlso`, in `en` and `fr`. Your `locales/<locale>.json`
|
|
183
|
+
overrides any of them, key by key:
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
// locales/en.json
|
|
187
|
+
{ "common": { "greeting": "Hi {name}," } }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
See [Shared messages](docs/guide/messages.md).
|
|
191
|
+
|
|
192
|
+
### Editor and type checking
|
|
193
|
+
|
|
194
|
+
With the [Setup](#setup) above, Vue's language tools (the **Vue - Official**
|
|
195
|
+
extension in the editor, `vue-tsc` in CI) know `brand` in every template, and
|
|
196
|
+
Maizzle's `.maizzle/prefixed-components.d.ts` gives them the `Nx*` components
|
|
197
|
+
and their props:
|
|
198
|
+
|
|
199
|
+
```vue
|
|
200
|
+
<!-- emails/welcome.vue -->
|
|
201
|
+
<template>
|
|
202
|
+
<NxTypography>Welcome to {{ brand.nam }}</NxTypography>
|
|
203
|
+
<!-- Property 'nam' does not exist on type 'Brand'. Did you mean 'name'? -->
|
|
204
|
+
</template>
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
```sh
|
|
208
|
+
bun add -d vue-tsc
|
|
209
|
+
bunx vue-tsc --noEmit # after maizzle prepare, as in CI
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`t`, `locale` and `placeholder` are typed by `@nxgt/mail-i18n`, which writes
|
|
213
|
+
its own file beside this one. See
|
|
214
|
+
[`brand` in templates](docs/guide/plugin.md#brand-in-templates), and
|
|
215
|
+
[`Property 'brand' does not exist`](docs/troubleshooting.md#the-editor-says-property-brand-does-not-exist-in-a-template)
|
|
216
|
+
if the editor does not know `brand`.
|
|
217
|
+
|
|
218
|
+
### Replacing a component
|
|
219
|
+
|
|
220
|
+
```vue
|
|
221
|
+
<!-- components/nx-badge.vue — replaces the package's <NxBadge> in every template -->
|
|
222
|
+
<template>
|
|
223
|
+
<span class="rounded-sm bg-primary px-2 text-xs text-primary-foreground"><slot /></span>
|
|
224
|
+
</template>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`components/NxBadge.vue` works as well: Maizzle names a component the same
|
|
228
|
+
from either case.
|
|
229
|
+
|
|
230
|
+
A file in your `components/` named as one of our tags wins over ours — in
|
|
231
|
+
your templates, in ours, and in a package's. A copy of ours must be renamed:
|
|
232
|
+
ours are named without the prefix `ui()` adds, so a copied `badge.vue` is
|
|
233
|
+
`<Badge>` and replaces nothing until it is `nx-badge.vue`. See
|
|
234
|
+
[The plugin](docs/guide/plugin.md#replacing-a-component) and
|
|
235
|
+
[A project's own component does not replace the package's](docs/troubleshooting.md#a-projects-own-component-does-not-replace-the-packages).
|
|
236
|
+
|
|
237
|
+
## Traps
|
|
238
|
+
|
|
239
|
+
**List `ui()` in `plugins`.** A component used without it fails the build:
|
|
240
|
+
`NxLayout: ui() is not in the plugins of defineMailConfig`.
|
|
241
|
+
|
|
242
|
+
**With `@nxgt/mail-i18n`, give it the shared messages.** `<NxLayout>` calls
|
|
243
|
+
`t('common.footer.why')` once the i18n plugin is listed, and the build fails
|
|
244
|
+
with `calls t('common.footer.why'), which is not a key of the catalogues`
|
|
245
|
+
unless `catalogues: [uiCatalogues]` is passed, or your catalogues hold the
|
|
246
|
+
key.
|
|
247
|
+
|
|
248
|
+
**Another locale writes the `common` keys itself.** `uiCatalogues` has `en`
|
|
249
|
+
and `fr` only: a project in `de` adds its nine keys — `common.greeting`,
|
|
250
|
+
`common.footer.why`, `common.footer.ignore`, `common.avatarGroup.more`,
|
|
251
|
+
`common.timeline.empty`, `common.metrics.ofTarget`,
|
|
252
|
+
`common.metrics.thisPeriod`, `common.metrics.lastPeriod` and
|
|
253
|
+
`common.seeAlso` — to `locales/de.json`.
|
|
254
|
+
|
|
255
|
+
**Icons are slots.** An e-mail has no icon font: pass an `<img>` with an
|
|
256
|
+
absolute URL, or a character, to `#icon`.
|
|
257
|
+
|
|
258
|
+
**Light only.** The layout declares `<meta name="color-scheme" content="light">`;
|
|
259
|
+
there is no dark theme.
|
|
260
|
+
|
|
261
|
+
**Use Maizzle's `<Spacer>` for vertical space**: `<Spacer height="24px" />`.
|
|
262
|
+
|
|
263
|
+
**Never branch on a placeholder** in a component or a template:
|
|
264
|
+
`v-if="link.startsWith('https:')"` is decided on the string `{{ link }}`.
|
|
265
|
+
|
|
266
|
+
## Type safety, counted
|
|
267
|
+
|
|
268
|
+
**8 plausible mistakes, 8 refused** at compile time, each measured by a
|
|
269
|
+
`@ts-expect-error` in
|
|
270
|
+
[`test/types/refusals.ts`](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-ui/test/types/refusals.ts)
|
|
271
|
+
that fails the typecheck the moment it stops holding:
|
|
272
|
+
|
|
273
|
+
1. `ui()` without a `brand`.
|
|
274
|
+
2. A `brand` given as its name alone, rather than `{ name }`.
|
|
275
|
+
3. A `logo` given as its URL alone, rather than `{ src }`.
|
|
276
|
+
4. A logo `width` that is not a number of pixels (`'96px'`).
|
|
277
|
+
5. A `theme` value that is not a CSS string (`{ 'radius-lg': 4 }`).
|
|
278
|
+
6. A template that assigns `brand`.
|
|
279
|
+
7. A `logo` without its `src`.
|
|
280
|
+
8. A `brand.url` that is not a string.
|
|
281
|
+
|
|
282
|
+
The same file holds the calls that must keep compiling: a refusal that refuses
|
|
283
|
+
the correct call is a bug.
|
|
284
|
+
|
|
285
|
+
**2 template mistakes, 2 refused** by `.maizzle/nxgt-mail-ui.d.ts`, each
|
|
286
|
+
measured by a `@vue-expect-error` in
|
|
287
|
+
[`test/fixture/types/refusals.vue`](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-ui/test/fixture/types/refusals.vue),
|
|
288
|
+
checked by `vue-tsc` after `maizzle prepare` (`bun run typecheck:templates`):
|
|
289
|
+
|
|
290
|
+
1. A field the brand does not have (`brand.nmae`).
|
|
291
|
+
2. `brand.logo.src` without `?.`: the logo is optional.
|
|
292
|
+
|
|
293
|
+
A theme token is a `string`: whether `theme.css` declares it is checked when
|
|
294
|
+
`ui()` is called. A component's props are not in this count: Maizzle declares
|
|
295
|
+
the components in `.maizzle/prefixed-components.d.ts`, and Vue's language
|
|
296
|
+
tools check them in the editor.
|
|
297
|
+
|
|
298
|
+
## Documentation
|
|
299
|
+
|
|
300
|
+
- [The guides](docs/README.md) — one page per area, with every option,
|
|
301
|
+
component and error.
|
|
302
|
+
- [Troubleshooting](docs/troubleshooting.md) — an error message, its cause and
|
|
303
|
+
its fix.
|
|
304
|
+
- [Roadmap](docs/roadmap.md) — what is next, and what is deliberately not
|
|
305
|
+
planned.
|
|
306
|
+
- [Vocabulary](https://github.com/softistx/nxgt-mail/blob/develop/docs/vocabulary.md)
|
|
307
|
+
— the words these pages use, defined once.
|
|
308
|
+
|
|
309
|
+
## Licence
|
|
310
|
+
|
|
311
|
+
MIT
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs, useSlots } from 'vue';
|
|
4
|
+
|
|
5
|
+
/** material-vue's Alert: a tinted block with a thick bar on its left. */
|
|
6
|
+
type Variant =
|
|
7
|
+
| 'primary'
|
|
8
|
+
| 'secondary'
|
|
9
|
+
| 'error'
|
|
10
|
+
| 'success'
|
|
11
|
+
| 'info'
|
|
12
|
+
| 'warning'
|
|
13
|
+
| 'foreground';
|
|
14
|
+
|
|
15
|
+
defineOptions({ inheritAttrs: false });
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(
|
|
18
|
+
defineProps<{
|
|
19
|
+
variant?: Variant;
|
|
20
|
+
title?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
}>(),
|
|
23
|
+
{ variant: 'primary' },
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const VARIANT: Record<Variant, string> = {
|
|
27
|
+
primary: 'border-primary bg-primary-5',
|
|
28
|
+
secondary: 'border-secondary bg-secondary-5',
|
|
29
|
+
error: 'border-error bg-error-5',
|
|
30
|
+
success: 'border-success bg-success-5',
|
|
31
|
+
info: 'border-info bg-info-5',
|
|
32
|
+
warning: 'border-warning bg-warning-5',
|
|
33
|
+
foreground: 'border-foreground bg-muted',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** material-vue colours the icon with the variant: `[&_svg]:text-<variant>`. */
|
|
37
|
+
const ICON: Record<Variant, string> = {
|
|
38
|
+
primary: 'text-primary',
|
|
39
|
+
secondary: 'text-secondary',
|
|
40
|
+
error: 'text-error',
|
|
41
|
+
success: 'text-success',
|
|
42
|
+
info: 'text-info',
|
|
43
|
+
warning: 'text-warning',
|
|
44
|
+
foreground: 'text-foreground',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const attrs = useAttrs();
|
|
48
|
+
const slots = useSlots();
|
|
49
|
+
const classes = computed(() =>
|
|
50
|
+
twMerge(
|
|
51
|
+
'border-0 border-l-8 border-solid p-4',
|
|
52
|
+
VARIANT[props.variant],
|
|
53
|
+
attrs.class as string,
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<template>
|
|
59
|
+
<table class="mb-4 w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
60
|
+
<tr>
|
|
61
|
+
<td v-bind="{ ...attrs, class: undefined }" :class="classes">
|
|
62
|
+
<table class="w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
63
|
+
<tr>
|
|
64
|
+
<td v-if="slots.icon" :class="`w-6 pr-2 align-top ${ICON[props.variant]}`"><slot name="icon" /></td>
|
|
65
|
+
<td class="align-top">
|
|
66
|
+
<p v-if="title || slots.title" class="m-0 mb-1 text-base font-bold text-foreground"><slot name="title">{{ title }}</slot></p>
|
|
67
|
+
<p v-if="description || slots.description" class="m-0 text-sm text-muted-foreground"><slot name="description">{{ description }}</slot></p>
|
|
68
|
+
<slot />
|
|
69
|
+
</td>
|
|
70
|
+
</tr>
|
|
71
|
+
</table>
|
|
72
|
+
</td>
|
|
73
|
+
</tr>
|
|
74
|
+
</table>
|
|
75
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, inject, useAttrs } from 'vue';
|
|
4
|
+
import { AVATAR_SIZE } from './ui';
|
|
5
|
+
|
|
6
|
+
/** material-vue's AvatarFallback: initials, centred on a muted ground. */
|
|
7
|
+
defineOptions({ inheritAttrs: false });
|
|
8
|
+
|
|
9
|
+
const attrs = useAttrs();
|
|
10
|
+
const px = inject(AVATAR_SIZE, 32);
|
|
11
|
+
const classes = computed(() =>
|
|
12
|
+
twMerge(
|
|
13
|
+
'block rounded-full bg-muted text-center text-[12px] font-medium text-foreground',
|
|
14
|
+
attrs.class as string,
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<span v-bind="{ ...attrs, class: undefined }" :class="classes" :style="`width: ${px}px; height: ${px}px; line-height: ${px}px;`"><slot /></span>
|
|
21
|
+
</template>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, getCurrentInstance, provide, useAttrs, useSlots } from 'vue';
|
|
4
|
+
import { AVATAR_SIZE, sharedMessage, slotComponents } from './ui';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* material-vue's AvatarGroup: its `NxAvatar`s in a row, each ringed with the
|
|
8
|
+
* background, and past `max`, one more reading `+N`. They sit side by side
|
|
9
|
+
* rather than overlapping: Gmail drops the negative margin that stacks them.
|
|
10
|
+
*/
|
|
11
|
+
type Size = 'sm' | 'md' | 'lg';
|
|
12
|
+
|
|
13
|
+
defineOptions({ inheritAttrs: false });
|
|
14
|
+
|
|
15
|
+
const props = withDefaults(defineProps<{ max?: number; size?: Size }>(), {
|
|
16
|
+
size: 'md',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const SIZE: Record<Size, number> = { sm: 24, md: 32, lg: 40 };
|
|
20
|
+
|
|
21
|
+
const attrs = useAttrs();
|
|
22
|
+
const slots = useSlots();
|
|
23
|
+
provide(AVATAR_SIZE, SIZE[props.size]);
|
|
24
|
+
const avatars = computed(() => slotComponents(slots.default?.()));
|
|
25
|
+
// As material-vue's: no `max`, or one under 1, shows them all.
|
|
26
|
+
const shown = computed(() =>
|
|
27
|
+
props.max === undefined || props.max < 1
|
|
28
|
+
? avatars.value
|
|
29
|
+
: avatars.value.slice(0, props.max),
|
|
30
|
+
);
|
|
31
|
+
const rest = computed(() => avatars.value.length - shown.value.length);
|
|
32
|
+
const globals: Record<string, unknown> =
|
|
33
|
+
getCurrentInstance()?.appContext.config.globalProperties ?? {};
|
|
34
|
+
const moreLabel = computed(() =>
|
|
35
|
+
sharedMessage(globals, 'common.avatarGroup.more', `${rest.value} more`, {
|
|
36
|
+
count: rest.value,
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
const classes = computed(() => twMerge('mb-4', attrs.class as string));
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<div v-bind="{ ...attrs, class: undefined }" :class="classes"><span v-for="(avatar, index) in shown" :key="index" class="mr-1 inline-block rounded-full border-2 border-solid border-background align-middle"><component :is="avatar" /></span><span v-if="rest > 0" class="inline-block rounded-full border-2 border-solid border-background align-middle" :title="moreLabel" role="img" :aria-label="moreLabel"><NxAvatar><NxAvatarFallback>+{{ rest }}</NxAvatarFallback></NxAvatar></span></div>
|
|
44
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, inject, useAttrs } from 'vue';
|
|
4
|
+
import { AVATAR_SIZE } from './ui';
|
|
5
|
+
|
|
6
|
+
/** material-vue's AvatarImage: the picture, by absolute URL, cut round. */
|
|
7
|
+
defineOptions({ inheritAttrs: false });
|
|
8
|
+
|
|
9
|
+
defineProps<{ src: string; alt?: string }>();
|
|
10
|
+
|
|
11
|
+
const attrs = useAttrs();
|
|
12
|
+
const px = inject(AVATAR_SIZE, 32);
|
|
13
|
+
const classes = computed(() =>
|
|
14
|
+
twMerge('block rounded-full', attrs.class as string),
|
|
15
|
+
);
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<img v-bind="{ ...attrs, class: undefined }" :src="src" :alt="alt ?? ''" :width="px" :height="px" :class="classes">
|
|
20
|
+
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, inject, provide, useAttrs } from 'vue';
|
|
4
|
+
import { AVATAR_SIZE } from './ui';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* material-vue's Avatar: a round box of `size` pixels (32 by default, or its
|
|
8
|
+
* `NxAvatarGroup`'s) holding an `NxAvatarImage` or an `NxAvatarFallback`.
|
|
9
|
+
* An e-mail cannot fall back when an image fails: give one or the other.
|
|
10
|
+
*/
|
|
11
|
+
defineOptions({ inheritAttrs: false });
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{ size?: number }>();
|
|
14
|
+
|
|
15
|
+
const attrs = useAttrs();
|
|
16
|
+
const inherited = inject(AVATAR_SIZE, 32);
|
|
17
|
+
const px = computed(() => props.size ?? inherited);
|
|
18
|
+
provide(AVATAR_SIZE, px.value);
|
|
19
|
+
const classes = computed(() =>
|
|
20
|
+
twMerge(
|
|
21
|
+
'inline-block overflow-hidden rounded-full align-middle',
|
|
22
|
+
attrs.class as string,
|
|
23
|
+
),
|
|
24
|
+
);
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<span v-bind="{ ...attrs, class: undefined }" :class="classes" :style="`width: ${px}px; height: ${px}px;`"><slot /></span>
|
|
29
|
+
</template>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs } from 'vue';
|
|
4
|
+
|
|
5
|
+
/** material-vue's Badge: a small pill of text. */
|
|
6
|
+
type Variant =
|
|
7
|
+
| 'default'
|
|
8
|
+
| 'secondary'
|
|
9
|
+
| 'destructive'
|
|
10
|
+
| 'error'
|
|
11
|
+
| 'success'
|
|
12
|
+
| 'info'
|
|
13
|
+
| 'warning'
|
|
14
|
+
| 'outline'
|
|
15
|
+
| 'outlined';
|
|
16
|
+
|
|
17
|
+
defineOptions({ inheritAttrs: false });
|
|
18
|
+
|
|
19
|
+
const props = withDefaults(defineProps<{ variant?: Variant }>(), {
|
|
20
|
+
variant: 'default',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const VARIANT: Record<Variant, string> = {
|
|
24
|
+
default: 'border-primary bg-primary text-primary-foreground',
|
|
25
|
+
secondary: 'border-secondary bg-secondary text-secondary-foreground',
|
|
26
|
+
destructive: 'border-error bg-error text-white',
|
|
27
|
+
error: 'border-error bg-error text-error-foreground',
|
|
28
|
+
success: 'border-success bg-success text-success-foreground',
|
|
29
|
+
info: 'border-info bg-info text-info-foreground',
|
|
30
|
+
warning: 'border-warning bg-warning text-warning-foreground',
|
|
31
|
+
outline: 'border-border text-foreground',
|
|
32
|
+
outlined: 'border-border text-foreground',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const attrs = useAttrs();
|
|
36
|
+
const classes = computed(() =>
|
|
37
|
+
twMerge(
|
|
38
|
+
'inline-block whitespace-nowrap rounded-full border border-solid px-2 py-0.5 text-xs font-medium',
|
|
39
|
+
VARIANT[props.variant],
|
|
40
|
+
attrs.class as string,
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<span v-bind="{ ...attrs, class: undefined }" :class="classes"><slot /></span>
|
|
47
|
+
</template>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs, useSlots } from 'vue';
|
|
4
|
+
import type { Tone } from './ui';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* material-vue's Banner: a status in a tinted, bordered box, with an
|
|
8
|
+
* `action` on the right. Its icon is a slot: an e-mail has no icon font.
|
|
9
|
+
*/
|
|
10
|
+
defineOptions({ inheritAttrs: false });
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(
|
|
13
|
+
defineProps<{
|
|
14
|
+
tone?: Tone;
|
|
15
|
+
title?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
}>(),
|
|
18
|
+
{ tone: 'info' },
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const TONE: Record<Tone, string> = {
|
|
22
|
+
info: 'border-info-40 bg-info-10',
|
|
23
|
+
success: 'border-success-40 bg-success-10',
|
|
24
|
+
warning: 'border-warning-40 bg-warning-10',
|
|
25
|
+
error: 'border-error-40 bg-error-10',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const attrs = useAttrs();
|
|
29
|
+
const slots = useSlots();
|
|
30
|
+
const classes = computed(() =>
|
|
31
|
+
twMerge(
|
|
32
|
+
'rounded-md border border-solid px-4 py-3',
|
|
33
|
+
TONE[props.tone],
|
|
34
|
+
attrs.class as string,
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<table class="mb-4 w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
41
|
+
<tr>
|
|
42
|
+
<td v-bind="{ ...attrs, class: undefined }" :class="classes">
|
|
43
|
+
<table class="w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
44
|
+
<tr>
|
|
45
|
+
<td v-if="slots.icon" :class="`w-4 pr-3 align-top text-${tone}`"><slot name="icon" /></td>
|
|
46
|
+
<td class="align-top text-foreground">
|
|
47
|
+
<p v-if="title" class="m-0 text-sm font-semibold">{{ title }}</p>
|
|
48
|
+
<p v-if="description" class="m-0 text-sm text-muted-foreground">{{ description }}</p>
|
|
49
|
+
<slot />
|
|
50
|
+
</td>
|
|
51
|
+
<td v-if="slots.action" class="pl-3 text-right align-middle"><slot name="action" /></td>
|
|
52
|
+
</tr>
|
|
53
|
+
</table>
|
|
54
|
+
</td>
|
|
55
|
+
</tr>
|
|
56
|
+
</table>
|
|
57
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* material-vue's BreakdownCard: parts of a whole, each a label, its value
|
|
4
|
+
* (its share when none is given) and a thin `NxProgress` of its share.
|
|
5
|
+
*/
|
|
6
|
+
defineProps<{
|
|
7
|
+
label: string;
|
|
8
|
+
items: readonly { label: string; value?: string; percent: number }[];
|
|
9
|
+
}>();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<NxCard class="py-4">
|
|
14
|
+
<NxCardContent class="px-4">
|
|
15
|
+
<NxCardDescription class="mt-0">{{ label }}</NxCardDescription>
|
|
16
|
+
<template v-for="(item, position) in items" :key="item.label">
|
|
17
|
+
<table :class="['w-full', position === 0 ? 'mt-4' : 'mt-3']" role="presentation" cellpadding="0" cellspacing="0">
|
|
18
|
+
<tr>
|
|
19
|
+
<td class="align-bottom text-sm font-medium text-foreground">{{ item.label }}</td>
|
|
20
|
+
<td class="whitespace-nowrap pl-3 text-right align-bottom text-sm text-muted-foreground">{{ item.value ?? `${Math.round(item.percent)}%` }}</td>
|
|
21
|
+
</tr>
|
|
22
|
+
</table>
|
|
23
|
+
<NxProgress class="mb-0 mt-1.5" :model-value="item.percent" :height="6" />
|
|
24
|
+
</template>
|
|
25
|
+
</NxCardContent>
|
|
26
|
+
</NxCard>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs } from 'vue';
|
|
4
|
+
|
|
5
|
+
/** material-vue's CardContent: a row of the card, padded as the others. */
|
|
6
|
+
defineOptions({ inheritAttrs: false });
|
|
7
|
+
|
|
8
|
+
const attrs = useAttrs();
|
|
9
|
+
const classes = computed(() => twMerge('px-6', attrs.class as string));
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<tr>
|
|
14
|
+
<td v-bind="{ ...attrs, class: undefined }" :class="classes"><slot /></td>
|
|
15
|
+
</tr>
|
|
16
|
+
</template>
|