@madgex/design-system 14.1.0 → 14.3.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.
Files changed (29) hide show
  1. package/dist/assets/icons.json +1 -1
  2. package/dist/css/index.css +1 -1
  3. package/dist/js/components/mds-consent-gate-standalone.js +5 -0
  4. package/dist/js/components/mds-image-cropper-standalone.js +1 -1
  5. package/dist/js/components/mds-timeout-dialog-standalone.js +1 -1
  6. package/dist/js/consent-gate-DVi5q-ir.js +1 -0
  7. package/dist/js/index.js +1 -1
  8. package/package.json +1 -1
  9. package/src/components/_macro-index.njk +1 -0
  10. package/src/components/conditional-section/README.md +2 -2
  11. package/src/components/conditional-section/conditional-section.config.js +54 -0
  12. package/src/components/conditional-section/conditional-section.js +91 -26
  13. package/src/components/conditional-section/conditional-section.spec.js +402 -0
  14. package/src/components/consent-gate/README.md +168 -0
  15. package/src/components/consent-gate/_macro.njk +30 -0
  16. package/src/components/consent-gate/_template.njk +14 -0
  17. package/src/components/consent-gate/consent-gate-standalone.scss +2 -0
  18. package/src/components/consent-gate/consent-gate.config.js +6 -0
  19. package/src/components/consent-gate/consent-gate.js +289 -0
  20. package/src/components/consent-gate/consent-gate.njk +203 -0
  21. package/src/components/consent-gate/consent-gate.scss +138 -0
  22. package/src/components/consent-gate/consent-gate.spec.js +582 -0
  23. package/src/components/consent-gate/consent-store.js +109 -0
  24. package/src/components/consent-gate/consent-store.spec.js +164 -0
  25. package/src/components/consent-gate/mds-consent-gate-standalone.js +40 -0
  26. package/src/helpers/fluid-video/README.md +8 -0
  27. package/src/helpers/prose/prose.js +6 -1
  28. package/src/js/index.js +5 -0
  29. package/src/scss/components/__index.scss +1 -0
