@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duffcloudservices/cms",
3
- "version": "0.11.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
- "@duffcloudservices/cms-core": "0.4.3",
70
- "js-yaml": "^4.1.0"
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 class="dcs-review-card__rating" :aria-label="`${review.rating} out of 5 stars`">
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: string
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
- [props.imageKey]: props.fallbackSrc,
31
- ...(props.altKey ? { [props.altKey]: props.fallbackAlt } : {}),
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
- const imageSrc = computed(() => t(props.imageKey, props.fallbackSrc))
36
- const imageAlt = computed(() => (props.altKey ? t(props.altKey, props.fallbackAlt) : props.fallbackAlt))
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
- 'data-dcs-image-key': props.imageKey,
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