@xenterprises/nuxt-x-marketing 1.8.0 → 1.10.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 +29 -0
- package/app/app.config.ts +3 -0
- package/app/assets/css/x-marketing.css +9 -0
- package/app/components/X/Mark/Table/index.vue +120 -0
- package/app/components/X/X/Legal/index.vue +58 -0
- package/app/components/X/X/SkipLink/index.vue +11 -2
- package/app/components/content/XMarkProseTable.vue +24 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,35 @@ 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.10.0] - 2026-10-12
|
|
34
|
+
|
|
35
|
+
Additive; existing output (including markdown tables) is identical to 1.9.0.
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **`XMarkTable`**: `columns` (`[{ key, label, align? }]`), `rows`, `caption` (a real `<caption>`),
|
|
40
|
+
`container` (XMarkSection values), `striped`, `scrollLabel`, `rowKey`, and `#cell-<key>` slots.
|
|
41
|
+
The table sits in an `overflow-x-auto` wrapper that is a keyboard-focusable `role="region"`
|
|
42
|
+
named by `scrollLabel`, then `caption`, then "Table".
|
|
43
|
+
- **`XMarkProseTable`** (opt-in): Nuxt UI's ProseTable with its scroll wrapper made a labelled,
|
|
44
|
+
focusable region. Enable with `content: { renderer: { alias: { table: 'XMarkProseTable' } } }`.
|
|
45
|
+
|
|
46
|
+
## [1.9.0] - 2026-10-12
|
|
47
|
+
|
|
48
|
+
All additions are opt-in; with none of them used, rendered HTML and pixels are identical to 1.8.0.
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- **`XXLegal` affiliate disclosure** (`disclosure` prop, or `appConfig.xMarketing.footer.disclosure`),
|
|
53
|
+
shaped `{ before, link: { label, to }, after? }`. Renders one `<p>` above the copyright row with a
|
|
54
|
+
same-tab `NuxtLink` inline. Follows the 1.8.0 `container` prop and `xMarketing.container: '7xl'`.
|
|
55
|
+
`disclosure: false` suppresses an appConfig value on one instance.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- **`XXSkipLink`** now moves focus to the target on activation (adds `tabindex="-1"` when the
|
|
60
|
+
target is not focusable), so the next Tab continues inside main content. No style changes.
|
|
61
|
+
|
|
33
62
|
## [1.8.0] - 2026-10-12
|
|
34
63
|
|
|
35
64
|
All additions are opt-in; with none of them used, rendered HTML and pixels are identical to 1.7.0.
|
package/app/app.config.ts
CHANGED
|
@@ -57,6 +57,9 @@ export default defineAppConfig({
|
|
|
57
57
|
footer: {
|
|
58
58
|
/** Set false to opt out of the default footer in the shipped shell. */
|
|
59
59
|
active: true as boolean,
|
|
60
|
+
/** 1.9.0 opt-in affiliate disclosure rendered by XXLegal:
|
|
61
|
+
* { before: string, link: { label, to }, after?: string } */
|
|
62
|
+
disclosure: undefined as undefined | { before: string; link: { label: string; to: string }; after?: string },
|
|
60
63
|
logo: {
|
|
61
64
|
src: undefined,
|
|
62
65
|
alt: undefined,
|
|
@@ -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>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<footer
|
|
3
|
+
v-if="!resolvedDisclosure"
|
|
3
4
|
class="bg-gray-900 text-gray-400 text-xs relative overflow-visible pt-2"
|
|
4
5
|
>
|
|
5
6
|
<img
|
|
@@ -13,6 +14,40 @@
|
|
|
13
14
|
{{ resolvedCopyright }}
|
|
14
15
|
</div>
|
|
15
16
|
|
|
17
|
+
<nav
|
|
18
|
+
v-if="resolvedLinks.length"
|
|
19
|
+
class="text-center md:text-right flex flex-wrap justify-center gap-4"
|
|
20
|
+
>
|
|
21
|
+
<a
|
|
22
|
+
v-for="link in resolvedLinks"
|
|
23
|
+
:key="link.label"
|
|
24
|
+
:href="link.href"
|
|
25
|
+
class="hover:underline"
|
|
26
|
+
target="_blank"
|
|
27
|
+
>
|
|
28
|
+
{{ link.label }}
|
|
29
|
+
</a>
|
|
30
|
+
</nav>
|
|
31
|
+
</div>
|
|
32
|
+
</footer>
|
|
33
|
+
<footer
|
|
34
|
+
v-else
|
|
35
|
+
class="bg-gray-900 text-gray-400 text-xs relative overflow-visible pt-2"
|
|
36
|
+
>
|
|
37
|
+
<img
|
|
38
|
+
v-if="resolvedLogo.src"
|
|
39
|
+
:src="resolvedLogo.src"
|
|
40
|
+
:alt="resolvedLogo.alt"
|
|
41
|
+
class="h-6 w-6 mx-auto -mt-4 relative z-10"
|
|
42
|
+
/>
|
|
43
|
+
<p :class="disclosureClass" data-x-disclosure>
|
|
44
|
+
{{ resolvedDisclosure.before }}<NuxtLink :to="resolvedDisclosure.link.to" class="underline hover:text-white">{{ resolvedDisclosure.link.label }}</NuxtLink>{{ resolvedDisclosure.after }}
|
|
45
|
+
</p>
|
|
46
|
+
<div :class="innerClass">
|
|
47
|
+
<div class="text-center md:text-left">
|
|
48
|
+
{{ resolvedCopyright }}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
16
51
|
<nav
|
|
17
52
|
v-if="resolvedLinks.length"
|
|
18
53
|
class="text-center md:text-right flex flex-wrap justify-center gap-4"
|
|
@@ -58,6 +93,15 @@ const props = defineProps({
|
|
|
58
93
|
default: "",
|
|
59
94
|
validator: (v) => ["", "default", "lg"].includes(v),
|
|
60
95
|
},
|
|
96
|
+
/**
|
|
97
|
+
* Affiliate disclosure (1.9.0+, opt-in). { before, link: { label, to }, after? } rendered as one
|
|
98
|
+
* <p> with a same-tab NuxtLink inline, above the copyright row. Falls back to
|
|
99
|
+
* appConfig.xMarketing.footer.disclosure. Pass false to suppress the appConfig value.
|
|
100
|
+
*/
|
|
101
|
+
disclosure: {
|
|
102
|
+
type: [Object, Boolean],
|
|
103
|
+
default: null,
|
|
104
|
+
},
|
|
61
105
|
/** Logo object { src, alt } (defaults to X Enterprises logo). Pass false to hide. href is ignored; logo is display-only. */
|
|
62
106
|
logo: {
|
|
63
107
|
type: [Object, Boolean],
|
|
@@ -73,6 +117,20 @@ const innerClass = computed(() => {
|
|
|
73
117
|
: "flex flex-col md:flex-row justify-between items-center px-6 pt-3 pb-5 max-w-screen-2xl mx-auto gap-4";
|
|
74
118
|
});
|
|
75
119
|
|
|
120
|
+
const isDisclosure = (d) =>
|
|
121
|
+
!!d && typeof d === "object" && typeof d.before === "string" && !!d.link && !!d.link.label && !!d.link.to;
|
|
122
|
+
const resolvedDisclosure = computed(() => {
|
|
123
|
+
if (props.disclosure === false) return null;
|
|
124
|
+
const d = props.disclosure || appConfig?.xMarketing?.footer?.disclosure;
|
|
125
|
+
return isDisclosure(d) ? { before: d.before, link: d.link, after: d.after || "" } : null;
|
|
126
|
+
});
|
|
127
|
+
const disclosureClass = computed(() => {
|
|
128
|
+
const c = props.container || (appConfig?.xMarketing?.container === "7xl" ? "lg" : "default");
|
|
129
|
+
return c === "lg"
|
|
130
|
+
? "px-4 sm:px-6 lg:px-8 pt-3 max-w-7xl mx-auto text-center md:text-left"
|
|
131
|
+
: "px-6 pt-3 max-w-screen-2xl mx-auto text-center md:text-left";
|
|
132
|
+
});
|
|
133
|
+
|
|
76
134
|
// Default X Enterprises values
|
|
77
135
|
const defaultLogo = {
|
|
78
136
|
src: "https://cdn.x.enterprises/logo-x.png",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<a :href="`#${target}`" class="x-skip-link">{{ label }}</a>
|
|
2
|
+
<a :href="`#${target}`" class="x-skip-link" @click="onActivate">{{ label }}</a>
|
|
3
3
|
</template>
|
|
4
4
|
|
|
5
5
|
<script setup>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Visually hidden until focused (first Tab), then shown top-left.
|
|
9
9
|
* Pair with <main id="main"> (or set target).
|
|
10
10
|
*/
|
|
11
|
-
defineProps({
|
|
11
|
+
const props = defineProps({
|
|
12
12
|
/** id of the main content element, without # */
|
|
13
13
|
target: {
|
|
14
14
|
type: String,
|
|
@@ -20,4 +20,13 @@ defineProps({
|
|
|
20
20
|
default: "Skip to content",
|
|
21
21
|
},
|
|
22
22
|
});
|
|
23
|
+
|
|
24
|
+
/** 1.9.0: move focus to the target so the next Tab continues from main content. */
|
|
25
|
+
function onActivate() {
|
|
26
|
+
if (typeof document === "undefined") return;
|
|
27
|
+
const el = document.getElementById(props.target);
|
|
28
|
+
if (!el) return;
|
|
29
|
+
if (!el.hasAttribute("tabindex") && el.tabIndex < 0) el.setAttribute("tabindex", "-1");
|
|
30
|
+
el.focus();
|
|
31
|
+
}
|
|
23
32
|
</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>
|