srcdev-nuxt-components 9.0.15 → 9.0.17

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 (112) hide show
  1. package/.claude/settings.json +25 -0
  2. package/.claude/skills/component-aria-landmark.md +68 -0
  3. package/.claude/skills/component-dynamic-slots.md +150 -0
  4. package/.claude/skills/component-export-types.md +61 -0
  5. package/.claude/skills/component-local-style-override.md +126 -0
  6. package/.claude/skills/component-prop-driven-container-layout.md +42 -0
  7. package/.claude/skills/components/accordian-core.md +159 -0
  8. package/.claude/skills/components/contact-section.md +101 -0
  9. package/.claude/skills/components/expanding-panel.md +156 -0
  10. package/.claude/skills/components/eyebrow-text.md +25 -0
  11. package/.claude/skills/components/hero-text.md +25 -0
  12. package/.claude/skills/components/layout-grid-by-cols.md +147 -0
  13. package/.claude/skills/components/layout-row.md +35 -0
  14. package/.claude/skills/components/link-text.md +33 -0
  15. package/.claude/skills/components/page-hero-highlights.md +224 -0
  16. package/.claude/skills/components/services-card.md +28 -0
  17. package/.claude/skills/components/services-section.md +25 -0
  18. package/.claude/skills/components/stepper-list.md +227 -0
  19. package/.claude/skills/css-grid-max-width-gutters.md +67 -0
  20. package/.claude/skills/index.md +15 -3
  21. package/.claude/skills/storybook-add-story.md +60 -0
  22. package/.claude/skills/testing-add-unit-test.md +56 -0
  23. package/app/assets/styles/setup/01.config/index.css +0 -1
  24. package/app/assets/styles/setup/03.theming/default/_dark.css +2 -2
  25. package/app/assets/styles/setup/04.elements/forms/02.typography.css +1 -0
  26. package/app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-link.css +14 -14
  27. package/app/assets/styles/setup/index.css +0 -1
  28. package/app/components/01.atoms/card/CardCore.vue +92 -0
  29. package/app/components/01.atoms/card/stories/CardCore.stories.ts +132 -0
  30. package/app/components/01.atoms/card/tests/CardCore.spec.ts +207 -0
  31. package/app/components/01.atoms/card/tests/__snapshots__/CardCore.spec.ts.snap +43 -0
  32. package/app/components/01.atoms/content-wrappers/content-columns-2/ContentColumns2.vue +51 -0
  33. package/app/components/01.atoms/content-wrappers/content-columns-2/stories/ContentColumns2.stories.ts +110 -0
  34. package/app/components/01.atoms/content-wrappers/content-columns-2/tests/ContentColumns2.spec.ts +105 -0
  35. package/app/components/01.atoms/content-wrappers/content-columns-2/tests/__snapshots__/ContentColumns2.spec.ts.snap +14 -0
  36. package/app/components/01.atoms/content-wrappers/content-width/ContentWidth.vue +88 -0
  37. package/app/components/01.atoms/content-wrappers/content-width/stories/ContentWidth.stories.ts +362 -0
  38. package/app/components/01.atoms/content-wrappers/content-width/tests/ContentWidth.spec.ts +132 -0
  39. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue +71 -0
  40. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/stories/LayoutGridByCols.stories.ts +219 -0
  41. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/LayoutGridByCols.spec.ts +174 -0
  42. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/__snapshots__/LayoutGrid.spec.ts.snap +36 -0
  43. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/__snapshots__/LayoutGridByCols.spec.ts.snap +36 -0
  44. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/LayoutGridByWidth.vue +70 -0
  45. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/stories/LayoutGridByWidth.stories.ts +220 -0
  46. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/LayoutGridByWidth.spec.ts +174 -0
  47. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGrid.spec.ts.snap +36 -0
  48. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGridByCols.spec.ts.snap +36 -0
  49. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGridByWidth.spec.ts.snap +36 -0
  50. package/app/components/01.atoms/text-blocks/eyebrow-text/stories/EyebrowText.stories.ts +1 -1
  51. package/app/components/01.atoms/text-blocks/hero-text/stories/HeroText.stories.ts +1 -1
  52. package/app/components/01.atoms/text-blocks/link-text/stories/LinkText.stories.ts +1 -1
  53. package/app/components/02.molecules/contact-section/stories/ContactSection.stories.ts +5 -0
  54. package/app/components/02.molecules/contact-section/tests/ContactSection.spec.ts +15 -0
  55. package/app/components/02.molecules/contact-section/tests/ContactSection.vue +25 -17
  56. package/app/components/{accordian → 02.molecules/expandable/accordian}/stories/AccordianCore.stories.ts +1 -1
  57. package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +245 -0
  58. package/app/components/02.molecules/expandable/expanding-panel/tests/ExpandingPanel.spec.ts +351 -0
  59. package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +38 -0
  60. package/app/components/02.molecules/navigation/navigation-horizontal/NavigationHorizontal.vue +162 -0
  61. package/app/components/02.molecules/navigation/navigation-horizontal/stories/NavigationHorizontal.stories.ts +373 -0
  62. package/app/components/02.molecules/navigation/navigation-horizontal/tests/NavigationHorizontal.spec.ts +152 -0
  63. package/app/components/02.molecules/navigation/navigation-horizontal/tests/__snapshots__/NavigationHorizontal.spec.ts.snap +17 -0
  64. package/app/components/02.molecules/profile-section/ProfileSection.vue +2 -3
  65. package/app/components/02.molecules/profile-section/tests/ProfileSection.spec.ts +2 -2
  66. package/app/components/02.molecules/stepper-list/StepperList.vue +131 -92
  67. package/app/components/02.molecules/stepper-list/stories/StepperList.stories.ts +31 -0
  68. package/app/components/02.molecules/stepper-list/tests/StepperList.spec.ts +24 -0
  69. package/app/components/02.molecules/stepper-list/tests/__snapshots__/StepperList.spec.ts.snap +22 -9
  70. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +782 -0
  71. package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +233 -0
  72. package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +226 -0
  73. package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +69 -0
  74. package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +1 -1
  75. package/app/components/03.organisms/services/services-grids/ServicesSectionGrid.vue +1 -1
  76. package/app/components/03.organisms/services/services-section/ServicesSection.vue +2 -3
  77. package/app/components/04.templates/page-hero-highlights/PageHeroHighlights.vue +239 -0
  78. package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlights.stories.ts +404 -0
  79. package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlights.spec.ts +198 -0
  80. package/app/components/04.templates/page-hero-highlights/tests/__snapshots__/PageHeroHighlights.spec.ts.snap +19 -0
  81. package/app/components/container-glow/ContainerGlowCore.vue +20 -27
  82. package/app/components/forms/input-button/InputButtonCore.vue +105 -104
  83. package/app/components/glowing-border/stories/GlowingBorder.stories.ts +21 -21
  84. package/app/composables/useAriaLabelledById.ts +13 -0
  85. package/app/layouts/default.vue +8 -3
  86. package/app/pages/forms/examples/buttons/index.vue +6 -6
  87. package/app/pages/forms/examples/material/checkbox-radio-panels.vue +3 -3
  88. package/app/pages/forms/examples/material/text-fields.vue +607 -610
  89. package/app/pages/page-hero-highlights.vue +81 -0
  90. package/app/pages/ui/{display-card.vue → card-core.vue} +15 -15
  91. package/app/pages/ui/contact-section.vue +1 -1
  92. package/app/pages/ui/container-glow.vue +1 -1
  93. package/app/pages/ui/content-width.vue +126 -0
  94. package/app/pages/ui/glowing-border.vue +9 -9
  95. package/app/pages/ui/navigation/navigation-horizontal.vue +484 -0
  96. package/app/pages/ui/services/services-section/[slug].vue +3 -1
  97. package/app/types/components/index.ts +1 -0
  98. package/app/types/components/navigation-horizontal.d.ts +11 -0
  99. package/package.json +2 -2
  100. package/app/assets/styles/setup/01.config/_basic-resets.css +0 -9
  101. package/app/components/content-columns/TwoColumns.vue +0 -59
  102. package/app/components/content-columns/stories/TwoColumns.stories.ts +0 -561
  103. package/app/components/content-containers/ContentContainer.vue +0 -89
  104. package/app/components/content-containers/stories/ContentContainer.stories.ts +0 -465
  105. package/app/components/content-grid/ContentGrid.vue +0 -85
  106. package/app/components/display-card/DisplayCard.vue +0 -122
  107. package/app/components/image-galleries/SliderGallery.vue +0 -786
  108. package/app/pages/ui/content-container.vue +0 -112
  109. /package/app/components/{accordian → 02.molecules/expandable/accordian}/AccordianCore.vue +0 -0
  110. /package/app/components/{accordian → 02.molecules/expandable/accordian}/tests/AccordianCore.spec.ts +0 -0
  111. /package/app/components/{accordian → 02.molecules/expandable/accordian}/tests/__snapshots__/AccordianCore.spec.ts.snap +0 -0
  112. /package/app/components/{expanding-panel → 02.molecules/expandable/expanding-panel}/ExpandingPanel.vue +0 -0
