srcdev-nuxt-components 9.2.11 → 9.3.1

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 (73) hide show
  1. package/.claude/hooks/check-component-update.sh +66 -0
  2. package/.claude/settings.json +1 -1
  3. package/.claude/skills/component-local-style-override.md +45 -0
  4. package/.claude/skills/components/accordian-core.md +26 -5
  5. package/.claude/skills/components/expanding-panel-classic.md +221 -0
  6. package/.claude/skills/components/expanding-panel.md +8 -8
  7. package/.claude/skills/components/navigation-items.md +51 -0
  8. package/.claude/skills/components/responsive-header.md +161 -0
  9. package/.claude/skills/components/site-header.md +98 -0
  10. package/.claude/skills/index.md +6 -1
  11. package/.claude/skills/page-transitions.md +116 -0
  12. package/.claude/skills/theming-form-geometry-tokens.md +12 -0
  13. package/.claude/skills/theming-override-default.md +14 -0
  14. package/.claude/skills/theming-partial-override.md +17 -0
  15. package/.vscode/srcdev-component-accordian-core.code-snippets +74 -0
  16. package/.vscode/srcdev-component-expanding-panel-classic.code-snippets +121 -0
  17. package/.vscode/srcdev-component-expanding-panel.code-snippets +1 -3
  18. package/.vscode/srcdev-component-responsive-header.code-snippets +46 -0
  19. package/.vscode/srcdev-component-site-header.code-snippets +74 -0
  20. package/app/app.config.ts +1 -1
  21. package/app/assets/styles/setup/01.config/index.css +2 -2
  22. package/app/assets/styles/setup/02.colours/index.css +8 -8
  23. package/app/assets/styles/setup/03.theming/index.css +6 -6
  24. package/app/assets/styles/setup/04.elements/forms/index.css +2 -2
  25. package/app/assets/styles/setup/04.elements/index.css +1 -1
  26. package/app/assets/styles/setup/05.typography/01.tokens/index.css +3 -3
  27. package/app/assets/styles/setup/05.typography/02.utility-classes/index.css +6 -6
  28. package/app/assets/styles/setup/05.typography/index.css +2 -2
  29. package/app/assets/styles/setup/06.utility-classes/animations/index.css +4 -4
  30. package/app/assets/styles/setup/06.utility-classes/index.css +3 -3
  31. package/app/assets/styles/setup/06.utility-classes/spacing/index.css +3 -3
  32. package/app/assets/styles/setup/a11y/index.css +1 -1
  33. package/app/assets/styles/setup/index.css +7 -7
  34. package/app/components/01.atoms/content-wrappers/docs-pages/tests/ContentDocs.spec.ts +3 -1
  35. package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +9 -5
  36. package/app/components/02.molecules/expandable/accordian/AccordianCore.vue +19 -11
  37. package/app/components/02.molecules/expandable/accordian/CONSUMER-STYLING.md +93 -0
  38. package/app/components/02.molecules/expandable/accordian/stories/AccordianCore.stories.ts +31 -0
  39. package/app/components/02.molecules/expandable/accordian/tests/AccordianCore.spec.ts +31 -0
  40. package/app/components/02.molecules/expandable/accordian/tests/__snapshots__/AccordianCore.spec.ts.snap +7 -21
  41. package/app/components/02.molecules/expandable/expanding-panel/CONSUMER-STYLING.md +10 -10
  42. package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +35 -37
  43. package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +5 -6
  44. package/app/components/02.molecules/expandable/expanding-panel/tests/ExpandingPanel.spec.ts +6 -0
  45. package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +5 -13
  46. package/app/components/02.molecules/expandable/expanding-panel-classic/CONSUMER-STYLING.md +103 -0
  47. package/app/components/02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue +191 -0
  48. package/app/components/02.molecules/expandable/expanding-panel-classic/stories/ExpandingPanelClassic.stories.ts +293 -0
  49. package/app/components/02.molecules/expandable/expanding-panel-classic/tests/ExpandingPanelClassic.spec.ts +514 -0
  50. package/app/components/02.molecules/expandable/expanding-panel-classic/tests/__snapshots__/ExpandingPanelClassic.spec.ts.snap +59 -0
  51. package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +9 -5
  52. package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +9 -5
  53. package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +5 -1
  54. package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +66 -0
  55. package/app/components/{responsive-header → 03.organisms/responsive-header}/NavigationItems.vue +21 -28
  56. package/app/components/{responsive-header → 03.organisms/responsive-header}/ResponsiveHeader.vue +141 -43
  57. package/app/components/03.organisms/responsive-header/stories/NavigationItems.stories.ts +80 -0
  58. package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +122 -0
  59. package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +155 -0
  60. package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +319 -0
  61. package/app/components/03.organisms/responsive-header/tests/__snapshots__/NavigationItems.spec.ts.snap +34 -0
  62. package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +72 -0
  63. package/app/components/03.organisms/site-header/CONSUMER-STYLING.md +60 -0
  64. package/app/components/03.organisms/site-header/SiteHeader.vue +96 -0
  65. package/app/components/03.organisms/site-header/stories/SiteHeader.stories.ts +135 -0
  66. package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +103 -0
  67. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +15 -0
  68. package/app/components/alert-mask/tests/AlertMaskCore.spec.ts +9 -5
  69. package/app/composables/tests/useContainerBreakpoints.spec.ts +27 -7
  70. package/app/composables/tests/useWhatsApp.spec.ts +10 -4
  71. package/app/layouts/default.vue +64 -67
  72. package/nuxt.config.ts +9 -0
  73. package/package.json +29 -21
