@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,91 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs } from 'vue';
|
|
4
|
+
import { type Color, type ColourVariant, colourVariant } from './ui';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* material-vue's Button as a link: its variants, colours and sizes, on
|
|
8
|
+
* Maizzle's `<Button>`, which pads it for Outlook. Named `nx-button.vue`,
|
|
9
|
+
* not `button.vue`: there, Vue would read `<Button>` as this file itself.
|
|
10
|
+
*/
|
|
11
|
+
type Size = 'xs' | 'sm' | 'default' | 'lg';
|
|
12
|
+
|
|
13
|
+
defineOptions({ inheritAttrs: false });
|
|
14
|
+
|
|
15
|
+
const props = withDefaults(
|
|
16
|
+
defineProps<{
|
|
17
|
+
href: string;
|
|
18
|
+
variant?: ColourVariant;
|
|
19
|
+
color?: Color;
|
|
20
|
+
size?: Size;
|
|
21
|
+
align?: 'left' | 'center' | 'right';
|
|
22
|
+
}>(),
|
|
23
|
+
{ variant: 'filled', color: 'primary', size: 'default' },
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
/** The chip's colours, plus what a link must reset. */
|
|
27
|
+
const EXTRA: Record<ColourVariant, string> = {
|
|
28
|
+
filled: '',
|
|
29
|
+
tonal: '',
|
|
30
|
+
outlined: 'bg-transparent',
|
|
31
|
+
ghost: 'bg-transparent',
|
|
32
|
+
link: 'no-underline',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Padding, and the Outlook values Maizzle's Button derives the same padding from. */
|
|
36
|
+
const SIZE: Record<
|
|
37
|
+
Size,
|
|
38
|
+
{ class: string; msoPt: string; msoPb: string; msoPx: number }
|
|
39
|
+
> = {
|
|
40
|
+
xs: {
|
|
41
|
+
class: 'px-2 py-1 text-xs leading-4',
|
|
42
|
+
msoPt: '4px',
|
|
43
|
+
msoPb: '7px',
|
|
44
|
+
msoPx: 67,
|
|
45
|
+
},
|
|
46
|
+
sm: {
|
|
47
|
+
class: 'px-3 py-2 text-sm leading-4',
|
|
48
|
+
msoPt: '8px',
|
|
49
|
+
msoPb: '15px',
|
|
50
|
+
msoPx: 86,
|
|
51
|
+
},
|
|
52
|
+
default: {
|
|
53
|
+
class: 'px-4 py-2.5 text-sm leading-4',
|
|
54
|
+
msoPt: '10px',
|
|
55
|
+
msoPb: '19px',
|
|
56
|
+
msoPx: 114,
|
|
57
|
+
},
|
|
58
|
+
lg: {
|
|
59
|
+
class: 'px-6 py-3 text-sm leading-4',
|
|
60
|
+
msoPt: '12px',
|
|
61
|
+
msoPb: '23px',
|
|
62
|
+
msoPx: 171,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const attrs = useAttrs();
|
|
67
|
+
const size = computed(() => SIZE[props.size]);
|
|
68
|
+
const classes = computed(() =>
|
|
69
|
+
twMerge(
|
|
70
|
+
props.variant === 'link'
|
|
71
|
+
? 'font-medium font-sans'
|
|
72
|
+
: `rounded-full font-medium font-sans ${size.value.class}`,
|
|
73
|
+
colourVariant(props.variant, props.color),
|
|
74
|
+
EXTRA[props.variant],
|
|
75
|
+
attrs.class as string,
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<template>
|
|
81
|
+
<Button
|
|
82
|
+
v-bind="{ ...attrs, class: undefined }"
|
|
83
|
+
:href="href"
|
|
84
|
+
:variant="variant === 'link' ? 'link' : 'solid'"
|
|
85
|
+
:align="align ?? null"
|
|
86
|
+
:mso-pt="size.msoPt"
|
|
87
|
+
:mso-pb="size.msoPb"
|
|
88
|
+
:mso-px="size.msoPx"
|
|
89
|
+
:class="classes"
|
|
90
|
+
><slot /></Button>
|
|
91
|
+
</template>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs } from 'vue';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* material-vue's Progress, still: a bar of `modelValue` out of `max`, on a
|
|
7
|
+
* track at 20% of the primary colour. The fill is a table cell of that width,
|
|
8
|
+
* which every client draws; material-vue's transform and pulse are not.
|
|
9
|
+
*
|
|
10
|
+
* The share is computed when the e-mail is built: a value that is not a
|
|
11
|
+
* number — a placeholder, filled only at send time — **throws**, rather than
|
|
12
|
+
* drawing an empty bar.
|
|
13
|
+
*/
|
|
14
|
+
defineOptions({ inheritAttrs: false });
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(
|
|
17
|
+
defineProps<{ modelValue?: number; max?: number; height?: number }>(),
|
|
18
|
+
{ modelValue: 0, max: 100, height: 8 },
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
for (const [name, value] of [
|
|
22
|
+
['modelValue', props.modelValue],
|
|
23
|
+
['max', props.max],
|
|
24
|
+
['height', props.height],
|
|
25
|
+
] as const) {
|
|
26
|
+
if (
|
|
27
|
+
typeof value !== 'number' ||
|
|
28
|
+
!Number.isFinite(value) ||
|
|
29
|
+
(name === 'height' && value <= 0)
|
|
30
|
+
) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`NxProgress: ${name} must be a number known when the e-mail is built — a placeholder is filled only when it is sent`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const attrs = useAttrs();
|
|
38
|
+
const percent = computed(() =>
|
|
39
|
+
props.max > 0
|
|
40
|
+
? Math.min(
|
|
41
|
+
100,
|
|
42
|
+
Math.max(0, Math.round((props.modelValue / props.max) * 100)),
|
|
43
|
+
)
|
|
44
|
+
: 0,
|
|
45
|
+
);
|
|
46
|
+
/** Outlook's shape for a thin cell, as `NxSeparator`'s: height, line and font alike. */
|
|
47
|
+
const cell = computed(
|
|
48
|
+
() =>
|
|
49
|
+
`height: ${props.height}px; line-height: ${props.height}px; font-size: ${props.height}px; mso-line-height-rule: exactly;`,
|
|
50
|
+
);
|
|
51
|
+
const classes = computed(() =>
|
|
52
|
+
twMerge('mb-4 w-full rounded-full bg-primary-20', attrs.class as string),
|
|
53
|
+
);
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<!-- Left out of the plain-text version, where a bar says nothing: Maizzle keeps what is inside in the HTML. -->
|
|
58
|
+
<div data-maizzle-html-only>
|
|
59
|
+
<table v-bind="{ ...attrs, class: undefined }" :class="classes" role="progressbar" :aria-valuenow="modelValue" aria-valuemin="0" :aria-valuemax="max" cellpadding="0" cellspacing="0">
|
|
60
|
+
<tr>
|
|
61
|
+
<td v-if="percent > 0" class="rounded-full bg-primary" :height="height" :style="`width: ${percent}%; ${cell}`">‍</td>
|
|
62
|
+
<td v-if="percent < 100" :height="height" :style="cell">‍</td>
|
|
63
|
+
</tr>
|
|
64
|
+
</table>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { EYEBROW } from './ui';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* material-vue's RatioCard: two figures side by side, and a bar of the left
|
|
6
|
+
* one's share, on a track at 15% of the primary. The fill is NxProgress's,
|
|
7
|
+
* rounded at both ends where material-vue's ends square.
|
|
8
|
+
*/
|
|
9
|
+
defineProps<{
|
|
10
|
+
label: string;
|
|
11
|
+
left: { label: string; value: string };
|
|
12
|
+
right: { label: string; value: string };
|
|
13
|
+
percent: number;
|
|
14
|
+
}>();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<NxCard class="py-4">
|
|
19
|
+
<NxCardContent class="px-4">
|
|
20
|
+
<NxCardDescription class="mt-0">{{ label }}</NxCardDescription>
|
|
21
|
+
<table class="mt-3 w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
22
|
+
<tr>
|
|
23
|
+
<td class="align-top">
|
|
24
|
+
<p :class="EYEBROW">{{ left.label }}</p>
|
|
25
|
+
<p class="m-0 mt-0.5 text-lg font-semibold tracking-tight text-foreground">{{ left.value }}</p>
|
|
26
|
+
</td>
|
|
27
|
+
<td class="pl-3 text-right align-top">
|
|
28
|
+
<p :class="EYEBROW">{{ right.label }}</p>
|
|
29
|
+
<p class="m-0 mt-0.5 text-lg font-semibold tracking-tight text-muted-foreground">{{ right.value }}</p>
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
</table>
|
|
33
|
+
<NxProgress class="mb-0 mt-3 rounded bg-primary-15" :model-value="percent" />
|
|
34
|
+
</NxCardContent>
|
|
35
|
+
</NxCard>
|
|
36
|
+
</template>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, getCurrentInstance, useAttrs } from 'vue';
|
|
4
|
+
import { EYEBROW, type SeeAlsoItem, sharedMessage } from './ui';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* material-vue's SeeAlso: a line, a small uppercase label, and links one per
|
|
8
|
+
* row. Every link of an e-mail opens a browser, so each carries the arrow
|
|
9
|
+
* material-vue gives an external one, as a character. The line is a cell's
|
|
10
|
+
* top border, and the space above it padding, which Outlook keeps and a
|
|
11
|
+
* margin not.
|
|
12
|
+
*/
|
|
13
|
+
defineOptions({ inheritAttrs: false });
|
|
14
|
+
|
|
15
|
+
const props = defineProps<{ items: readonly SeeAlsoItem[]; label?: string }>();
|
|
16
|
+
|
|
17
|
+
const globals: Record<string, unknown> =
|
|
18
|
+
getCurrentInstance()?.appContext.config.globalProperties ?? {};
|
|
19
|
+
const heading = computed(
|
|
20
|
+
() => props.label ?? sharedMessage(globals, 'common.seeAlso', 'See also'),
|
|
21
|
+
);
|
|
22
|
+
const attrs = useAttrs();
|
|
23
|
+
const classes = computed(() =>
|
|
24
|
+
twMerge(
|
|
25
|
+
'mb-4 w-full',
|
|
26
|
+
attrs.class as string,
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<table v-if="items.length > 0" v-bind="{ ...attrs, class: undefined }" :class="classes" role="presentation" cellpadding="0" cellspacing="0">
|
|
33
|
+
<tr>
|
|
34
|
+
<td class="pt-10">
|
|
35
|
+
<table class="w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
36
|
+
<tr>
|
|
37
|
+
<td class="border-t [border-top-style:solid] border-border pt-8">
|
|
38
|
+
<p :class="`${EYEBROW} mb-3`">{{ heading }}</p>
|
|
39
|
+
<table class="w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
40
|
+
<tr v-for="item in items" :key="item.id ?? item.href">
|
|
41
|
+
<td class="px-2 py-2.5">
|
|
42
|
+
<a :href="item.href" class="text-sm text-muted-foreground no-underline">{{ item.title }}</a>
|
|
43
|
+
</td>
|
|
44
|
+
<td class="w-1 px-2 text-right align-middle text-xs text-muted-foreground"><span aria-hidden="true"><span data-maizzle-html-only>↗</span></span></td>
|
|
45
|
+
</tr>
|
|
46
|
+
</table>
|
|
47
|
+
</td>
|
|
48
|
+
</tr>
|
|
49
|
+
</table>
|
|
50
|
+
</td>
|
|
51
|
+
</tr>
|
|
52
|
+
</table>
|
|
53
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs } from 'vue';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* material-vue's Separator, horizontal: a 1px line in the border colour.
|
|
7
|
+
* Its space above and below is padding, which Outlook keeps and a margin not.
|
|
8
|
+
*/
|
|
9
|
+
defineOptions({ inheritAttrs: false });
|
|
10
|
+
|
|
11
|
+
const attrs = useAttrs();
|
|
12
|
+
const classes = computed(() => twMerge('py-6', attrs.class as string));
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<table class="w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
17
|
+
<tr>
|
|
18
|
+
<td v-bind="{ ...attrs, class: undefined }" :class="classes">
|
|
19
|
+
<table class="w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
20
|
+
<tr>
|
|
21
|
+
<td class="bg-border" style="height: 1px; line-height: 1px; font-size: 1px;">‍</td>
|
|
22
|
+
</tr>
|
|
23
|
+
</table>
|
|
24
|
+
</td>
|
|
25
|
+
</tr>
|
|
26
|
+
</table>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, useSlots } from 'vue';
|
|
3
|
+
import { type DeltaTone, deltaOf } from './ui';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* material-vue's StatCard: a label, a figure, and under it a delta and a
|
|
7
|
+
* hint, in an `NxCard`. The delta's arrow is a character (an e-mail has no
|
|
8
|
+
* icon font). There is no loading state: an e-mail does not load. A reader
|
|
9
|
+
* hears the delta as written, without material-vue's spoken "Up" or "Down".
|
|
10
|
+
*/
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
label: string;
|
|
13
|
+
value?: string | number;
|
|
14
|
+
hint?: string;
|
|
15
|
+
delta?: number | string;
|
|
16
|
+
deltaTone?: DeltaTone;
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
const slots = useSlots();
|
|
20
|
+
const delta = computed(() =>
|
|
21
|
+
props.delta === undefined || props.delta === ''
|
|
22
|
+
? null
|
|
23
|
+
: deltaOf(props.delta, props.deltaTone),
|
|
24
|
+
);
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<NxCard class="py-4">
|
|
29
|
+
<NxCardContent class="px-4">
|
|
30
|
+
<table class="w-full" role="presentation" cellpadding="0" cellspacing="0">
|
|
31
|
+
<tr>
|
|
32
|
+
<td class="align-top text-sm text-muted-foreground">{{ label }}</td>
|
|
33
|
+
<td v-if="slots.icon" class="w-1 whitespace-nowrap pl-3 text-right align-top text-muted-foreground"><slot name="icon" /></td>
|
|
34
|
+
</tr>
|
|
35
|
+
</table>
|
|
36
|
+
<p class="m-0 mt-2 text-2xl font-semibold text-foreground">{{ value }}</p>
|
|
37
|
+
<p v-if="hint || delta" class="m-0 mt-2 text-xs">
|
|
38
|
+
<span v-if="delta" :class="`pr-2 font-medium ${delta.colour}`"><span aria-hidden="true"><span data-maizzle-html-only>{{ delta.glyph }}</span></span> {{ delta.label }}</span>
|
|
39
|
+
<span v-if="hint" class="text-muted-foreground">{{ hint }}</span>
|
|
40
|
+
</p>
|
|
41
|
+
</NxCardContent>
|
|
42
|
+
</NxCard>
|
|
43
|
+
</template>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs } from 'vue';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* material-vue's StatusIndicator: a coloured dot before a label. The dot is
|
|
7
|
+
* a character, which every client sizes, where an empty span is dropped.
|
|
8
|
+
*/
|
|
9
|
+
type Tone = 'neutral' | 'primary' | 'success' | 'info' | 'warning' | 'error';
|
|
10
|
+
|
|
11
|
+
defineOptions({ inheritAttrs: false });
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(defineProps<{ tone?: Tone }>(), { tone: 'neutral' });
|
|
14
|
+
|
|
15
|
+
const DOT: Record<Tone, string> = {
|
|
16
|
+
neutral: 'text-muted-foreground',
|
|
17
|
+
primary: 'text-primary',
|
|
18
|
+
success: 'text-success',
|
|
19
|
+
info: 'text-info',
|
|
20
|
+
warning: 'text-warning',
|
|
21
|
+
error: 'text-error',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const attrs = useAttrs();
|
|
25
|
+
const classes = computed(() =>
|
|
26
|
+
twMerge(
|
|
27
|
+
'whitespace-nowrap text-sm font-medium text-foreground',
|
|
28
|
+
attrs.class as string,
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<span v-bind="{ ...attrs, class: undefined }" :class="classes"><span aria-hidden="true" :class="`${DOT[props.tone]} pr-1.5 text-[10px]`">●</span><slot /></span>
|
|
35
|
+
</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 StepsItem: a numbered circle, a title and the text under it,
|
|
7
|
+
* with a line from the circle down to the next step. Placed in `NxSteps`,
|
|
8
|
+
* which numbers it and says whether it is the `last`.
|
|
9
|
+
*
|
|
10
|
+
* Two rows: the circle and the title, then the text beside a cell whose right
|
|
11
|
+
* border is the line — in the same row, so the line runs as far down as the
|
|
12
|
+
* text goes, in every client.
|
|
13
|
+
*/
|
|
14
|
+
defineOptions({ inheritAttrs: false });
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(
|
|
17
|
+
defineProps<{ title?: string; index?: number; last?: boolean }>(),
|
|
18
|
+
{ last: true },
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const attrs = useAttrs();
|
|
22
|
+
const slots = useSlots();
|
|
23
|
+
const classes = computed(() =>
|
|
24
|
+
twMerge(
|
|
25
|
+
'pl-3 align-top',
|
|
26
|
+
props.last ? 'pb-0' : 'pb-8',
|
|
27
|
+
attrs.class as string,
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<tr>
|
|
34
|
+
<td colspan="2" class="w-8 align-top">
|
|
35
|
+
<span class="block h-8 w-8 rounded-full border border-solid border-primary-25 bg-background text-center text-xs font-semibold leading-[30px] text-primary"><slot name="index">{{ index }}</slot></span>
|
|
36
|
+
</td>
|
|
37
|
+
<td class="pl-3 align-top">
|
|
38
|
+
<p v-if="title" class="m-0 pt-1 text-base font-semibold tracking-tight text-foreground">{{ title }}</p>
|
|
39
|
+
</td>
|
|
40
|
+
</tr>
|
|
41
|
+
<tr>
|
|
42
|
+
<td :class="['w-4 text-[1px] leading-px', !last && 'border-r [border-right-style:solid] border-border']"><span data-maizzle-html-only>‍</span></td>
|
|
43
|
+
<td class="w-4 text-[1px] leading-px"><span data-maizzle-html-only>‍</span></td>
|
|
44
|
+
<td v-bind="{ ...attrs, class: undefined }" :class="classes">
|
|
45
|
+
<div v-if="slots.default" class="mt-2 text-sm leading-6 text-muted-foreground"><slot /></div>
|
|
46
|
+
</td>
|
|
47
|
+
</tr>
|
|
48
|
+
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { cloneVNode, computed, useAttrs, useSlots } from 'vue';
|
|
4
|
+
import { slotComponents } from './ui';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* material-vue's Steps: its `NxStepsItem`s one under the other, numbered in
|
|
8
|
+
* order unless an item says its `index`, and joined by a line.
|
|
9
|
+
*/
|
|
10
|
+
defineOptions({ inheritAttrs: false });
|
|
11
|
+
|
|
12
|
+
const attrs = useAttrs();
|
|
13
|
+
const slots = useSlots();
|
|
14
|
+
/** Each item, told its number and whether a line runs on under it. */
|
|
15
|
+
const items = computed(() => {
|
|
16
|
+
const nodes = slotComponents(slots.default?.());
|
|
17
|
+
return nodes.map((node, position) =>
|
|
18
|
+
cloneVNode(node, {
|
|
19
|
+
index: node.props?.index ?? position + 1,
|
|
20
|
+
last: position === nodes.length - 1,
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
const classes = computed(() => twMerge('mb-4 w-full', attrs.class as string));
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<table v-bind="{ ...attrs, class: undefined }" :class="classes" role="presentation" cellpadding="0" cellspacing="0">
|
|
29
|
+
<component :is="item" v-for="(item, position) in items" :key="position" />
|
|
30
|
+
</table>
|
|
31
|
+
</template>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs } from 'vue';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* material-vue's SummaryData: labels and their values, a row each, or on
|
|
7
|
+
* one line with `inline`. A row without a value is left out — `0` is a
|
|
8
|
+
* value — unless `showEmpty`.
|
|
9
|
+
*/
|
|
10
|
+
interface SummaryDatum {
|
|
11
|
+
label: string;
|
|
12
|
+
value?: string | number | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
defineOptions({ inheritAttrs: false });
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(
|
|
18
|
+
defineProps<{
|
|
19
|
+
data?: SummaryDatum[];
|
|
20
|
+
inline?: boolean;
|
|
21
|
+
showEmpty?: boolean;
|
|
22
|
+
}>(),
|
|
23
|
+
{ data: () => [], inline: false, showEmpty: false },
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const attrs = useAttrs();
|
|
27
|
+
const rows = computed(() =>
|
|
28
|
+
props.showEmpty
|
|
29
|
+
? props.data
|
|
30
|
+
: props.data.filter(({ value }) => value === 0 || Boolean(value)),
|
|
31
|
+
);
|
|
32
|
+
const classes = computed(() => twMerge('mb-4 w-full', attrs.class as string));
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
<p v-if="inline" v-bind="{ ...attrs, class: undefined }" :class="twMerge('m-0 mb-4 text-sm', attrs.class as string)">
|
|
37
|
+
<template v-for="(row, index) in rows" :key="index"><span class="whitespace-nowrap"><span class="font-medium text-muted-foreground">{{ row.label }}:</span> {{ row.value }}</span><template v-if="index < rows.length - 1">  </template></template>
|
|
38
|
+
</p>
|
|
39
|
+
<table v-else v-bind="{ ...attrs, class: undefined }" :class="classes" role="presentation" cellpadding="0" cellspacing="0">
|
|
40
|
+
<tr v-for="(row, index) in rows" :key="index">
|
|
41
|
+
<td class="border-b [border-bottom-style:solid] border-primary-40 py-2 pr-2 text-sm text-muted-foreground">{{ row.label }}</td>
|
|
42
|
+
<td class="border-b [border-bottom-style:solid] border-primary-40 py-2 text-right text-sm text-foreground">{{ row.value }}</td>
|
|
43
|
+
</tr>
|
|
44
|
+
</table>
|
|
45
|
+
</template>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { provide } from 'vue';
|
|
3
|
+
import { TABLE_PART } from './ui';
|
|
4
|
+
|
|
5
|
+
/** material-vue's TableBody: the rows, each with a line under it. */
|
|
6
|
+
provide(TABLE_PART, 'body');
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<tbody><slot /></tbody>
|
|
11
|
+
</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 TableCaption: a muted line under the table. */
|
|
6
|
+
defineOptions({ inheritAttrs: false });
|
|
7
|
+
|
|
8
|
+
const attrs = useAttrs();
|
|
9
|
+
const classes = computed(() =>
|
|
10
|
+
twMerge('mt-4 text-sm text-muted-foreground', attrs.class as string),
|
|
11
|
+
);
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<caption v-bind="{ ...attrs, class: undefined }" align="bottom" :class="classes" style="caption-side: bottom"><slot /></caption>
|
|
16
|
+
</template>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, inject, useAttrs } from 'vue';
|
|
4
|
+
import { TABLE_PART, type TablePart } from './ui';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* material-vue's TableCell. Unlike material-vue's, its text wraps: a value too
|
|
8
|
+
* long for one line would widen the e-mail past a phone's screen.
|
|
9
|
+
*/
|
|
10
|
+
defineOptions({ inheritAttrs: false });
|
|
11
|
+
|
|
12
|
+
const part = inject<TablePart>(TABLE_PART, 'body');
|
|
13
|
+
const attrs = useAttrs();
|
|
14
|
+
const classes = computed(() =>
|
|
15
|
+
twMerge(
|
|
16
|
+
'p-2 align-middle',
|
|
17
|
+
// One side's style only: `border-0 border-solid` would end as `border: 0`.
|
|
18
|
+
part === 'footer'
|
|
19
|
+
? 'border-t [border-top-style:solid] border-border bg-muted font-medium'
|
|
20
|
+
: 'border-b [border-bottom-style:solid] border-border',
|
|
21
|
+
attrs.class as string,
|
|
22
|
+
),
|
|
23
|
+
);
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<td v-bind="{ ...attrs, class: undefined }" :class="classes"><slot /></td>
|
|
28
|
+
</template>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, useAttrs } from 'vue';
|
|
4
|
+
|
|
5
|
+
/** material-vue's TableEmpty: one row across `colspan` columns, its text centred. */
|
|
6
|
+
defineOptions({ inheritAttrs: false });
|
|
7
|
+
|
|
8
|
+
withDefaults(defineProps<{ colspan?: number }>(), { colspan: 1 });
|
|
9
|
+
|
|
10
|
+
const attrs = useAttrs();
|
|
11
|
+
const classes = computed(() =>
|
|
12
|
+
twMerge(
|
|
13
|
+
'px-4 py-10 text-center align-middle text-sm text-foreground',
|
|
14
|
+
attrs.class as string,
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<tr>
|
|
21
|
+
<td v-bind="{ ...attrs, class: undefined }" :colspan="colspan" :class="classes"><slot /></td>
|
|
22
|
+
</tr>
|
|
23
|
+
</template>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { provide } from 'vue';
|
|
3
|
+
import { TABLE_PART } from './ui';
|
|
4
|
+
|
|
5
|
+
/** material-vue's TableFooter: rows on a muted ground, in medium weight, with a line above. */
|
|
6
|
+
provide(TABLE_PART, 'footer');
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<tfoot><slot /></tfoot>
|
|
11
|
+
</template>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, inject, useAttrs } from 'vue';
|
|
4
|
+
import { TABLE_PART, type TablePart } from './ui';
|
|
5
|
+
|
|
6
|
+
/** material-vue's TableHead: a column's name, left-aligned, in medium weight. */
|
|
7
|
+
defineOptions({ inheritAttrs: false });
|
|
8
|
+
|
|
9
|
+
const part = inject<TablePart>(TABLE_PART, 'body');
|
|
10
|
+
const attrs = useAttrs();
|
|
11
|
+
const classes = computed(() =>
|
|
12
|
+
twMerge(
|
|
13
|
+
'h-10 whitespace-nowrap px-2 text-left align-middle font-medium text-foreground',
|
|
14
|
+
// One side's style only: `border-0 border-solid` would end as `border: 0`.
|
|
15
|
+
part === 'footer'
|
|
16
|
+
? 'border-t [border-top-style:solid] border-border bg-muted'
|
|
17
|
+
: 'border-b [border-bottom-style:solid] border-border',
|
|
18
|
+
attrs.class as string,
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<th v-bind="{ ...attrs, class: undefined }" :class="classes"><slot /></th>
|
|
25
|
+
</template>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { provide } from 'vue';
|
|
3
|
+
import { TABLE_PART } from './ui';
|
|
4
|
+
|
|
5
|
+
/** material-vue's TableHeader: its cells are `NxTableHead`s, with a line under them. */
|
|
6
|
+
provide(TABLE_PART, 'header');
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<thead><slot /></thead>
|
|
11
|
+
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useAttrs } from 'vue';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* material-vue's TableRow. Its line is drawn by its cells: most mail clients
|
|
6
|
+
* draw no border on a row.
|
|
7
|
+
*/
|
|
8
|
+
defineOptions({ inheritAttrs: false });
|
|
9
|
+
|
|
10
|
+
const attrs = useAttrs();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<tr v-bind="attrs"><slot /></tr>
|
|
15
|
+
</template>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { twMerge } from '@maizzle/framework';
|
|
3
|
+
import { computed, provide, useAttrs } from 'vue';
|
|
4
|
+
import { TABLE_PART } from './ui';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* material-vue's Table: a full-width table of `text-sm` rows. Its parts are
|
|
8
|
+
* `NxTableHeader`, `NxTableBody`, `NxTableFooter`, `NxTableRow`,
|
|
9
|
+
* `NxTableHead`, `NxTableCell`, `NxTableCaption` and `NxTableEmpty`.
|
|
10
|
+
*/
|
|
11
|
+
defineOptions({ inheritAttrs: false });
|
|
12
|
+
|
|
13
|
+
// A table in a footer cell starts again from body cells.
|
|
14
|
+
provide(TABLE_PART, 'body');
|
|
15
|
+
|
|
16
|
+
const attrs = useAttrs();
|
|
17
|
+
const classes = computed(() =>
|
|
18
|
+
twMerge('mb-4 w-full text-sm text-foreground', attrs.class as string),
|
|
19
|
+
);
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<!-- biome-ignore lint/a11y/noRedundantRoles: Maizzle marks a table without a role as role="none", a layout -->
|
|
24
|
+
<table v-bind="{ ...attrs, class: undefined }" :class="classes" role="table" cellpadding="0" cellspacing="0"><slot /></table>
|
|
25
|
+
</template>
|