@xenterprises/nuxt-x-marketing 1.2.2 → 1.4.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/app/app.config.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { XMarketingConfig } from "./types/config";
2
+
1
3
  export default defineAppConfig({
2
4
  ui: {
3
5
  primary: "brand",
@@ -25,19 +27,24 @@ export default defineAppConfig({
25
27
  },
26
28
  },
27
29
  xMarketing: {
28
- name: "X Enterprises",
29
- url: "https://x-enterprises.com",
30
+ /** Site name used by the shipped shell for the SEO title template
31
+ * ("%s | name") and og:site_name. Leave undefined to keep bare titles. */
32
+ name: undefined,
33
+ /** Canonical site URL — used as og:url on the shipped home page. */
34
+ url: undefined,
30
35
  config: {
31
- markerProjectId: "",
36
+ markerProjectId: undefined,
32
37
  emailMarketingNewsletters: {
33
- organizationId: "",
38
+ organizationId: undefined,
34
39
  },
35
40
  },
36
41
  header: {
42
+ /** Set false to opt out of the default navbar in the shipped shell. */
43
+ active: true as boolean,
37
44
  logo: {
38
- src: "",
39
- srcDark: "",
40
- alt: "",
45
+ src: undefined,
46
+ srcDark: undefined,
47
+ alt: undefined,
41
48
  },
42
49
  nav: {
43
50
  buttons: [],
@@ -45,23 +52,29 @@ export default defineAppConfig({
45
52
  },
46
53
  },
47
54
  footer: {
55
+ /** Set false to opt out of the default footer in the shipped shell. */
56
+ active: true as boolean,
48
57
  logo: {
49
- src: "",
50
- alt: "",
58
+ src: undefined,
59
+ alt: undefined,
51
60
  },
52
61
  bg: {
53
- color: "",
62
+ color: undefined,
54
63
  img: {
55
- src: "",
56
- alt: "",
64
+ src: undefined,
65
+ alt: undefined,
57
66
  },
58
67
  },
59
- body: "",
68
+ body: undefined,
60
69
  socials: [],
61
70
  columns: [],
62
71
  },
72
+ /** Cookie-consent banner in the shipped shell; gates tracking injection. */
73
+ consent: {
74
+ active: true as boolean,
75
+ },
63
76
  blog: {
64
- active: true,
77
+ active: true as boolean,
65
78
  title: "Blog",
66
79
  description:
67
80
  "Welcome to our blog. Here you can find the latest news, updates, and articles.",
@@ -94,121 +107,14 @@ export default defineAppConfig({
94
107
  scripts: [],
95
108
  // ---- Behavior ----
96
109
  /** Auto-inject configured scripts on consent. Default: true. */
97
- autoInject: true,
110
+ autoInject: true as boolean,
98
111
  },
99
112
  },
100
113
  });
101
114
 
102
115
  declare module "@nuxt/schema" {
103
116
  interface AppConfigInput {
104
- xMarketing?: {
105
- /** Project name */
106
- name?: string;
107
- /** Project URL */
108
- url?: string;
109
- config?: {
110
- markerProjectId?: string;
111
- emailMarketingNewsletters?: {
112
- organizationId?: string;
113
- };
114
- };
115
- header?: {
116
- logo?: {
117
- src?: string;
118
- srcDark?: string;
119
- alt?: string;
120
- };
121
- nav?: {
122
- buttons?: {
123
- label: string;
124
- to: string;
125
- icon?: string;
126
- target?: string;
127
- variant?: string;
128
- square?: boolean;
129
- color?: string;
130
- }[];
131
- links?: NavLink[];
132
- };
133
- };
134
- footer?: {
135
- body?: string;
136
- bg?: {
137
- color?: string;
138
- img?: {
139
- src?: string;
140
- alt?: string;
141
- };
142
- };
143
- logo?: {
144
- src?: string;
145
- alt?: string;
146
- };
147
- socials?: {
148
- name: string;
149
- url: string;
150
- icon: string;
151
- }[];
152
- columns?: {
153
- headerLabel: string;
154
- links: {
155
- label: string;
156
- to: string;
157
- target?: string;
158
- }[];
159
- }[];
160
- /**
161
- * Tracking + analytics config. Read by
162
- * `<XMarkPrivacyCookieConsent>` and `useConsentTracking()`.
163
- * Only fires after the matching consent category is granted.
164
- */
165
- tracking?: XMarketingTracking;
166
- };
117
+ xMarketing?: XMarketingConfig;
167
118
  }
168
119
  }
169
120
 
170
- /**
171
- * Tracking entry — use the convenience IDs for the common tools
172
- * (GTM, GA4, Clarity) or `scripts[]` for everything else. Scripts
173
- * are injected only after the visitor grants consent for the
174
- * matching `category` (default: "analytics").
175
- */
176
- interface XMarketingTracking {
177
- /** Google Tag Manager container ID, e.g. "GTM-XXXXXXX". */
178
- gtmId?: string;
179
- /** Google Analytics 4 measurement ID, e.g. "G-XXXXXXXX". */
180
- ga4Id?: string;
181
- /** Microsoft Clarity project ID, e.g. "abc123def4". */
182
- clarityId?: string;
183
- /**
184
- * Arbitrary scripts (Meta Pixel, Hotjar, Segment, LinkedIn Insight,
185
- * Pinterest, TikTok, etc.). Each fires once the matching consent
186
- * category is granted. Use `inline` for hand-written snippets like
187
- * `gtag('js', new Date())`.
188
- */
189
- scripts?: XMarketingTrackingScript[];
190
- /** Auto-inject configured scripts on consent. Default: true. */
191
- autoInject?: boolean;
192
- }
193
-
194
- interface XMarketingTrackingScript {
195
- /** Stable ID — used to dedupe re-injection. */
196
- id: string;
197
- /** External script URL (e.g. "https://www.googletagmanager.com/gtag/js?id=G-XXX"). */
198
- src?: string;
199
- /** Inline script body (no `src` required). */
200
- inline?: string;
201
- /** Consent category gate. "necessary" fires immediately; "analytics" / "marketing" gated. */
202
- category?: "necessary" | "analytics" | "marketing";
203
- /** Extra attributes applied to the injected <script>. */
204
- attrs?: Record<string, string>;
205
- }
206
- }
207
-
208
- interface NavLink {
209
- label: string;
210
- to?: string;
211
- target?: string;
212
- children?: NavLink[];
213
- }
214
-
package/app/app.vue CHANGED
@@ -1,17 +1,9 @@
1
1
  <template>
2
2
  <div class="min-h-screen flex flex-col">
3
- <!-- Navbar -->
4
- <XMarkNavbar
5
- :links="navLinks"
6
- :logo-light="logoLight"
7
- :logo-dark="logoDark"
8
- logo-alt="Company"
9
- >
10
- <template #actions>
11
- <UButton variant="ghost" color="neutral">Sign In</UButton>
12
- <UButton color="primary">Get Started</UButton>
13
- </template>
14
- </XMarkNavbar>
3
+ <!-- Default navbar — reads appConfig.xMarketing.header (logo, nav.links,
4
+ nav.buttons). Opt out with xMarketing.header.active: false or by
5
+ shipping your own app/app.vue. -->
6
+ <XHeaderNav v-if="headerActive" />
15
7
 
16
8
  <!-- Main Content -->
17
9
  <main class="flex-grow">
@@ -23,30 +15,20 @@
23
15
  </UApp>
24
16
  </main>
25
17
 
26
- <!-- Newsletter Section -->
27
- <XMarkSection bg="subtle" padding="lg">
28
- <XMarkNewsletter
29
- title="Stay in the loop"
30
- description="Get the latest updates, tips, and insights delivered to your inbox."
31
- variant="default"
32
- @submit="handleNewsletterSubmit"
33
- />
34
- </XMarkSection>
35
-
36
- <!-- Footer -->
37
- <XMarkFooter
38
- :logo="logoDark"
39
- logo-alt="Company"
40
- description="Building the future of modern software."
41
- :social="socialLinks"
42
- :columns="footerColumns"
43
- :legal-links="legalLinks"
44
- />
18
+ <!-- Default footer — reads appConfig.xMarketing.footer (logo, body,
19
+ socials, columns, bg). Shows the newsletter form when
20
+ xMarketing.config.emailMarketingNewsletters.organizationId is set.
21
+ Opt out with xMarketing.footer.active: false. -->
22
+ <XFooter v-if="footerActive" />
45
23
 
46
24
  <!-- Cookie consent banner — auto-shows on first visit and fires
47
25
  any tracking scripts configured in app.config.xMarketing.tracking
48
- once the visitor accepts. -->
49
- <XMarkPrivacyCookieConsent policy-url="/cookies" privacy-url="/privacy" />
26
+ once the visitor accepts. Opt out with xMarketing.consent.active: false. -->
27
+ <XMarkPrivacyCookieConsent
28
+ v-if="consentActive"
29
+ policy-url="/cookies"
30
+ privacy-url="/privacy"
31
+ />
50
32
  </div>
51
33
  </template>
52
34
  <script setup>
@@ -54,79 +36,28 @@ import { onMounted } from "vue";
54
36
  import markerSDK from "@marker.io/browser";
55
37
 
56
38
  const appConfig = useAppConfig();
39
+ const xMarketing = appConfig?.xMarketing;
57
40
 
58
- useSeoMeta({
59
- titleTemplate: `%s`,
60
- });
61
-
62
- // Logo configuration (replace with actual logos)
63
- const logoLight = "/logo-white.svg";
64
- const logoDark = "/logo-dark.svg";
41
+ const headerActive = computed(() => xMarketing?.header?.active !== false);
42
+ const footerActive = computed(() => xMarketing?.footer?.active !== false);
43
+ const consentActive = computed(() => xMarketing?.consent?.active !== false);
65
44
 
66
- // Navigation links
67
- const navLinks = [
68
- { label: "Features", to: "/#features" },
69
- { label: "Pricing", to: "/pricing" },
70
- { label: "Blog", to: "/blog" },
71
- { label: "About", to: "/about" },
72
- ];
45
+ // xMarketing.name/url feed the default SEO wiring: when `name` is set the
46
+ // title template becomes "<page> | <name>" and og:site_name is emitted;
47
+ // the shipped home page also reads `url` for og:url.
48
+ const siteName = computed(() => xMarketing?.name);
73
49
 
74
- // Social links
75
- const socialLinks = [
76
- { name: "Twitter", href: "https://twitter.com", icon: "i-lucide-twitter" },
77
- { name: "GitHub", href: "https://github.com", icon: "i-lucide-github" },
78
- { name: "LinkedIn", href: "https://linkedin.com", icon: "i-lucide-linkedin" },
79
- ];
80
-
81
- // Footer columns
82
- const footerColumns = [
83
- {
84
- title: "Product",
85
- links: [
86
- { label: "Features", to: "/#features" },
87
- { label: "Pricing", to: "/pricing" },
88
- { label: "Integrations", to: "/integrations" },
89
- { label: "Changelog", to: "/changelog" },
90
- ],
91
- },
92
- {
93
- title: "Company",
94
- links: [
95
- { label: "About", to: "/about" },
96
- { label: "Blog", to: "/blog" },
97
- { label: "Careers", to: "/careers" },
98
- { label: "Contact", to: "/contact" },
99
- ],
100
- },
101
- {
102
- title: "Resources",
103
- links: [
104
- { label: "Documentation", to: "/docs" },
105
- { label: "Help Center", to: "/help" },
106
- { label: "Community", to: "/community" },
107
- { label: "Status", to: "/status" },
108
- ],
109
- },
110
- ];
111
-
112
- // Legal links
113
- const legalLinks = [
114
- { label: "Privacy Policy", to: "/privacy" },
115
- { label: "Terms of Service", to: "/terms" },
116
- { label: "Cookie Policy", to: "/cookies" },
117
- ];
118
-
119
- // Newsletter submit handler
120
- const handleNewsletterSubmit = (email) => {
121
- console.log("Newsletter signup:", email);
122
- // Implement your newsletter signup logic here
123
- };
50
+ useSeoMeta({
51
+ titleTemplate: (title) =>
52
+ title ? (siteName.value ? `${title} | ${siteName.value}` : title) : (siteName.value ?? ""),
53
+ ogSiteName: siteName,
54
+ });
124
55
 
125
- // Marker.io widget
126
- if (appConfig && appConfig?.xMarketing?.config?.markerProjectId) {
56
+ // Marker.io widget — only loads when a project ID is configured.
57
+ if (xMarketing?.config?.markerProjectId) {
127
58
  onMounted(() => {
128
59
  markerSDK.loadWidget({
129
- project: appConfig?.xMarketing?.config?.markerProjectId,
60
+ project: xMarketing?.config?.markerProjectId,
130
61
  });
131
62
  });
132
63
  }
@@ -1,3 +1,13 @@
1
+ /* NOTE — the `x*` utilities and `--x-*` tokens in this file are duplicated in
2
+ * `nuxt-x-affiliate` (`app/assets/css/main.css`), which was ported from here.
3
+ * 32 class names overlap with identical values. A site extending BOTH layers
4
+ * loads both stylesheets and gets whichever is injected last — harmless while
5
+ * they match, a silent divergence if either is edited alone. Keep them in sync.
6
+ *
7
+ * Only `.xGlow-divider` / `.xGlow-divider-subtle` are unique to this file;
8
+ * the affiliate layer draws that rule with `<XAFDividerGlow>` instead.
9
+ */
10
+
1
11
  @import "tailwindcss";
2
12
  @import "@nuxt/ui";
3
13
  @plugin "@tailwindcss/typography";
@@ -32,7 +32,7 @@
32
32
  class="flex flex-col lg:flex-row lg:justify-between gap-8 lg:gap-12"
33
33
  >
34
34
  <div class="shrink-0 lg:max-w-xl">
35
- <a class="mb-8 inline-block" href="/">
35
+ <a v-if="footer?.logo?.src" class="mb-8 inline-block" href="/">
36
36
  <img
37
37
  :src="footer?.logo?.src"
38
38
  :alt="footer?.logo?.alt"
@@ -85,14 +85,14 @@ const handleScroll = () => {
85
85
  };
86
86
 
87
87
  onMounted(() => {
88
- if (process.client) {
88
+ if (import.meta.client) {
89
89
  window.addEventListener('scroll', handleScroll, { passive: true });
90
90
  handleScroll();
91
91
  }
92
92
  });
93
93
 
94
94
  onUnmounted(() => {
95
- if (process.client) {
95
+ if (import.meta.client) {
96
96
  window.removeEventListener('scroll', handleScroll);
97
97
  }
98
98
  });
@@ -1,7 +1,8 @@
1
1
  <template>
2
2
  <section
3
3
  :class="[
4
- 'relative overflow-hidden rounded-2xl',
4
+ 'relative overflow-hidden',
5
+ rounded ? 'rounded-2xl' : 'rounded-none',
5
6
  paddingClasses,
6
7
  bgColorClasses,
7
8
  ]"
@@ -127,6 +128,11 @@ const props = defineProps({
127
128
  default: "md",
128
129
  validator: (v) => ["sm", "md", "lg"].includes(v),
129
130
  },
131
+ /** Card corners. Set false for full-bleed pre-footer CTAs. */
132
+ rounded: {
133
+ type: Boolean,
134
+ default: true,
135
+ },
130
136
  });
131
137
 
132
138
  // Default bg values
@@ -201,23 +207,30 @@ const containerClasses = computed(() => {
201
207
  });
202
208
 
203
209
  const iconContainerClasses = computed(() => {
210
+ // Keep the badge square: flex parents (inline/default column layout)
211
+ // default to stretch on the cross axis and can squash fixed width/height.
212
+ const centered = ["stacked", "centered"].includes(props.variant);
204
213
  return [
205
- "flex items-center justify-center rounded-xl",
206
- props.size === "lg" ? "w-16 h-16" : "w-12 h-12",
214
+ "flex shrink-0 items-center justify-center rounded-xl aspect-square",
215
+ centered ? "self-center" : "self-start",
216
+ props.size === "lg"
217
+ ? "size-16 min-w-16 min-h-16"
218
+ : "size-12 min-w-12 min-h-12",
207
219
  isBoldBg.value ? "bg-white/20" : "bg-primary-100 dark:bg-primary-900/50",
208
220
  ];
209
221
  });
210
222
 
211
223
  const iconClasses = computed(() => {
212
224
  return [
213
- props.size === "lg" ? "w-8 h-8" : "w-6 h-6",
225
+ "shrink-0 block",
226
+ props.size === "lg" ? "size-8" : "size-6",
214
227
  isBoldBg.value ? "text-white" : "text-primary-500",
215
228
  ];
216
229
  });
217
230
 
218
231
  const contentClasses = computed(() => {
219
- if (props.variant === "default") {
220
- return "flex-1";
232
+ if (props.variant === "default" || props.variant === "inline") {
233
+ return "flex-1 min-w-0";
221
234
  }
222
235
  return "";
223
236
  });
@@ -93,10 +93,10 @@
93
93
  {{ category.description }}
94
94
  </p>
95
95
  </div>
96
- <UToggle
96
+ <USwitch
97
97
  :model-value="selections[category.id]"
98
98
  :disabled="category.required"
99
- @update:model-value="(val) => (selections[category.id] = val)"
99
+ @update:model-value="(val: boolean) => (selections[category.id] = val)"
100
100
  />
101
101
  </div>
102
102
  </div>
@@ -149,6 +149,13 @@
149
149
  <script setup lang="ts">
150
150
  type CategoryId = "necessary" | "analytics" | "marketing" | "preferences";
151
151
 
152
+ const DEFAULT_SELECTIONS: Record<CategoryId, boolean> = {
153
+ necessary: false,
154
+ analytics: false,
155
+ marketing: false,
156
+ preferences: false,
157
+ };
158
+
152
159
  interface Category {
153
160
  id: CategoryId;
154
161
  label: string;
@@ -251,17 +258,12 @@ const tracking = useConsentTracking();
251
258
  // - The visitor hasn't made a decision yet (no localStorage key).
252
259
  const bannerVisible = ref(false);
253
260
  const prefsOpen = ref(false);
254
- const selections = ref<Record<CategoryId, boolean>>({});
261
+ const selections = ref<Record<CategoryId, boolean>>({ ...DEFAULT_SELECTIONS });
255
262
 
256
263
  const effectiveCategories = computed(() => props.categories);
257
264
 
258
265
  function buildStateFromSelections(): Record<CategoryId, boolean> {
259
- const out: Record<CategoryId, boolean> = {
260
- necessary: true,
261
- analytics: false,
262
- marketing: false,
263
- preferences: false,
264
- };
266
+ const out: Record<CategoryId, boolean> = { ...DEFAULT_SELECTIONS };
265
267
  for (const c of props.categories) {
266
268
  out[c.id] = c.required ? true : Boolean(selections.value[c.id]);
267
269
  }
@@ -269,12 +271,7 @@ function buildStateFromSelections(): Record<CategoryId, boolean> {
269
271
  }
270
272
 
271
273
  function buildStateAll(value: boolean): Record<CategoryId, boolean> {
272
- const out: Record<CategoryId, boolean> = {
273
- necessary: true,
274
- analytics: false,
275
- marketing: false,
276
- preferences: false,
277
- };
274
+ const out: Record<CategoryId, boolean> = { ...DEFAULT_SELECTIONS };
278
275
  for (const c of props.categories) {
279
276
  out[c.id] = c.required ? true : value;
280
277
  }
@@ -282,11 +279,13 @@ function buildStateAll(value: boolean): Record<CategoryId, boolean> {
282
279
  }
283
280
 
284
281
  function initSelections() {
282
+ const next: Record<CategoryId, boolean> = { ...DEFAULT_SELECTIONS };
285
283
  for (const c of props.categories) {
286
- selections.value[c.id] = c.required
284
+ next[c.id] = c.required
287
285
  ? true
288
286
  : Boolean(c.enabledByDefault ?? false);
289
287
  }
288
+ selections.value = next;
290
289
  }
291
290
 
292
291
  function syncFromStored() {
@@ -47,7 +47,7 @@
47
47
  {{ category.description }}
48
48
  </p>
49
49
  </div>
50
- <UToggle
50
+ <USwitch
51
51
  v-model="selections[category.id]"
52
52
  :disabled="category.required"
53
53
  />