@@ -0,0 +1,121 @@
1
+ {
2
+ "SRCDEV ExpandingPanelClassic Basic": {
3
+ "description": "Basic uncontrolled ExpandingPanelClassic with summary and content slots",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<ExpandingPanelClassic 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
+ "</ExpandingPanelClassic>"
14
+ ]
15
+ },
16
+ "SRCDEV ExpandingPanelClassic Controlled": {
17
+ "description": "ExpandingPanelClassic controlled via v-model with external toggle",
18
+ "scope": "vue,html",
19
+ "body": [
20
+ "<ExpandingPanelClassic 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
+ "</ExpandingPanelClassic>"
28
+ ]
29
+ },
30
+ "SRCDEV ExpandingPanelClassic Accordion Group": {
31
+ "description": "Multiple ExpandingPanelClassic panels grouped into a native accordion (shared name, only one open at a time)",
32
+ "scope": "vue,html",
33
+ "body": [
34
+ "<ExpandingPanelClassic name=\"$1faq\">",
35
+ " <template #summary>",
36
+ " <span>$2Question one?</span>",
37
+ " </template>",
38
+ " <template #content>",
39
+ " <p>$3Answer one.</p>",
40
+ " </template>",
41
+ "</ExpandingPanelClassic>",
42
+ "<ExpandingPanelClassic name=\"$1faq\">",
43
+ " <template #summary>",
44
+ " <span>$4Question two?</span>",
45
+ " </template>",
46
+ " <template #content>",
47
+ " <p>$5Answer two.</p>",
48
+ " </template>",
49
+ "</ExpandingPanelClassic>"
50
+ ]
51
+ },
52
+ "SRCDEV ExpandingPanelClassic Force Opened": {
53
+ "description": "ExpandingPanelClassic locked open, no toggle icon, cannot be collapsed",
54
+ "scope": "vue,html",
55
+ "body": [
56
+ "<ExpandingPanelClassic 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
+ "</ExpandingPanelClassic>"
64
+ ]
65
+ },
66
+ "SRCDEV ExpandingPanelClassic Content Is On Top": {
67
+ "description": "ExpandingPanelClassic with content overlaying page content instead of pushing layout down",
68
+ "scope": "vue,html",
69
+ "body": [
70
+ "<ExpandingPanelClassic",
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
+ " <div class=\"$2my-overlay-panel-body\">",
80
+ " <p>$4Positioned absolutely below the summary, doesn't push page content down.</p>",
81
+ " </div>",
82
+ " </template>",
83
+ "</ExpandingPanelClassic>"
84
+ ]
85
+ },
86
+ "SRCDEV ExpandingPanelClassic Custom Icon": {
87
+ "description": "ExpandingPanelClassic with a custom toggle icon via #icon slot",
88
+ "scope": "vue,html",
89
+ "body": [
90
+ "<ExpandingPanelClassic name=\"$1custom-icon\">",
91
+ " <template #summary>",
92
+ " <span>$2Section title</span>",
93
+ " </template>",
94
+ " <template #icon>",
95
+ " <span style=\"font-size:1.2rem\">$3+</span>",
96
+ " </template>",
97
+ " <template #content>",
98
+ " <p>$4Content here.</p>",
99
+ " </template>",
100
+ "</ExpandingPanelClassic>"
101
+ ]
102
+ },
103
+ "SRCDEV ExpandingPanelClassic CSS Override": {
104
+ "description": "CSS override scaffold for ExpandingPanelClassic tokens",
105
+ "scope": "css",
106
+ "body": [
107
+ ".$1my-panel {",
108
+ " .expanding-panel-classic {",
109
+ " --expanding-panel-classic-summary-gap: $2;",
110
+ " --expanding-panel-classic-summary-padding-block: $3;",
111
+ " --expanding-panel-classic-icon-size: $4;",
112
+ "",
113
+ " &.content-is-on-top {",
114
+ " --expanding-panel-classic-content-gap: $5;",
115
+ " --expanding-panel-classic-content-z-index: $6;",
116
+ " }",
117
+ " }",
118
+ "}"
119
+ ]
120
+ }
121
+ }
@@ -64,7 +64,7 @@
64
64
  ]
