srcdev-nuxt-components 9.2.5 → 9.2.7

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 (35) 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/01.atoms/page-row/PageRow.vue +2 -0
  18. package/app/components/02.molecules/expandable/expanding-panel/CONSUMER-STYLING.md +103 -0
  19. package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +97 -56
  20. package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +50 -2
  21. package/app/components/02.molecules/expandable/expanding-panel/tests/ExpandingPanel.spec.ts +79 -2
  22. package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +25 -4
  23. package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +2 -1
  24. package/app/components/05.forms/input-button/InputButtonCore.vue +12 -1
  25. package/app/components/05.forms/input-button/tests/InputButtonCore.spec.ts +28 -0
  26. package/app/components/layout-grids/LayoutGridA.vue +59 -59
  27. package/app/composables/tests/useContainerBreakpoints.spec.ts +91 -0
  28. package/app/composables/useContainerBreakpoints.ts +71 -0
  29. package/app/layouts/default.vue +1 -0
  30. package/app/pages/ui/expanding-panel.vue +266 -129
  31. package/app/pages/ui/layout-content-docs.vue +72 -0
  32. package/app/pages/ui/layout-grid-a.vue +5 -58
  33. package/app/types/components/content-docs.d.ts +5 -0
  34. package/app/types/components/index.ts +1 -0
  35. package/package.json +1 -1
