@xenterprises/nuxt-x-marketing 1.4.14 → 1.5.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/CHANGELOG.md CHANGED
@@ -30,6 +30,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
30
30
  section and its sample product data were dropped with them. Every other section of the
31
31
  demo page is unchanged.
32
32
 
33
+ ## [1.5.0] - 2026-10-12
34
+
35
+ ### Added
36
+
37
+ - `XMarkHero` `overlay="heavier"`: flat 75% black (title about 8:1 on the worst measured desk-lamps image area, versus about 5.9:1 with `heavy`).
38
+ - `XMarkHero` `overlay="scrim-left"` and `overlay="scrim-bottom"`: 90% black where the text sits, fading to 30%. White title holds 7:1 (subtitle 5.2:1) even over pure white image areas. `scrim-left` is a full 90% below `md`.
39
+ - `XMarkHero` `height` prop: `full` (default, unchanged), `tall` (75svh), `short` (50svh).
40
+ - `XMarkHero` `imagePosition` prop: CSS `object-position` for the background image or video, e.g. `left center`.
41
+
42
+ Existing values are unchanged: `heavy` stays 60%, and the default height is still full viewport.
43
+
44
+ ## [1.4.15] - 2026-10-12
45
+
46
+ ### Fixed
47
+
48
+ - Scroll reveal never initialized from the global `marketing.client.ts` plugin. `useScrollReveal()` returned a no-op stub outside a component instance, so `.xFadeUp` / `.xFadeIn` / `[data-reveal]` content (including hero text) stayed at opacity 0. The composable now returns working `initScrollReveal` / `cleanup` outside components (lifecycle hooks are still only registered inside one), and the plugin runs it on `page:finish`, re-scanning on every client navigation. Consumers who added an `app.vue` workaround can remove it.
49
+
33
50
  ## [1.4.14] - 2026-09-06
34
51
 
35
52
  ### Added
@@ -1,7 +1,8 @@
1
1
  <template>
2
2
  <section
3
- class="relative min-h-[calc(100svh-var(--ui-header-height))] flex overflow-hidden"
3
+ class="relative flex overflow-hidden"
4
4
  :class="[
5
+ heightClass,
5
6
  alignmentClasses,
6
7
  verticalAlignClasses,
7
8
  { 'pt-20': includeNavPadding },
@@ -21,6 +22,7 @@
21
22
  muted
22
23
  playsinline
23
24
  class="absolute inset-0 w-full h-full object-cover xParallax"
25
+ :style="imagePositionStyle"
24
26
  data-parallax-speed="0.3"
25
27
  />
26
28
 
@@ -31,6 +33,7 @@
31
33
  :alt="img.alt || ''"
32
34
  fetchpriority="high"
33
35
  class="absolute inset-0 w-full h-full object-cover xParallax"
36
+ :style="imagePositionStyle"
34
37
  data-parallax-speed="0.3"
35
38
  />
36
39
  </slot>