65
65
  },
66
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",
67
+ "description": "ExpandingPanel with content overlaying page content instead of pushing layout down",
68
68
  "scope": "vue,html",
69
69
  "body": [
70
70
  "<ExpandingPanel",
@@ -76,8 +76,6 @@
76
76
  " <span>$3Open me — content overlays what's below</span>",
77
77
  " </template>",
78
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
79
  " <div class=\"$2my-overlay-panel-body\">",
82
80
  " <p>$4Positioned absolutely below the summary, doesn't push page content down.</p>",
83
81
  " </div>",
@@ -0,0 +1,46 @@
1
+ {
2
+ "SRCDEV ResponsiveHeader Basic": {
3
+ "description": "ResponsiveHeader with dropdown groups and a secondaryNavigation slot",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<ResponsiveHeader",
7
+ " :responsive-nav-links=\"$1responsiveNavLinks\"",
8
+ " :style-class-passthrough=\"['site-header-nav']\"",
9
+ ">",
10
+ " <template #secondaryNavigation>",
11
+ " <NuxtLink to=\"$2/settings\" aria-label=\"$3Settings\">",
12
+ " <Icon name=\"$4material-symbols:settings-outline-rounded\" />",
13
+ " </NuxtLink>",
14
+ " </template>",
15
+ "</ResponsiveHeader>"
16
+ ]
17
+ },
18
+ "SRCDEV ResponsiveHeader Nav Data (with dropdown)": {
19
+ "description": "responsiveNavLinks data with a dropdown group",
20
+ "scope": "typescript,vue",
21
+ "body": [
22
+ "const $1responsiveNavLinks = {",
23
+ " firstNav: [",
24
+ " { name: \"$2Home\", path: \"$3/\" },",
25
+ " {",
26
+ " name: \"$4Components\",",
27
+ " childLinksTitle: \"$5UI Components\",",
28
+ " childLinks: [{ name: \"$6Item\", path: \"$7/path\" }],",
29
+ " },",
30
+ " ],",
31
+ "};"
32
+ ]
33
+ },
34
+ "SRCDEV ResponsiveHeader CSS Override": {
35
+ "description": "CSS override scaffold for ResponsiveHeader token styling — sets a fixed (non-fluid) link font-size",
36
+ "scope": "css",
37
+ "body": [
38
+ ".$1site-header-nav {",
39
+ " --responsive-header-link-color: $2inherit;",
40
+ " --responsive-header-bg: $3transparent;",
41
+ " /* Fixed, NOT var(--step-*) — see responsive-header.md's measurement-pipeline note. */",
42
+ " --responsive-header-link-font-size: ${4:1.4rem};",
43
+ "}"
44
+ ]
45
+ }
46
+ }
@@ -0,0 +1,74 @@
1
+ {
2
+ "SRCDEV SiteHeader Basic": {
3
+ "description": "SiteHeader wrapping PageRow + SkipLinks + ResponsiveHeader, with a branding slot",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<SiteHeader",
7
+ " :responsive-nav-links=\"$1responsiveNavLinks\"",
8
+ " :style-class-passthrough=\"['header']\"",
9
+ " :nav-style-class-passthrough=\"['site-header-nav']\"",
10
+ ">",
11
+ " <template #branding>",
12
+ " <NuxtLink to=\"/\" class=\"home-link\">$2Brand</NuxtLink>",
13
+ " </template>",
14
+ "</SiteHeader>"
15
+ ]
16
+ },
17
+ "SRCDEV SiteHeader With Secondary Navigation": {
18
+ "description": "SiteHeader with the secondaryNavigation slot (e.g. a settings icon link)",
19
+ "scope": "vue,html",
20
+ "body": [
21
+ "<SiteHeader :responsive-nav-links=\"$1responsiveNavLinks\">",
22
+ " <template #branding>",
23
+ " <NuxtLink to=\"/\" class=\"home-link\">$2Brand</NuxtLink>",
24
+ " </template>",
25
+ " <template #secondaryNavigation>",
26
+ " <NuxtLink to=\"$3/settings\" aria-label=\"$4Settings\">",
27
+ " <Icon name=\"$5material-symbols:settings-outline-rounded\" />",
28
+ " </NuxtLink>",
29
+ " </template>",
30
+ "</SiteHeader>"
31
+ ]
32
+ },
33
+ "SRCDEV SiteHeader Nav Data (flat)": {
34
+ "description": "Simple flat responsiveNavLinks data with no dropdowns, for a few-page site",
35
+ "scope": "typescript,vue",
36
+ "body": [
37
+ "const $1responsiveNavLinks = {",
38
+ " main: [",
39
+ " { name: \"$2Home\", path: \"$3/\" },",
40
+ " { name: \"$4About\", path: \"$5/about\" },",
41
+ " { name: \"$6Contact\", path: \"$7/contact\" },",
42
+ " ],",
43
+ "};"
44
+ ]
45
+ },
46
+ "SRCDEV SiteHeader Nav Data (with dropdown)": {
47
+ "description": "responsiveNavLinks data with a dropdown group, for the full overflow-nav experience",
48
+ "scope": "typescript,vue",
49
+ "body": [
50
+ "const $1responsiveNavLinks = {",
51
+ " firstNav: [",
52
+ " {",
53
+ " name: \"$2Components\",",
54
+ " childLinksTitle: \"$3UI Components\",",
55
+ " childLinks: [{ name: \"$4Item\", path: \"$5/path\" }],",
56
+ " },",
57
+ " { name: \"$6Typography\", path: \"$7/typography\" },",
58
+ " ],",
59
+ "};"
60
+ ]
61
+ },
62
+ "SRCDEV SiteHeader CSS Override": {
63
+ "description": "CSS override scaffold for SiteHeader token styling",
64
+ "scope": "css",
65
+ "body": [
66
+ ".$1header {",
67
+ " --site-header-gap: $2 2.4rem;",
68
+ " --site-header-padding-inline: $3 2.4rem;",
69
+ " --site-header-bg: $4transparent;",
70
+ " /* --site-header-position: sticky; */",
71
+ "}"
72
+ ]
73
+ }
74
+ }
package/app/app.config.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { SemanticTheme, DisplayPromptTheme, DisplayToastTheme, DisplayToastPosition, DisplayToastAlignment } from "~/types/components";
1
+ import type { SemanticTheme, DisplayPromptTheme, DisplayToastTheme, DisplayToastPosition, DisplayToastAlignment } from "./types/components";
2
2
 
