@xenterprises/nuxt-x-marketing 1.9.0 → 1.11.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,33 @@ 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.11.0] - 2026-10-12
|
|
34
|
+
|
|
35
|
+
Additive; menus with only flat children render byte-identical to 1.10.0.
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **`XXHeaderNav` grouped dropdowns**: an item's `children` may be
|
|
40
|
+
`[{ heading, links: [{ label, to, target? }] }]`. Desktop renders each group as a
|
|
41
|
+
`role="group"` column labelled by its heading inside the Nuxt UI dropdown (Enter/Space opens,
|
|
42
|
+
Tab moves through links, Escape closes and returns focus). Mobile renders each group as a
|
|
43
|
+
nested, open-by-default section (heading button with `aria-expanded`, links in a labelled region).
|
|
44
|
+
When groups are present the dropdown panel sizes to its content and flat sibling dropdowns get a
|
|
45
|
+
16rem panel.
|
|
46
|
+
|
|
47
|
+
## [1.10.0] - 2026-10-12
|
|
48
|
+
|
|
49
|
+
Additive; existing output (including markdown tables) is identical to 1.9.0.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- **`XMarkTable`**: `columns` (`[{ key, label, align? }]`), `rows`, `caption` (a real `<caption>`),
|
|
54
|
+
`container` (XMarkSection values), `striped`, `scrollLabel`, `rowKey`, and `#cell-<key>` slots.
|
|
55
|
+
The table sits in an `overflow-x-auto` wrapper that is a keyboard-focusable `role="region"`
|
|
56
|
+
named by `scrollLabel`, then `caption`, then "Table".
|
|
57
|
+
- **`XMarkProseTable`** (opt-in): Nuxt UI's ProseTable with its scroll wrapper made a labelled,
|
|
58
|
+
focusable region. Enable with `content: { renderer: { alias: { table: 'XMarkProseTable' } } }`.
|
|
59
|
+
|
|
33
60
|
## [1.9.0] - 2026-10-12
|
|
34
61
|
|
|
35
62
|
All additions are opt-in; with none of them used, rendered HTML and pixels are identical to 1.8.0.
|
|
@@ -724,3 +724,12 @@ html.x-container-7xl {
|
|
|
724
724
|
outline: 2px solid #111;
|
|
725
725
|
outline-offset: 2px;
|
|
726
726
|
}
|
|
727
|
+
|
|
728
|
+
/* 1.10.0: XMarkTable / XMarkProseTable scroll region focus ring (keyboard only) */
|
|
729
|
+
.x-table-scroll:focus-visible {
|
|
730
|
+
outline: 2px solid #111827;
|
|
731
|
+
outline-offset: 2px;
|
|
732
|
+
}
|
|
733
|
+
.dark .x-table-scroll:focus-visible {
|
|
734
|
+
outline-color: #ffffff;
|
|
735
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="containerClasses">
|
|
3
|
+
<div
|
|
4
|
+
class="x-table-scroll overflow-x-auto"
|
|
5
|
+
role="region"
|
|
6
|
+
tabindex="0"
|
|
7
|
+
:aria-label="regionLabel"
|
|
8
|
+
>
|
|
9
|
+
<table class="w-full border-collapse text-left text-sm text-neutral-700 dark:text-neutral-300">
|
|
10
|
+
<caption
|
|
11
|
+
v-if="caption"
|
|
12
|
+
class="caption-top pb-3 text-left text-sm font-medium text-neutral-900 dark:text-white"
|
|
13
|
+
>
|
|
14
|
+
{{ caption }}
|
|
15
|
+
</caption>
|
|
16
|
+
<thead>
|
|
17
|
+
<tr class="border-b border-neutral-300 dark:border-neutral-700">
|
|
18
|
+
<th
|
|
19
|
+
v-for="col in columns"
|
|
20
|
+
:key="col.key"
|
|
21
|
+
scope="col"
|
|
22
|
+
class="whitespace-nowrap px-4 py-3 font-semibold text-neutral-900 dark:text-white"
|
|
23
|
+
:class="alignClass(col.align)"
|
|
24
|
+
>
|
|
25
|
+
{{ col.label }}
|
|
26
|
+
</th>
|
|
27
|
+
</tr>
|
|
28
|
+
</thead>
|
|
29
|
+
<tbody>
|
|
30
|
+
<tr
|
|
31
|
+
v-for="(row, i) in rows"
|
|
32
|
+
:key="rowKey ? row[rowKey] : i"
|
|
33
|
+
class="border-b border-neutral-200 dark:border-neutral-800"
|
|
34
|
+
:class="striped && i % 2 === 1 ? 'bg-neutral-50 dark:bg-neutral-900' : ''"
|
|
35
|
+
>
|
|
36
|
+
<td
|
|
37
|
+
v-for="col in columns"
|
|
38
|
+
:key="col.key"
|
|
39
|
+
class="px-4 py-3 align-top"
|
|
40
|
+
:class="alignClass(col.align)"
|
|
41
|
+
>
|
|
42
|
+
<slot
|
|
43
|
+
:name="`cell-${col.key}`"
|
|
44
|
+
:row="row"
|
|
45
|
+
:value="row[col.key]"
|
|
46
|
+
>
|
|
47
|
+
{{ row[col.key] }}
|
|
48
|
+
</slot>
|
|
49
|
+
</td>
|
|
50
|
+
</tr>
|
|
51
|
+
</tbody>
|
|
52
|
+
</table>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</template>
|
|
56
|
+
|
|
57
|
+
<script setup>
|
|
58
|
+
/**
|
|
59
|
+
* XMarkTable (1.10.0) - simple data table for marketing pages.
|
|
60
|
+
* The scroll wrapper is a labelled, keyboard-focusable region so wide tables can be
|
|
61
|
+
* scrolled with arrow keys on small screens.
|
|
62
|
+
*/
|
|
63
|
+
const props = defineProps({
|
|
64
|
+
/** [{ key, label, align?: 'left' | 'center' | 'right' }] */
|
|
65
|
+
columns: {
|
|
66
|
+
type: Array,
|
|
67
|
+
required: true,
|
|
68
|
+
validator: (v) => Array.isArray(v) && v.every((c) => c && typeof c.key === "string" && typeof c.label === "string"),
|
|
69
|
+
},
|
|
70
|
+
/** Array of plain objects keyed by column key */
|
|
71
|
+
rows: {
|
|
72
|
+
type: Array,
|
|
73
|
+
default: () => [],
|
|
74
|
+
},
|
|
75
|
+
/** Rendered as a real <caption>; also names the scroll region */
|
|
76
|
+
caption: {
|
|
77
|
+
type: String,
|
|
78
|
+
default: "",
|
|
79
|
+
},
|
|
80
|
+
/** Same values and classes as XMarkSection container */
|
|
81
|
+
container: {
|
|
82
|
+
type: String,
|
|
83
|
+
default: "lg",
|
|
84
|
+
validator: (v) => ["sm", "md", "lg", "xl", "full", "none"].includes(v),
|
|
85
|
+
},
|
|
86
|
+
/** Zebra rows */
|
|
87
|
+
striped: {
|
|
88
|
+
type: Boolean,
|
|
89
|
+
default: false,
|
|
90
|
+
},
|
|
91
|
+
/** Accessible name for the scroll region; defaults to caption, then "Table" */
|
|
92
|
+
scrollLabel: {
|
|
93
|
+
type: String,
|
|
94
|
+
default: "",
|
|
95
|
+
},
|
|
96
|
+
/** Optional row field used as the v-for key */
|
|
97
|
+
rowKey: {
|
|
98
|
+
type: String,
|
|
99
|
+
default: "",
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const containerClasses = computed(() => {
|
|
104
|
+
const containers = {
|
|
105
|
+
sm: "max-w-[42rem] mx-auto px-4 sm:px-6",
|
|
106
|
+
md: "max-w-[64rem] mx-auto px-4 sm:px-6 lg:px-8",
|
|
107
|
+
lg: "max-w-[80rem] mx-auto px-4 sm:px-6 lg:px-8",
|
|
108
|
+
xl: "max-w-[96rem] mx-auto px-4 sm:px-6 lg:px-8",
|
|
109
|
+
full: "w-full px-4 sm:px-6 lg:px-8",
|
|
110
|
+
none: "",
|
|
111
|
+
};
|
|
112
|
+
return containers[props.container];
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const regionLabel = computed(() => props.scrollLabel || props.caption || "Table");
|
|
116
|
+
|
|
117
|
+
function alignClass(a) {
|
|
118
|
+
return a === "center" ? "text-center" : a === "right" ? "text-right" : "text-left";
|
|
119
|
+
}
|
|
120
|
+
</script>
|
|
@@ -16,7 +16,45 @@
|
|
|
16
16
|
<span v-else>{{ header?.logo?.alt }}</span>
|
|
17
17
|
</template>
|
|
18
18
|
|
|
19
|
-
<UNavigationMenu
|
|
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="
|
|
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>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ProseTable
|
|
3
|
+
role="region"
|
|
4
|
+
tabindex="0"
|
|
5
|
+
:aria-label="label"
|
|
6
|
+
>
|
|
7
|
+
<slot />
|
|
8
|
+
</ProseTable>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup>
|
|
12
|
+
/**
|
|
13
|
+
* XMarkProseTable (1.10.0, opt-in) - Nuxt UI's ProseTable (same look, same overflow-x-auto
|
|
14
|
+
* wrapper) with the wrapper made a labelled, keyboard-focusable region.
|
|
15
|
+
* Not used unless mapped: nuxt.config `content: { renderer: { alias: { table: 'XMarkProseTable' } } }`.
|
|
16
|
+
*/
|
|
17
|
+
defineProps({
|
|
18
|
+
/** Accessible name of the scroll region */
|
|
19
|
+
label: {
|
|
20
|
+
type: String,
|
|
21
|
+
default: "Scrollable table",
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
</script>
|