@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,16 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs } from 'vue';
|
|
4
|
+
|
|
5
|
+
/** material-vue's CardDescription, under the title. */
|
|
6
|
+
defineOptions({ inheritAttrs: false });
|
|
7
|
+
|
|
8
|
+
const attrs = useAttrs();
|
|
9
|
+
const classes = computed(() =>
|
|
10
|
+
twMerge('m-0 mt-1.5 text-sm text-muted-foreground', attrs.class as string),
|
|
11
|
+
);
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<p v-bind="{ ...attrs, class: undefined }" :class="classes"><slot /></p>
|
|
16
|
+
</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 CardFooter: the card's last row, as its actions. */
|
|
6
|
+
defineOptions({ inheritAttrs: false });
|
|
7
|
+
|
|
8
|
+
const attrs = useAttrs();
|
|
9
|
+
const classes = computed(() => twMerge('px-6 pt-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>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs, useSlots } from 'vue';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* material-vue's CardHeader: a title, a description under it, and an
|
|
7
|
+
* `action` on the right.
|
|
8
|
+
*/
|
|
9
|
+
defineOptions({ inheritAttrs: false });
|
|
10
|
+
|
|
11
|
+
defineProps<{ title?: string; description?: string }>();
|
|
12
|
+
|
|
13
|
+
const attrs = useAttrs();
|
|
14
|
+
const slots = useSlots();
|
|
15
|
+
const classes = computed(() => twMerge('px-6 pb-6', attrs.class as string));
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<tr>
|
|
20
|
+
<td v-bind="{ ...attrs, class: undefined }" :class="classes">
|
|
21
|
+
<table class="w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
22
|
+
<tr>
|
|
23
|
+
<td class="align-top">
|
|
24
|
+
<NxCardTitle v-if="title">{{ title }}</NxCardTitle>
|
|
25
|
+
<NxCardDescription v-if="description">{{ description }}</NxCardDescription>
|
|
26
|
+
<slot />
|
|
27
|
+
</td>
|
|
28
|
+
<td v-if="slots.action" class="pl-4 text-right align-top"><slot name="action" /></td>
|
|
29
|
+
</tr>
|
|
30
|
+
</table>
|
|
31
|
+
</td>
|
|
32
|
+
</tr>
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs } from 'vue';
|
|
4
|
+
|
|
5
|
+
/** material-vue's CardTitle. */
|
|
6
|
+
defineOptions({ inheritAttrs: false });
|
|
7
|
+
|
|
8
|
+
const attrs = useAttrs();
|
|
9
|
+
const classes = computed(() =>
|
|
10
|
+
twMerge(
|
|
11
|
+
'm-0 text-base font-semibold leading-none text-card-foreground',
|
|
12
|
+
attrs.class as string,
|
|
13
|
+
),
|
|
14
|
+
);
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<!-- biome-ignore lint/a11y/useHeadingContent: the heading's text is the slot, which Biome cannot see. -->
|
|
19
|
+
<h3 v-bind="{ ...attrs, class: undefined }" :class="classes"><slot /></h3>
|
|
20
|
+
</template>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs } from 'vue';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* material-vue's Card: a bordered, rounded box. Its parts —
|
|
7
|
+
* `NxCardHeader`, `NxCardContent`, `NxCardFooter` — are its rows.
|
|
8
|
+
*/
|
|
9
|
+
defineOptions({ inheritAttrs: false });
|
|
10
|
+
|
|
11
|
+
const attrs = useAttrs();
|
|
12
|
+
const classes = computed(() =>
|
|
13
|
+
twMerge(
|
|
14
|
+
'rounded-xl border border-solid border-border bg-card py-6 text-card-foreground shadow-sm',
|
|
15
|
+
attrs.class as string,
|
|
16
|
+
),
|
|
17
|
+
);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<table class="mb-4 w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
22
|
+
<tr>
|
|
23
|
+
<td v-bind="{ ...attrs, class: undefined }" :class="classes">
|
|
24
|
+
<table class="w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
25
|
+
<slot />
|
|
26
|
+
</table>
|
|
27
|
+
</td>
|
|
28
|
+
</tr>
|
|
29
|
+
</table>
|
|
30
|
+
</template>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, provide, useAttrs, useSlots } from 'vue';
|
|
4
|
+
import { AVATAR_SIZE, type Color, type ColourVariant, colourVariant } from './ui';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* material-vue's Chip, static: a rounded label with `avatar`, `leading` and
|
|
8
|
+
* `trailing` slots — `avatar` in place of `leading`, as in material-vue. An
|
|
9
|
+
* e-mail runs no script, so it has no dismiss button; `active` makes it
|
|
10
|
+
* `filled`, as in material-vue. Unlike material-vue's, the avatar is not
|
|
11
|
+
* pulled into the padding: Gmail drops a negative margin.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
defineOptions({ inheritAttrs: false });
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(
|
|
17
|
+
defineProps<{
|
|
18
|
+
label?: string;
|
|
19
|
+
variant?: ColourVariant;
|
|
20
|
+
color?: Color;
|
|
21
|
+
active?: boolean;
|
|
22
|
+
}>(),
|
|
23
|
+
{ variant: 'outlined', color: 'primary', active: false },
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// An avatar in a chip is 20px, as material-vue's `size-5`.
|
|
27
|
+
provide(AVATAR_SIZE, 20);
|
|
28
|
+
const attrs = useAttrs();
|
|
29
|
+
const slots = useSlots();
|
|
30
|
+
const classes = computed(() =>
|
|
31
|
+
twMerge(
|
|
32
|
+
'inline-block whitespace-nowrap rounded-full px-2.5 py-1 align-middle text-sm font-medium',
|
|
33
|
+
colourVariant(props.active ? 'filled' : props.variant, props.color),
|
|
34
|
+
attrs.class as string,
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<span v-bind="{ ...attrs, class: undefined }" :class="classes"><span v-if="slots.avatar" class="mr-1.5 inline-block align-middle"><slot name="avatar" /></span><span v-else-if="slots.leading" class="mr-1.5 inline-block align-middle"><slot name="leading" /></span><slot>{{ label }}</slot><span v-if="slots.trailing" class="ml-1.5 inline-block align-middle"><slot name="trailing" /></span></span>
|
|
41
|
+
</template>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs } from 'vue';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A code the reader types in, such as a one-time code: large, spaced and in
|
|
7
|
+
* monospace, on material-vue's muted background.
|
|
8
|
+
*/
|
|
9
|
+
defineOptions({ inheritAttrs: false });
|
|
10
|
+
|
|
11
|
+
const attrs = useAttrs();
|
|
12
|
+
const classes = computed(() =>
|
|
13
|
+
twMerge(
|
|
14
|
+
'rounded-lg bg-muted px-6 py-4 text-center font-mono text-3xl font-semibold tracking-[0.3em] text-foreground',
|
|
15
|
+
attrs.class as string,
|
|
16
|
+
),
|
|
17
|
+
);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<table class="mb-4 w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
22
|
+
<tr>
|
|
23
|
+
<td v-bind="{ ...attrs, class: undefined }" :class="classes"><slot /></td>
|
|
24
|
+
</tr>
|
|
25
|
+
</table>
|
|
26
|
+
</template>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, getCurrentInstance } from 'vue';
|
|
3
|
+
import { deltaOf, EYEBROW, sharedMessage } from './ui';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* material-vue's CompareCard: this period's figure beside the last one's, in
|
|
7
|
+
* two boxes, and the delta between them, toned.
|
|
8
|
+
*/
|
|
9
|
+
const props = defineProps<{
|
|
10
|
+
label: string;
|
|
11
|
+
current: { value: string; label?: string };
|
|
12
|
+
previous: { value: string; label?: string };
|
|
13
|
+
delta?: number;
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
const globals: Record<string, unknown> =
|
|
17
|
+
getCurrentInstance()?.appContext.config.globalProperties ?? {};
|
|
18
|
+
const currentLabel = computed(
|
|
19
|
+
() =>
|
|
20
|
+
props.current.label ??
|
|
21
|
+
sharedMessage(globals, 'common.metrics.thisPeriod', 'This period'),
|
|
22
|
+
);
|
|
23
|
+
const previousLabel = computed(
|
|
24
|
+
() =>
|
|
25
|
+
props.previous.label ??
|
|
26
|
+
sharedMessage(globals, 'common.metrics.lastPeriod', 'Last period'),
|
|
27
|
+
);
|
|
28
|
+
/** The two boxes: this period's, then the last one's, muted. */
|
|
29
|
+
const points = computed(() => [
|
|
30
|
+
{ label: currentLabel.value, value: props.current.value },
|
|
31
|
+
{ label: previousLabel.value, value: props.previous.value },
|
|
32
|
+
]);
|
|
33
|
+
const delta = computed(() =>
|
|
34
|
+
props.delta === undefined ? null : deltaOf(props.delta),
|
|
35
|
+
);
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<NxCard class="py-4">
|
|
40
|
+
<NxCardContent class="px-4">
|
|
41
|
+
<table class="w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
42
|
+
<tr>
|
|
43
|
+
<td class="align-middle"><NxCardDescription class="mt-0">{{ label }}</NxCardDescription></td>
|
|
44
|
+
<td v-if="delta" :class="`whitespace-nowrap pl-2 text-right align-middle text-xs font-medium ${delta.colour}`"><span aria-hidden="true"><span data-maizzle-html-only>{{ delta.glyph }}</span></span> {{ delta.label }}</td>
|
|
45
|
+
</tr>
|
|
46
|
+
</table>
|
|
47
|
+
<table class="mt-4 w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
48
|
+
<tr>
|
|
49
|
+
<td v-for="(point, side) in points" :key="side" :class="['w-1/2 align-top', side === 0 ? 'pr-1.5' : 'pl-1.5']">
|
|
50
|
+
<table class="w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
51
|
+
<tr>
|
|
52
|
+
<td class="rounded border border-solid border-border bg-background p-3">
|
|
53
|
+
<p :class="EYEBROW">{{ point.label }}</p>
|
|
54
|
+
<p :class="['m-0 mt-0.5 text-2xl font-semibold tracking-tight', side === 0 ? 'text-foreground' : 'text-muted-foreground']">{{ point.value }}</p>
|
|
55
|
+
</td>
|
|
56
|
+
</tr>
|
|
57
|
+
</table>
|
|
58
|
+
</td>
|
|
59
|
+
</tr>
|
|
60
|
+
</table>
|
|
61
|
+
</NxCardContent>
|
|
62
|
+
</NxCard>
|
|
63
|
+
</template>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs } from 'vue';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* material-vue's Description: a label over its value. Nothing is rendered
|
|
7
|
+
* without a value — `0` is one.
|
|
8
|
+
*/
|
|
9
|
+
defineOptions({ inheritAttrs: false });
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{ label: string; value?: string | number | null }>();
|
|
12
|
+
|
|
13
|
+
const attrs = useAttrs();
|
|
14
|
+
const shown = computed(() => props.value === 0 || Boolean(props.value));
|
|
15
|
+
const classes = computed(() => twMerge('mb-4', attrs.class as string));
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div v-if="shown" v-bind="{ ...attrs, class: undefined }" :class="classes">
|
|
20
|
+
<p class="m-0 text-base font-semibold text-foreground">{{ label }}</p>
|
|
21
|
+
<p class="m-0 text-sm text-muted-foreground">{{ value }}</p>
|
|
22
|
+
<slot />
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs, useSlots } from 'vue';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* material-vue's EntityHeader: what an e-mail is about — an icon, a title
|
|
7
|
+
* with its status, its details on one line, and actions on the right. Its
|
|
8
|
+
* shadow is a border here.
|
|
9
|
+
*/
|
|
10
|
+
defineOptions({ inheritAttrs: false });
|
|
11
|
+
|
|
12
|
+
withDefaults(
|
|
13
|
+
defineProps<{
|
|
14
|
+
title: string;
|
|
15
|
+
metadata?: { label: string; value?: string | number | null }[];
|
|
16
|
+
}>(),
|
|
17
|
+
{ metadata: () => [] },
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const attrs = useAttrs();
|
|
21
|
+
const slots = useSlots();
|
|
22
|
+
const classes = computed(() =>
|
|
23
|
+
twMerge(
|
|
24
|
+
'rounded border border-solid border-border bg-background p-4',
|
|
25
|
+
attrs.class as string,
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<table class="mb-4 w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
32
|
+
<tr>
|
|
33
|
+
<td v-bind="{ ...attrs, class: undefined }" :class="classes">
|
|
34
|
+
<table class="w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
35
|
+
<tr>
|
|
36
|
+
<td v-if="slots.icon" class="w-1 whitespace-nowrap pr-4 align-middle text-3xl leading-none"><slot name="icon" /></td>
|
|
37
|
+
<td class="align-middle">
|
|
38
|
+
<h2 class="m-0 text-lg font-semibold tracking-tight text-foreground">{{ title }} <span v-if="slots.status" class="pl-2 align-middle"><slot name="status" /></span></h2>
|
|
39
|
+
<NxSummaryData v-if="metadata.length > 0" inline class="mb-0 mt-0.5" :data="metadata" />
|
|
40
|
+
</td>
|
|
41
|
+
<td v-if="slots.actions" class="w-1 whitespace-nowrap pl-4 text-right align-middle"><slot name="actions" /></td>
|
|
42
|
+
</tr>
|
|
43
|
+
</table>
|
|
44
|
+
<div v-if="slots.default" class="mt-2"><slot /></div>
|
|
45
|
+
</td>
|
|
46
|
+
</tr>
|
|
47
|
+
</table>
|
|
48
|
+
</template>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, getCurrentInstance } from 'vue';
|
|
3
|
+
import { sharedMessage } from './ui';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* material-vue's GoalCard: a figure against its target, `of {target}`, and a
|
|
7
|
+
* thin `NxProgress` of the share reached.
|
|
8
|
+
*/
|
|
9
|
+
const props = defineProps<{
|
|
10
|
+
label: string;
|
|
11
|
+
value: number;
|
|
12
|
+
target: number;
|
|
13
|
+
unit?: string;
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
const globals: Record<string, unknown> =
|
|
17
|
+
getCurrentInstance()?.appContext.config.globalProperties ?? {};
|
|
18
|
+
const ofTarget = computed(() =>
|
|
19
|
+
sharedMessage(globals, 'common.metrics.ofTarget', `of ${props.target}`, {
|
|
20
|
+
target: String(props.target),
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<NxCard class="py-4">
|
|
27
|
+
<NxCardContent class="px-4">
|
|
28
|
+
<NxCardDescription class="mt-0">{{ label }}</NxCardDescription>
|
|
29
|
+
<p class="m-0 mt-3"><span class="text-2xl font-semibold tracking-tight text-foreground">{{ value }}{{ unit }}</span> <span class="text-sm text-muted-foreground">{{ ofTarget }}</span></p>
|
|
30
|
+
<NxProgress class="mb-0 mt-3" :model-value="value" :max="target" :height="6" />
|
|
31
|
+
</NxCardContent>
|
|
32
|
+
</NxCard>
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs, useSlots } from 'vue';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* material-vue's Hero: an eyebrow, a large title, a description and actions,
|
|
7
|
+
* in a rounded, bordered box. Its gradient and blurred shapes are a plain
|
|
8
|
+
* `primary-5` ground here: a mail client draws neither reliably.
|
|
9
|
+
*/
|
|
10
|
+
defineOptions({ inheritAttrs: false });
|
|
11
|
+
|
|
12
|
+
defineProps<{ eyebrow?: string; title: string; description?: string }>();
|
|
13
|
+
|
|
14
|
+
const attrs = useAttrs();
|
|
15
|
+
const slots = useSlots();
|
|
16
|
+
const classes = computed(() =>
|
|
17
|
+
twMerge(
|
|
18
|
+
'rounded-2xl border border-solid border-border bg-primary-5 px-8 py-10',
|
|
19
|
+
attrs.class as string,
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<table class="mb-4 w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
26
|
+
<tr>
|
|
27
|
+
<td v-bind="{ ...attrs, class: undefined }" :class="classes">
|
|
28
|
+
<p v-if="eyebrow" class="m-0 text-xs font-semibold uppercase tracking-wider text-primary">{{ eyebrow }}</p>
|
|
29
|
+
<h1 :class="['m-0 text-3xl font-semibold tracking-tight text-foreground', eyebrow && 'mt-3']">{{ title }}</h1>
|
|
30
|
+
<p v-if="description" class="m-0 mt-4 text-base text-muted-foreground">{{ description }}</p>
|
|
31
|
+
<div v-if="slots.actions" class="mt-6"><slot name="actions" /></div>
|
|
32
|
+
<div v-if="slots.default" class="mt-8"><slot /></div>
|
|
33
|
+
</td>
|
|
34
|
+
</tr>
|
|
35
|
+
</table>
|
|
36
|
+
</template>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { getCurrentInstance } from 'vue';
|
|
3
|
+
import { useUi } from './ui';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The page of an e-mail: the brand's header, the content on a card, and a
|
|
7
|
+
* footer. The theme's tokens are in its `<style>`, so every component inside
|
|
8
|
+
* uses them.
|
|
9
|
+
*/
|
|
10
|
+
const props = withDefaults(
|
|
11
|
+
defineProps<{
|
|
12
|
+
/** Default the template's locale from `@nxgt/mail-i18n`, else `en`. */
|
|
13
|
+
lang?: string;
|
|
14
|
+
/** The text a client shows beside the subject, before the e-mail is opened. */
|
|
15
|
+
preheader?: string;
|
|
16
|
+
/** The width of the card, in pixels. */
|
|
17
|
+
width?: number;
|
|
18
|
+
}>(),
|
|
19
|
+
{ width: 600 },
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const { brand, css } = useUi('NxLayout');
|
|
23
|
+
// Read loosely: `t` and `locale` exist only when @nxgt/mail-i18n is listed.
|
|
24
|
+
const globals: Record<string, unknown> =
|
|
25
|
+
getCurrentInstance()?.appContext.config.globalProperties ?? {};
|
|
26
|
+
const lang =
|
|
27
|
+
props.lang ?? (typeof globals.locale === 'string' ? globals.locale : 'en');
|
|
28
|
+
const style = `@import "@maizzle/tailwindcss";\n${css}`;
|
|
29
|
+
// With @nxgt/mail-i18n, the footer says why the e-mail came.
|
|
30
|
+
const why =
|
|
31
|
+
typeof globals.t === 'function'
|
|
32
|
+
? (globals.t('common.footer.why', { brand: brand.name }) as string)
|
|
33
|
+
: null;
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<Html :lang="lang">
|
|
38
|
+
<Head>
|
|
39
|
+
<meta name="color-scheme" content="light">
|
|
40
|
+
<meta name="supported-color-schemes" content="light">
|
|
41
|
+
<style v-html="style"></style>
|
|
42
|
+
</Head>
|
|
43
|
+
<Body class="bg-paper">
|
|
44
|
+
<Preheader v-if="preheader">{{ preheader }}</Preheader>
|
|
45
|
+
<table class="w-full bg-paper font-sans" role="presentation" cellpadding="0" cellspacing="0">
|
|
46
|
+
<tr>
|
|
47
|
+
<td align="center" class="px-4 py-8">
|
|
48
|
+
<Container :width="width">
|
|
49
|
+
<table class="w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
50
|
+
<tr>
|
|
51
|
+
<td class="pb-6 text-center">
|
|
52
|
+
<a v-if="brand.url" :href="brand.url" class="text-lg font-semibold text-foreground no-underline">
|
|
53
|
+
<img v-if="brand.logo" :src="brand.logo.src" :width="brand.logo.width ?? 120" :alt="brand.logo.alt ?? brand.name" class="max-w-full align-middle">
|
|
54
|
+
<template v-else>{{ brand.name }}</template>
|
|
55
|
+
</a>
|
|
56
|
+
<template v-else>
|
|
57
|
+
<img v-if="brand.logo" :src="brand.logo.src" :width="brand.logo.width ?? 120" :alt="brand.logo.alt ?? brand.name" class="max-w-full align-middle">
|
|
58
|
+
<span v-else class="text-lg font-semibold text-foreground">{{ brand.name }}</span>
|
|
59
|
+
</template>
|
|
60
|
+
</td>
|
|
61
|
+
</tr>
|
|
62
|
+
<tr>
|
|
63
|
+
<td class="rounded-xl border border-solid border-border bg-card p-8 text-sm text-card-foreground shadow-sm">
|
|
64
|
+
<slot />
|
|
65
|
+
</td>
|
|
66
|
+
</tr>
|
|
67
|
+
<tr>
|
|
68
|
+
<td class="px-6 pt-6 text-center text-xs text-muted-foreground">
|
|
69
|
+
<slot name="footer">
|
|
70
|
+
<p v-if="why" class="m-0 mb-2">{{ why }}</p>
|
|
71
|
+
</slot>
|
|
72
|
+
<p class="m-0">
|
|
73
|
+
<a v-if="brand.url" :href="brand.url" class="text-muted-foreground underline">{{ brand.name }}</a>
|
|
74
|
+
<template v-else>{{ brand.name }}</template>
|
|
75
|
+
</p>
|
|
76
|
+
</td>
|
|
77
|
+
</tr>
|
|
78
|
+
</table>
|
|
79
|
+
</Container>
|
|
80
|
+
</td>
|
|
81
|
+
</tr>
|
|
82
|
+
</table>
|
|
83
|
+
</Body>
|
|
84
|
+
</Html>
|
|
85
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs } from 'vue';
|
|
4
|
+
|
|
5
|
+
/** A link in text, in material-vue's link colour. Underlined: a phone has no hover. */
|
|
6
|
+
defineOptions({ inheritAttrs: false });
|
|
7
|
+
|
|
8
|
+
defineProps<{ href: string }>();
|
|
9
|
+
|
|
10
|
+
const attrs = useAttrs();
|
|
11
|
+
const classes = computed(() =>
|
|
12
|
+
twMerge('text-info underline', attrs.class as string),
|
|
13
|
+
);
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<!-- biome-ignore lint/a11y/useAnchorContent: the link's text is the slot, which Biome cannot see. -->
|
|
18
|
+
<a v-bind="{ ...attrs, class: undefined }" :href="href" :class="classes"><slot /></a>
|
|
19
|
+
</template>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs, useSlots } from 'vue';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* material-vue's ListTile: a title, a subtitle under it, and `leading` and
|
|
7
|
+
* `trailing` slots on its sides. With `href`, the title is a link. A ring is
|
|
8
|
+
* a border here: a mail client draws no box-shadow.
|
|
9
|
+
*/
|
|
10
|
+
type Size = 'sm' | 'md';
|
|
11
|
+
|
|
12
|
+
defineOptions({ inheritAttrs: false });
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(
|
|
15
|
+
defineProps<{
|
|
16
|
+
title: string;
|
|
17
|
+
subtitle?: string;
|
|
18
|
+
href?: string;
|
|
19
|
+
selected?: boolean;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
size?: Size;
|
|
22
|
+
}>(),
|
|
23
|
+
{ selected: false, disabled: false, size: 'md' },
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const SIZE: Record<Size, { idle: string; selected: string; title: string }> = {
|
|
27
|
+
md: {
|
|
28
|
+
idle: 'rounded bg-primary-5 px-4 py-2',
|
|
29
|
+
selected: 'rounded border border-solid border-primary-40 bg-primary-15 px-4 py-2',
|
|
30
|
+
title: 'text-sm font-semibold',
|
|
31
|
+
},
|
|
32
|
+
sm: {
|
|
33
|
+
idle: 'rounded px-2 py-1.5',
|
|
34
|
+
selected: 'rounded border border-solid border-primary-40 bg-primary-10 px-2 py-1.5',
|
|
35
|
+
title: 'text-[13px] font-medium',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const attrs = useAttrs();
|
|
40
|
+
const slots = useSlots();
|
|
41
|
+
const size = computed(() => SIZE[props.size]);
|
|
42
|
+
const classes = computed(() =>
|
|
43
|
+
twMerge(
|
|
44
|
+
props.selected ? size.value.selected : size.value.idle,
|
|
45
|
+
attrs.class as string,
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
const titleClass = computed(() =>
|
|
49
|
+
twMerge(
|
|
50
|
+
'no-underline',
|
|
51
|
+
size.value.title,
|
|
52
|
+
props.disabled ? 'text-muted-foreground' : 'text-foreground',
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
const gap = computed(() =>
|
|
56
|
+
props.size === 'md'
|
|
57
|
+
? { leading: 'pr-4', trailing: 'pl-4' }
|
|
58
|
+
: { leading: 'pr-2', trailing: 'pl-2' },
|
|
59
|
+
);
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<template>
|
|
63
|
+
<table class="mb-2 w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
64
|
+
<tr>
|
|
65
|
+
<td v-bind="{ ...attrs, class: undefined }" :class="classes">
|
|
66
|
+
<table class="w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
67
|
+
<tr>
|
|
68
|
+
<td v-if="slots.leading" :class="`w-1 whitespace-nowrap align-middle ${gap.leading}`"><slot name="leading" /></td>
|
|
69
|
+
<td class="align-middle">
|
|
70
|
+
<a v-if="href && !disabled" :href="href" :class="titleClass">{{ title }}</a>
|
|
71
|
+
<span v-else :class="titleClass">{{ title }}</span>
|
|
72
|
+
<p v-if="subtitle" class="m-0 text-sm text-muted-foreground">{{ subtitle }}</p>
|
|
73
|
+
</td>
|
|
74
|
+
<td v-if="slots.trailing" :class="`w-1 whitespace-nowrap text-right align-middle ${gap.trailing}`"><slot name="trailing" /></td>
|
|
75
|
+
</tr>
|
|
76
|
+
</table>
|
|
77
|
+
</td>
|
|
78
|
+
</tr>
|
|
79
|
+
</table>
|
|
80
|
+
</template>
|