@@ -0,0 +1,123 @@
1
+ {
2
+ "SRCDEV ExpandingPanel Basic": {
3
+ "description": "Basic uncontrolled ExpandingPanel with summary and content slots",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<ExpandingPanel name=\"$1delivery\">",
7
+ " <template #summary>",
8
+ " <span>$2Delivery &amp; Returns</span>",
9
+ " </template>",
10
+ " <template #content>",
11
+ " <p>$3Free standard delivery on orders over £50.</p>",
12
+ " </template>",
13
+ "</ExpandingPanel>"
14
+ ]
15
+ },
16
+ "SRCDEV ExpandingPanel Controlled": {
17
+ "description": "ExpandingPanel controlled via v-model with external toggle",
18
+ "scope": "vue,html",
19
+ "body": [
20
+ "<ExpandingPanel name=\"$1faq-1\" v-model=\"$2isOpen\">",
21
+ " <template #summary>",
22
+ " <span>$3What is your returns policy?</span>",
23
+ " </template>",
24
+ " <template #content>",
25
+ " <p>$4You can return any item within 30 days.</p>",
26
+ " </template>",
27
+ "</ExpandingPanel>"
28
+ ]
29
+ },
30
+ "SRCDEV ExpandingPanel Accordion Group": {
31
+ "description": "Multiple ExpandingPanels grouped into a native accordion (shared name, only one open at a time)",
32
+ "scope": "vue,html",
33
+ "body": [
34
+ "<ExpandingPanel name=\"$1faq\">",
35
+ " <template #summary>",
36
+ " <span>$2Question one?</span>",
37
+ " </template>",
38
+ " <template #content>",
39
+ " <p>$3Answer one.</p>",
40
+ " </template>",
41
+ "</ExpandingPanel>",
42
+ "<ExpandingPanel name=\"$1faq\">",
43
+ " <template #summary>",
44
+ " <span>$4Question two?</span>",
45
+ " </template>",
46
+ " <template #content>",
47
+ " <p>$5Answer two.</p>",
48
+ " </template>",
49
+ "</ExpandingPanel>"
50
+ ]
51
+ },
52
+ "SRCDEV ExpandingPanel Force Opened": {
53
+ "description": "ExpandingPanel locked open, no toggle icon, cannot be collapsed",
54
+ "scope": "vue,html",
55
+ "body": [
56
+ "<ExpandingPanel name=\"$1notice\" :force-opened=\"true\">",
57
+ " <template #summary>",
58
+ " <strong>$2Important notice</strong>",
59
+ " </template>",
60
+ " <template #content>",
61
+ " <p>$3This panel cannot be collapsed.</p>",
62
+ " </template>",
63
+ "</ExpandingPanel>"
64
+ ]
65
+ },
66
+ "SRCDEV ExpandingPanel Content Is On Top": {
67
+ "description": "ExpandingPanel with content overlaying page content instead of pushing layout down — styling must go on a wrapper INSIDE #content, never on .inner",
68
+ "scope": "vue,html",
69
+ "body": [
70
+ "<ExpandingPanel",
71
+ " name=\"$1overlay\"",
72
+ " :content-is-on-top=\"true\"",
73
+ " :style-class-passthrough=\"['$2my-overlay-panel']\"",
74
+ ">",
75
+ " <template #summary>",
76
+ " <span>$3Open me — content overlays what's below</span>",
77
+ " </template>",
78
+ " <template #content>",
79
+ " <!-- Wrapper INSIDE the slot carries background/padding/shadow — never .inner itself,",
80
+ " which must stay visually unstyled or it renders while the panel is closed. -->",
81
+ " <div class=\"$2my-overlay-panel-body\">",
82
+ " <p>$4Positioned absolutely below the summary, doesn't push page content down.</p>",
83
+ " </div>",
84
+ " </template>",
85
+ "</ExpandingPanel>"
86
+ ]
87
+ },
88
+ "SRCDEV ExpandingPanel Custom Icon": {
89
+ "description": "ExpandingPanel with a custom toggle icon via #icon slot",
90
+ "scope": "vue,html",
91
+ "body": [
92
+ "<ExpandingPanel name=\"$1custom-icon\">",
93
+ " <template #summary>",
94
+ " <span>$2Section title</span>",
95
+ " </template>",
96
+ " <template #icon>",
97
+ " <span style=\"font-size:1.2rem\">$3+</span>",
98
+ " </template>",
99
+ " <template #content>",
100
+ " <p>$4Content here.</p>",
101
+ " </template>",
102
+ "</ExpandingPanel>"
103
+ ]
104
+ },
105
+ "SRCDEV ExpandingPanel CSS Override": {
106
+ "description": "CSS override scaffold for ExpandingPanel tokens",
107
+ "scope": "css",
108
+ "body": [
109
+ ".$1my-panel {",
110
+ " .expanding-panel {",
111
+ " --expanding-panel-summary-gap: $2;",
112
+ " --expanding-panel-summary-padding-block: $3;",
113
+ " --expanding-panel-icon-size: $4;",
114
+ "",
115
+ " &.content-is-on-top {",
116
+ " --expanding-panel-content-gap: $5;",
117
+ " --expanding-panel-content-z-index: $6;",
118
+ " }",
119
+ " }",
120
+ "}"
121
+ ]
122
+ }
123
+ }
@@ -0,0 +1,410 @@
1
+ <template>
2
+ <component :is="tag" ref="rootEl" class="content-docs" :class="[elementClasses]">
3
+ <div class="content-docs-inner">
4
+ <div v-if="hasDocsNav" class="docs-nav">
5
+ <ExpandingPanel
6
+ :name="docsNavPanelName"
7
+ :animation-duration="200"
8
+ :force-opened="docsNavForceOpen"
9
+ :content-is-on-top="docsNavOnTop"
10
+ >
11
+ <template #summary>
12
+ <h3 class="docs-nav-heading">{{ docsNavLabel }}</h3>
13
+ </template>
14
+ <template #content>
15
+ <nav class="docs-nav-list" aria-label="Docs navigation">
16
+ <ul>
17
+ <li v-for="item in docsNavItems" :key="item.to">
18
+ <component
19
+ :is="NuxtLink"
20
+ :to="item.to"
21
+ class="docs-nav-link"
22
+ :class="{ 'is-active': item.to === activeNavItem }"
23
+ :aria-current="item.to === activeNavItem ? 'page' : undefined"
24
+ @click="activeNavItem = item.to"
25
+ >
26
+ <Icon v-if="item.icon" :name="item.icon" class="docs-nav-link-icon" aria-hidden="true" />
27
+ <span class="docs-nav-link-label">{{ item.label }}</span>
28
+ </component>
29
+ </li>
30
+ </ul>
31
+ </nav>
32
+ </template>
33
+ </ExpandingPanel>
34
+ </div>
35
+ <div v-if="hasDocsContent" class="docs-content">
36
+ <slot name="docsContent"></slot>
37
+ </div>
38
+ <div v-if="hasDocsPageNav" class="docs-page-nav">
39
+ <ExpandingPanel
40
+ :name="docsPageNavPanelName"
41
+ :animation-duration="200"
42
+ :force-opened="docsPageNavForceOpen"
43
+ :content-is-on-top="docsPageNavOnTop"
44
+ >
45
+ <template #summary>
46
+ <h3 class="docs-page-nav-heading">{{ docsPageNavLabel }}</h3>
47
+ </template>
48
+ <template #content>
49
+ <nav class="docs-page-nav-list" aria-label="On this page">
50
+ <ul>
51
+ <li v-for="item in docsPageNavItems" :key="item.to">
52
+ <component
53
+ :is="NuxtLink"
54
+ :to="item.to"
55
+ class="docs-page-nav-link"
56
+ :class="{ 'is-active': item.to === activePageNavItem }"
57
+ :aria-current="item.to === activePageNavItem ? 'page' : undefined"
58
+ @click="activePageNavItem = item.to"
59
+ >
60
+ <Icon v-if="item.icon" :name="item.icon" class="docs-page-nav-link-icon" aria-hidden="true" />
61
+ <span class="docs-page-nav-link-label">{{ item.label }}</span>
62
+ </component>
63
+ </li>
64
+ </ul>
65
+ </nav>
66
+ </template>
67
+ </ExpandingPanel>
68
+ </div>
69
+ </div>
70
+ </component>
71
+ </template>
72
+
73
+ <script setup lang="ts">
74
+ import type { DocsNavItem } from "~/types/components";
75
+
76
+ interface Props {
77
+ tag?: "div" | "section" | "article" | "main";
78
+ docsNavItems?: DocsNavItem[];
79
+ docsPageNavItems?: DocsNavItem[];
80
+ docsNavLabel?: string;
81
+ docsPageNavLabel?: string;
82
+ styleClassPassthrough?: string | string[];
83
+ }
84
+ const props = withDefaults(defineProps<Props>(), {
85
+ tag: "div",
86
+ docsNavItems: () => [],
87
+ docsPageNavItems: () => [],
88
+ docsNavLabel: "Navigation",
89
+ docsPageNavLabel: "On this page",
90
+ styleClassPassthrough: () => [],
91
+ });
92
+
93
+ const activeNavItem = defineModel<string | undefined>("activeNavItem");
94
+ const activePageNavItem = defineModel<string | undefined>("activePageNavItem");
95
+
96
+ const NuxtLink = resolveComponent("NuxtLink");
97
+
98
+ const slots = useSlots();
99
+ const hasDocsNav = computed(() => props.docsNavItems.length > 0);
100
+ const hasDocsContent = computed(() => !!slots.docsContent);
101
+ const hasDocsPageNav = computed(() => props.docsPageNavItems.length > 0);
102
+
103
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
104
+
105
+ watch(
106
+ () => props.styleClassPassthrough,
107
+ () => {
108
+ resetElementClasses(props.styleClassPassthrough);
109
+ }
110
+ );
111
+
112
+ // Breakpoints match this component's own @container queries below —
113
+ // keep both in sync if either changes.
114
+ const rootEl = ref<HTMLElement | null>(null);
115
+ const { greaterOrEqual, smaller } = useContainerBreakpoints({ tablet: 768, desktop: 1024 }, rootEl);
116
+ const isDesktop = greaterOrEqual("desktop");
117
+ const isMobile = smaller("tablet");
118
+
119
+ const docsNavForceOpen = computed(() => isDesktop.value);
120
+ const docsNavOnTop = computed(() => !isDesktop.value);
121
+ const docsPageNavForceOpen = computed(() => !isMobile.value);
122
+ const docsPageNavOnTop = computed(() => isMobile.value);
123
+
124
+ // Shared `name` groups the two <details> into a mutually-exclusive native accordion —
125
+ // wanted on mobile (neither is forceOpened), wrong on tablet/desktop where both are
126
+ // forceOpened simultaneously: a shared name would make the browser force-close one.
127
+ const panelGroupId = useId();
128
+ const docsNavPanelName = computed(() => (isMobile.value ? panelGroupId : `${panelGroupId}-docsNav`));
129
+ const docsPageNavPanelName = computed(() => (isMobile.value ? panelGroupId : `${panelGroupId}-docsPageNav`));
130
+ </script>
131
+
132
+ <style lang="css">
133
+ @layer components {
134
+ .content-docs {
135
+ /* Shared defaults — theme both docsNav and docsPageNav at once */
136
+ --_heading-font-size: var(--content-docs-heading-font-size, 1.4rem);
137
+ --_heading-font-weight: var(--content-docs-heading-font-weight, 700);
138
+ --_heading-color: var(--content-docs-heading-color, light-dark(var(--slate-10), var(--slate-01)));
139
+ --_heading-bg: var(--content-docs-heading-bg, transparent);
140
+ --_heading-margin: var(--content-docs-heading-margin, 0);
141
+ --_heading-padding-block: var(--content-docs-heading-padding-block, 0.4rem 0.8rem);
142
+ --_heading-padding-inline: var(--content-docs-heading-padding-inline, 0);
143
+
144
+ --_panel-bg: var(--content-docs-panel-bg, light-dark(var(--slate-00), var(--slate-10)));
145
+ --_panel-padding-block: var(--content-docs-panel-padding-block, 0.8rem);
146
+ --_panel-padding-inline: var(--content-docs-panel-padding-inline, 0.8rem);
147
+ --_panel-border-radius: var(--content-docs-panel-border-radius, 0.5rem);
148
+
149
+ /* Fixed-width grid tracks — the docsNav/docsPageNav columns at tablet/desktop.
150
+ docsPageNav gets a narrower track at tablet since docsNav is full-width there. */
151
+ --_nav-column-width: var(--content-docs-nav-column-width, 23rem);
152
+ --_page-nav-column-width: var(--content-docs-page-nav-column-width, 22rem);
153
+ --_page-nav-column-width-tablet: var(--content-docs-page-nav-column-width-tablet, 20rem);
154
+
155
+ --_link-font-size: var(--content-docs-link-font-size, 1.4rem);
156
+ --_link-padding-block: var(--content-docs-link-padding-block, 0.6rem);
157
+ --_link-padding-inline: var(--content-docs-link-padding-inline, 0.8rem);
158
+ --_link-margin-block: var(--content-docs-link-margin-block, 0);
159
+ --_link-border-radius: var(--content-docs-link-border-radius, 0.4rem);
160
+ --_link-color: var(--content-docs-link-color, light-dark(var(--slate-09), var(--slate-01)));
161
+ --_link-bg: var(--content-docs-link-bg, transparent);
162
+ --_link-hover-bg: var(--content-docs-link-hover-bg, light-dark(var(--slate-01), var(--slate-09)));
163
+ --_link-hover-color: var(--content-docs-link-hover-color, var(--_link-color));
164
+ --_link-active-bg: var(--content-docs-link-active-bg, light-dark(var(--green-01), var(--green-09)));
165
+ --_link-active-color: var(--content-docs-link-active-color, light-dark(var(--green-10), var(--green-04)));
166
+
167
+ /* Per-side overrides — each falls back to the shared token above */
168
+ --_nav-heading-font-size: var(--content-docs-nav-heading-font-size, var(--_heading-font-size));
169
+ --_nav-heading-font-weight: var(--content-docs-nav-heading-font-weight, var(--_heading-font-weight));
170
+ --_nav-heading-color: var(--content-docs-nav-heading-color, var(--_heading-color));
171
+ --_nav-heading-bg: var(--content-docs-nav-heading-bg, var(--_heading-bg));
172
+ --_nav-heading-margin: var(--content-docs-nav-heading-margin, var(--_heading-margin));
173
+ --_nav-heading-padding-block: var(--content-docs-nav-heading-padding-block, var(--_heading-padding-block));
174
+ --_nav-heading-padding-inline: var(--content-docs-nav-heading-padding-inline, var(--_heading-padding-inline));
175
+
176
+ --_page-nav-heading-font-size: var(--content-docs-page-nav-heading-font-size, var(--_heading-font-size));
177
+ --_page-nav-heading-font-weight: var(--content-docs-page-nav-heading-font-weight, var(--_heading-font-weight));
178
+ --_page-nav-heading-color: var(--content-docs-page-nav-heading-color, var(--_heading-color));
179
+ --_page-nav-heading-bg: var(--content-docs-page-nav-heading-bg, var(--_heading-bg));
180
+ --_page-nav-heading-margin: var(--content-docs-page-nav-heading-margin, var(--_heading-margin));
181
+ --_page-nav-heading-padding-block: var(
182
+ --content-docs-page-nav-heading-padding-block,
183
+ var(--_heading-padding-block)
184
+ );
185
+ --_page-nav-heading-padding-inline: var(
186
+ --content-docs-page-nav-heading-padding-inline,
187
+ var(--_heading-padding-inline)
188
+ );
189
+
190
+ --_nav-panel-bg: var(--content-docs-nav-panel-bg, var(--_panel-bg));
191
+ --_page-nav-panel-bg: var(--content-docs-page-nav-panel-bg, var(--_panel-bg));
192
+
193
+ --_nav-link-color: var(--content-docs-nav-link-color, var(--_link-color));
194
+ --_nav-link-hover-bg: var(--content-docs-nav-link-hover-bg, var(--_link-hover-bg));
195
+ --_nav-link-hover-color: var(--content-docs-nav-link-hover-color, var(--_link-hover-color));
196
+ --_nav-link-active-bg: var(--content-docs-nav-link-active-bg, var(--_link-active-bg));
197
+ --_nav-link-active-color: var(--content-docs-nav-link-active-color, var(--_link-active-color));
198
+
199
+ --_page-nav-link-color: var(--content-docs-page-nav-link-color, var(--_link-color));
200
+ --_page-nav-link-hover-bg: var(--content-docs-page-nav-link-hover-bg, var(--_link-hover-bg));
201
+ --_page-nav-link-hover-color: var(--content-docs-page-nav-link-hover-color, var(--_link-hover-color));
202
+ --_page-nav-link-active-bg: var(--content-docs-page-nav-link-active-bg, var(--_link-active-bg));
203
+ --_page-nav-link-active-color: var(--content-docs-page-nav-link-active-color, var(--_link-active-color));
204
+
205
+ container-type: inline-size;
206
+ container-name: contentDocs;
207
+
208
+ .content-docs-inner {
209
+ display: grid;
210
+ grid-template-areas:
211
+ "docsNav"
212
+ "docsPageNav"
213
+ "docsContent";
214
+ gap: 1.6rem;
215
+ align-items: start;
216
+
217
+ @container contentDocs (width >= 768px) {
218
+ grid-template-areas:
219
+ "docsNav docsPageNav"
220
+ "docsContent docsPageNav";
221
+ gap: 1.6rem;
222
+ grid-template-columns: 1fr var(--_page-nav-column-width-tablet);
223
+ grid-template-rows: auto 1fr;
224
+ }
225
+
226
+ @container contentDocs (width >= 1024px) {
227
+ grid-template-areas: "docsNav docsContent docsPageNav";
228
+ gap: 1.6rem;
229
+ grid-template-columns: var(--_nav-column-width) 1fr var(--_page-nav-column-width);
230
+ }
231
+
232
+ .docs-nav {
233
+ grid-area: docsNav;
234
+ align-items: start;
235
+
236
+ @container contentDocs (width >= 1024px) {
237
+ .expanding-panel {
238
+ & .expanding-panel-details {
239
+ .expanding-panel-summary {
240
+ cursor: default;
241
+ }
242
+ }
243
+ }
244
+ }
245
+ }
246
+ .docs-content {
247
+ grid-area: docsContent;
248
+ align-items: start;
249
+ }
250
+ .docs-page-nav {
251
+ grid-area: docsPageNav;
252
+ align-items: start;
253
+
254
+ @container contentDocs (width >= 768px) {
255
+ .expanding-panel {
256
+ & .expanding-panel-details {
257
+ .expanding-panel-summary {
258
+ cursor: default;
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ .docs-nav-heading {
267
+ font-size: var(--_nav-heading-font-size);
268
+ font-weight: var(--_nav-heading-font-weight);
269
+ color: var(--_nav-heading-color);
270
+ background-color: var(--_nav-heading-bg);
271
+ margin: var(--_nav-heading-margin);
272
+ padding-block: var(--_nav-heading-padding-block);
273
+ padding-inline: var(--_nav-heading-padding-inline);
274
+ }
275
+ .docs-page-nav-heading {
276
+ font-size: var(--_page-nav-heading-font-size);
277
+ font-weight: var(--_page-nav-heading-font-weight);
278
+ color: var(--_page-nav-heading-color);
279
+ background-color: var(--_page-nav-heading-bg);
280
+ margin: var(--_page-nav-heading-margin);
281
+ padding-block: var(--_page-nav-heading-padding-block);
282
+ padding-inline: var(--_page-nav-heading-padding-inline);
283
+ }
284
+
285
+ .docs-nav-list {
286
+ border-radius: var(--_panel-border-radius);
287
+ padding-block: var(--_panel-padding-block);
288
+ padding-inline: var(--_panel-padding-inline);
289
+ background-color: var(--_nav-panel-bg);
290
+
291
+ ul {
292
+ list-style: none;
293
+ margin: 0;
294
+ padding: 0;
295
+ display: flex;
296
+ flex-direction: column;
297
+ gap: 0.4rem;
298
+ }
299
+ }
300
+ .docs-page-nav-list {
301
+ border-radius: var(--_panel-border-radius);
302
+ padding-block: var(--_panel-padding-block);
303
+ padding-inline: var(--_panel-padding-inline);
304
+ background-color: var(--_page-nav-panel-bg);
305
+
306
+ ul {
307
+ list-style: none;
308
+ margin: 0;
309
+ padding: 0;
310
+ display: flex;
311
+ flex-direction: column;
312
+ gap: 0.4rem;
313
+ }
314
+ }
315
+
316
+ /* Grid (not flex) so the label column stays aligned at a fixed offset whether or
317
+ not an item has an icon — an icon-less item's label still starts in column 2,
318
+ instead of collapsing back to column 1 like it would with flex.
319
+
320
+ Icon at the end: set --docs-nav-link-icon-order to rtl. This mirrors which
321
+ physical side each grid column renders on without touching column sizing —
322
+ swapping grid-column values directly would leave the label squeezed into the
323
+ icon-sized track instead. The label/icon direction resets further down undo
324
+ the mirroring for their own content so text and icon glyphs don't visually flip. */
325
+ .docs-nav-link {
326
+ display: grid;
327
+ grid-template-columns: var(--docs-nav-link-icon-size, 1.6rem) 1fr;
328
+ align-items: center;
329
+ gap: var(--docs-nav-link-icon-gap, 0.6rem);
330
+ border-radius: var(--_link-border-radius);
331
+ font-size: var(--_link-font-size);
332
+ padding-block: var(--_link-padding-block);
333
+ padding-inline: var(--_link-padding-inline);
334
+ margin-block: var(--_link-margin-block);
335
+ background-color: var(--_link-bg);
336
+ text-decoration: none;
337
+ direction: var(--docs-nav-link-icon-order, ltr);
338
+ color: var(--_nav-link-color);
339
+ transition:
340
+ background-color var(--control-transition-duration, 200ms) var(--control-transition-ease, ease),
341
+ color var(--control-transition-duration, 200ms) var(--control-transition-ease, ease);
342
+
343
+ &:focus-visible {
344
+ outline: 0.2rem solid var(--theme-ring, currentcolor);
345
+ outline-offset: -0.2rem;
346
+ }
347
+
348
+ &:hover {
349
+ background-color: var(--_nav-link-hover-bg);
350
+ color: var(--_nav-link-hover-color);
351
+ }
352
+
353
+ &.is-active {
354
+ background-color: var(--_nav-link-active-bg);
355
+ color: var(--_nav-link-active-color);
356
+ font-weight: 600;
357
+ }
358
+ }
359
+ .docs-page-nav-link {
360
+ display: grid;
361
+ grid-template-columns: var(--docs-nav-link-icon-size, 1.6rem) 1fr;
362
+ align-items: center;
363
+ gap: var(--docs-nav-link-icon-gap, 0.6rem);
364
+ border-radius: var(--_link-border-radius);
365
+ font-size: var(--_link-font-size);
366
+ padding-block: var(--_link-padding-block);
367
+ padding-inline: var(--_link-padding-inline);
368
+ margin-block: var(--_link-margin-block);
369
+ background-color: var(--_link-bg);
370
+ text-decoration: none;
371
+ direction: var(--docs-page-nav-link-icon-order, ltr);
372
+ color: var(--_page-nav-link-color);
373
+ transition:
374
+ background-color var(--control-transition-duration, 200ms) var(--control-transition-ease, ease),
375
+ color var(--control-transition-duration, 200ms) var(--control-transition-ease, ease);
376
+
377
+ &:focus-visible {
378
+ outline: 0.2rem solid var(--theme-ring, currentcolor);
379
+ outline-offset: -0.2rem;
380
+ }
381
+
382
+ &:hover {
383
+ background-color: var(--_page-nav-link-hover-bg);
384
+ color: var(--_page-nav-link-hover-color);
385
+ }
386
+
387
+ &.is-active {
388
+ background-color: var(--_page-nav-link-active-bg);
389
+ color: var(--_page-nav-link-active-color);
390
+ font-weight: 600;
391
+ }
392
+ }
393
+
394
+ .docs-nav-link-icon,
395
+ .docs-page-nav-link-icon {
396
+ grid-column: 1;
397
+ direction: ltr;
398
+ flex-shrink: 0;
399
+ width: var(--docs-nav-link-icon-size, 1.6rem);
400
+ height: var(--docs-nav-link-icon-size, 1.6rem);
401
+ }
402
+
403
+ .docs-nav-link-label,
404
+ .docs-page-nav-link-label {
405
+ grid-column: 2;
406
+ direction: ltr;
407
+ }
408
+ }
409
+ }
410
+ </style>
@@ -0,0 +1,53 @@
1
+ import { test, expect, type Page } from "@playwright/test";
2
+
3
+ // --- Config ---
4
+ const STORYBOOK_URL = "http://127.0.0.1:6006";
5
+ const STORY_BASE = "atoms-content-wrappers-contentdocs";
6
+ const ELEMENT_TIMEOUT = 15_000;
7
+
8
+ // --- Helpers ---
9
+ const getStoryUrl = (story: string): string => `${STORYBOOK_URL}/?path=/story/${STORY_BASE}--${story}`;
10
+
11
+ const getElement = async (page: Page, story: string, viewport?: { width: number; height: number }) => {
12
+ if (viewport) await page.setViewportSize(viewport);
13
+ await page.goto(getStoryUrl(story), { waitUntil: "load" });
14
+ const frame = page.frameLocator("#storybook-preview-iframe");
15
+ await frame.locator("#storybook-root > *").waitFor({ state: "visible", timeout: ELEMENT_TIMEOUT });
16
+ const el = frame.locator(".content-docs");
17
+ await el.waitFor({ state: "visible", timeout: ELEMENT_TIMEOUT });
18
+ return el;
19
+ };
20
+
21
+ // -------------------------
22
+ // Baseline (default story, default args)
23
+ // -------------------------
24
+ test.describe("ContentDocs — baseline", () => {
25
+ test("default matches snapshot", async ({ page }) => {
26
+ const el = await getElement(page, "default");
27
+ await expect(el).toHaveScreenshot("default.png");
28
+ });
29
+ });
30
+
31
+ // -------------------------
32
+ // Breakpoint states — docsNav (toggleable) and docsPageNav (forceOpened) summary
33
+ // rows must render at the same height so their content columns start aligned.
34
+ // See ExpandingPanel's icon-wrapper--hidden fix: the icon-wrapper is always kept
35
+ // in the DOM (not v-if'd away) so a forceOpened panel's row doesn't shrink relative
36
+ // to a toggleable sibling's.
37
+ // -------------------------
38
+ test.describe("ContentDocs — breakpoint states", () => {
39
+ test("tablet: docsNav toggleable + docsPageNav forceOpened stay row-aligned", async ({ page }) => {
40
+ const el = await getElement(page, "default", { width: 1300, height: 700 });
41
+ await expect(el).toHaveScreenshot("state-tablet.png");
42
+ });
43
+
44
+ test("desktop: both panels forceOpened", async ({ page }) => {
45
+ const el = await getElement(page, "default", { width: 1800, height: 700 });
46
+ await expect(el).toHaveScreenshot("state-desktop.png");
47
+ });
48
+
49
+ test("mobile: both panels toggleable, collapsed by default", async ({ page }) => {
50
+ const el = await getElement(page, "default", { width: 500, height: 900 });
51
+ await expect(el).toHaveScreenshot("state-mobile.png");
52
+ });
53
+ });