srcdev-nuxt-components 9.2.5 → 9.2.6

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 (32) hide show
  1. package/.claude/settings.json +35 -1
  2. package/.claude/skills/components/content-docs.md +165 -0
  3. package/.claude/skills/components/expanding-panel.md +55 -0
  4. package/.claude/skills/index.md +4 -2
  5. package/.claude/skills/qa-panel.md +5 -6
  6. package/.claude/skills/theming-typography-tokens.md +109 -0
  7. package/.vscode/srcdev-component-content-docs.code-snippets +129 -0
  8. package/.vscode/srcdev-component-expanding-panel.code-snippets +123 -0
  9. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +410 -0
  10. package/app/components/01.atoms/content-wrappers/docs-pages/playwright/content-docs.playwright.ts +53 -0
  11. package/app/components/01.atoms/content-wrappers/docs-pages/playwright/content-docs.playwright.ts-snapshots/default-chromium-darwin.png +0 -0
  12. package/app/components/01.atoms/content-wrappers/docs-pages/playwright/content-docs.playwright.ts-snapshots/state-desktop-chromium-darwin.png +0 -0
  13. package/app/components/01.atoms/content-wrappers/docs-pages/playwright/content-docs.playwright.ts-snapshots/state-mobile-chromium-darwin.png +0 -0
  14. package/app/components/01.atoms/content-wrappers/docs-pages/playwright/content-docs.playwright.ts-snapshots/state-tablet-chromium-darwin.png +0 -0
  15. package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +172 -0
  16. package/app/components/01.atoms/content-wrappers/docs-pages/tests/ContentDocs.spec.ts +218 -0
  17. package/app/components/02.molecules/expandable/expanding-panel/CONSUMER-STYLING.md +103 -0
  18. package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +97 -56
  19. package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +50 -2
  20. package/app/components/02.molecules/expandable/expanding-panel/tests/ExpandingPanel.spec.ts +79 -2
  21. package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +25 -4
  22. package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +2 -1
  23. package/app/components/layout-grids/LayoutGridA.vue +59 -59
  24. package/app/composables/tests/useContainerBreakpoints.spec.ts +91 -0
  25. package/app/composables/useContainerBreakpoints.ts +71 -0
  26. package/app/layouts/default.vue +1 -0
  27. package/app/pages/ui/expanding-panel.vue +266 -129
  28. package/app/pages/ui/layout-content-docs.vue +72 -0
  29. package/app/pages/ui/layout-grid-a.vue +5 -58
  30. package/app/types/components/content-docs.d.ts +5 -0
  31. package/app/types/components/index.ts +1 -0
  32. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="expanding-panel" :class="[elementClasses]">
2
+ <div class="expanding-panel" :class="[elementClasses, { 'content-is-on-top': contentIsOnTop }]">
3
3
  <details class="expanding-panel-details" :name :open @toggle="onDetailsToggle">
4
4
  <summary
5
5
  :id="`id-${name}-trigger`"
@@ -8,14 +8,14 @@
8
8
  :aria-expanded="open"
9
9
  @click.prevent.stop="handleToggle"
10
10
  >
11
- <span class="label-wrapper">
11
+ <div class="label-wrapper">
12
12
  <slot name="summary"></slot>
13
- </span>
14
- <span v-if="!forceOpened" class="icon-wrapper">
13
+ </div>
14
+ <div class="icon-wrapper" :class="{ 'icon-wrapper--hidden': forceOpened }">
15
15
  <slot name="icon">
16
16
  <Icon name="bi:caret-down-fill" class="icon mi-12" />
17
17
  </slot>
18
- </span>
18
+ </div>
19
19
  </summary>
20
20
  </details>
21
21
  <div
@@ -36,6 +36,7 @@ interface Props {
36
36
  name?: string;
37
37
  animationDuration?: number;
38
38
  forceOpened?: boolean;
39
+ contentIsOnTop?: boolean;
39
40
  styleClassPassthrough?: string | string[];
40
41
  }
41
42
 
@@ -43,16 +44,35 @@ const props = withDefaults(defineProps<Props>(), {
43
44
  name: undefined,
44
45
  animationDuration: 400,
45
46
  forceOpened: false,
47
+ contentIsOnTop: false,
46
48
  styleClassPassthrough: () => [],
47
49
  });
