@duffcloudservices/cms 0.11.0 → 0.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/dist/editor/editorBridge.d.ts +41 -1
- package/dist/editor/editorBridge.js +68 -2
- package/dist/editor/editorBridge.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +85 -2
- package/dist/plugins/index.js +100 -7
- package/dist/plugins/index.js.map +1 -1
- package/package.json +12 -13
- package/src/components/DcsReviewShowcase.vue +5 -1
- package/src/components/ManagedImage.test.ts +60 -0
- package/src/components/ManagedImage.vue +53 -6
- package/src/composables/useResponsiveImage.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duffcloudservices/cms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Vue 3 composables and Vite plugins for DCS CMS integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -46,15 +46,6 @@
|
|
|
46
46
|
"src/components",
|
|
47
47
|
"src/composables"
|
|
48
48
|
],
|
|
49
|
-
"scripts": {
|
|
50
|
-
"build": "tsup",
|
|
51
|
-
"dev": "tsup --watch",
|
|
52
|
-
"test": "vitest run",
|
|
53
|
-
"test:watch": "vitest",
|
|
54
|
-
"type-check": "tsc --noEmit",
|
|
55
|
-
"lint": "eslint src --ext .ts",
|
|
56
|
-
"prepublishOnly": "pnpm run build"
|
|
57
|
-
},
|
|
58
49
|
"peerDependencies": {
|
|
59
50
|
"vue": "^3.4.0",
|
|
60
51
|
"@unhead/vue": "^1.9.0",
|
|
@@ -66,8 +57,8 @@
|
|
|
66
57
|
}
|
|
67
58
|
},
|
|
68
59
|
"dependencies": {
|
|
69
|
-
"
|
|
70
|
-
"
|
|
60
|
+
"js-yaml": "^4.1.0",
|
|
61
|
+
"@duffcloudservices/cms-core": "0.7.0"
|
|
71
62
|
},
|
|
72
63
|
"devDependencies": {
|
|
73
64
|
"@types/js-yaml": "^4.0.9",
|
|
@@ -106,5 +97,13 @@
|
|
|
106
97
|
},
|
|
107
98
|
"publishConfig": {
|
|
108
99
|
"access": "public"
|
|
100
|
+
},
|
|
101
|
+
"scripts": {
|
|
102
|
+
"build": "tsup",
|
|
103
|
+
"dev": "tsup --watch",
|
|
104
|
+
"test": "vitest run",
|
|
105
|
+
"test:watch": "vitest",
|
|
106
|
+
"type-check": "tsc --noEmit",
|
|
107
|
+
"lint": "eslint src --ext .ts"
|
|
109
108
|
}
|
|
110
|
-
}
|
|
109
|
+
}
|
|
@@ -148,7 +148,11 @@ function isEditorContext(): boolean {
|
|
|
148
148
|
</div>
|
|
149
149
|
</div>
|
|
150
150
|
|
|
151
|
-
<div
|
|
151
|
+
<div
|
|
152
|
+
class="dcs-review-card__rating"
|
|
153
|
+
role="img"
|
|
154
|
+
:aria-label="`${review.rating} out of 5 stars`"
|
|
155
|
+
>
|
|
152
156
|
<svg
|
|
153
157
|
v-for="star in 5"
|
|
154
158
|
:key="star"
|
|
@@ -73,3 +73,63 @@ describe('ManagedImage self-healing fallback', () => {
|
|
|
73
73
|
expect(wrapper.find('img').attributes('src')).toBe(ORIGINAL)
|
|
74
74
|
})
|
|
75
75
|
})
|
|
76
|
+
|
|
77
|
+
describe('ManagedImage layout-shift (CLS) dimension hints', () => {
|
|
78
|
+
it('emits width/height on the <img> when explicit dimensions are provided', () => {
|
|
79
|
+
const wrapper = mount(ManagedImage, {
|
|
80
|
+
props: {
|
|
81
|
+
pageSlug: 'home',
|
|
82
|
+
imageKey: 'hero.image',
|
|
83
|
+
fallbackSrc: ORIGINAL,
|
|
84
|
+
fallbackAlt: 'Hero',
|
|
85
|
+
context: 'hero',
|
|
86
|
+
width: 1920,
|
|
87
|
+
height: 1080,
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const img = wrapper.find('img')
|
|
92
|
+
expect(img.attributes('width')).toBe('1920')
|
|
93
|
+
expect(img.attributes('height')).toBe('1080')
|
|
94
|
+
// Hero context also gets the LCP priority hint.
|
|
95
|
+
expect(img.attributes('fetchpriority')).toBe('high')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('emits NO width/height when dimensions are unknown (never fabricated)', () => {
|
|
99
|
+
const wrapper = mount(ManagedImage, {
|
|
100
|
+
props: {
|
|
101
|
+
pageSlug: 'home',
|
|
102
|
+
imageKey: 'hero.image',
|
|
103
|
+
fallbackSrc: ORIGINAL,
|
|
104
|
+
fallbackAlt: 'Hero',
|
|
105
|
+
context: 'hero',
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const img = wrapper.find('img')
|
|
110
|
+
expect(img.attributes('width')).toBeUndefined()
|
|
111
|
+
expect(img.attributes('height')).toBeUndefined()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('reads .src/.width/.height from a D1-A dot-suffix imageBase and targets the .src key for the VE', () => {
|
|
115
|
+
const wrapper = mount(ManagedImage, {
|
|
116
|
+
props: {
|
|
117
|
+
pageSlug: 'home',
|
|
118
|
+
imageBase: 'hero.image',
|
|
119
|
+
fallbackSrc: ORIGINAL,
|
|
120
|
+
fallbackAlt: 'Hero',
|
|
121
|
+
context: 'hero',
|
|
122
|
+
width: 1600,
|
|
123
|
+
height: 900,
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const img = wrapper.find('img')
|
|
128
|
+
// Dimensions flow through to the CLS hint.
|
|
129
|
+
expect(img.attributes('width')).toBe('1600')
|
|
130
|
+
expect(img.attributes('height')).toBe('900')
|
|
131
|
+
// The VE stages replacements against the `<base>.src` key.
|
|
132
|
+
expect(img.attributes('data-dcs-image-key')).toBe('hero.image.src')
|
|
133
|
+
expect(img.attributes('data-dcs-image-url')).toBe(ORIGINAL)
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -8,38 +8,82 @@ defineOptions({ inheritAttrs: false })
|
|
|
8
8
|
|
|
9
9
|
const props = withDefaults(defineProps<{
|
|
10
10
|
pageSlug: string
|
|
11
|
-
imageKey
|
|
11
|
+
imageKey?: string
|
|
12
12
|
fallbackSrc: string
|
|
13
13
|
altKey?: string
|
|
14
14
|
fallbackAlt?: string
|
|
15
15
|
context?: ImageContext
|
|
16
16
|
sizes?: string
|
|
17
17
|
original?: boolean
|
|
18
|
+
/**
|
|
19
|
+
* D1-A structured image base (Q-168=A): a single dot-suffix key group, e.g.
|
|
20
|
+
* `hero.image`, from which `.src` / `.alt` / `.width` / `.height` are read.
|
|
21
|
+
* When set it supersedes `imageKey`/`altKey`. The legacy two-key form keeps
|
|
22
|
+
* working forever for backward compatibility. The `.width`/`.height` values
|
|
23
|
+
* feed the CLS layout-shift hint; when they are absent no hint is emitted
|
|
24
|
+
* (never fabricated).
|
|
25
|
+
*/
|
|
26
|
+
imageBase?: string
|
|
27
|
+
/**
|
|
28
|
+
* Explicit intrinsic dimensions — a fallback when no `.width`/`.height`
|
|
29
|
+
* content key exists (e.g. a static hero whose size is known at build time).
|
|
30
|
+
* Both must be positive to emit a hint.
|
|
31
|
+
*/
|
|
32
|
+
width?: number
|
|
33
|
+
height?: number
|
|
18
34
|
}>(), {
|
|
35
|
+
imageKey: '',
|
|
19
36
|
altKey: '',
|
|
20
37
|
fallbackAlt: '',
|
|
21
38
|
context: 'content',
|
|
22
39
|
sizes: undefined,
|
|
23
40
|
original: false,
|
|
41
|
+
imageBase: '',
|
|
42
|
+
width: undefined,
|
|
43
|
+
height: undefined,
|
|
24
44
|
})
|
|
25
45
|
|
|
46
|
+
// Resolve the effective content keys. `imageBase` (D1-A) derives the four
|
|
47
|
+
// dot-suffix keys; otherwise fall back to the legacy imageKey/altKey pair.
|
|
48
|
+
const srcKey = computed(() => (props.imageBase ? `${props.imageBase}.src` : props.imageKey))
|
|
49
|
+
const effectiveAltKey = computed(() => (props.imageBase ? `${props.imageBase}.alt` : props.altKey))
|
|
50
|
+
const widthKey = computed(() => (props.imageBase ? `${props.imageBase}.width` : ''))
|
|
51
|
+
const heightKey = computed(() => (props.imageBase ? `${props.imageBase}.height` : ''))
|
|
52
|
+
|
|
26
53
|
const attrs = useAttrs()
|
|
27
54
|
const { t } = useTextContent({
|
|
28
55
|
pageSlug: props.pageSlug,
|
|
29
56
|
defaults: {
|
|
30
|
-
[
|
|
31
|
-
...(
|
|
57
|
+
...(srcKey.value ? { [srcKey.value]: props.fallbackSrc } : {}),
|
|
58
|
+
...(effectiveAltKey.value ? { [effectiveAltKey.value]: props.fallbackAlt } : {}),
|
|
59
|
+
...(widthKey.value && props.width ? { [widthKey.value]: String(props.width) } : {}),
|
|
60
|
+
...(heightKey.value && props.height ? { [heightKey.value]: String(props.height) } : {}),
|
|
32
61
|
},
|
|
33
62
|
})
|
|
34
63
|
|
|
35
|
-
|
|
36
|
-
|
|
64
|
+
/** Parse a content value (always a string) into a positive integer, or undefined. */
|
|
65
|
+
function toPositiveInt(value: string | undefined): number | undefined {
|
|
66
|
+
if (!value) return undefined
|
|
67
|
+
const n = Number.parseInt(value, 10)
|
|
68
|
+
return Number.isFinite(n) && n > 0 ? n : undefined
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const imageSrc = computed(() => t(srcKey.value, props.fallbackSrc))
|
|
72
|
+
const imageAlt = computed(() => (effectiveAltKey.value ? t(effectiveAltKey.value, props.fallbackAlt) : props.fallbackAlt))
|
|
73
|
+
const imageWidth = computed(() =>
|
|
74
|
+
toPositiveInt(widthKey.value ? t(widthKey.value, '') : undefined) ?? (props.width && props.width > 0 ? props.width : undefined),
|
|
75
|
+
)
|
|
76
|
+
const imageHeight = computed(() =>
|
|
77
|
+
toPositiveInt(heightKey.value ? t(heightKey.value, '') : undefined) ?? (props.height && props.height > 0 ? props.height : undefined),
|
|
78
|
+
)
|
|
37
79
|
const image = useResponsiveImage({
|
|
38
80
|
src: imageSrc,
|
|
39
81
|
alt: imageAlt,
|
|
40
82
|
context: () => props.context,
|
|
41
83
|
sizes: () => props.sizes,
|
|
42
84
|
original: () => props.original,
|
|
85
|
+
width: imageWidth,
|
|
86
|
+
height: imageHeight,
|
|
43
87
|
})
|
|
44
88
|
|
|
45
89
|
// Self-heal: a fit-variant (`-md`/`-lg`) may legitimately be absent when the
|
|
@@ -61,7 +105,10 @@ const imgAttrs = computed(() => ({
|
|
|
61
105
|
// When a variant has failed, render the original (also exposed as
|
|
62
106
|
// data-dcs-image-url for the visual editor) instead of the 404'd variant.
|
|
63
107
|
...(failed.value ? { src: imageSrc.value } : {}),
|
|
64
|
-
|
|
108
|
+
// The VE stages a replacement against the SRC key (the key that holds the
|
|
109
|
+
// image URL), which is the dot-suffix `<base>.src` when imageBase is used,
|
|
110
|
+
// or the legacy imageKey otherwise.
|
|
111
|
+
'data-dcs-image-key': srcKey.value,
|
|
65
112
|
'data-dcs-image-url': imageSrc.value,
|
|
66
113
|
'data-dcs-image-alt': imageAlt.value,
|
|
67
114
|
}))
|
|
@@ -50,6 +50,10 @@ export interface UseResponsiveImageOptions {
|
|
|
50
50
|
sizes?: MaybeRefOrGetter<string | undefined>
|
|
51
51
|
/** Skip variant resolution and use the original URL only. */
|
|
52
52
|
original?: MaybeRefOrGetter<boolean | undefined>
|
|
53
|
+
/** Intrinsic width — emitted as a layout-shift hint when paired with `height`. */
|
|
54
|
+
width?: MaybeRefOrGetter<number | undefined>
|
|
55
|
+
/** Intrinsic height — emitted as a layout-shift hint when paired with `width`. */
|
|
56
|
+
height?: MaybeRefOrGetter<number | undefined>
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
/**
|
|
@@ -67,6 +71,8 @@ export function useResponsiveImage(options: UseResponsiveImageOptions): Responsi
|
|
|
67
71
|
context: toValue(options.context),
|
|
68
72
|
sizes: toValue(options.sizes),
|
|
69
73
|
original: toValue(options.original) ?? undefined,
|
|
74
|
+
width: toValue(options.width) ?? undefined,
|
|
75
|
+
height: toValue(options.height) ?? undefined,
|
|
70
76
|
}),
|
|
71
77
|
)
|
|
72
78
|
|