@xenterprises/nuxt-x-marketing 1.10.0 → 1.12.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,31 @@ 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.12.0] - 2026-10-13
34
+
35
+ Opt-in; the default (`aspectRatio="16/9"`, cropped with object-cover) renders byte-identical to 1.11.0.
36
+
37
+ ### Added
38
+
39
+ - **`XMarkMedia aspectRatio="auto"`**: the image renders at its natural ratio with no crop
40
+ (`block w-full h-auto`, no aspect-ratio style, no object-cover, no absolute positioning).
41
+ `img.width` / `img.height` are passed through as attributes in this mode to reserve space.
42
+ `aspect="auto"` is accepted as an alias (it only takes `"auto"`).
43
+
44
+ ## [1.11.0] - 2026-10-12
45
+
46
+ Additive; menus with only flat children render byte-identical to 1.10.0.
47
+
48
+ ### Added
49
+
50
+ - **`XXHeaderNav` grouped dropdowns**: an item's `children` may be
51
+ `[{ heading, links: [{ label, to, target? }] }]`. Desktop renders each group as a
52
+ `role="group"` column labelled by its heading inside the Nuxt UI dropdown (Enter/Space opens,
53
+ Tab moves through links, Escape closes and returns focus). Mobile renders each group as a
54
+ nested, open-by-default section (heading button with `aria-expanded`, links in a labelled region).
55
+ When groups are present the dropdown panel sizes to its content and flat sibling dropdowns get a
56
+ 16rem panel.
57
+
33
58
  ## [1.10.0] - 2026-10-12
34
59
 
35
60
  Additive; existing output (including markdown tables) is identical to 1.9.0.
@@ -2,6 +2,40 @@
2
2
  <div :class="containerClasses">
3
3
  <figure class="m-0">
4
4
  <div
5
+ v-if="isAuto"
6
+ class="relative w-full overflow-hidden"
7
+ :class="roundedClass"
8
+ >
9
+ <picture v-if="img?.sources?.length">
10
+ <source
11
+ v-for="(source, i) in img.sources"
12
+ :key="i"
13
+ :srcset="source.srcset"
14
+ :type="source.type"
15
+ :media="source.media"
16
+ :sizes="source.sizes"
17
+ >
18
+ <img
19
+ :src="img.src"
20
+ :alt="img.alt || ''"
21
+ :width="img.width"
22
+ :height="img.height"
23
+ :loading="loading"
24
+ class="block w-full h-auto"
25
+ >
26
+ </picture>
27
+ <img
28
+ v-else
29
+ :src="img.src"
30
+ :alt="img.alt || ''"
31
+ :width="img.width"
32
+ :height="img.height"
33
+ :loading="loading"
34
+ class="block w-full h-auto"
35
+ >
36
+ </div>
37
+ <div
38
+ v-else
5
39
  class="relative w-full overflow-hidden"
6
40
  :class="roundedClass"
7
41
  :style="aspectStyle"
@@ -59,11 +93,20 @@ const props = defineProps({
59
93
  (Array.isArray(v.sources) &&
60
94
  v.sources.every((x) => x && typeof x.srcset === "string"))),
61
95
  },
62
- /** CSS aspect-ratio, e.g. "16/9", "21/9", "4/3" */
96
+ /**
97
+ * CSS aspect-ratio, e.g. "16/9", "21/9", "4/3" (image is cropped to fill, object-cover).
98
+ * 1.12.0: "auto" shows the image at its natural ratio with no crop (w-full h-auto).
99
+ */
63
100
  aspectRatio: {
64
101
  type: String,
65
102
  default: "16/9",
66
- validator: (v) => /^\d+(\.\d+)?\s*\/\s*\d+(\.\d+)?$/.test(v),
103
+ validator: (v) => v === "auto" || /^\d+(\.\d+)?\s*\/\s*\d+(\.\d+)?$/.test(v),
104
+ },
105
+ /** 1.12.0 alias: aspect="auto" is the same as aspectRatio="auto" (only "auto" is accepted). */
106
+ aspect: {
107
+ type: String,
108
+ default: "",
109
+ validator: (v) => v === "" || v === "auto",
67
110
  },
68
111
  /** Same values and classes as XMarkSection container */