@@ -137,7 +140,15 @@ const props = defineProps({
137
140
  overlay: {
138
141
  type: String,
139
142
  default: "gradient",
140
- validator: (v) => ["light", "heavy", "gradient", "none"].includes(v),
143
+ validator: (v) => [
144
+ "light",
145
+ "heavy",
146
+ "heavier",
147
+ "gradient",
148
+ "scrim-left",
149
+ "scrim-bottom",
150
+ "none",
151
+ ].includes(v),
141
152
  },
142
153
  /** Small text above title */
143
154
  eyebrow: {
@@ -177,6 +188,17 @@ const props = defineProps({
177
188
  type: Boolean,
178
189
  default: true,
179
190
  },
191
+ /** Section height: full (viewport minus header, default), tall (75svh), short (50svh) */
192
+ height: {
193
+ type: String,
194
+ default: "full",
195
+ validator: (v) => ["full", "tall", "short"].includes(v),
196
+ },
197
+ /** CSS object-position for the background image/video, e.g. "left center" */
198
+ imagePosition: {
199
+ type: String,
200
+ default: "",
201
+ },
180
202
  /** Add top padding for non-transparent nav */
181
203
  includeNavPadding: {
182
204
  type: Boolean,
@@ -188,11 +210,33 @@ const overlayClass = computed(() => {
188
210
  const overlays = {
189
211
  light: "bg-black/30",
190
212
  heavy: "bg-black/60",
213
+ // 75% flat: white title >= 8:1 on image areas up to ~0.32 relative luminance
214
+ heavier: "bg-black/75",
191
215
  gradient: "bg-gradient-to-t from-black/80 via-black/40 to-transparent",
216
+ // 90% where text sits: white title >= 7:1 even over pure white image areas.
217
+ // Full 90% on small screens where text spans the width.
218
+ "scrim-left":
219
+ "bg-black/90 md:bg-transparent md:bg-gradient-to-r md:from-black/90 md:via-black/90 md:via-55% md:to-black/30",
220
+ // 90% across the bottom half; pair with verticalAlign="bottom"
221
+ "scrim-bottom":
222
+ "bg-gradient-to-t from-black/90 via-black/90 via-50% to-black/30",
192
223
  };
193
224
  return overlays[props.overlay] || "";
194
225
  });
195
226
 
227
+ const heightClass = computed(() => {
228
+ const heights = {
229
+ full: "min-h-[calc(100svh-var(--ui-header-height))]",
230
+ tall: "min-h-[75svh]",
231
+ short: "min-h-[50svh]",
232
+ };
233
+ return heights[props.height] || heights.full;
234
+ });
235
+
236
+ const imagePositionStyle = computed(() =>
237
+ props.imagePosition ? { objectPosition: props.imagePosition } : undefined,
238
+ );
239
+
196
240
  const alignmentClasses = computed(() => {
197
241
  const aligns = {
198
242
  left: "text-left",
@@ -9,9 +9,9 @@
9
9
  * - .xFadeUp-stagger for staggered child animations
10
10
  */
11
11
  export const useScrollReveal = (options = {}) => {
12
- // Only register lifecycle hooks when inside a component instance
12
+ // Lifecycle hooks are only registered inside a component instance.
13
+ // Outside one (e.g. a plugin) the caller drives initScrollReveal/cleanup itself.
13
14
  const instance = getCurrentInstance();
14
- if (!instance) return { initScrollReveal: () => {}, cleanup: () => {} };
15
15
 
16
16
  const defaultOptions = {
17
17
  threshold: 0.15,
@@ -33,7 +33,8 @@ export const useScrollReveal = (options = {}) => {
33
33
  ].join(', ');
34
34
 
35
35
  const initScrollReveal = () => {
36
- if (import.meta.client) {
36
+ if (import.meta.client && typeof IntersectionObserver !== 'undefined') {
37
+ if (observer.value) observer.value.disconnect();
37
38
  observer.value = new IntersectionObserver((entries) => {
38
39
  entries.forEach((entry) => {
39
40
  if (entry.isIntersecting) {
@@ -62,7 +63,7 @@ export const useScrollReveal = (options = {}) => {
62
63
  }
63
64
  };
64
65
 
65
- if (import.meta.client) {
66
+ if (import.meta.client && instance) {
66
67
  onMounted(() => {
67
68
  // Small delay to ensure DOM is ready
68
69
  setTimeout(initScrollReveal, 100);
@@ -4,9 +4,15 @@
4
4
  *
5
5
  * From spec-prompt.md specification
6
6
  */
7
- export default defineNuxtPlugin(() => {
8
- // Initialize scroll reveal globally
9
- useScrollReveal();
7
+ export default defineNuxtPlugin((nuxtApp) => {
8
+ // Initialize scroll reveal globally. A plugin has no component instance,
9
+ // so drive the observer from page lifecycle hooks: initial hydration and
10
+ // every client-side navigation re-scan the DOM for reveal elements.
11
+ const { initScrollReveal, cleanup } = useScrollReveal();
12
+ nuxtApp.hook('page:finish', () => {
13
+ cleanup();
14
+ nextTick(() => setTimeout(initScrollReveal, 100));
15
+ });
10
16
 
11
17
  // Initialize parallax globally
12
18
  useParallax();
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@xenterprises/nuxt-x-marketing",
3
3
  "license": "SEE LICENSE IN LICENSE",
4
4
  "type": "module",
5
- "version": "1.4.14",
5
+ "version": "1.5.0",
6
6
  "main": "./nuxt.config.ts",
7
7
  "scripts": {
8
8
  "dev": "nuxi dev .playground",