@@ -0,0 +1,362 @@
1
+ import type { Meta, StoryObj } from "@nuxtjs/storybook";
2
+ import { computed } from "vue";
3
+ import ContentWidthComponent from "../ContentWidth.vue";
4
+
5
+ interface ContentWidthArgs {
6
+ dataTestid: string;
7
+ tag: string;
8
+ id?: string;
9
+ styleClassPassthrough: string[];
10
+ isLandmark: boolean;
11
+ justifyContent: "start" | "center" | "end";
12
+ showBackground: boolean;
13
+ }
14
+
15
+ type Story = StoryObj<ContentWidthArgs>;
16
+
17
+ export default {
18
+ title: "Atoms/Content Wrappers/Content Width",
19
+ component: ContentWidthComponent,
20
+ argTypes: {
21
+ tag: {
22
+ control: { type: "select" },
23
+ options: ["div", "section", "article", "aside", "header", "footer", "main", "nav", "ul", "ol"],
24
+ description: "HTML tag to render as",
25
+ table: { category: "Semantic" },
26
+ },
27
+ isLandmark: {
28
+ control: { type: "boolean" },
29
+ description: "Whether this element should be a landmark (adds tabindex and aria-label)",
30
+ table: { category: "Accessibility" },
31
+ },
32
+ justifyContent: {
33
+ control: { type: "select" },
34
+ options: ["start", "center", "end"],
35
+ description: "Horizontal alignment of the content track at ≥1092px",
36
+ table: { category: "Layout" },
37
+ },
38
+ showBackground: {
39
+ control: { type: "boolean" },
40
+ description: "Highlight the wrapper background to compare wrapper vs content track width",
41
+ table: { category: "Debug" },
42
+ },
43
+ dataTestid: {
44
+ control: { type: "text" },
45
+ description: "Test ID for the inner content width element",
46
+ table: { category: "Testing" },
47
+ },
48
+ id: {
49
+ control: { type: "text" },
50
+ description: "ID attribute for the component",
51
+ table: { category: "HTML" },
52
+ },
53
+ styleClassPassthrough: {
54
+ table: { disable: true },
55
+ },
56
+ },
57
+ args: {
58
+ tag: "div",
59
+ isLandmark: false,
60
+ justifyContent: "center",
61
+ showBackground: false,
62
+ dataTestid: "content-width",
63
+ id: "",
64
+ styleClassPassthrough: [],
65
+ },
66
+ parameters: {
67
+ docs: {
68
+ description: {
69
+ component:
70
+ "A responsive content width wrapper that uses CSS Container Queries to provide optimal content width. Automatically adjusts from fluid width on small screens to a maximum 1064px width on larger screens (≥1092px) with proper gutters.",
71
+ },
72
+ },
73
+ },
74
+ } as Meta<ContentWidthArgs>;
75
+
76
+ // ===== BASIC STORIES =====
77
+
78
+ export const Default: Story = {
79
+ args: {},
80
+ render: (args) => ({
81
+ components: { ContentWidthComponent },
82
+ setup() {
83
+ const longText =
84
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.";
85
+ const wrapperStyle = computed(() => (args.showBackground ? { background: "rgba(99, 102, 241, 0.15)" } : {}));
86
+ return { args, longText, wrapperStyle };
87
+ },
88
+ template: `
89
+ <div style="background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); min-height: 100vh; padding: 20px 0;">
90
+ <ContentWidthComponent
91
+ :data-testid="args.dataTestid"
92
+ :tag="args.tag"
93
+ :style-class-passthrough="args.styleClassPassthrough"
94
+ :is-landmark="args.isLandmark"
95
+ :justify-content="args.justifyContent"
96
+ :style="wrapperStyle"
97
+ >
98
+ <div style="padding: 20px; background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);">
99
+ <h2 style="margin: 0 0 16px 0; color: #374151; font-size: 24px;">Content Width Example</h2>
100
+ <p style="margin: 0 0 16px 0; color: #6b7280; line-height: 1.6;">{{ longText }}</p>
101
+ <div style="padding: 16px; background: #f3f4f6; border-radius: 8px; margin: 16px 0;">
102
+ <h3 style="margin: 0 0 8px 0; color: #374151; font-size: 16px;">Container Behavior</h3>
103
+ <ul style="margin: 0; color: #6b7280; line-height: 1.5; padding-left: 20px;">
104
+ <li><strong>Small screens:</strong> Fluid width with 16px gutters</li>
105
+ <li><strong>≥1092px screens:</strong> Fixed 1064px max-width, centered</li>
106
+ <li><strong>Container queries:</strong> Responsive layout without media queries</li>
107
+ </ul>
108
+ </div>
109
+ <p style="margin: 0; color: #6b7280; font-size: 14px; font-family: monospace; background: #e5e7eb; padding: 8px; border-radius: 4px;">
110
+ Tag: {{ args.tag }} | Landmark: {{ args.isLandmark ? 'Yes' : 'No' }}
111
+ </p>
112
+ </div>
113
+ </ContentWidthComponent>
114
+ </div>
115
+ `,
116
+ }),
117
+ };
118
+
119
+ // ===== SEMANTIC TAG STORIES =====
120
+
121
+ const semanticRender = (args: ContentWidthArgs) => ({
122
+ components: { ContentWidthComponent },
123
+ setup() {
124
+ const tagInfo: Record<string, { description: string; use: string }> = {
125
+ div: { description: "Generic container", use: "Default wrapper element" },
126
+ section: { description: "Thematic grouping", use: "Groups related content together" },
127
+ article: { description: "Self-contained content", use: "Blog posts, news articles, user comments" },
128
+ aside: { description: "Tangentially related", use: "Sidebars, pull quotes, advertising" },
129
+ header: { description: "Introductory content", use: "Page headers, section headers" },
130
+ footer: { description: "Footer information", use: "Page footers, section footers" },
131
+ main: { description: "Main content", use: "Primary content of the document" },
132
+ nav: { description: "Navigation links", use: "Navigation menus, breadcrumbs" },
133
+ };
134
+ const info = computed(() => tagInfo[args.tag] ?? { description: "Generic container", use: "Default usage" });
135
+ const wrapperStyle = computed(() => (args.showBackground ? { background: "rgba(99, 102, 241, 0.15)" } : {}));
136
+ return { args, info, wrapperStyle };
137
+ },
138
+ template: `
139
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 60vh; padding: 40px 0;">
140
+ <ContentWidthComponent
141
+ :data-testid="args.dataTestid"
142
+ :tag="args.tag"
143
+ :style-class-passthrough="args.styleClassPassthrough"
144
+ :is-landmark="args.isLandmark"
145
+ :justify-content="args.justifyContent"
146
+ :style="wrapperStyle"
147
+ >
148
+ <div style="padding: 30px; background: white; border-radius: 12px; text-align: center;">
149
+ <div style="display: inline-block; padding: 8px 16px; background: #667eea; color: white; border-radius: 6px; font-family: monospace; font-size: 16px; margin-bottom: 20px;">
150
+ &lt;{{ args.tag }}&gt;
151
+ </div>
152
+ <h2 style="margin: 0 0 12px 0; color: #374151; font-size: 22px;">{{ info.description }}</h2>
153
+ <p style="margin: 0 0 20px 0; color: #6b7280; line-height: 1.5; max-width: 400px; margin-left: auto; margin-right: auto;">{{ info.use }}</p>
154
+ <div style="padding: 16px; background: #f8fafc; border-radius: 8px; border-left: 4px solid #667eea;">
155
+ <p style="margin: 0; color: #374151; font-size: 14px;">
156
+ This semantic element helps structure your content meaningfully for both users and assistive technologies.
157
+ </p>
158
+ </div>
159
+ </div>
160
+ </ContentWidthComponent>
161
+ </div>
162
+ `,
163
+ });
164
+
165
+ export const SemanticSection: Story = { args: { tag: "section" }, render: semanticRender };
166
+ export const SemanticArticle: Story = { args: { tag: "article" }, render: semanticRender };
167
+ export const SemanticAside: Story = { args: { tag: "aside" }, render: semanticRender };
168
+ export const SemanticHeader: Story = { args: { tag: "header" }, render: semanticRender };
169
+ export const SemanticMain: Story = { args: { tag: "main", isLandmark: true }, render: semanticRender };
170
+ export const SemanticNav: Story = { args: { tag: "nav", isLandmark: true }, render: semanticRender };
171
+
172
+ // ===== CONTENT EXAMPLES =====
173
+
174
+ export const BlogArticle: Story = {
175
+ args: { tag: "article" },
176
+ parameters: {
177
+ docs: {
178
+ description: {
179
+ story:
180
+ "Example of a blog article layout showing how ContentWidth provides optimal reading width and responsive behavior.",
181
+ },
182
+ },
183
+ },
184
+ render: (args) => ({
185
+ components: { ContentWidthComponent },
186
+ setup() {
187
+ const wrapperStyle = computed(() => (args.showBackground ? { background: "rgba(99, 102, 241, 0.15)" } : {}));
188
+ return { args, wrapperStyle };
189
+ },
190
+ template: `
191
+ <div style="background: #f9fafb;">
192
+ <ContentWidthComponent
193
+ :data-testid="args.dataTestid"
194
+ :tag="args.tag"
195
+ :style-class-passthrough="args.styleClassPassthrough"
196
+ :is-landmark="args.isLandmark"
197
+ :justify-content="args.justifyContent"
198
+ :style="wrapperStyle"
199
+ >
200
+ <article style="padding: 40px; background: white; border-radius: 0;">
201
+ <header style="margin-bottom: 30px; text-align: center;">
202
+ <h1 style="margin: 0 0 8px 0; color: #1f2937; font-size: 32px; font-weight: 700;">The Art of Responsive Design</h1>
203
+ <p style="margin: 0; color: #6b7280; font-size: 16px;">How container queries are revolutionizing web layouts</p>
204
+ </header>
205
+ <div style="line-height: 1.7; color: #374151;">
206
+ <p style="margin: 0 0 24px 0; font-size: 18px; color: #4b5563;">
207
+ Container queries represent a paradigm shift in responsive design, moving beyond viewport-based breakpoints
208
+ to component-centric layouts that adapt to their container's size rather than the entire screen.
209
+ </p>
210
+ <pre style="background: #1f2937; color: #e5e7eb; padding: 20px; border-radius: 8px; overflow-x: auto; margin: 24px 0; font-size: 14px; line-height: 1.4;"><code>container-type: inline-size;
211
+ container-name: content-width;
212
+
213
+ @container content-width (width >= 1092px) {
214
+ --content-max-width: 1064px;
215
+ --gutter: 0;
216
+ }</code></pre>
217
+ <p style="margin: 0;">
218
+ This approach creates more maintainable, flexible layouts that work consistently regardless of
219
+ where the component is placed in your application.
220
+ </p>
221
+ </div>
222
+ </article>
223
+ </ContentWidthComponent>
224
+ </div>
225
+ `,
226
+ }),
227
+ };
228
+
229
+ // ===== ACCESSIBILITY STORIES =====
230
+
231
+ const accessibilityRender = (args: ContentWidthArgs) => ({
232
+ components: { ContentWidthComponent },
233
+ setup() {
234
+ const wrapperStyle = computed(() => (args.showBackground ? { background: "rgba(99, 102, 241, 0.15)" } : {}));
235
+ return { args, wrapperStyle };
236
+ },
237
+ template: `
238
+ <div style="background: #f3f4f6; padding: 20px;">
239
+ <div style="background: white; border-radius: 12px; padding: 20px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);">
240
+ <h3 style="margin: 0 0 16px 0; color: #374151;">Accessibility Features</h3>
241
+ <div style="padding: 12px; background: #f3f4f6; border-radius: 6px; border-left: 3px solid #6366f1;">
242
+ <strong style="color: #4338ca;">Current state:</strong>
243
+ {{ args.isLandmark ? 'Landmark enabled (focusable, labeled)' : 'Standard wrapper (no landmark features)' }}
244
+ </div>
245
+ </div>
246
+ <ContentWidthComponent
247
+ :data-testid="args.dataTestid"
248
+ :tag="args.tag"
249
+ :style-class-passthrough="args.styleClassPassthrough"
250
+ :is-landmark="args.isLandmark"
251
+ :justify-content="args.justifyContent"
252
+ :style="wrapperStyle"
253
+ >
254
+ <div style="padding: 24px; background: white; border-radius: 8px; border: 2px solid #d1d5db;">
255
+ <div :style="{
256
+ padding: '16px',
257
+ background: args.isLandmark ? '#dbeafe' : '#f9fafb',
258
+ borderRadius: '8px',
259
+ border: args.isLandmark ? '2px solid #3b82f6' : '2px solid #e5e7eb'
260
+ }">
261
+ <h3 style="margin: 0 0 12px 0; color: #374151;">
262
+ {{ args.isLandmark ? '🏷️ Landmark Wrapper' : '📦 Standard Wrapper' }}
263
+ </h3>
264
+ <div style="font-family: monospace; font-size: 12px; background: rgba(0, 0, 0, 0.05); padding: 8px; border-radius: 4px;">
265
+ <div>tabIndex: {{ args.isLandmark ? '0' : 'null' }}</div>
266
+ <div>aria-label: {{ args.isLandmark ? '"Content Width Landmark"' : 'undefined' }}</div>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ </ContentWidthComponent>
271
+ </div>
272
+ `,
273
+ });
274
+
275
+ export const WithLandmark: Story = {
276
+ args: { tag: "section", isLandmark: true },
277
+ parameters: {
278
+ docs: {
279
+ description: {
280
+ story:
281
+ "ContentWidth with landmark accessibility features enabled - makes the wrapper focusable and properly labeled for assistive technologies.",
282
+ },
283
+ },
284
+ },
285
+ render: accessibilityRender,
286
+ };
287
+
288
+ export const WithoutLandmark: Story = {
289
+ args: { tag: "div", isLandmark: false },
290
+ parameters: {
291
+ docs: {
292
+ description: {
293
+ story: "Standard ContentWidth without landmark features - regular content width wrapper behavior.",
294
+ },
295
+ },
296
+ },
297
+ render: accessibilityRender,
298
+ };
299
+
300
+ // ===== RESPONSIVE DEMONSTRATION =====
301
+
302
+ export const ResponsiveBehavior: Story = {
303
+ parameters: {
304
+ docs: {
305
+ description: {
306
+ story:
307
+ "Interactive demonstration of how ContentWidth responds to different container widths using CSS Container Queries instead of media queries.",
308
+ },
309
+ },
310
+ },
311
+ render: () => ({
312
+ components: { ContentWidthComponent },
313
+ template: `
314
+ <div style="background: #f8fafc; padding: 20px;">
315
+ <div style="margin-bottom: 40px;">
316
+ <h2 style="text-align: center; margin: 0 0 16px 0; color: #374151; font-size: 28px;">Responsive Behavior Demonstration</h2>
317
+ <p style="text-align: center; margin: 0 0 32px 0; color: #6b7280; max-width: 600px; margin-left: auto; margin-right: auto; line-height: 1.6;">
318
+ ContentWidth adapts its layout based on its own width using container queries.
319
+ Resize your browser to see the responsive behavior in action.
320
+ </p>
321
+ </div>
322
+ <div style="margin-bottom: 40px;">
323
+ <h3 style="margin: 0 0 16px 0; color: #374151;">Narrow Container (&lt; 1092px)</h3>
324
+ <div style="max-width: 800px; border: 2px dashed #cbd5e1; padding: 20px; margin-bottom: 20px;">
325
+ <ContentWidthComponent tag="div">
326
+ <div style="padding: 20px; background: #fef3c7; border-radius: 8px; text-align: center;">
327
+ <h4 style="margin: 0 0 8px 0; color: #92400e;">Fluid Width Mode</h4>
328
+ <p style="margin: 0; color: #b45309; font-size: 14px;">Content uses available width with 16px gutters on each side</p>
329
+ </div>
330
+ </ContentWidthComponent>
331
+ </div>
332
+ </div>
333
+ <div style="margin-bottom: 40px;">
334
+ <h3 style="margin: 0 0 16px 0; color: #374151;">Wide Container (≥ 1092px)</h3>
335
+ <div style="border: 2px dashed #cbd5e1; padding: 20px;">
336
+ <ContentWidthComponent tag="div">
337
+ <div style="padding: 20px; background: #dcfce7; border-radius: 8px; text-align: center;">
338
+ <h4 style="margin: 0 0 8px 0; color: #166534;">Fixed Width Mode</h4>
339
+ <p style="margin: 0; color: #15803d; font-size: 14px;">Content is constrained to 1064px max-width and centered within the container</p>
340
+ </div>
341
+ </ContentWidthComponent>
342
+ </div>
343
+ </div>
344
+ <ContentWidthComponent tag="section">
345
+ <div style="padding: 24px; background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);">
346
+ <h3 style="margin: 0 0 16px 0; color: #374151;">How It Works</h3>
347
+ <div style="display: grid; gap: 16px;">
348
+ <div style="padding: 16px; background: #f1f5f9; border-radius: 8px; border-left: 4px solid #3b82f6;">
349
+ <h4 style="margin: 0 0 8px 0; color: #1e40af; font-size: 14px; font-weight: 600;">Container Queries</h4>
350
+ <p style="margin: 0; color: #475569; font-size: 14px; line-height: 1.5;">Uses <code>@container</code> rules to respond to the container's own width, not the viewport width.</p>
351
+ </div>
352
+ <div style="padding: 16px; background: #f0fdf4; border-radius: 8px; border-left: 4px solid #22c55e;">
353
+ <h4 style="margin: 0 0 8px 0; color: #15803d; font-size: 14px; font-weight: 600;">CSS Grid Layout</h4>
354
+ <p style="margin: 0; color: #166534; font-size: 14px; line-height: 1.5;">Named grid lines create flexible layouts that adapt to content needs automatically.</p>
355
+ </div>
356
+ </div>
357
+ </div>
358
+ </ContentWidthComponent>
359
+ </div>
360
+ `,
361
+ }),
362
+ };
@@ -0,0 +1,132 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import ContentWidth from "../ContentWidth.vue";
4
+
5
+ const mockElementClasses = { value: "" };
6
+
7
+ const mockUseStyleClassPassthrough = vi.fn(() => ({
8
+ elementClasses: mockElementClasses,
9
+ }));
10
+
11
+ vi.mock("#imports", () => ({
12
+ useStyleClassPassthrough: mockUseStyleClassPassthrough,
13
+ useId: () => "test-id",
14
+ }));
15
+
16
+ describe("ContentWidth", () => {
17
+ let wrapper: Awaited<ReturnType<typeof mountSuspended>>;
18
+
19
+ const createWrapper = async (props = {}, slots = {}) => {
20
+ wrapper = await mountSuspended(ContentWidth, {
21
+ props: { styleClassPassthrough: [], ...props },
22
+ slots,
23
+ });
24
+ return wrapper;
25
+ };
26
+
27
+ beforeEach(() => {
28
+ vi.clearAllMocks();
29
+ mockElementClasses.value = "";
30
+ });
31
+
32
+ afterEach(() => {
33
+ wrapper?.unmount();
34
+ });
35
+
36
+ describe("Component Rendering", () => {
37
+ it("mounts without error", async () => {
38
+ await createWrapper();
39
+ expect(wrapper.find(".content-width-wrapper").exists()).toBe(true);
40
+ });
41
+
42
+ it("renders the correct inner structure", async () => {
43
+ await createWrapper();
44
+ expect(wrapper.find(".content-width").exists()).toBe(true);
45
+ expect(wrapper.find(".content-width-inner").exists()).toBe(true);
46
+ });
47
+
48
+ it("renders as a div by default", async () => {
49
+ await createWrapper();
50
+ expect(wrapper.find(".content-width-wrapper").element.tagName).toBe("DIV");
51
+ });
52
+ });
53
+
54
+ describe("Tag Prop", () => {
55
+ const tags = ["section", "article", "aside", "header", "footer", "main", "nav"] as const;
56
+
57
+ tags.forEach((tag) => {
58
+ it(`renders as <${tag}> when tag="${tag}"`, async () => {
59
+ await createWrapper({ tag });
60
+ expect(wrapper.find(".content-width-wrapper").element.tagName).toBe(tag.toUpperCase());
61
+ });
62
+ });
63
+ });
64
+
65
+ describe("isLandmark Prop", () => {
66
+ it("does not set tabindex or aria-label by default", async () => {
67
+ await createWrapper();
68
+ const el = wrapper.find(".content-width-wrapper");
69
+ expect(el.attributes("tabindex")).toBeUndefined();
70
+ expect(el.attributes("aria-label")).toBeUndefined();
71
+ });
72
+
73
+ it("sets tab-index=0 when isLandmark is true", async () => {
74
+ await createWrapper({ isLandmark: true });
75
+ expect(wrapper.find(".content-width-wrapper").attributes("tab-index")).toBe("0");
76
+ });
77
+
78
+ it("sets aria-label when isLandmark is true", async () => {
79
+ await createWrapper({ isLandmark: true });
80
+ expect(wrapper.find(".content-width-wrapper").attributes("aria-label")).toBe(
81
+ "Content Width Landmark"
82
+ );
83
+ });
84
+ });
85
+
86
+ describe("Slot", () => {
87
+ it("renders default slot content", async () => {
88
+ await createWrapper({}, { default: "<p>Slot content</p>" });
89
+ expect(wrapper.find(".content-width-inner").html()).toContain("Slot content");
90
+ });
91
+
92
+ it("renders nested slot content", async () => {
93
+ await createWrapper(
94
+ {},
95
+ { default: "<section><h2>Title</h2><p>Body</p></section>" }
96
+ );
97
+ expect(wrapper.find(".content-width-inner h2").text()).toBe("Title");
98
+ });
99
+
100
+ it("renders without slot content gracefully", async () => {
101
+ await createWrapper();
102
+ expect(wrapper.find(".content-width-inner").exists()).toBe(true);
103
+ });
104
+ });
105
+
106
+ describe("styleClassPassthrough Prop", () => {
107
+ it("accepts a string value", async () => {
108
+ await createWrapper({ styleClassPassthrough: "custom-class" });
109
+ expect(wrapper.find(".content-width-wrapper").exists()).toBe(true);
110
+ });
111
+
112
+ it("accepts an array of strings", async () => {
113
+ await createWrapper({ styleClassPassthrough: ["class-a", "class-b"] });
114
+ expect(wrapper.find(".content-width-wrapper").exists()).toBe(true);
115
+ });
116
+
117
+ it("applies elementClasses from useStyleClassPassthrough to wrapper", async () => {
118
+ mockElementClasses.value = "injected-class";
119
+ await createWrapper({ styleClassPassthrough: "injected-class" });
120
+ expect(wrapper.find(".content-width-wrapper").classes()).toContain("injected-class");
121
+ });
122
+ });
123
+
124
+ describe("DOM Structure", () => {
125
+ it("maintains correct nesting: wrapper > content-width > inner > slot", async () => {
126
+ await createWrapper({}, { default: "<span>nested</span>" });
127
+ const inner = wrapper.find(".content-width-wrapper .content-width .content-width-inner");
128
+ expect(inner.exists()).toBe(true);
129
+ expect(inner.find("span").text()).toBe("nested");
130
+ });
131
+ });
132
+ });
@@ -0,0 +1,71 @@
1
+ <template>
2
+ <component
3
+ :is="tag"
4
+ class="layout-grid-by-cols"
5
+ :aria-labelledby="ariaLabelledby"
6
+ :class="[elementClasses]"
7
+ >
8
+ <p v-if="ariaLabelledby" :id="headingId" class="sr-only">
9
+ {{ props.label || "If tag='section' then a label is required" }}
10
+ </p>
11
+ <div class="layout-grid-inner">
12
+ <template v-for="(_, name) in $slots" :key="name">
13
+ <slot :name="name"></slot>
14
+ </template>
15
+ </div>
16
+ </component>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ interface Props {
21
+ tag?: "div" | "section";
22
+ label?: string;
23
+ columnCount?: 2 | 3 | 4 | 5 | 6;
24
+ gap?: string;
25
+ singleColBelow?: string;
26
+ styleClassPassthrough?: string | string[];
27
+ }
28
+
29
+ const props = withDefaults(defineProps<Props>(), {
30
+ tag: "div",
31
+ label: "",
32
+ columnCount: 2,
33
+ gap: "1rem",
34
+ singleColBelow: "768px",
35
+ styleClassPassthrough: () => [],
36
+ });
37
+
38
+ const { headingId, ariaLabelledby } = useAriaLabelledById(() => props.tag);
39
+ const columnCount = computed(() => (props.columnCount < 2 ? 2 : props.columnCount));
40
+
41
+ const { elementClasses, updateElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
42
+
43
+ watch(
44
+ () => props.styleClassPassthrough,
45
+ () => {
46
+ updateElementClasses(props.styleClassPassthrough);
47
+ }
48
+ );
49
+ </script>
50
+
51
+ <style lang="css">
52
+ @layer components {
53
+ .layout-grid-by-cols {
54
+ container-type: inline-size;
55
+ container-name: layoutGrid;
56
+
57
+ --_gap: v-bind(gap);
58
+
59
+ .layout-grid-inner {
60
+ display: grid;
61
+ grid-auto-flow: row;
62
+ gap: var(--_gap);
63
+
64
+ @container layoutGrid (width >= 768px) {
65
+ grid-template-columns: repeat(v-bind(columnCount), 1fr);
66
+ gap: var(--_gap);
67
+ }
68
+ }
69
+ }
70
+ }
71
+ </style>