@@ -0,0 +1,402 @@
1
+ // eslint-disable-next-line n/no-unpublished-import
2
+ import { describe, it, expect, afterEach } from 'vitest';
3
+ import { MdsConditionalSection, shouldShow } from './conditional-section.js';
4
+
5
+ if (!customElements.get('mds-conditional-section')) {
6
+ customElements.define('mds-conditional-section', MdsConditionalSection);
7
+ }
8
+
9
+ function emitInput(target) {
10
+ target.dispatchEvent(new Event('input', { bubbles: true }));
11
+ }
12
+
13
+ function mountScenario({ inputsHtml, fieldName, showWhen, includeShowWhen = true }) {
14
+ const container = document.createElement('div');
15
+ const showWhenAttr = includeShowWhen ? `show-when="${String(showWhen).replace(/"/g, '"')}"` : '';
16
+ container.innerHTML = `
17
+ <form id="test-conditional-form">
18
+ ${inputsHtml}
19
+ <mds-conditional-section field-name="${fieldName}" ${showWhenAttr}>
20
+ <p data-testid="conditional-content">Conditional content</p>
21
+ </mds-conditional-section>
22
+ </form>
23
+ `;
24
+ document.body.appendChild(container);
25
+ const form = container.querySelector('form');
26
+ const section = container.querySelector('mds-conditional-section');
27
+ return { container, form, section };
28
+ }
29
+
30
+ describe('shouldShow', () => {
31
+ it('is true when any checked value matches a comma-separated trigger (OR)', () => {
32
+ const form = document.createElement('form');
33
+ form.innerHTML = `
34
+ <input type="checkbox" name="i" value="sports" checked />
35
+ <input type="checkbox" name="i" value="music" />
36
+ `;
37
+ expect(shouldShow(form.elements.i, 'sports,music')).toBe(true);
38
+
39
+ form.querySelector('[value="sports"]').checked = false;
40
+ form.querySelector('[value="music"]').checked = true;
41
+ expect(shouldShow(form.elements.i, 'sports,music')).toBe(true);
42
+ });
43
+
44
+ it('is false when no checked value matches triggers', () => {
45
+ const form = document.createElement('form');
46
+ form.innerHTML = `
47
+ <input type="checkbox" name="i" value="sports" />
48
+ <input type="checkbox" name="i" value="music" />
49
+ <input type="checkbox" name="i" value="travel" checked />
50
+ `;
51
+ expect(shouldShow(form.elements.i, 'sports,music')).toBe(false);
52
+ });
53
+
54
+ it('is false when a checkbox group has nothing checked', () => {
55
+ const form = document.createElement('form');
56
+ form.innerHTML = `
57
+ <input type="checkbox" name="i" value="sports" />
58
+ <input type="checkbox" name="i" value="music" />
59
+ `;
60
+ expect(shouldShow(form.elements.i, 'sports')).toBe(false);
61
+ });
62
+
63
+ it('trims trigger segments and ignores empty parts', () => {
64
+ const form = document.createElement('form');
65
+ form.innerHTML = `
66
+ <input type="checkbox" name="i" value="a" checked />
67
+ <input type="checkbox" name="i" value="b" />
68
+ `;
69
+ expect(shouldShow(form.elements.i, ' a , b ')).toBe(true);
70
+ });
71
+
72
+ it('is false when show-when is null or empty', () => {
73
+ const form = document.createElement('form');
74
+ form.innerHTML = `<input type="checkbox" name="i" value="x" checked />`;
75
+ expect(shouldShow(form.elements.i, null)).toBe(false);
76
+ expect(shouldShow(form.elements.i, '')).toBe(false);
77
+ });
78
+
79
+ it('for a lone checkbox uses true/false against triggers', () => {
80
+ const form = document.createElement('form');
81
+ form.innerHTML = '<input type="checkbox" name="c" />';
82
+ const field = form.elements.c;
83
+ expect(shouldShow(field, 'false')).toBe(true);
84
+ expect(shouldShow(field, 'true')).toBe(false);
85
+ field.checked = true;
86
+ expect(shouldShow(field, 'true')).toBe(true);
87
+ expect(shouldShow(field, 'false')).toBe(false);
88
+ });
89
+
90
+ it('for a lone checkbox with a custom value matches show-when against that value when checked', () => {
91
+ const form = document.createElement('form');
92
+ form.innerHTML = '<input type="checkbox" name="c" value="yes" />';
93
+ const field = form.elements.c;
94
+ expect(shouldShow(field, 'yes')).toBe(false);
95
+ field.checked = true;
96
+ expect(shouldShow(field, 'yes')).toBe(true);
97
+ expect(shouldShow(field, 'no')).toBe(false);
98
+ });
99
+
100
+ it('for a lone checkbox with a custom value still supports show-when true/false', () => {
101
+ const form = document.createElement('form');
102
+ form.innerHTML = '<input type="checkbox" name="c" value="yes" />';
103
+ const field = form.elements.c;
104
+ field.checked = true;
105
+ expect(shouldShow(field, 'true')).toBe(true);
106
+ expect(shouldShow(field, 'false')).toBe(false);
107
+ field.checked = false;
108
+ expect(shouldShow(field, 'false')).toBe(true);
109
+ });
110
+
111
+ it('for a checkbox group matches checked values', () => {
112
+ const form = document.createElement('form');
113
+ form.innerHTML = `
114
+ <input type="checkbox" name="i" value="sports" />
115
+ <input type="checkbox" name="i" value="music" checked />
116
+ <input type="checkbox" name="i" value="travel" />
117
+ `;
118
+ expect(shouldShow(form.elements.i, 'music')).toBe(true);
119
+ });
120
+
121
+ it('uses "on" when a checked checkbox has no value attribute', () => {
122
+ const form = document.createElement('form');
123
+ form.innerHTML = `
124
+ <input type="checkbox" name="i" checked />
125
+ <input type="checkbox" name="i" />
126
+ `;
127
+ expect(shouldShow(form.elements.i, 'on')).toBe(true);
128
+ });
129
+
130
+ it('for a radio group matches the selected value', () => {
131
+ const form = document.createElement('form');
132
+ form.innerHTML = `
133
+ <input type="radio" name="r" value="a" />
134
+ <input type="radio" name="r" value="b" checked />
135
+ `;
136
+ expect(shouldShow(form.elements.r, 'b')).toBe(true);
137
+ expect(shouldShow(form.elements.r, 'a')).toBe(false);
138
+ });
139
+
140
+ it('reads multiple same-name controls from a RadioNodeList', () => {
141
+ const form = document.createElement('form');
142
+ form.innerHTML = `
143
+ <input type="checkbox" name="i" value="a" />
144
+ <input type="checkbox" name="i" value="b" />
145
+ `;
146
+ const list = form.elements.i;
147
+ expect(list).toBeInstanceOf(RadioNodeList);
148
+ expect(list.length).toBe(2);
149
+ list[1].checked = true;
150
+ expect(shouldShow(list, 'b')).toBe(true);
151
+ });
152
+
153
+ it('for a multiple select matches any selected option against comma-separated triggers', () => {
154
+ const form = document.createElement('form');
155
+ form.innerHTML = `
156
+ <select name="skills" id="skills" multiple>
157
+ <option value="js">JavaScript</option>
158
+ <option value="css">CSS</option>
159
+ <option value="html">HTML</option>
160
+ </select>
161
+ `;
162
+ const select = form.elements.skills;
163
+ expect(shouldShow(select, 'js,css')).toBe(false);
164
+
165
+ select.options[1].selected = true;
166
+ expect(shouldShow(select, 'js,css')).toBe(true);
167
+
168
+ select.options[1].selected = false;
169
+ select.options[2].selected = true;
170
+ expect(shouldShow(select, 'js,css')).toBe(false);
171
+ });
172
+
173
+ it('for a multiple select matches when a non-first selected option matches show-when alone', () => {
174
+ const form = document.createElement('form');
175
+ form.innerHTML = `
176
+ <select name="skills" id="skills" multiple>
177
+ <option value="js">JavaScript</option>
178
+ <option value="css">CSS</option>
179
+ </select>
180
+ `;
181
+ const select = form.elements.skills;
182
+ select.options[0].selected = true;
183
+ select.options[1].selected = true;
184
+ expect(shouldShow(select, 'css')).toBe(true);
185
+ });
186
+
187
+ it('for a multiple select stays visible while any matching option remains selected', () => {
188
+ const form = document.createElement('form');
189
+ form.innerHTML = `
190
+ <select name="skills" id="skills" multiple>
191
+ <option value="js">JavaScript</option>
192
+ <option value="css">CSS</option>
193
+ </select>
194
+ `;
195
+ const select = form.elements.skills;
196
+ select.options[0].selected = true;
197
+ select.options[1].selected = true;
198
+ expect(shouldShow(select, 'js,css')).toBe(true);
199
+
200
+ select.options[0].selected = false;
201
+ expect(shouldShow(select, 'js,css')).toBe(true);
202
+
203
+ select.options[1].selected = false;
204
+ expect(shouldShow(select, 'js,css')).toBe(false);
205
+ });
206
+ });
207
+
208
+ describe('MdsConditionalSection', () => {
209
+ let container;
210
+
211
+ afterEach(() => {
212
+ container?.remove();
213
+ container = undefined;
214
+ });
215
+
216
+ describe('checkbox group', () => {
217
+ it('shows when a non-first checkbox matching show-when is checked', () => {
218
+ ({ container } = mountScenario({
219
+ fieldName: 'interests',
220
+ showWhen: 'music',
221
+ inputsHtml: `
222
+ <input type="checkbox" name="interests" value="sports" id="cb-sports" />
223
+ <input type="checkbox" name="interests" value="music" id="cb-music" />
224
+ <input type="checkbox" name="interests" value="travel" id="cb-travel" />
225
+ `,
226
+ }));
227
+ const section = container.querySelector('mds-conditional-section');
228
+ expect(section.hidden).toBe(true);
229
+
230
+ const music = container.querySelector('#cb-music');
231
+ music.checked = true;
232
+ emitInput(music);
233
+
234
+ expect(section.hidden).toBe(false);
235
+ });
236
+
237
+ it('matches any comma-separated trigger (OR)', () => {
238
+ ({ container } = mountScenario({
239
+ fieldName: 'interests',
240
+ showWhen: 'sports,music',
241
+ inputsHtml: `
242
+ <input type="checkbox" name="interests" value="sports" id="cb-sports" />
243
+ <input type="checkbox" name="interests" value="music" id="cb-music" />
244
+ <input type="checkbox" name="interests" value="travel" id="cb-travel" />
245
+ `,
246
+ }));
247
+ const section = container.querySelector('mds-conditional-section');
248
+ const travel = container.querySelector('#cb-travel');
249
+ travel.checked = true;
250
+ emitInput(travel);
251
+ expect(section.hidden).toBe(true);
252
+
253
+ const music = container.querySelector('#cb-music');
254
+ music.checked = true;
255
+ emitInput(music);
256
+ expect(section.hidden).toBe(false);
257
+ });
258
+
259
+ it('stays visible if one matching trigger is unchecked but another remains checked', () => {
260
+ ({ container } = mountScenario({
261
+ fieldName: 'interests',
262
+ showWhen: 'sports,music',
263
+ inputsHtml: `
264
+ <input type="checkbox" name="interests" value="sports" id="cb-sports" />
265
+ <input type="checkbox" name="interests" value="music" id="cb-music" />
266
+ `,
267
+ }));
268
+ const section = container.querySelector('mds-conditional-section');
269
+ const sports = container.querySelector('#cb-sports');
270
+ const music = container.querySelector('#cb-music');
271
+ sports.checked = true;
272
+ music.checked = true;
273
+ emitInput(sports);
274
+
275
+ expect(section.hidden).toBe(false);
276
+
277
+ sports.checked = false;
278
+ emitInput(sports);
279
+ expect(section.hidden).toBe(false);
280
+
281
+ music.checked = false;
282
+ emitInput(music);
283
+ expect(section.hidden).toBe(true);
284
+ });
285
+ });
286
+
287
+ describe('regressions', () => {
288
+ it('single checkbox with show-when true toggles visibility', () => {
289
+ ({ container } = mountScenario({
290
+ fieldName: 'agree',
291
+ showWhen: 'true',
292
+ inputsHtml: '<input type="checkbox" name="agree" id="agree" />',
293
+ }));
294
+ const section = container.querySelector('mds-conditional-section');
295
+ const cb = container.querySelector('#agree');
296
+ expect(section.hidden).toBe(true);
297
+ cb.checked = true;
298
+ emitInput(cb);
299
+ expect(section.hidden).toBe(false);
300
+ });
301
+
302
+ it('hide-salary pattern: show-when false when checkbox unchecked', () => {
303
+ ({ container } = mountScenario({
304
+ fieldName: 'HideSalary',
305
+ showWhen: 'false',
306
+ inputsHtml: '<input type="checkbox" name="HideSalary" id="HideSalary" />',
307
+ }));
308
+ const section = container.querySelector('mds-conditional-section');
309
+ expect(section.hidden).toBe(false);
310
+ const cb = container.querySelector('#HideSalary');
311
+ cb.checked = true;
312
+ emitInput(cb);
313
+ expect(section.hidden).toBe(true);
314
+ });
315
+
316
+ it('single checkbox with custom value and show-when toggles visibility', () => {
317
+ ({ container } = mountScenario({
318
+ fieldName: 'newsletter',
319
+ showWhen: 'weekly',
320
+ inputsHtml: '<input type="checkbox" name="newsletter" id="newsletter" value="weekly" />',
321
+ }));
322
+ const section = container.querySelector('mds-conditional-section');
323
+ const cb = container.querySelector('#newsletter');
324
+ expect(section.hidden).toBe(true);
325
+ cb.checked = true;
326
+ emitInput(cb);
327
+ expect(section.hidden).toBe(false);
328
+ });
329
+
330
+ it('radio group show-when matches selected value', () => {
331
+ ({ container } = mountScenario({
332
+ fieldName: 'option',
333
+ showWhen: 'Broccoli',
334
+ inputsHtml: `
335
+ <input type="radio" name="option" value="Donkey" id="r1" />
336
+ <input type="radio" name="option" value="Broccoli" id="r2" />
337
+ `,
338
+ }));
339
+ const section = container.querySelector('mds-conditional-section');
340
+ expect(section.hidden).toBe(true);
341
+ container.querySelector('#r2').checked = true;
342
+ emitInput(container.querySelector('#r2'));
343
+ expect(section.hidden).toBe(false);
344
+ });
345
+
346
+ it('select show-when matches option value', () => {
347
+ ({ container } = mountScenario({
348
+ fieldName: 'pets',
349
+ showWhen: 'cat',
350
+ inputsHtml: `
351
+ <select name="pets" id="pets">
352
+ <option value="">Choose</option>
353
+ <option value="dog">Dog</option>
354
+ <option value="cat">Cat</option>
355
+ </select>
356
+ `,
357
+ }));
358
+ const section = container.querySelector('mds-conditional-section');
359
+ const sel = container.querySelector('#pets');
360
+ sel.value = 'cat';
361
+ emitInput(sel);
362
+ expect(section.hidden).toBe(false);
363
+ });
364
+
365
+ it('multiple select show-when matches any selected option (OR)', () => {
366
+ ({ container } = mountScenario({
367
+ fieldName: 'skills',
368
+ showWhen: 'js,css',
369
+ inputsHtml: `
370
+ <label for="skills">Pick your skills (hold Ctrl/Cmd to select several):</label>
371
+ <select name="skills" id="skills" multiple size="3">
372
+ <option value="js">JavaScript</option>
373
+ <option value="css">CSS</option>
374
+ <option value="html">HTML</option>
375
+ </select>
376
+ `,
377
+ }));
378
+ const section = container.querySelector('mds-conditional-section');
379
+ const sel = container.querySelector('#skills');
380
+ expect(section.hidden).toBe(true);
381
+
382
+ sel.options[1].selected = true;
383
+ emitInput(sel);
384
+ expect(section.hidden).toBe(false);
385
+ });
386
+ });
387
+
388
+ describe('missing show-when', () => {
389
+ it('keeps section hidden when show-when attribute is omitted', () => {
390
+ ({ container } = mountScenario({
391
+ fieldName: 'interests',
392
+ showWhen: '',
393
+ includeShowWhen: false,
394
+ inputsHtml: `
395
+ <input type="checkbox" name="interests" value="sports" checked />
396
+ `,
397
+ }));
398
+ const section = container.querySelector('mds-conditional-section');
399
+ expect(section.hidden).toBe(true);
400
+ });
401
+ });
402
+ });
@@ -0,0 +1,168 @@
1
+ # mds-consent-gate
2
+
3
+ Generic consent gate for third-party embeds and widgets.
4
+
5
+ The component keeps third-party markup inside a child `<template>` so it does not load or run until the user grants consent for a category. On grant it clones the template into the gate's content area; on revoke it removes the clone.
6
+
7
+ ## Why
8
+
9
+ Many third-party embeds make outbound network requests on page load, before user intent and consent are established. This component defers those requests until explicit opt-in and provides a built-in revoke flow.
10
+
11
+ ## Usage
12
+
13
+ Always wrap third-party content in a direct child `<template>`. Provide a fallback link in `slot="fallback"` for the no-JS / unloaded state.
14
+
15
+ ### Bundled (light DOM) example
16
+
17
+ ```html
18
+ <mds-consent-gate category="youtube" aspect-ratio="16 / 9">
19
+ <span slot="heading">Video</span>
20
+ <template>
21
+ <iframe
22
+ src="https://www.youtube-nocookie.com/embed/aqz-KE-bpKQ"
23
+ title="Big Buck Bunny"
24
+ loading="lazy"
25
+ referrerpolicy="strict-origin-when-cross-origin"
26
+ allow="autoplay; encrypted-media; picture-in-picture; fullscreen"
27
+ allowfullscreen
28
+ ></iframe>
29
+ </template>
30
+ <a slot="fallback" href="https://www.youtube.com/watch?v=aqz-KE-bpKQ">Watch on YouTube</a>
31
+ </mds-consent-gate>
32
+ ```
33
+
34
+ ### Standalone (shadow DOM, bundled CSS) example
35
+
36
+ ```html
37
+ <mds-consent-gate-standalone category="youtube" aspect-ratio="16 / 9">
38
+ <span slot="heading">Video</span>
39
+ <template>
40
+ <iframe src="https://www.youtube-nocookie.com/embed/aqz-KE-bpKQ" title="Big Buck Bunny" loading="lazy"></iframe>
41
+ </template>
42
+ <a slot="fallback" href="https://www.youtube.com/watch?v=aqz-KE-bpKQ">Watch on YouTube</a>
43
+ </mds-consent-gate-standalone>
44
+
45
+ <script type="module" src="/path/to/mds-consent-gate-standalone.js"></script>
46
+ ```
47
+
48
+ ### Script placement
49
+
50
+ Load the module from `<head>`. Module scripts defer by default so this does not block parsing.
51
+
52
+ ```html
53
+ <head>
54
+ <script type="module" src="/path/to/mds-consent-gate-standalone.js"></script>
55
+ </head>
56
+ ```
57
+
58
+ Pre-mount and no-JS handling is driven by component CSS keyed off the absence of `data-state`. The fallback link remains visible until the element upgrades.
59
+
60
+ ## Consent behaviour
61
+
62
+ Default consent is stored in `localStorage` under the key `mds-consent`.
63
+
64
+ Consent is per-category. Granting one category reveals every consent-gate instance with the same `category` value, including both `<mds-consent-gate>` and `<mds-consent-gate-standalone>`. Revoking hides them all.
65
+
66
+ ## CMP integration
67
+
68
+ Use `setConsentAdapter` to delegate to OneTrust or another CMP.
69
+
70
+ > **Note:** The adapter and all consent-gate elements must share the same package instance. A standalone script loaded from a separate bundle will have its own isolated store and will not reflect consent changes made via this adapter.
71
+
72
+ ```js
73
+ import { setConsentAdapter } from '@madgex/design-system';
74
+
75
+ setConsentAdapter({
76
+ has: (category) => false,
77
+ grant: (category) => {},
78
+ revoke: (category) => {},
79
+ subscribe: (fn) => {
80
+ // call fn(category, granted) whenever consent changes
81
+ return () => {};
82
+ },
83
+ });
84
+ ```
85
+
86
+ Adapter contract:
87
+
88
+ - `has(category): boolean` — must be synchronous
89
+ - `grant(category): void`
90
+ - `revoke(category): void`
91
+ - `subscribe(fn): () => void`
92
+
93
+ Pass `null` to restore the default localStorage adapter.
94
+
95
+ ## Attributes
96
+
97
+ | Attribute | Purpose |
98
+ | --------------- | ----------------------------------------------------------------------------------------- |
99
+ | `category` | Consent category key. Required for gating behaviour. |
100
+ | `i18n` | JSON object with optional string keys `acceptLabel` and `consentMessage`. Invalid values, empty strings, or missing keys fall back to English defaults. Omit the attribute to use defaults only. |
101
+ | `aspect-ratio` | Sets `--mds-consent-gate-aspect-ratio` on host. No default ratio is applied by component. |
102
+ | `preview-image` | Sets `--mds-consent-gate-preview-image` on host (`url(...)`). |
103
+
104
+ ## Slots
105
+
106
+ | Slot | Purpose |
107
+ | -------------------------- | ------------------------------------------------------------------------ |
108
+ | default `<template>` child | Required source markup, kept inert until consent. |
109
+ | `heading` | Optional heading content shown over the placeholder. |
110
+ | `fallback` | Optional fallback link/content shown before consent and in error states. |
111
+
112
+ ## Parts
113
+
114
+ Style internals via `::part(...)`:
115
+
116
+ | Part | Element |
117
+ | ----------------- | ------------------------------------------------------- |
118
+ | `placeholder` | The placeholder UI container shown awaiting consent. |
119
+ | `consent-message` | The disclosure paragraph. |
120
+ | `accept` | The Accept button. |
121
+ | `content` | The container the cloned template content is placed in. |
122
+
123
+ ## State
124
+
125
+ Visibility is driven by a single `data-state` host attribute which CSS keys off:
126
+
127
+ | `data-state` | Meaning |
128
+ | ------------- | --------------------------------------------------------- |
129
+ | _absent_ | Pre-mount / not-yet-upgraded. Only fallback visible. |
130
+ | `error` | Config error (no category or no template). Fallback only. |
131
+ | `placeholder` | Awaiting consent. Heading + placeholder + fallback. |
132
+ | `consented` | Content cloned into `[part="content"]`. |
133
+
134
+ ## Events
135
+
136
+ | Event | Bubbles | Composed | Detail |
137
+ | ------------------------- | ------- | -------- | ----------------------------------------- |
138
+ | `mds-consent-gate:grant` | yes | yes | `{ category, container }` |
139
+ | `mds-consent-gate:revoke` | yes | yes | `{ category }` |
140
+ | `mds-consent-gate:error` | yes | yes | `{ reason }` |
141
+ | `mds-consent:change` | window | n/a | `{ category, granted }` (default backend) |
142
+
143
+ `error` reasons:
144
+
145
+ - `no-category`
146
+ - `no-template`
147
+ - `unwrapped-content`
148
+
149
+ ## VPPA/GDPR safety guard
150
+
151
+ Direct child network-loading elements outside `<template>` are treated as authoring errors:
152
+
153
+ - `<iframe>`
154
+ - `<embed>`
155
+ - `<object>`
156
+ - `<script src>`
157
+
158
+ In that case the component warns in the console and emits `mds-consent-gate:error` with `reason: 'unwrapped-content'`.
159
+
160
+ ## Styling
161
+
162
+ Use `::part(...)` for both the bundled and standalone builds. The component exposes the following CSS custom properties for theming:
163
+
164
+ - `--mds-consent-gate-aspect-ratio`
165
+ - `--mds-consent-gate-preview-image`
166
+ - `--mds-consent-gate-bg`
167
+
168
+ Internal layout uses standard DS variables (`--mds-color-button-*`, `--mds-color-neutral-darkest`, `--mds-color-text-invert`, `--mds-size-border-radius-base`).
@@ -0,0 +1,30 @@
1
+ {% from "../../sub-components/attributes/macro.njk" import MdsAttributes %}
2
+ {#
3
+ MdsConsentGate macro
4
+
5
+ params:
6
+ category — required, consent category key
7
+ i18n — optional object `{ acceptLabel?, consentMessage? }` (passed as `i18n` host attribute via `| dump`)
8
+ aspectRatio — optional, e.g. "16 / 9"
9
+ previewImage — optional, URL of self-hosted preview image
10
+ heading — optional, plain heading text
11
+ contentHtml — required, markup to defer (placed inside <template>)
12
+ fallbackHref — optional
13
+ fallbackText — optional
14
+ attributes — optional extra host attributes
15
+ #}
16
+ <mds-consent-gate
17
+ {% if params.category %}category="{{params.category}}"{% endif %}
18
+ {% if params.i18n is defined %}i18n="{{ params.i18n | dump }}"{% endif %}
19
+ {% if params.aspectRatio %}aspect-ratio="{{params.aspectRatio}}"{% endif %}
20
+ {% if params.previewImage %}preview-image="{{params.previewImage}}"{% endif %}
21
+ {{- MdsAttributes(params.attributes) -}}
22
+ >
23
+ {% if params.heading %}<span slot="heading">{{params.heading}}</span>{% endif %}
24
+ <template>
25
+ {{ params.contentHtml | default('') | safe }}
26
+ </template>
27
+ {% if params.fallbackHref and params.fallbackText %}
28
+ <a slot="fallback" href="{{params.fallbackHref}}">{{params.fallbackText}}</a>
29
+ {% endif %}
30
+ </mds-consent-gate>
@@ -0,0 +1,14 @@
1
+ <mds-consent-gate
2
+ {% if category %}category="{{category}}"{% endif %}
3
+ {% if i18n is defined %}i18n="{{ i18n | dump }}"{% endif %}
4
+ {% if aspectRatio %}aspect-ratio="{{aspectRatio}}"{% endif %}
5
+ {% if previewImage %}preview-image="{{previewImage}}"{% endif %}
6
+ >
7
+ {% if heading %}<span slot="heading">{{heading}}</span>{% endif %}
8
+ <template>
9
+ {{ contentHtml | default('') | safe }}
10
+ </template>
11
+ {% if fallbackHref and fallbackText %}
12
+ <a slot="fallback" href="{{fallbackHref}}">{{fallbackText}}</a>
13
+ {% endif %}
14
+ </mds-consent-gate>
@@ -0,0 +1,2 @@
1
+ /* relying on purgecss to remove unused css */
2
+ @import '../../scss/index.scss';
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ title: 'Consent Gate',
3
+ label: 'Consent Gate',
4
+ status: 'ready',
5
+ context: {},
6
+ };