48
50
 
49
- const name = props.name || useId();
51
+ const fallbackName = useId();
52
+ const name = computed(() => props.name || fallbackName);
50
53
  const isPanelOpen = defineModel<boolean>({ default: false });
51
54
  const animationDurationStr = computed(() => `${props.animationDuration}ms`);
52
55
  const open = computed(() => props.forceOpened || isPanelOpen.value);
53
56
 
54
57
  const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
55
58
 
59
+ if (import.meta.dev) {
60
+ watch(
61
+ () => props.contentIsOnTop,
62
+ (value) => {
63
+ if (!value) return;
64
+ console.warn(
65
+ `ExpandingPanel${props.name ? ` "${props.name}"` : ""}: contentIsOnTop is enabled. ` +
66
+ "The content overlay is absolutely positioned and does not reserve space, so a sibling " +
67
+ "element (e.g. another ExpandingPanel) placed directly after this one will be visually " +
68
+ "covered when this panel opens. Intended for a single panel overlaying unrelated trailing " +
69
+ "page content — avoid stacking multiple contentIsOnTop panels as direct siblings."
70
+ );
71
+ },
72
+ { immediate: true }
73
+ );
74
+ }
75
+
56
76
  const handleToggle = (event: Event) => {
57
77
  if (props.forceOpened) {
58
78
  event.preventDefault();
@@ -61,80 +81,101 @@ const handleToggle = (event: Event) => {
61
81
  };
62
82
 
63
83
  const onDetailsToggle = (event: Event) => {
84
+ // forceOpened drives the native `open` attribute directly, which fires its own
85
+ // 'toggle' event — ignore it here or it leaks into isPanelOpen and gets stuck
86
+ // once forceOpened later reverts to false.
87
+ if (props.forceOpened) return;
64
88
  isPanelOpen.value = (event.target as HTMLDetailsElement).open;
65
89
  };
66
90
  </script>
67
91
 
68
92
  <style lang="css">
69
93
  @layer components {
70
- .expanding-panel {
71
- .expanding-panel-details {
72
- &:hover {
73
- cursor: pointer;
74
- }
75
- .expanding-panel-summary {
76
- display: flex;
77
- align-items: center;
78
- justify-content: space-between;
79
- flex-direction: row;
80
- gap: 1rem;
81
- list-style: none;
82
-
83
- padding-block: 0.5rem;
84
-
85
- &::-webkit-details-marker,
86
- &::marker {
87
- display: none;
88
- }
89
-
90
- overflow: hidden;
91
-
92
- .label-wrapper {
93
- display: inline-block;
94
+ .expanding-panel {
95
+ .expanding-panel-details {
96
+ &:hover {
97
+ cursor: pointer;
94
98
  }
95
- .icon-wrapper {
99
+ .expanding-panel-summary {
96
100
  display: flex;
97
101
  align-items: center;
98
102
  justify-content: space-between;
103
+ flex-direction: row;
104
+ gap: var(--expanding-panel-summary-gap, 1rem);
105
+ list-style: none;
99
106
 
100
- aspect-ratio: 1;
101
- overflow: hidden;
107
+ padding-block: var(--expanding-panel-summary-padding-block, 0.5rem);
102
108
 
103
- .icon {
104
- display: block;
105
- transform: scaleY(1);
106
- transition: transform v-bind(animationDurationStr) ease-in-out;
107
- font-size: 1.2rem;
108
- will-change: transform;
109
+ &::-webkit-details-marker,
110
+ &::marker {
111
+ display: none;
109
112
  }
110
- }
111
- }
112
113
 
113
- &[open] {
114
- .expanding-panel-summary {
114
+ overflow: hidden;
115
+
116
+ .label-wrapper {
117
+ display: inline-block;
118
+ }
115
119
  .icon-wrapper {
120
+ display: flex;
121
+ align-items: center;
122
+ justify-content: space-between;
123
+
124
+ aspect-ratio: 1;
125
+ overflow: hidden;
126
+
127
+ /* Space is always reserved (not v-if'd away) so a forceOpened panel's summary
128
+ row stays the same height as a toggleable sibling's — only the glyph hides. */
129
+ &.icon-wrapper--hidden {
130
+ visibility: hidden;
131
+ }
132
+
116
133
  .icon {
117
- transform: scaleY(-1);
134
+ display: block;
135
+ transform: scaleY(1);
136
+ transition: transform v-bind(animationDurationStr) ease-in-out;
137
+ font-size: var(--expanding-panel-icon-size, 1.2rem);
138
+ will-change: transform;
118
139
  }
119
140
  }
120
141
  }
121
- + .expanding-panel-content {
122
- grid-template-rows: 1fr;
142
+
143
+ &[open] {
144
+ .expanding-panel-summary {
145
+ .icon-wrapper {
146
+ .icon {
147
+ transform: scaleY(-1);
148
+ }
149
+ }
150
+ }
151
+ + .expanding-panel-content {
152
+ grid-template-rows: 1fr;
153
+ }
123
154
  }
124
155
  }
125
- }
126
156
 
127
- .expanding-panel-content {
128
- display: grid;
129
- grid-template-rows: 0fr;
130
- transition: grid-template-rows v-bind(animationDurationStr) ease-in-out;
131
- will-change: grid-template-rows;
157
+ .expanding-panel-content {
158
+ display: grid;
159
+ grid-template-rows: 0fr;
160
+ transition: grid-template-rows v-bind(animationDurationStr) ease-in-out;
161
+ will-change: grid-template-rows;
132
162
 
133
- .inner {
134
- overflow: hidden;
135
- margin-top: 0;
163
+ .inner {
164
+ overflow: hidden;
165
+ margin-top: 0;
166
+ }
167
+ }
168
+
169
+ &.content-is-on-top {
170
+ position: relative;
171
+
172
+ .expanding-panel-content {
173
+ position: absolute;
174
+ top: calc(100% + var(--expanding-panel-content-gap, 0px));
175
+ inset-inline: 0;
176
+ z-index: var(--expanding-panel-content-z-index, 10);
177
+ }
136
178
  }
137
179
  }
138
180
  }
139
- }
140
181
  </style>
@@ -7,8 +7,7 @@ const meta: Meta<typeof ExpandingPanel> = {
7
7
  argTypes: {
8
8
  name: {
9
9
  control: { type: "text" },
10
- description:
11
- "Unique name used for ARIA attributes and to group panels in a native <details> accordion",
10
+ description: "Unique name used for ARIA attributes and to group panels in a native <details> accordion",
12
11
  },
13
12
  animationDuration: {
14
13
  control: { type: "number", min: 0, step: 50 },
@@ -18,6 +17,11 @@ const meta: Meta<typeof ExpandingPanel> = {
18
17
  control: { type: "boolean" },
19
18
  description: "When true, the panel is always open and the toggle icon is hidden",
20
19
  },
20
+ contentIsOnTop: {
21
+ control: { type: "boolean" },
22
+ description:
23
+ "When true, the content region is positioned absolutely below the summary and raised above surrounding page content via z-index, instead of pushing layout down",
24
+ },
21
25
  styleClassPassthrough: {
22
26
  control: "object",
23
27
  description: "Additional CSS classes applied to the root element",
@@ -27,6 +31,7 @@ const meta: Meta<typeof ExpandingPanel> = {
27
31
  name: "panel-demo",
28
32
  animationDuration: 400,
29
33
  forceOpened: false,
34
+ contentIsOnTop: false,
30
35
  styleClassPassthrough: [],
31
36
  },
32
37
  };
@@ -218,6 +223,49 @@ export const RichContent: Story = {
218
223
  }),
219
224
  };
220
225
 
226
+ export const ContentIsOnTop: Story = {
227
+ name: "Content Is On Top",
228
+ args: {
229
+ name: "content-is-on-top",
230
+ contentIsOnTop: true,
231
+ },
232
+ render: (args) => ({
233
+ components: { ExpandingPanel },
234
+ setup() {
235
+ return { args };
236
+ },
237
+ template: `
238
+ <div style="position:relative;padding-bottom:4rem">
239
+ <ExpandingPanel v-bind="args">
240
+ <template #summary>
241
+ <span>Open me — content overlays what's below</span>
242
+ </template>
243
+ <template #content>
244
+ <!--
245
+ The background/border/shadow live on this wrapper — never on .inner
246
+ itself. .inner has no explicit height when collapsed (grid-template-rows: 0fr)
247
+ and relies on overflow:hidden to clip its children to 0px; but padding/border/
248
+ background set directly on .inner are part of ITS OWN box, not overflow content,
249
+ so they'd still render as a visible gap under the summary while closed. Styling
250
+ a wrapper *inside* the slot keeps it clipped correctly by .inner's overflow:hidden.
251
+ -->
252
+ <div style="color: #808080; background-color:#fff;border:1px solid #ddd;border-radius:0.4rem;padding:1rem;box-shadow:0 4px 12px rgb(0 0 0 / 15%)">
253
+ <p style="margin:0">
254
+ With <code>contentIsOnTop</code>, this content is absolutely positioned below the
255
+ summary and raised above surrounding page content via <code>z-index</code>,
256
+ instead of pushing the "Page content below" text further down.
257
+ </p>
258
+ </div>
259
+ </template>
260
+ </ExpandingPanel>
261
+ <p style="margin-top:1rem;padding:1rem;">
262
+ Page content below — stays in place when the panel opens.
263
+ </p>
264
+ </div>
265
+ `,
266
+ }),
267
+ };
268
+
221
269
  export const WithStyleClassPassthrough: Story = {
222
270
  name: "With styleClassPassthrough",
223
271
  args: {
@@ -41,6 +41,13 @@ describe("ExpandingPanel", () => {
41
41
  expect(wrapper.html()).toMatchSnapshot();
42
42
  });
43
43
 
44
+ it("renders correct HTML structure with contentIsOnTop true", async () => {
45
+ const wrapper = await mountSuspended(ExpandingPanel, {
46
+ props: { name: "snap-content-on-top", contentIsOnTop: true },
47
+ });
48
+ expect(wrapper.html()).toMatchSnapshot();
49
+ });
50
+
44
51
  it("renders correct HTML structure with populated slots", async () => {
45
52
  const wrapper = await mountSuspended(ExpandingPanel, {
46
53
  props: { name: "snap-slots" },
@@ -91,6 +98,7 @@ describe("ExpandingPanel", () => {
91
98
  });
92
99
  expect(wrapper.props("animationDuration")).toBe(400);
93
100
  expect(wrapper.props("forceOpened")).toBe(false);
101
+ expect(wrapper.props("contentIsOnTop")).toBe(false);
94
102
  expect(wrapper.props("styleClassPassthrough")).toEqual([]);
95
103
  });
96
104
 
@@ -286,11 +294,14 @@ describe("ExpandingPanel", () => {
286
294
 
287
295
  // ─── forceOpened ──────────────────────────────────────────────────────────
288
296
 
289
- it("hides the icon-wrapper when forceOpened is true", async () => {
297
+ it("visually hides the icon-wrapper when forceOpened is true, without removing it from layout", async () => {
290
298
  const wrapper = await mountSuspended(ExpandingPanel, {
291
299
  props: { name: "force-icon", forceOpened: true },
292
300
  });
293
- expect(wrapper.find(".icon-wrapper").exists()).toBe(false);
301
+ // Kept in the DOM (not v-if'd away) so the summary row height stays consistent
302
+ // with a toggleable sibling panel's — only visually hidden via the modifier class.
303
+ expect(wrapper.find(".icon-wrapper").exists()).toBe(true);
304
+ expect(wrapper.find(".icon-wrapper").classes()).toContain("icon-wrapper--hidden");
294
305
  });
295
306
 
296
307
  it("shows the icon-wrapper when forceOpened is false", async () => {
@@ -298,6 +309,7 @@ describe("ExpandingPanel", () => {
298
309
  props: { name: "icon-visible", forceOpened: false },
299
310
  });
300
311
  expect(wrapper.find(".icon-wrapper").exists()).toBe(true);
312
+ expect(wrapper.find(".icon-wrapper").classes()).not.toContain("icon-wrapper--hidden");
301
313
  });
302
314
 
303
315
  it("keeps open true after click when forceOpened is true", async () => {
@@ -309,6 +321,23 @@ describe("ExpandingPanel", () => {
309
321
  expect(vm.open).toBe(true);
310
322
  });
311
323
 
324
+ it("does not leak forceOpened into isPanelOpen, so open reverts to false when forceOpened turns back off", async () => {
325
+ const wrapper = await mountSuspended(ExpandingPanel, {
326
+ props: { name: "force-then-unforce", forceOpened: true },
327
+ });
328
+ const vm = wrapper.vm as unknown as ExpandingPanelInstance;
329
+ expect(vm.open).toBe(true);
330
+
331
+ // The <details> element's `open` attribute changing (driven by forceOpened) fires
332
+ // its own native 'toggle' event, independent of any user click.
333
+ await wrapper.find("details").trigger("toggle");
334
+ await nextTick();
335
+ expect(vm.isPanelOpen).toBe(false);
336
+
337
+ await wrapper.setProps({ forceOpened: false });
338
+ expect(vm.open).toBe(false);
339
+ });
340
+
312
341
  // ─── Slots ────────────────────────────────────────────────────────────────
313
342
 
314
343
  it("renders summary slot content inside .label-wrapper", async () => {
@@ -364,6 +393,40 @@ describe("ExpandingPanel", () => {
364
393
  expect(wrapper.find(".expanding-panel").classes()).toContain("single-class");
365
394
  });
366
395
 
396
+ // ─── contentIsOnTop ───────────────────────────────────────────────────────
397
+
398
+ it("does not apply content-is-on-top class to root by default", async () => {
399
+ const wrapper = await mountSuspended(ExpandingPanel, {
400
+ props: { name: "content-on-top-default" },
401
+ });
402
+ expect(wrapper.find(".expanding-panel").classes()).not.toContain("content-is-on-top");
403
+ });
404
+
405
+ it("applies content-is-on-top class to root when contentIsOnTop is true", async () => {
406
+ const wrapper = await mountSuspended(ExpandingPanel, {
407
+ props: { name: "content-on-top-true", contentIsOnTop: true },
408
+ });
409
+ expect(wrapper.find(".expanding-panel").classes()).toContain("content-is-on-top");
410
+ });
411
+
412
+ it("does not apply content-is-on-top class to the content region itself", async () => {
413
+ const wrapper = await mountSuspended(ExpandingPanel, {
414
+ props: { name: "content-on-top-scope", contentIsOnTop: true },
415
+ });
416
+ expect(wrapper.find(".expanding-panel-content").classes()).not.toContain("content-is-on-top");
417
+ });
418
+
419
+ it("still toggles open state when summary is clicked with contentIsOnTop true", async () => {
420
+ const wrapper = await mountSuspended(ExpandingPanel, {
421
+ props: { name: "content-on-top-click", contentIsOnTop: true },
422
+ });
423
+ const vm = wrapper.vm as unknown as ExpandingPanelInstance;
424
+ expect(vm.isPanelOpen).toBe(false);
425
+
426
+ await wrapper.find("summary").trigger("click");
427
+ expect(vm.isPanelOpen).toBe(true);
428
+ });
429
+
367
430
  // ─── name prop / useId fallback ───────────────────────────────────────────
368
431
 
369
432
  it("uses the provided name in ARIA attributes", async () => {
@@ -381,4 +444,18 @@ describe("ExpandingPanel", () => {
381
444
  expect(summaryId).toMatch(/^id-.+-trigger$/);
382
445
  expect(contentId).toMatch(/^id-.+-content$/);
383
446
  });
447
+
448
+ it("updates the details name and ARIA ids when the name prop changes after mount", async () => {
449
+ const wrapper = await mountSuspended(ExpandingPanel, {
450
+ props: { name: "panel-a" },
451
+ });
452
+ expect(wrapper.find("details").attributes("name")).toBe("panel-a");
453
+ expect(wrapper.find("summary").attributes("id")).toBe("id-panel-a-trigger");
454
+
455
+ await wrapper.setProps({ name: "panel-b" });
456
+
457
+ expect(wrapper.find("details").attributes("name")).toBe("panel-b");
458
+ expect(wrapper.find("summary").attributes("id")).toBe("id-panel-b-trigger");
459
+ expect(wrapper.find(".expanding-panel-content").attributes("id")).toBe("id-panel-b-content");
460
+ });
384
461
  });
@@ -3,8 +3,9 @@
3
3
  exports[`ExpandingPanel > renders correct HTML structure with all props set 1`] = `
4
4
  "<div class="expanding-panel extra-class">
5
5
  <details class="expanding-panel-details" name="snap-all" open="">
6
- <summary id="id-snap-all-trigger" class="expanding-panel-summary" aria-controls="id-snap-all-content" aria-expanded="true"><span class="label-wrapper"></span>
7
- <!--v-if-->
6
+ <summary id="id-snap-all-trigger" class="expanding-panel-summary" aria-controls="id-snap-all-content" aria-expanded="true">
7
+ <div class="label-wrapper"></div>
8
+ <div class="icon-wrapper icon-wrapper--hidden"><span class="iconify i-bi:caret-down-fill icon mi-12" aria-hidden="true"></span></div>
8
9
  </summary>
9
10
  </details>
10
11
  <div id="id-snap-all-content" class="expanding-panel-content" aria-labelledby="id-snap-all-trigger" role="region">
@@ -13,10 +14,27 @@ exports[`ExpandingPanel > renders correct HTML structure with all props set 1`]
13
14
  </div>"
14
15
  `;
15
16
 
17
+ exports[`ExpandingPanel > renders correct HTML structure with contentIsOnTop true 1`] = `
18
+ "<div class="expanding-panel content-is-on-top">
19
+ <details class="expanding-panel-details" name="snap-content-on-top">
20
+ <summary id="id-snap-content-on-top-trigger" class="expanding-panel-summary" aria-controls="id-snap-content-on-top-content" aria-expanded="false">
21
+ <div class="label-wrapper"></div>
22
+ <div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon mi-12" aria-hidden="true"></span></div>
23
+ </summary>
24
+ </details>
25
+ <div id="id-snap-content-on-top-content" class="expanding-panel-content" aria-labelledby="id-snap-content-on-top-trigger" role="region">
26
+ <div class="inner"></div>
27
+ </div>
28
+ </div>"
29
+ `;
30
+
16
31
  exports[`ExpandingPanel > renders correct HTML structure with default props 1`] = `
17
32
  "<div class="expanding-panel">
18
33
  <details class="expanding-panel-details" name="snap-default">
19
- <summary id="id-snap-default-trigger" class="expanding-panel-summary" aria-controls="id-snap-default-content" aria-expanded="false"><span class="label-wrapper"></span><span class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon mi-12" aria-hidden="true"></span></span></summary>
34
+ <summary id="id-snap-default-trigger" class="expanding-panel-summary" aria-controls="id-snap-default-content" aria-expanded="false">
35
+ <div class="label-wrapper"></div>
36
+ <div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon mi-12" aria-hidden="true"></span></div>
37
+ </summary>
20
38
  </details>
21
39
  <div id="id-snap-default-content" class="expanding-panel-content" aria-labelledby="id-snap-default-trigger" role="region">
22
40
  <div class="inner"></div>
@@ -27,7 +45,10 @@ exports[`ExpandingPanel > renders correct HTML structure with default props 1`]
27
45
  exports[`ExpandingPanel > renders correct HTML structure with populated slots 1`] = `
28
46
  "<div class="expanding-panel">
29
47
  <details class="expanding-panel-details" name="snap-slots">
30
- <summary id="id-snap-slots-trigger" class="expanding-panel-summary" aria-controls="id-snap-slots-content" aria-expanded="false"><span class="label-wrapper"><span>Summary text</span></span><span class="icon-wrapper"><span>▸</span></span></summary>
48
+ <summary id="id-snap-slots-trigger" class="expanding-panel-summary" aria-controls="id-snap-slots-content" aria-expanded="false">
49
+ <div class="label-wrapper"><span>Summary text</span></div>
50
+ <div class="icon-wrapper"><span>▸</span></div>
51
+ </summary>
31
52
  </details>
32
53
  <div id="id-snap-slots-content" class="expanding-panel-content" aria-labelledby="id-snap-slots-trigger" role="region">
33
54
  <div class="inner">
@@ -109,7 +109,8 @@ describe("PricingCard", () => {
109
109
  });
110
110
 
111
111
  const button = wrapper.find("button");
112
- expect(button.attributes("disabled")).toBeDefined();
112
+ expect(button.attributes("readonly")).toBeDefined();
113
+ expect(button.attributes("aria-disabled")).toBe("true");
113
114
  });
114
115
 
115
116
  it("renders custom features via slot", () => {
@@ -23,83 +23,83 @@ const props = defineProps({
23
23
  type: [String, Array] as PropType<string | string[]>,
24
24
  default: () => [],
25
25
  },
26
- })
26
+ });
27
27
 
28
- const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough)
28
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
29
29
 
30
30
  watch(
31
31
  () => props.styleClassPassthrough,
32
32
  () => {
33
- resetElementClasses(props.styleClassPassthrough)
33
+ resetElementClasses(props.styleClassPassthrough);
34
34
  }
35
- )
35
+ );
36
36
  </script>
37
37
 
38
38
  <style lang="css">
39
39
  @layer components {
40
- .layout-grid-a-wrapper {
41
- container-type: inline-size;
40
+ .layout-grid-a-wrapper {
41
+ container-type: inline-size;
42
42
 
43
- .layout-grid-a {
44
- display: grid;
45
- gap: 20px;
43
+ .layout-grid-a {
44
+ display: grid;
45
+ gap: 20px;
46
46
 
47
- & > div {
48
- padding: 20px;
49
- outline: 1px solid light-dark(black, white);
50
- border-radius: 5px;
51
- }
52
-
53
- @container (min-width: 768px) {
54
- grid-template-columns: repeat(2, auto);
55
- grid-template-rows: repeat(3, auto);
56
-
57
- .slot1 {
58
- grid-column: 1 / span 2;
59
- grid-row: 1;
60
- }
61
-
62
- .slot2 {
63
- grid-column: 1 / span 2;
64
- grid-row: 2;
65
- }
66
-
67
- .slot3 {
68
- grid-column: 1;
69
- grid-row: 3;
70
- }
71
-
72
- .slot4 {
73
- grid-column: 2;
74
- grid-row: 3;
75
- }
76
- }
77
-
78
- @container (min-width: 1060px) {
79
- grid-template-columns: repeat(3, auto);
80
- grid-template-rows: repeat(2, auto);
81
-
82
- .slot1 {
83
- grid-column: 1;
84
- grid-row: 1 / span 2;
85
- }
86
-
87
- .slot2 {
88
- grid-column: 2 / span 2;
89
- grid-row: 1;
47
+ & > div {
48
+ padding: 20px;
49
+ outline: 1px solid light-dark(black, white);
50
+ border-radius: 5px;
90
51
  }
91
52
 
92
- .slot3 {
93
- grid-column: 2;
94
- grid-row: 2;
53
+ @container (min-width: 768px) {
54
+ grid-template-columns: repeat(2, auto);
55
+ grid-template-rows: repeat(3, auto);
56
+
57
+ .slot1 {
58
+ grid-column: 1 / span 2;
59
+ grid-row: 1;
60
+ }
61
+
62
+ .slot2 {
63
+ grid-column: 1 / span 2;
64
+ grid-row: 2;
65
+ }
66
+
67
+ .slot3 {
68
+ grid-column: 1;
69
+ grid-row: 3;
70
+ }
71
+
72
+ .slot4 {
73
+ grid-column: 2;
74
+ grid-row: 3;
75
+ }
95
76
  }
96
77
 
97
- .slot4 {
98
- grid-column: 3;
99
- grid-row: 2;
78
+ @container (min-width: 1060px) {
79
+ grid-template-columns: repeat(3, auto);
80
+ grid-template-rows: repeat(2, auto);
81
+
82
+ .slot1 {
83
+ grid-column: 1;
84
+ grid-row: 1 / span 2;
85
+ }
86
+
87
+ .slot2 {
88
+ grid-column: 2 / span 2;
89
+ grid-row: 1;
90
+ }
91
+
92
+ .slot3 {
93
+ grid-column: 2;
94
+ grid-row: 2;
95
+ }
96
+
97
+ .slot4 {
98
+ grid-column: 3;
99
+ grid-row: 2;
100
+ }
100
101
  }
101
102
  }
102
103
  }
103
104
  }
104
- }
105
105
  </style>