nuxt-generation-emails 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.
@@ -0,0 +1,161 @@
1
+ <script setup>
2
+ import { ref, computed, onUnmounted, onMounted } from "vue";
3
+ import { useRoute, useRouter } from "#imports";
4
+ const props = defineProps({
5
+ templates: { type: Array, required: true }
6
+ });
7
+ const route = useRoute();
8
+ const router = useRouter();
9
+ const isOpen = ref(false);
10
+ const expandedDirs = ref(/* @__PURE__ */ new Set());
11
+ const currentTemplate = computed(() => {
12
+ return route.path.replace("/__emails/", "") || "Select a template";
13
+ });
14
+ const flatNodes = computed(() => {
15
+ const dirSet = /* @__PURE__ */ new Set();
16
+ const entries = [];
17
+ props.templates.forEach((template) => {
18
+ const parts = template.split("/");
19
+ entries.push({ parts, template });
20
+ for (let i = 1; i < parts.length; i++) {
21
+ dirSet.add(parts.slice(0, i).join("/"));
22
+ }
23
+ });
24
+ const root = [];
25
+ entries.forEach(({ parts, template }) => {
26
+ let currentLevel = root;
27
+ parts.forEach((part, index) => {
28
+ const isLast = index === parts.length - 1;
29
+ const path = parts.slice(0, index + 1).join("/");
30
+ let existing = currentLevel.find((n) => n.name === part && n.isDirectory === !isLast);
31
+ if (!existing) {
32
+ existing = {
33
+ name: part,
34
+ path: isLast ? template : path,
35
+ isDirectory: !isLast,
36
+ children: []
37
+ };
38
+ currentLevel.push(existing);
39
+ }
40
+ if (!isLast) {
41
+ currentLevel = existing.children;
42
+ }
43
+ });
44
+ });
45
+ const result = [];
46
+ function flatten(nodes, depth) {
47
+ const sorted = [...nodes].sort((a, b) => {
48
+ if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
49
+ return a.name.localeCompare(b.name);
50
+ });
51
+ for (const node of sorted) {
52
+ result.push({ name: node.name, path: node.path, isDirectory: node.isDirectory, depth });
53
+ if (node.isDirectory && expandedDirs.value.has(node.path)) {
54
+ flatten(node.children, depth + 1);
55
+ }
56
+ }
57
+ }
58
+ flatten(root, 0);
59
+ return result;
60
+ });
61
+ function selectTemplate(template) {
62
+ router.push(`/__emails/${template}`);
63
+ isOpen.value = false;
64
+ }
65
+ function toggleDirectory(path) {
66
+ if (expandedDirs.value.has(path)) {
67
+ expandedDirs.value.delete(path);
68
+ } else {
69
+ expandedDirs.value.add(path);
70
+ }
71
+ }
72
+ function toggleDropdown() {
73
+ isOpen.value = !isOpen.value;
74
+ }
75
+ function handleClickOutside(event) {
76
+ const target = event.target;
77
+ if (!target.closest(".nge-template-selector")) {
78
+ isOpen.value = false;
79
+ }
80
+ }
81
+ onMounted(() => {
82
+ document.addEventListener("click", handleClickOutside);
83
+ });
84
+ onUnmounted(() => {
85
+ document.removeEventListener("click", handleClickOutside);
86
+ });
87
+ </script>
88
+
89
+ <template>
90
+ <div class="nge-template-selector">
91
+ <button
92
+ class="nge-template-selector__trigger"
93
+ @click="toggleDropdown"
94
+ >
95
+ <span class="nge-template-selector__label">{{ currentTemplate }}</span>
96
+ <svg
97
+ class="nge-template-selector__icon"
98
+ :class="{ 'nge-template-selector__icon--open': isOpen }"
99
+ width="16"
100
+ height="16"
101
+ viewBox="0 0 16 16"
102
+ fill="none"
103
+ >
104
+ <path
105
+ d="M4 6L8 10L12 6"
106
+ stroke="currentColor"
107
+ stroke-width="2"
108
+ stroke-linecap="round"
109
+ stroke-linejoin="round"
110
+ />
111
+ </svg>
112
+ </button>
113
+ <div
114
+ v-if="isOpen"
115
+ class="nge-template-selector__dropdown"
116
+ >
117
+ <template
118
+ v-for="node in flatNodes"
119
+ :key="node.path"
120
+ >
121
+ <div
122
+ v-if="node.isDirectory"
123
+ class="nge-template-selector__dir-header"
124
+ :style="{ paddingLeft: `${16 + node.depth * 16}px` }"
125
+ @click.stop="toggleDirectory(node.path)"
126
+ >
127
+ <svg
128
+ class="nge-template-selector__dir-icon"
129
+ :class="{ 'nge-template-selector__dir-icon--open': expandedDirs.has(node.path) }"
130
+ width="12"
131
+ height="12"
132
+ viewBox="0 0 16 16"
133
+ fill="none"
134
+ >
135
+ <path
136
+ d="M6 4L10 8L6 12"
137
+ stroke="currentColor"
138
+ stroke-width="2"
139
+ stroke-linecap="round"
140
+ stroke-linejoin="round"
141
+ />
142
+ </svg>
143
+ <span>{{ node.name }}</span>
144
+ </div>
145
+ <div
146
+ v-else
147
+ class="nge-template-selector__item"
148
+ :class="{ 'nge-template-selector__item--active': node.path === currentTemplate }"
149
+ :style="{ paddingLeft: `${16 + node.depth * 16}px` }"
150
+ @click="selectTemplate(node.path)"
151
+ >
152
+ {{ node.name }}
153
+ </div>
154
+ </template>
155
+ </div>
156
+ </div>
157
+ </template>
158
+
159
+ <style scoped>
160
+ .nge-template-selector{position:relative}.nge-template-selector__trigger{align-items:center;backdrop-filter:blur(10px);background:hsla(0,0%,100%,.15);border:1px solid hsla(0,0%,100%,.25);border-radius:8px;color:#fff;cursor:pointer;display:flex;font-family:DM Sans,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:13px;font-weight:500;gap:8px;justify-content:space-between;letter-spacing:-.01em;min-width:200px;padding:8px 16px;transition:all .2s cubic-bezier(.4,0,.2,1)}.nge-template-selector__trigger:hover{background:hsla(0,0%,100%,.25)}.nge-template-selector__label{flex:1;overflow:hidden;text-align:left;text-overflow:ellipsis;white-space:nowrap}.nge-template-selector__icon{flex-shrink:0;transition:transform .2s cubic-bezier(.4,0,.2,1)}.nge-template-selector__icon--open{transform:rotate(180deg)}.nge-template-selector__dropdown{animation:slideDown .2s cubic-bezier(.4,0,.2,1);background:#fff;border:1px solid #e5e7eb;border-radius:8px;box-shadow:0 10px 25px rgba(0,0,0,.1),0 4px 10px rgba(0,0,0,.05);left:0;max-height:300px;overflow-y:auto;position:absolute;right:0;top:calc(100% + 8px);z-index:1000}@keyframes slideDown{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.nge-template-selector__item{border-bottom:1px solid #f3f4f6;color:#374151;cursor:pointer;font-family:DM Sans,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:13px;font-weight:450;letter-spacing:-.01em;padding:10px 16px;transition:all .15s ease}.nge-template-selector__item:last-child{border-bottom:none}.nge-template-selector__item:hover{background:#f9fafb;color:#00dc82}.nge-template-selector__item--active{background:#f0fdf4;color:#00dc82;font-weight:500}.nge-template-selector__item--active:before{color:#00dc82;content:"✓";margin-right:8px}.nge-template-selector__directory{border-bottom:1px solid #f3f4f6}.nge-template-selector__directory:last-child{border-bottom:none}.nge-template-selector__dir-header{align-items:center;background:#fafafa;color:#111827;cursor:pointer;display:flex;font-family:DM Sans,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:13px;font-weight:600;gap:8px;letter-spacing:-.01em;padding:10px 16px;transition:all .15s ease}.nge-template-selector__dir-header:hover{background:#f3f4f6}.nge-template-selector__dir-icon{color:#6b7280;flex-shrink:0;transition:transform .2s cubic-bezier(.4,0,.2,1)}.nge-template-selector__dir-icon--open{transform:rotate(90deg)}.nge-template-selector__dir-content{background:#fff}.nge-template-selector__dir-content .nge-template-selector__item{border-bottom:1px solid #f9fafb;padding-left:36px}.nge-template-selector__dir-content .nge-template-selector__item:last-child{border-bottom:none;color:#00dc82}
161
+ </style>
@@ -0,0 +1,6 @@
1
+ type __VLS_Props = {
2
+ templates: string[];
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
5
+ declare const _default: typeof __VLS_export;
6
+ export default _default;
@@ -0,0 +1,20 @@
1
+ interface TreeNode {
2
+ name: string;
3
+ path: string;
4
+ isDirectory: boolean;
5
+ children?: TreeNode[];
6
+ }
7
+ type __VLS_Props = {
8
+ node: TreeNode;
9
+ currentTemplate: string;
10
+ expandedDirs: Set<string>;
11
+ };
12
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
13
+ select: (template: string) => any;
14
+ toggle: (path: string, event: MouseEvent) => any;
15
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
+ onSelect?: ((template: string) => any) | undefined;
17
+ onToggle?: ((path: string, event: MouseEvent) => any) | undefined;
18
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
+ declare const _default: typeof __VLS_export;
20
+ export default _default;
@@ -0,0 +1,64 @@
1
+ <script setup>
2
+ defineOptions({ name: "NgeTreeNode" });
3
+ defineProps({
4
+ node: { type: Object, required: true },
5
+ currentTemplate: { type: String, required: true },
6
+ expandedDirs: { type: Set, required: true }
7
+ });
8
+ const emit = defineEmits(["select", "toggle"]);
9
+ </script>
10
+
11
+ <template>
12
+ <div
13
+ v-if="node.isDirectory"
14
+ class="nge-template-selector__directory"
15
+ >
16
+ <div
17
+ class="nge-template-selector__dir-header"
18
+ @click="emit('toggle', node.path, $event)"
19
+ >
20
+ <svg
21
+ class="nge-template-selector__dir-icon"
22
+ :class="{ 'nge-template-selector__dir-icon--open': expandedDirs.has(node.path) }"
23
+ width="12"
24
+ height="12"
25
+ viewBox="0 0 16 16"
26
+ fill="none"
27
+ >
28
+ <path
29
+ d="M6 4L10 8L6 12"
30
+ stroke="currentColor"
31
+ stroke-width="2"
32
+ stroke-linecap="round"
33
+ stroke-linejoin="round"
34
+ />
35
+ </svg>
36
+ <span>{{ node.name }}</span>
37
+ </div>
38
+ <div
39
+ v-if="expandedDirs.has(node.path)"
40
+ class="nge-template-selector__dir-content"
41
+ >
42
+ <template
43
+ v-for="child in node.children"
44
+ :key="child.path"
45
+ >
46
+ <NgeTreeNode
47
+ :node="child"
48
+ :current-template="currentTemplate"
49
+ :expanded-dirs="expandedDirs"
50
+ @select="(t) => emit('select', t)"
51
+ @toggle="(p, e) => emit('toggle', p, e)"
52
+ />
53
+ </template>
54
+ </div>
55
+ </div>
56
+ <div
57
+ v-else
58
+ class="nge-template-selector__item"
59
+ :class="{ 'nge-template-selector__item--active': node.path === currentTemplate }"
60
+ @click="emit('select', node.path)"
61
+ >
62
+ {{ node.name }}
63
+ </div>
64
+ </template>
@@ -0,0 +1,20 @@
1
+ interface TreeNode {
2
+ name: string;
3
+ path: string;
4
+ isDirectory: boolean;
5
+ children?: TreeNode[];
6
+ }
7
+ type __VLS_Props = {
8
+ node: TreeNode;
9
+ currentTemplate: string;
10
+ expandedDirs: Set<string>;
11
+ };
12
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
13
+ select: (template: string) => any;
14
+ toggle: (path: string, event: MouseEvent) => any;
15
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
+ onSelect?: ((template: string) => any) | undefined;
17
+ onToggle?: ((path: string, event: MouseEvent) => any) | undefined;
18
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
+ declare const _default: typeof __VLS_export;
20
+ export default _default;
@@ -0,0 +1,25 @@
1
+ interface PropDefinition {
2
+ name: string;
3
+ type: 'string' | 'number' | 'boolean' | 'object' | 'unknown';
4
+ }
5
+ type __VLS_Props = {
6
+ /** Reactive object containing current prop values (managed by the wrapper) */
7
+ emailProps?: Record<string, unknown>;
8
+ /** Flat list of prop definitions extracted from the SFC at build time */
9
+ propDefinitions?: PropDefinition[];
10
+ };
11
+ declare var __VLS_1: {}, __VLS_19: {};
12
+ type __VLS_Slots = {} & {
13
+ default?: (props: typeof __VLS_1) => any;
14
+ } & {
15
+ default?: (props: typeof __VLS_19) => any;
16
+ };
17
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
19
+ declare const _default: typeof __VLS_export;
20
+ export default _default;
21
+ type __VLS_WithSlots<T, S> = T & {
22
+ new (): {
23
+ $slots: S;
24
+ };
25
+ };
@@ -0,0 +1,151 @@
1
+ <script setup>
2
+ import EmailTemplateSelector from "../components/EmailTemplateSelector.vue";
3
+ import ApiTester from "../components/ApiTester.vue";
4
+ import { computed, generateShareableUrl, ref, useAttrs, useRoute, useRouter } from "#imports";
5
+ defineOptions({ inheritAttrs: false });
6
+ const attrs = useAttrs();
7
+ const props = defineProps({
8
+ emailProps: { type: Object, required: false },
9
+ propDefinitions: { type: Array, required: false }
10
+ });
11
+ const route = useRoute();
12
+ const router = useRouter();
13
+ const isServerRequest = computed(() => route.query.server === "true");
14
+ const templates = computed(() => {
15
+ return router.getRoutes().filter((route2) => route2.path.startsWith("/__emails/")).map((route2) => route2.path.replace("/__emails/", "")).sort();
16
+ });
17
+ const showControls = ref(true);
18
+ const copySuccess = ref(false);
19
+ const editableFields = computed(() => {
20
+ if (!props.propDefinitions) return [];
21
+ return props.propDefinitions.filter((p) => p.type === "string" || p.type === "number");
22
+ });
23
+ const hasControls = computed(() => !!props.emailProps && editableFields.value.length > 0);
24
+ const previewRenderKey = computed(() => {
25
+ if (!props.emailProps) return route.fullPath;
26
+ try {
27
+ return `${route.fullPath}:${JSON.stringify(props.emailProps)}`;
28
+ } catch {
29
+ return route.fullPath;
30
+ }
31
+ });
32
+ function formatFieldLabel(key) {
33
+ const words = key.replace(/([A-Z])/g, " $1").trim();
34
+ return words.charAt(0).toUpperCase() + words.slice(1);
35
+ }
36
+ function getFieldId(key) {
37
+ return `field-${key.replace(/[^\w-]/g, "-")}`;
38
+ }
39
+ function updateProp(key, value, type) {
40
+ if (!props.emailProps) return;
41
+ if (type === "number") {
42
+ const parsed = Number(value);
43
+ props.emailProps[key] = Number.isNaN(parsed) ? 0 : parsed;
44
+ } else {
45
+ props.emailProps[key] = value;
46
+ }
47
+ }
48
+ async function copyShareableUrl() {
49
+ if (!props.emailProps) return;
50
+ const url = generateShareableUrl(props.emailProps);
51
+ try {
52
+ await navigator.clipboard.writeText(url);
53
+ copySuccess.value = true;
54
+ setTimeout(() => {
55
+ copySuccess.value = false;
56
+ }, 2e3);
57
+ } catch (error) {
58
+ console.error("Failed to copy URL:", error);
59
+ }
60
+ }
61
+ </script>
62
+
63
+ <template>
64
+ <!-- Server mode: render only the email content -->
65
+ <slot v-if="isServerRequest" />
66
+
67
+ <!-- Client mode: render full preview UI -->
68
+ <div
69
+ v-else
70
+ v-bind="attrs"
71
+ class="nge-email-preview"
72
+ >
73
+ <div class="nge-email-preview__toolbar">
74
+ <div class="nge-email-preview__title">
75
+ Email Template
76
+ </div>
77
+ <div class="nge-email-preview__toolbar-actions">
78
+ <EmailTemplateSelector :templates="templates" />
79
+ <button
80
+ v-if="emailProps"
81
+ class="nge-email-preview__toggle"
82
+ @click="copyShareableUrl"
83
+ >
84
+ {{ copySuccess ? "\u2713 Copied!" : "Share URL" }}
85
+ </button>
86
+ <button
87
+ v-if="hasControls"
88
+ class="nge-email-preview__toggle"
89
+ @click="showControls = !showControls"
90
+ >
91
+ {{ showControls ? "Hide" : "Show" }} Controls
92
+ </button>
93
+ </div>
94
+ </div>
95
+ <div class="nge-email-preview__container">
96
+ <div
97
+ v-if="hasControls && showControls"
98
+ class="nge-email-preview__controls"
99
+ >
100
+ <div class="nge-email-preview__controls-header">
101
+ <h3>Template Props</h3>
102
+ </div>
103
+ <div class="nge-email-preview__controls-content">
104
+ <div
105
+ v-for="field in editableFields"
106
+ :key="field.name"
107
+ class="nge-email-preview__control"
108
+ >
109
+ <label :for="getFieldId(field.name)">{{ formatFieldLabel(field.name) }}</label>
110
+ <input
111
+ v-if="field.type === 'string'"
112
+ :id="getFieldId(field.name)"
113
+ :value="emailProps?.[field.name]"
114
+ type="text"
115
+ class="nge-email-preview__input"
116
+ @input="updateProp(field.name, $event.target.value, field.type)"
117
+ >
118
+ <input
119
+ v-else-if="field.type === 'number'"
120
+ :id="getFieldId(field.name)"
121
+ :value="emailProps?.[field.name]"
122
+ type="number"
123
+ class="nge-email-preview__input"
124
+ @input="updateProp(field.name, $event.target.value, field.type)"
125
+ >
126
+ </div>
127
+ <ApiTester
128
+ v-if="emailProps"
129
+ :data-object="emailProps"
130
+ />
131
+ </div>
132
+ </div>
133
+ <div class="nge-email-preview__content">
134
+ <ClientOnly>
135
+ <div :key="previewRenderKey">
136
+ <slot />
137
+ </div>
138
+ <template #fallback>
139
+ <div class="nge-email-preview__placeholder">
140
+ Loading preview...
141
+ </div>
142
+ </template>
143
+ </ClientOnly>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </template>
148
+
149
+ <style scoped>
150
+ .nge-email-preview{background:#f9fafb;bottom:0;display:flex;flex-direction:column;left:0;margin:0;min-height:100vh;padding:0;position:fixed;right:0;top:0}.nge-email-preview__toolbar{align-items:center;background:linear-gradient(90deg,#047857,#059669);box-shadow:0 1px 3px rgba(0,0,0,.1);color:#fff;display:flex;font-family:DM Sans,system-ui,-apple-system,sans-serif;font-size:14px;justify-content:space-between;padding:14px 24px;position:sticky;top:0;z-index:1000}.nge-email-preview__toolbar-actions{align-items:center;display:flex;gap:12px}.nge-email-preview__title{align-items:center;display:flex;font-size:15px;font-weight:600;gap:8px;letter-spacing:-.01em}.nge-email-preview__title:before{content:"✉";font-size:18px}.nge-email-preview__toggle{backdrop-filter:blur(10px);background:hsla(0,0%,100%,.15);border:1px solid hsla(0,0%,100%,.25);border-radius:8px;color:#fff;cursor:pointer;font-family:DM Sans,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:13px;font-weight:500;letter-spacing:-.01em;padding:8px 16px;transition:all .2s cubic-bezier(.4,0,.2,1)}.nge-email-preview__toggle:hover{background:hsla(0,0%,100%,.25);box-shadow:0 4px 12px rgba(0,0,0,.15);transform:translateY(-1px)}.nge-email-preview__toggle:active{transform:translateY(0)}.nge-email-preview__container{display:flex;flex:1;overflow:hidden}.nge-email-preview__controls{background:#fff;border-right:1px solid #e5e7eb;box-shadow:2px 0 8px rgba(0,0,0,.04);display:flex;flex-direction:column;overflow:hidden;width:320px}.nge-email-preview__controls-header{background:#fff;border-bottom:1px solid #e5e7eb;flex-shrink:0;padding:20px 24px}.nge-email-preview__controls-header h3{color:#111827;font-family:DM Sans,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:15px;font-weight:600;letter-spacing:-.01em;margin:0}.nge-email-preview__controls-content{flex:1;overflow-y:auto;padding:24px}.nge-email-preview__control{margin-bottom:20px}.nge-email-preview__control:last-child{margin-bottom:0}.nge-email-preview__control label{color:#374151;display:block;font-size:13px;font-weight:500;letter-spacing:-.01em;margin-bottom:8px}.nge-email-preview__control label,.nge-email-preview__input{font-family:DM Sans,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}.nge-email-preview__input{background:#fff;border:1px solid #d1d5db;border-radius:8px;box-shadow:0 1px 2px rgba(0,0,0,.05);box-sizing:border-box;color:#111827;font-size:14px;padding:10px 14px;transition:all .2s cubic-bezier(.4,0,.2,1);width:100%}.nge-email-preview__input:hover{border-color:#9ca3af}.nge-email-preview__input:focus{border-color:#047857;box-shadow:0 0 0 3px rgba(4,120,87,.1),0 1px 2px rgba(0,0,0,.05);outline:none}.nge-email-preview__input::-moz-placeholder{color:#9ca3af}.nge-email-preview__input::placeholder{color:#9ca3af}.nge-email-preview__content{background:#f9fafb;flex:1;overflow-y:auto}.nge-email-preview__placeholder{color:#6b7280;font-family:DM Sans,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:14px;padding:24px}
151
+ </style>
@@ -0,0 +1,25 @@
1
+ interface PropDefinition {
2
+ name: string;
3
+ type: 'string' | 'number' | 'boolean' | 'object' | 'unknown';
4
+ }
5
+ type __VLS_Props = {
6
+ /** Reactive object containing current prop values (managed by the wrapper) */
7
+ emailProps?: Record<string, unknown>;
8
+ /** Flat list of prop definitions extracted from the SFC at build time */
9
+ propDefinitions?: PropDefinition[];
10
+ };
11
+ declare var __VLS_1: {}, __VLS_19: {};
12
+ type __VLS_Slots = {} & {
13
+ default?: (props: typeof __VLS_1) => any;
14
+ } & {
15
+ default?: (props: typeof __VLS_19) => any;
16
+ };
17
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
19
+ declare const _default: typeof __VLS_export;
20
+ export default _default;
21
+ type __VLS_WithSlots<T, S> = T & {
22
+ new (): {
23
+ $slots: S;
24
+ };
25
+ };
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../../.nuxt/tsconfig.server.json",
3
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Get the sendGenEmails handler function from runtime config
3
+ * This allows users to provide a custom function for sending emails
4
+ * while keeping the logic decoupled from the API routes
5
+ *
6
+ * @returns The sendGenEmails function if provided, or null
7
+ */
8
+ export declare function getSendGenEmailsHandler(): ((html: string, data: Record<string, unknown>) => Promise<void> | void) | null;
@@ -0,0 +1,8 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ export function getSendGenEmailsHandler() {
3
+ const config = useRuntimeConfig();
4
+ if (config.nuxtGenEmails?.sendGenEmails && typeof config.nuxtGenEmails.sendGenEmails === "function") {
5
+ return config.nuxtGenEmails.sendGenEmails;
6
+ }
7
+ return null;
8
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Encode a data store object into URL search parameters
3
+ */
4
+ export declare function encodeStoreToUrlParams(store: Record<string, unknown>): string;
5
+ /**
6
+ * Generate a shareable URL for the current template with encoded data
7
+ */
8
+ export declare function generateShareableUrl(store: Record<string, unknown>): string;
@@ -0,0 +1,18 @@
1
+ export function encodeStoreToUrlParams(store) {
2
+ const params = new URLSearchParams();
3
+ Object.entries(store).forEach(([key, value]) => {
4
+ if (value !== null && value !== void 0) {
5
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
6
+ params.set(key, String(value));
7
+ }
8
+ }
9
+ });
10
+ const paramsString = params.toString();
11
+ return paramsString ? `?${paramsString}` : "";
12
+ }
13
+ export function generateShareableUrl(store) {
14
+ if (typeof window === "undefined") return "";
15
+ const baseUrl = `${window.location.origin}${window.location.pathname}`;
16
+ const params = encodeStoreToUrlParams(store);
17
+ return `${baseUrl}${params}`;
18
+ }
@@ -0,0 +1,3 @@
1
+ export { default } from './module.mjs'
2
+
3
+ export { type ModuleOptions } from './module.mjs'
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "nuxt-generation-emails",
3
+ "version": "0.1.0",
4
+ "description": "A Nuxt module for authoring, previewing, and sending transactional email templates with Vue Email.",
5
+ "author": "nullcarry@icloud.com",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/treygrr/nuxt-generation-emails.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/treygrr/nuxt-generation-emails/issues"
12
+ },
13
+ "homepage": "https://github.com/treygrr/nuxt-generation-emails#readme",
14
+ "license": "MIT",
15
+ "type": "module",
16
+ "keywords": [
17
+ "nuxt",
18
+ "nuxt-module",
19
+ "email",
20
+ "vue-email",
21
+ "transactional-email",
22
+ "email-templates",
23
+ "sendgrid",
24
+ "tailwind"
25
+ ],
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/types.d.mts",
29
+ "import": "./dist/module.mjs"
30
+ }
31
+ },
32
+ "bin": {
33
+ "nuxt-gen-emails": "./dist/cli/index.mjs"
34
+ },
35
+ "main": "./dist/module.mjs",
36
+ "types": "./dist/types.d.mts",
37
+ "typesVersions": {
38
+ "*": {
39
+ ".": [
40
+ "./dist/types.d.mts"
41
+ ]
42
+ }
43
+ },
44
+ "files": [
45
+ "dist"
46
+ ],
47
+ "workspaces": [
48
+ "playground"
49
+ ],
50
+ "scripts": {
51
+ "prepack": "nuxt-module-build build",
52
+ "dev": "npm run dev:prepare && nuxt dev playground",
53
+ "dev:build": "nuxt build playground",
54
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
55
+ "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
56
+ "lint": "eslint .",
57
+ "test": "vitest run",
58
+ "test:watch": "vitest watch",
59
+ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
60
+ "cli:add": "cd playground && node ../dist/cli/index.mjs add"
61
+ },
62
+ "dependencies": {
63
+ "@nuxt/kit": "^4.3.1",
64
+ "@vitejs/plugin-vue": "^6.0.4",
65
+ "@vue-email/components": "^0.0.21",
66
+ "@vue-email/render": "^0.0.9",
67
+ "citty": "^0.1.6",
68
+ "consola": "^3.4.0",
69
+ "pathe": "^2.0.3"
70
+ },
71
+ "peerDependencies": {
72
+ "nuxt": ">=4.0.0",
73
+ "vue": "^3.5.0"
74
+ },
75
+ "devDependencies": {
76
+ "@nuxt/devtools": "^3.1.1",
77
+ "@nuxt/eslint-config": "^1.14.0",
78
+ "@nuxt/module-builder": "^1.0.2",
79
+ "@nuxt/schema": "^4.3.1",
80
+ "@nuxt/test-utils": "^4.0.0",
81
+ "@types/node": "latest",
82
+ "changelogen": "^0.6.2",
83
+ "eslint": "^10.0.0",
84
+ "nuxt": "^4.3.1",
85
+ "tailwindcss": "^4.1.18",
86
+ "typescript": "~5.9.3",
87
+ "vitest": "^4.0.18",
88
+ "vue-tsc": "^3.2.4"
89
+ }
90
+ }