3
3
  export default defineAppConfig({
4
4
  srcdev: {
@@ -1,2 +1,2 @@
1
- @import "./_normalise";
2
- @import "./_head";
1
+ @import "./_normalise.css";
2
+ @import "./_head.css";
@@ -1,8 +1,8 @@
1
- @import "./_theme-params";
2
- @import "./_amber";
3
- @import "./_blue";
4
- @import "./_green";
5
- @import "./_orange";
6
- @import "./_red";
7
- @import "./_slate";
8
- @import "./_sunset";
1
+ @import "./_theme-params.css";
2
+ @import "./_amber.css";
3
+ @import "./_blue.css";
4
+ @import "./_green.css";
5
+ @import "./_orange.css";
6
+ @import "./_red.css";
7
+ @import "./_slate.css";
8
+ @import "./_sunset.css";
@@ -1,6 +1,6 @@
1
- @import "./theme-ramp";
2
- @import "./_theme-slots";
3
- @import "./_default";
4
- @import "./_error";
5
- @import "./_success";
6
- @import "./_warning";
1
+ @import "./theme-ramp.css";
2
+ @import "./_theme-slots.css";
3
+ @import "./_default.css";
4
+ @import "./_error.css";
5
+ @import "./_success.css";
6
+ @import "./_warning.css";
@@ -1,7 +1,7 @@
1
- @import "./00.element-defaults";
1
+ @import "./00.element-defaults.css";
2
2
  @import "./01.field-layout-container-level.css";
3
3
  @import "./02.typography.css";
4
- @import "./03.generic-input-geometry";
4
+ @import "./03.generic-input-geometry.css";
5
5
  @import "./04.slot-icon-system.css";
6
6
  @import "./05.checkbox-radio-geometry.css";
7
7
  @import "./06.button-geometry.css";
@@ -1 +1 @@
1
- @import "./forms/";
1
+ @import "./forms/index.css";
@@ -1,3 +1,3 @@
1
- @import "./_font-family";
2
- @import "./_reponsive-font-sizes";
3
- @import "./_timing-functions";
1
+ @import "./_font-family.css";
2
+ @import "./_reponsive-font-sizes.css";
3
+ @import "./_timing-functions.css";
@@ -1,9 +1,9 @@
1
- @import "./_font-classes-page-heading";
2
- @import "./_font-classes-page-body";
3
- @import "./_font-classes-page-link";
4
- @import "./_font-classes-section";
5
- @import "./_font-classes-article";
6
- @import "./_font-classes-card";
1
+ @import "./_font-classes-page-heading.css";
2
+ @import "./_font-classes-page-body.css";
3
+ @import "./_font-classes-page-link.css";
4
+ @import "./_font-classes-section.css";
5
+ @import "./_font-classes-article.css";
6
+ @import "./_font-classes-card.css";
7
7
  @import "./_generic-font-classes.css";
8
8
  @import "./_generic-font-variation-settings.css";
9
9
  @import "./_generic-font-weights.css";
@@ -1,2 +1,2 @@
1
- @import "./01.tokens";
2
- @import "./02.utility-classes";
1
+ @import "./01.tokens/index.css";
2
+ @import "./02.utility-classes/index.css";
@@ -1,5 +1,5 @@
1
- @import "./_entry-zoom-reveal";
2
- @import "./_entry-slide-in";
3
- @import "./_entry-exit-blur";
4
- @import "./_auto-rotate";
1
+ @import "./_entry-zoom-reveal.css";
2
+ @import "./_entry-slide-in.css";
3
+ @import "./_entry-exit-blur.css";
4
+ @import "./_auto-rotate.css";
5
5
  @import "./_animation-scroller-x.css";
@@ -1,4 +1,4 @@
1
- @import "./animations/";
2
- @import "./spacing/";
1
+ @import "./animations/index.css";
2
+ @import "./spacing/index.css";
3
3
  /* @import "./layout/"; */
4
- @import "./_a11y";
4
+ @import "./_a11y.css";
@@ -1,3 +1,3 @@
1
- @import "./_fluid-spacing";
2
- @import "./_margin";
3
- @import "./_padding";
1
+ @import "./_fluid-spacing.css";
2
+ @import "./_margin.css";
3
+ @import "./_padding.css";
@@ -1 +1 @@
1
- @import "./_variables";
1
+ @import "./_variables.css";
@@ -1,9 +1,9 @@
1
1
  @layer reset, colours, theming, form-tokens, typography, a11y, components, utilities, consumer;
2
2
 
3
- @import "./01.config/" layer(reset);
4
- @import "./02.colours/" layer(colours);
5
- @import "./03.theming/" layer(theming);
6
- @import "./04.elements/" layer(form-tokens);
7
- @import "./05.typography/" layer(typography);
8
- @import "./a11y/" layer(a11y);
9
- @import "./06.utility-classes/" layer(utilities);
3
+ @import "./01.config/index.css" layer(reset);
4
+ @import "./02.colours/index.css" layer(colours);
5
+ @import "./03.theming/index.css" layer(theming);
6
+ @import "./04.elements/index.css" layer(form-tokens);
7
+ @import "./05.typography/index.css" layer(typography);
8
+ @import "./a11y/index.css" layer(a11y);
9
+ @import "./06.utility-classes/index.css" layer(utilities);
@@ -9,7 +9,9 @@ beforeEach(() => {
9
9
  resizeCallback = null;
10
10
  vi.stubGlobal(
11
11
  "ResizeObserver",
12
- vi.fn((callback: ResizeObserverCallback) => {
12
+ // Vitest 4: a mock's implementation must be a regular function (not an
13
+ // arrow function) to remain constructible via `new`.
14
+ vi.fn(function (callback: ResizeObserverCallback) {
13
15
  resizeCallback = callback;
14
16
  return {
15
17
  observe: vi.fn(),
@@ -2,11 +2,15 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
  import { mountSuspended } from "@nuxt/test-utils/runtime";
3
3
  import AlertMaskedContent from "../AlertMaskedContent.vue";
4
4
 
5
- const mockResizeObserver = vi.fn(() => ({
6
- observe: vi.fn(),
7
- unobserve: vi.fn(),
8
- disconnect: vi.fn(),
9
- }));
5
+ // Vitest 4: a mock's implementation must be a regular function (not an
6
+ // arrow function) to remain constructible via `new`.
7
+ const mockResizeObserver = vi.fn(function () {
8
+ return {
9
+ observe: vi.fn(),
10
+ unobserve: vi.fn(),
11
+ disconnect: vi.fn(),
12
+ };
13
+ });
10
14
 
11
15
  vi.stubGlobal("ResizeObserver", mockResizeObserver);
12
16
 
@@ -1,6 +1,7 @@
1
1
  <template>
2
2
  <div class="display-accordian" :class="[elementClasses]">
3
- <ExpandingPanel
3
+ <component
4
+ :is="panelComponent"
4
5
  v-for="(item, key) in itemCount"
5
6
  :key="key"
6
7
  :name
@@ -17,15 +18,19 @@
17
18
  <template #content>
18
19
  <slot :name="`accordian-${key}-content`"></slot>
19
20
  </template>
20
- </ExpandingPanel>
21
+ </component>
21
22
  </div>
22
23
  </template>
23
24
 
24
25
  <script setup lang="ts">
26
+ import ExpandingPanel from "../expanding-panel/ExpandingPanel.vue";
27
+ import ExpandingPanelClassic from "../expanding-panel-classic/ExpandingPanelClassic.vue";
28
+
25
29
  interface Props {
26
30
  name?: string;
27
31
  itemCount?: number;
28
32
  animationDuration?: number;
33
+ variant?: "modern" | "classic";
29
34
  styleClassPassthrough?: string | string[];
30
35
  }
31
36
 
@@ -33,26 +38,29 @@ const props = withDefaults(defineProps<Props>(), {
33
38
  name: undefined,
34
39
  itemCount: 0,
35
40
  animationDuration: 300,
41
+ variant: "modern",
36
42
  styleClassPassthrough: () => [],
37
43
  });
38
44
 
39
45
  const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
40
46
  const animationDurationStr = computed(() => `${props.animationDuration}ms`);
47
+ const panelComponent = computed(() => (props.variant === "classic" ? ExpandingPanelClassic : ExpandingPanel));
41
48
  </script>
42
49
 
43
50
  <style lang="css">
44
51
  @layer components {
45
- .display-accordian {
46
- max-width: 600px;
47
- margin: 0 auto;
52
+ .display-accordian {
53
+ max-width: 600px;
54
+ margin: 0 auto;
48
55
 
49
- .accordian-item {
50
- &.expanding-panel {
51
- transition:
52
- margin-block-end v-bind(animationDurationStr) ease-in-out,
53
- border-radius v-bind(animationDurationStr) ease-in-out;
56
+ .accordian-item {
57
+ &.expanding-panel,
58
+ &.expanding-panel-classic {
59
+ transition:
60
+ margin-block-end v-bind(animationDurationStr) ease-in-out,
61
+ border-radius v-bind(animationDurationStr) ease-in-out;
62
+ }
54
63
  }
55
64
  }
56
65
  }
57
- }
58
66
  </style>
@@ -0,0 +1,93 @@
1
+ # AccordianCore — Consumer Styling Guide
2
+
3
+ ## No tokens of its own
4
+
5
+ `AccordianCore` doesn't define any `--accordian-*` custom properties. It's a thin wrapper that
6
+ renders `itemCount` copies of a panel component (`ExpandingPanel` by default, or
7
+ `ExpandingPanelClassic` when `variant="classic"`) and forwards `name`/`animationDuration` to each
8
+ one — all real styling surface lives on whichever panel component is active. See that
9
+ component's own `CONSUMER-STYLING.md`:
10
+
11
+ - [../expanding-panel/CONSUMER-STYLING.md](../expanding-panel/CONSUMER-STYLING.md) (default, `variant="modern"`)
12
+ - [../expanding-panel-classic/CONSUMER-STYLING.md](../expanding-panel-classic/CONSUMER-STYLING.md) (`variant="classic"`)
13
+
14
+ `--expanding-panel-*` tokens set at any scope (global, page) apply to `AccordianCore`'s panels
15
+ too, since they're the same underlying component — no `AccordianCore`-specific override is
16
+ needed for summary gap, icon size, etc.
17
+
18
+ ---
19
+
20
+ ## The `.accordian-item` hook
21
+
22
+ Every panel gets `style-class-passthrough="['accordian-item']"` automatically — this is the one
23
+ `AccordianCore`-specific override surface, used to target *all* panels in the group without
24
+ reaching into `ExpandingPanel`/`ExpandingPanelClassic` internals.
25
+
26
+ ```css
27
+ /* In the consuming page's unscoped <style> block */
28
+ .display-accordian {
29
+ .accordian-item.expanding-panel {
30
+ /* modern variant (default) */
31
+ border-block-end: 1px solid var(--theme-border, #e0e0e0);
32
+ }
33
+
34
+ /* If this instance uses variant="classic", target the classic root class instead: */
35
+ .accordian-item.expanding-panel-classic {
36
+ border-block-end: 1px solid var(--theme-border, #e0e0e0);
37
+ }
38
+ }
39
+ ```
40
+
41
+ Note the root class differs by variant (`.expanding-panel` vs `.expanding-panel-classic`) — a
42
+ selector written for one won't match the other. If you switch `variant` on an existing instance,
43
+ check any `.accordian-item.expanding-panel*` overrides still target the right class.
44
+
45
+ ---
46
+
47
+ ## Page-scoped overrides
48
+
49
+ No `:deep()` is required (component styles are unscoped).
50
+
51
+ ```css
52
+ .faq-section {
53
+ .display-accordian {
54
+ max-width: 480px;
55
+
56
+ .accordian-item.expanding-panel {
57
+ --expanding-panel-summary-padding-block: 1.6rem;
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Per-instance overrides via styleClassPassthrough
66
+
67
+ Use sparingly — prefer global or page-scoped CSS. `styleClassPassthrough` on `AccordianCore`
68
+ targets the root `.display-accordian` element, not the individual panels (those already get
69
+ `.accordian-item` automatically):
70
+
71
+ ```vue
72
+ <AccordianCore name="faq" :item-count="3" :style-class-passthrough="['compact-faq']">
73
+ ...
74
+ </AccordianCore>
75
+ ```
76
+
77
+ ```css
78
+ .display-accordian.compact-faq {
79
+ max-width: 400px;
80
+ }
81
+ ```
82
+
83
+ ---
84
+
85
+ ## Notes
86
+
87
+ - `variant` is group-wide: every panel in one `AccordianCore` renders the same component, so
88
+ there's no per-panel styling divergence caused by variant — just remember which root class
89
+ (`.expanding-panel` vs `.expanding-panel-classic`) your `.accordian-item` selectors need.
90
+ - `contentIsOnTop` isn't exposed on `AccordianCore` — it isn't meaningful for a grouped accordion
91
+ (see `ExpandingPanel`'s own docs for why: it's for a single panel overlaying trailing page
92
+ content, not stacked/grouped panels). Use `ExpandingPanel`/`ExpandingPanelClassic` directly if
93
+ you need an overlay panel.
@@ -18,6 +18,14 @@ const meta: Meta<typeof AccordianCore> = {
18
18
  control: { type: "number", min: 0, step: 50 },
19
19
  description: "Expand/collapse animation duration in milliseconds",
20
20
  },
21
+ variant: {
22
+ control: { type: "radio" },
23
+ options: ["modern", "classic"],
24
+ description:
25
+ "Which panel implementation to render: 'modern' (ExpandingPanel, ::details-content based) or " +
26
+ "'classic' (ExpandingPanelClassic, grid-template-rows based) — use classic when the open/close " +
27
+ "animation must run identically in every browser today",
28
+ },
21
29
  styleClassPassthrough: {
22
30
  control: "object",
23
31
  description: "Additional CSS classes applied to the root element",
@@ -27,6 +35,7 @@ const meta: Meta<typeof AccordianCore> = {
27
35
  name: undefined,
28
36
  itemCount: 3,
29
37
  animationDuration: 300,
38
+ variant: "modern",
30
39
  styleClassPassthrough: [],
31
40
  },
32
41
  };
@@ -71,6 +80,28 @@ export const Default: Story = {
71
80
  }),
72
81
  };
73
82
 
83
+ export const ClassicVariant: Story = {
84
+ name: "Classic Variant",
85
+ args: {
86
+ itemCount: 3,
87
+ animationDuration: 300,
88
+ variant: "classic",
89
+ },
90
+ render: (args) => ({
91
+ components: { AccordianCore },
92
+ setup() {
93
+ return { args, slots: buildGenericSlots(args.itemCount ?? 3) };
94
+ },
95
+ template: `
96
+ <AccordianCore v-bind="args">
97
+ <template v-for="(content, name) in slots" #[name] :key="name">
98
+ <span v-html="content" />
99
+ </template>
100
+ </AccordianCore>
101
+ `,
102
+ }),
103
+ };
104
+
74
105
  export const SinglePanel: Story = {
75
106
  args: {
76
107
  itemCount: 1,