69
112
  container: {
@@ -112,6 +155,8 @@ const roundedClass = computed(() => {
112
155
  return r[props.rounded];
113
156
  });
114
157
 
158
+ const isAuto = computed(() => props.aspectRatio === "auto" || props.aspect === "auto");
159
+
115
160
  const aspectStyle = computed(() => ({ aspectRatio: props.aspectRatio.replace(/\s+/g, " ") }));
116
161
 
117
162
  const positionStyle = computed(() =>
@@ -16,7 +16,45 @@
16
16
  <span v-else>{{ header?.logo?.alt }}</span>
17
17
  </template>
18
18
 
19
- <UNavigationMenu :items="links" />
19
+ <UNavigationMenu
20
+ :items="desktopLinks"
21
+ :ui="desktopUi"
22
+ >
23
+ <template #x-nav-groups-content="{ item }">
24
+ <div
25
+ class="grid gap-6 p-4"
26
+ :class="item.xGroupsGrid"
27
+ >
28
+ <div
29
+ v-for="(group, gi) in item.xGroups"
30
+ :key="gi"
31
+ role="group"
32
+ :aria-labelledby="`${item.xGroupsId}-${gi}`"
33
+ >
34
+ <p
35
+ :id="`${item.xGroupsId}-${gi}`"
36
+ class="mb-2 px-2 text-xs font-semibold uppercase tracking-wide text-muted"
37
+ >
38
+ {{ group.heading }}
39
+ </p>
40
+ <ul class="space-y-0.5">
41
+ <li
42
+ v-for="(link, li) in group.links"
43
+ :key="li"
44
+ >
45
+ <ULink
46
+ :to="link.to"
47
+ :target="link.target"
48
+ class="block rounded-md px-2 py-1.5 text-sm text-default hover:bg-elevated focus-visible:outline-2 focus-visible:outline-primary"
49
+ >
50
+ {{ link.label }}
51
+ </ULink>
52
+ </li>
53
+ </ul>
54
+ </div>
55
+ </div>
56
+ </template>
57
+ </UNavigationMenu>
20
58
 
21
59
  <template #right>
22
60
  <span v-for="(button, index) in header?.nav?.buttons" :key="index">
@@ -33,7 +71,7 @@
33
71
  </template>
34
72
 
35
73
  <template #body>
36
- <UNavigationMenu orientation="vertical" :items="links" class="-mx-2.5" />
74
+ <UNavigationMenu orientation="vertical" :items="mobileLinks" class="-mx-2.5" />
37
75
  <div class="mt-5 space-y-2">
38
76
  <UButton
39
77
  v-for="(button, index) in header?.nav?.buttons"
@@ -61,4 +99,50 @@
61
99
  const appConfig = useAppConfig();
62
100
  const header = appConfig?.xMarketing?.header;
63
101
  const links = header?.nav?.links || [];
102
+
103
+ /**
104
+ * 1.11.0: grouped dropdowns. An item's children may be [{ heading, links: [{ label, to }] }].
105
+ * Desktop renders labelled role="group" columns in the dropdown; mobile renders each group
106
+ * as a nested, open-by-default section. Flat children are passed through untouched.
107
+ */
108
+ const isGrouped = (item) =>
109
+ Array.isArray(item?.children) && item.children.length > 0 && item.children.every((c) => c && Array.isArray(c.links));
110
+ const hasGroups = links.some(isGrouped);
111
+ const gridCols = { 1: "", 2: "sm:grid-cols-2 w-[32rem]", 3: "sm:grid-cols-3 w-[44rem]", 4: "sm:grid-cols-4 w-[56rem]" };
112
+ const desktopLinks = hasGroups
113
+ ? links.map((item, i) => {
114
+ if (!isGrouped(item)) {
115
+ // flat dropdowns next to grouped ones get a fixed panel width (the viewport now sizes to content)
116
+ return item.children?.length ? { ...item, ui: { content: "sm:w-64", ...item.ui } } : item;
117
+ }
118
+ const { children, ...rest } = item;
119
+ return {
120
+ ...rest,
121
+ slot: "x-nav-groups",
122
+ xGroups: children,
123
+ xGroupsId: `x-nav-group-${i}`,
124
+ xGroupsGrid: gridCols[Math.min(children.length, 4)] || "w-64",
125
+ ui: { content: "sm:w-auto", ...item.ui },
126
+ };
127
+ })
128
+ : links;
129
+ // Grouped panels are wider than the nav bar, so let the viewport size to its content
130
+ // (only when groups exist; flat-only menus get ui undefined and render exactly as before).
131
+ const desktopUi = hasGroups
132
+ ? { viewport: "sm:w-(--reka-navigation-menu-viewport-width) shrink-0" }
133
+ : undefined;
134
+ const mobileLinks = hasGroups
135
+ ? links.map((item) =>
136
+ isGrouped(item)
137
+ ? {
138
+ ...item,
139
+ children: item.children.map((g) => ({
140
+ label: g.heading,
141
+ defaultOpen: true,
142
+ children: g.links,
143
+ })),
144
+ }
145
+ : item,
146
+ )
147
+ : links;
64
148
  </script>
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.10.0",
5
+ "version": "1.12.0",
6
6
  "main": "./nuxt.config.ts",
7
7
  "scripts": {
8
8
  "dev": "nuxi dev .playground",