srcdev-nuxt-components 9.4.5 → 9.4.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 (104) hide show
  1. package/.claude/commands/migrate-component.md +74 -5
  2. package/.claude/component-ledger/audit.json +1 -1
  3. package/.claude/component-ledger/build.mjs +27 -0
  4. package/.claude/component-ledger/output.html +8 -3
  5. package/.claude/component-ledger/template.html +7 -2
  6. package/.claude/skills/components/breadcrumb.md +1 -0
  7. package/.claude/skills/components/column-flow-grid.md +93 -0
  8. package/.claude/skills/components/content-docs.md +2 -0
  9. package/.claude/skills/components/display-tooltip-defined.md +3 -0
  10. package/.claude/skills/components/display-tooltip.md +1 -0
  11. package/.claude/skills/components/entry-animation.md +88 -0
  12. package/.claude/skills/components/input-copy.md +2 -0
  13. package/.claude/skills/components/input-text-core.md +186 -0
  14. package/.claude/skills/components/marquee-scroller.md +130 -0
  15. package/.claude/skills/components/masonry-grid.md +153 -0
  16. package/.claude/skills/components/navigation-items.md +1 -0
  17. package/.claude/skills/components/pop-over.md +100 -0
  18. package/.claude/skills/components/responsive-header.md +3 -0
  19. package/.claude/skills/components/site-header.md +4 -0
  20. package/.claude/skills/components/site-navigation.md +1 -0
  21. package/.claude/skills/components/tab-navigation.md +1 -0
  22. package/.claude/skills/index.md +7 -1
  23. package/.vscode/srcdev-component-breadcrumb.code-snippets +13 -0
  24. package/.vscode/srcdev-component-column-flow-grid.code-snippets +39 -0
  25. package/.vscode/srcdev-component-content-docs.code-snippets +18 -0
  26. package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +15 -0
  27. package/.vscode/srcdev-component-display-tooltip.code-snippets +11 -0
  28. package/.vscode/srcdev-component-entry-animation.code-snippets +29 -0
  29. package/.vscode/srcdev-component-input-text.code-snippets +107 -0
  30. package/.vscode/srcdev-component-marquee-scroller.code-snippets +93 -0
  31. package/.vscode/srcdev-component-masonry-grid.code-snippets +51 -0
  32. package/.vscode/srcdev-component-pop-over.code-snippets +44 -0
  33. package/.vscode/srcdev-component-responsive-header.code-snippets +12 -0
  34. package/.vscode/srcdev-component-site-header.code-snippets +17 -0
  35. package/.vscode/srcdev-component-site-navigation.code-snippets +30 -0
  36. package/.vscode/srcdev-component-slider-gallery.code-snippets +38 -0
  37. package/.vscode/srcdev-component-tab-navigation.code-snippets +30 -0
  38. package/app/components/01.atoms/animations/entry/EntryAnimation.vue +7 -1
  39. package/app/components/01.atoms/animations/entry/stories/EntryAnimation.stories.ts +47 -0
  40. package/app/components/01.atoms/animations/entry/tests/EntryAnimation.spec.ts +57 -0
  41. package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +94 -0
  42. package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +331 -0
  43. package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +151 -0
  44. package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +315 -0
  45. package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +28 -0
  46. package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +9 -10
  47. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +8 -2
  48. package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +12 -13
  49. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +4 -1
  50. package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +33 -0
  51. package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +55 -0
  52. package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +178 -0
  53. package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +75 -0
  54. package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +35 -0
  55. package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +178 -0
  56. package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +199 -0
  57. package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +158 -0
  58. package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +4 -1
  59. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +54 -0
  60. package/app/components/01.atoms/pop-over/PopOver.vue +201 -0
  61. package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +141 -0
  62. package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +195 -0
  63. package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +11 -0
  64. package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +8 -2
  65. package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +16 -3
  66. package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +1 -1
  67. package/app/components/02.molecules/input-copy/InputCopy.vue +14 -0
  68. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +15 -0
  69. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +41 -0
  70. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +4 -1
  71. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +4 -1
  72. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +16 -4
  73. package/app/components/03.organisms/responsive-header/NavigationItems.vue +4 -1
  74. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +16 -3
  75. package/app/components/03.organisms/site-header/SiteHeader.vue +16 -1
  76. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
  77. package/app/components/05.forms/input-select/InputSelectCore.vue +1 -1
  78. package/app/components/05.forms/input-text/InputTextCore.vue +6 -0
  79. package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +49 -20
  80. package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +41 -22
  81. package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +48 -22
  82. package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +43 -17
  83. package/app/components/05.forms/input-text/tests/InputPasswordWithLabel.spec.ts +56 -0
  84. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +61 -0
  85. package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +60 -0
  86. package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +46 -0
  87. package/app/components/05.forms/input-text/variants/InputPasswordWithLabel.vue +7 -1
  88. package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +8 -2
  89. package/app/components/05.forms/input-text/variants/InputTextWithLabel.vue +6 -0
  90. package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +20 -20
  91. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +32 -26
  92. package/app/types/components/index.ts +1 -0
  93. package/app/types/components/marquee-scroller.d.ts +10 -0
  94. package/app/types/forms/types.forms.d.ts +1 -1
  95. package/package.json +1 -1
  96. package/app/components/marquee-scroller/MarqueeScroller.vue +0 -289
  97. package/app/components/masonry-grid/MasonryGrid.vue +0 -68
  98. package/app/components/masonry-grid-ordered/MasonryGridOrdered.vue +0 -163
  99. package/app/components/masonry-grid-ordered/MasonryGridOrderedGridExperiment.vue +0 -259
  100. package/app/components/masonry-grid-ordered/stories/MasonryGridOrdered.stories.ts +0 -354
  101. package/app/components/masonry-grid-sorted/MasonryGridSorted.vue +0 -120
  102. package/app/components/pop-over/PopOver.vue +0 -90
  103. package/app/layouts/default.vue +0 -308
  104. package/app/layouts/site-navigation-demo.vue +0 -188
@@ -0,0 +1,195 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import PopOver from "../PopOver.vue";
4
+
5
+ interface PopOverInstance {
6
+ popoverId: string;
7
+ anchorName: string;
8
+ handleToggle: (event: Event) => void;
9
+ }
10
+
11
+ const createWrapper = async (props: Record<string, unknown> = {}, slots: Record<string, string> = {}) => {
12
+ return mountSuspended(PopOver, { props, slots });
13
+ };
14
+
15
+ describe("PopOver", () => {
16
+ let wrapper: Awaited<ReturnType<typeof createWrapper>>;
17
+
18
+ beforeEach(() => {
19
+ // Popover API is not implemented in jsdom — define stubs so we can test
20
+ Object.defineProperty(HTMLElement.prototype, "hidePopover", {
21
+ value: vi.fn(),
22
+ writable: true,
23
+ configurable: true,
24
+ });
25
+ Object.defineProperty(HTMLElement.prototype, "showPopover", {
26
+ value: vi.fn(),
27
+ writable: true,
28
+ configurable: true,
29
+ });
30
+ });
31
+
32
+ afterEach(() => {
33
+ wrapper?.unmount();
34
+ vi.restoreAllMocks();
35
+ delete (HTMLElement.prototype as unknown as Record<string, unknown>)["hidePopover"];
36
+ delete (HTMLElement.prototype as unknown as Record<string, unknown>)["showPopover"];
37
+ });
38
+
39
+ it("mounts without error", async () => {
40
+ wrapper = await createWrapper();
41
+ expect(wrapper.vm).toBeTruthy();
42
+ });
43
+
44
+ it("renders correct HTML structure", async () => {
45
+ wrapper = await createWrapper(
46
+ {},
47
+ { trigger: "<span>Open</span>", content: "<p>Details</p>" }
48
+ );
49
+ expect(wrapper.html()).toMatchSnapshot();
50
+ });
51
+
52
+ describe("Trigger", () => {
53
+ it("renders the trigger slot", async () => {
54
+ wrapper = await createWrapper({}, { trigger: '<span class="slot-trigger">Open</span>' });
55
+ expect(wrapper.find(".slot-trigger").text()).toBe("Open");
56
+ });
57
+
58
+ it("trigger button has type='button'", async () => {
59
+ wrapper = await createWrapper();
60
+ expect(wrapper.find(".pop-over-trigger").attributes("type")).toBe("button");
61
+ });
62
+
63
+ it("does not set aria-label on the trigger by default", async () => {
64
+ wrapper = await createWrapper();
65
+ expect(wrapper.find(".pop-over-trigger").attributes("aria-label")).toBeUndefined();
66
+ });
67
+
68
+ it("sets aria-label on the trigger when triggerAriaLabel is given", async () => {
69
+ wrapper = await createWrapper({ triggerAriaLabel: "Show filters" });
70
+ expect(wrapper.find(".pop-over-trigger").attributes("aria-label")).toBe("Show filters");
71
+ });
72
+
73
+ it("trigger button popovertarget matches the popover id", async () => {
74
+ wrapper = await createWrapper();
75
+ const vm = wrapper.vm as unknown as PopOverInstance;
76
+ expect(wrapper.find(".pop-over-trigger").attributes("popovertarget")).toBe(vm.popoverId);
77
+ });
78
+
79
+ it("trigger button has popovertargetaction='toggle'", async () => {
80
+ wrapper = await createWrapper();
81
+ expect(wrapper.find(".pop-over-trigger").attributes("popovertargetaction")).toBe("toggle");
82
+ });
83
+ });
84
+
85
+ describe("Popover panel", () => {
86
+ it("renders the content slot", async () => {
87
+ wrapper = await createWrapper({}, { content: '<p class="slot-content">Details</p>' });
88
+ expect(wrapper.find(".pop-over-popover .slot-content").text()).toBe("Details");
89
+ });
90
+
91
+ it("has the popover attribute", async () => {
92
+ wrapper = await createWrapper();
93
+ expect(wrapper.find(".pop-over-popover").attributes("popover")).toBeDefined();
94
+ });
95
+
96
+ it("does not set aria-label by default", async () => {
97
+ wrapper = await createWrapper();
98
+ expect(wrapper.find(".pop-over-popover").attributes("aria-label")).toBeUndefined();
99
+ });
100
+
101
+ it("sets aria-label when popoverAriaLabel is given", async () => {
102
+ wrapper = await createWrapper({ popoverAriaLabel: "Filter options" });
103
+ expect(wrapper.find(".pop-over-popover").attributes("aria-label")).toBe("Filter options");
104
+ });
105
+
106
+ it("sets data-placement on the root element", async () => {
107
+ wrapper = await createWrapper({ placement: "top" });
108
+ expect(wrapper.find(".pop-over").attributes("data-placement")).toBe("top");
109
+ });
110
+
111
+ it("defaults placement to right", async () => {
112
+ wrapper = await createWrapper();
113
+ expect(wrapper.find(".pop-over").attributes("data-placement")).toBe("right");
114
+ });
115
+ });
116
+
117
+ describe("Close button", () => {
118
+ it("has a default aria-label", async () => {
119
+ wrapper = await createWrapper();
120
+ expect(wrapper.find(".pop-over-close-button").attributes("aria-label")).toBe("Close");
121
+ });
122
+
123
+ it("uses a custom closeButtonAriaLabel", async () => {
124
+ wrapper = await createWrapper({ closeButtonAriaLabel: "Dismiss" });
125
+ expect(wrapper.find(".pop-over-close-button").attributes("aria-label")).toBe("Dismiss");
126
+ });
127
+
128
+ it("has popovertargetaction='hide' pointing at the popover", async () => {
129
+ wrapper = await createWrapper();
130
+ const vm = wrapper.vm as unknown as PopOverInstance;
131
+ const closeButton = wrapper.find(".pop-over-close-button");
132
+ expect(closeButton.attributes("popovertargetaction")).toBe("hide");
133
+ expect(closeButton.attributes("popovertarget")).toBe(vm.popoverId);
134
+ });
135
+ });
136
+
137
+ describe("handleToggle focus management", () => {
138
+ it("focuses the close button when the popover opens", async () => {
139
+ wrapper = await createWrapper();
140
+ const closeButtonEl = wrapper.find(".pop-over-close-button").element as HTMLElement;
141
+ const focusSpy = vi.spyOn(closeButtonEl, "focus");
142
+
143
+ const vm = wrapper.vm as unknown as PopOverInstance;
144
+ vm.handleToggle(Object.assign(new Event("toggle"), { newState: "open" }));
145
+
146
+ expect(focusSpy).toHaveBeenCalledOnce();
147
+ });
148
+
149
+ it("does not move focus when the popover closes", async () => {
150
+ wrapper = await createWrapper();
151
+ const closeButtonEl = wrapper.find(".pop-over-close-button").element as HTMLElement;
152
+ const focusSpy = vi.spyOn(closeButtonEl, "focus");
153
+
154
+ const vm = wrapper.vm as unknown as PopOverInstance;
155
+ vm.handleToggle(Object.assign(new Event("toggle"), { newState: "closed" }));
156
+
157
+ expect(focusSpy).not.toHaveBeenCalled();
158
+ });
159
+ });
160
+
161
+ describe("Anchor name", () => {
162
+ it("sets a unique --_anchor-name on the root element style", async () => {
163
+ wrapper = await createWrapper();
164
+ const style = wrapper.find(".pop-over").attributes("style") ?? "";
165
+ expect(style).toContain("--_anchor-name: --pop-over-anchor-");
166
+ });
167
+ });
168
+
169
+ describe("styleClassPassthrough", () => {
170
+ it("applies a string class to the root element", async () => {
171
+ wrapper = await createWrapper({ styleClassPassthrough: "custom-pop-over" });
172
+ expect(wrapper.find(".pop-over").classes()).toContain("custom-pop-over");
173
+ });
174
+
175
+ it("applies an array of classes to the root element", async () => {
176
+ wrapper = await createWrapper({ styleClassPassthrough: ["pop-over-a", "pop-over-b"] });
177
+ expect(wrapper.find(".pop-over").classes()).toContain("pop-over-a");
178
+ expect(wrapper.find(".pop-over").classes()).toContain("pop-over-b");
179
+ });
180
+
181
+ it("root element always has the 'pop-over' class", async () => {
182
+ wrapper = await createWrapper({ styleClassPassthrough: "extra-class" });
183
+ expect(wrapper.find(".pop-over").classes()).toContain("pop-over");
184
+ });
185
+
186
+ it("resets classes when styleClassPassthrough prop changes", async () => {
187
+ wrapper = await createWrapper({ styleClassPassthrough: "initial-class" });
188
+ expect(wrapper.find(".pop-over").classes()).toContain("initial-class");
189
+
190
+ await wrapper.setProps({ styleClassPassthrough: "updated-class" });
191
+ expect(wrapper.find(".pop-over").classes()).not.toContain("initial-class");
192
+ expect(wrapper.find(".pop-over").classes()).toContain("updated-class");
193
+ });
194
+ });
195
+ });
@@ -0,0 +1,11 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`PopOver > renders correct HTML structure 1`] = `
4
+ "<div class="pop-over" style="--_anchor-name: --pop-over-anchor-v-0-0;" data-placement="right"><button popovertarget="pop-over-v-0-0" popovertargetaction="toggle" type="button" class="pop-over-trigger"><span>Open</span></button>
5
+ <div id="pop-over-v-0-0" popover="" class="pop-over-popover"><button popovertarget="pop-over-v-0-0" popovertargetaction="hide" type="button" class="pop-over-close-button" aria-label="Close"><span class="iconify i-lucide:x pop-over-close-button-icon" aria-hidden="true"></span></button>
6
+ <div class="pop-over-content">
7
+ <p>Details</p>
8
+ </div>
9
+ </div>
10
+ </div>"
11
+ `;
@@ -74,10 +74,16 @@ export const Default: Story = {
74
74
  render: (args) => ({
75
75
  components: { ActionMenu, ActionMenuItemCore },
76
76
  setup() {
77
- const { itemCount, ...componentArgs } = args;
77
+ // `args` is Storybook's own reactive object — read from it directly (`args.x`) inside
78
+ // these computeds rather than destructuring it into local variables at setup-time, which
79
+ // would snapshot the values once and stop reflecting later Controls-panel changes.
80
+ const componentArgs = computed(() => {
81
+ const { itemCount, ...rest } = args;
82
+ return rest;
83
+ });
78
84
  const lastAction = ref<string | null>(null);
79
85
  const items = computed(() =>
80
- actionItems.slice(0, itemCount ?? 5).map((item, i) => ({
86
+ actionItems.slice(0, args.itemCount ?? 5).map((item, i) => ({
81
87
  ...item,
82
88
  slotName: `item-${i}`,
83
89
  }))
@@ -1,5 +1,9 @@
1
1
  <template>
2
- <DisplayTooltip :tooltip-id="tooltipId" :style-class-passthrough="styleClassPassthrough">
2
+ <DisplayTooltip
3
+ :tooltip-id="tooltipId"
4
+ :trigger-aria-label="triggerAriaLabel"
5
+ :style-class-passthrough="styleClassPassthrough"
6
+ >
3
7
  <template v-if="$slots.triggerContent" #triggerContent>
4
8
  <slot name="triggerContent"></slot>
5
9
  </template>
@@ -26,9 +30,9 @@
26
30
  :popovertarget="tooltipId"
27
31
  popovertargetaction="hide"
28
32
  class="display-tooltip-close-button"
29
- aria-label="Close tool tip"
33
+ :aria-label="closeButtonAriaLabel"
30
34
  >
31
- Close
35
+ {{ closeButtonText }}
32
36
  </button>
33
37
  </div>
34
38
  </template>
@@ -41,12 +45,21 @@ import type { TooltipContentText } from "~/types/components";
41
45
  interface Props {
42
46
  tooltipId?: string;
43
47
  contentText?: TooltipContentText;
48
+ /** aria-label on the trigger button — override for localisation. */
49
+ triggerAriaLabel?: string;
50
+ /** Visible text on the close button — override for localisation. */
51
+ closeButtonText?: string;
52
+ /** aria-label on the close button — override for localisation. */
53
+ closeButtonAriaLabel?: string;
44
54
  styleClassPassthrough?: string | string[];
45
55
  }
46
56
 
47
57
  const props = withDefaults(defineProps<Props>(), {
48
58
  tooltipId: "",
49
59
  contentText: () => ({}),
60
+ triggerAriaLabel: "Toggle the popover",
61
+ closeButtonText: "Close",
62
+ closeButtonAriaLabel: "Close tool tip",
50
63
  styleClassPassthrough: () => [],
51
64
  });
52
65
 
@@ -9,7 +9,7 @@ exports[`DisplayTooltipDefined > renders correct HTML structure 1`] = `
9
9
  <div class="display-tooltip-popover-content">
10
10
  <div class="popover-content-defined">
11
11
  <h4 class="tooltip-title subtitle-sm">Title</h4>
12
- <p class="tooltip-body body-sm">Body copy</p><span class="tooltip-action input-value">Learn more</span><button popovertarget="nuxt-tooltip-fixed" popovertargetaction="hide" class="display-tooltip-close-button" aria-label="Close tool tip"> Close </button>
12
+ <p class="tooltip-body body-sm">Body copy</p><span class="tooltip-action input-value">Learn more</span><button popovertarget="nuxt-tooltip-fixed" popovertargetaction="hide" class="display-tooltip-close-button" aria-label="Close tool tip">Close</button>
13
13
  </div>
14
14
  </div>
15
15
  </div>
@@ -2,8 +2,10 @@
2
2
  <div class="input-copy" :class="elementClasses">
3
3
  <div class="input-copy__wrapper">
4
4
  <input
5
+ :id="resolvedId"
5
6
  type="text"
6
7
  class="input-copy__input"
8
+ :name="name || resolvedId"
7
9
  :value="value"
8
10
  :aria-label="ariaLabel || label"
9
11
  readonly
@@ -26,6 +28,9 @@
26
28
  <script setup lang="ts">
27
29
  interface Props {
28
30
  value: string;
31
+ /** Defaults to an auto-generated id — pass one explicitly only if something needs to target this input. */
32
+ id?: string;
33
+ name?: string;
29
34
  label?: string;
30
35
  ariaLabel?: string;
31
36
  description?: string;
@@ -37,6 +42,8 @@ interface Props {
37
42
  }
38
43
 
39
44
  const props = withDefaults(defineProps<Props>(), {
45
+ id: undefined,
46
+ name: undefined,
40
47
  label: undefined,
41
48
  ariaLabel: undefined,
42
49
  description: undefined,
@@ -47,6 +54,13 @@ const props = withDefaults(defineProps<Props>(), {
47
54
  styleClassPassthrough: () => [],
48
55
  });
49
56
 
57
+ // Readonly text inputs still need an id/name to satisfy "a form field element should have an
58
+ // id or name attribute" — auto-generated so every existing call site (none of which pass one)
59
+ // gets a valid, unique id for free. Named resolvedId, not id, so it can't be confused with (or
60
+ // silently shadow) the `id` prop it falls back to.
61
+ const autoId = useId();
62
+ const resolvedId = computed(() => props.id ?? autoId);
63
+
50
64
  const emit = defineEmits<{
51
65
  copy: [value: string];
52
66
  copied: [value: string];
@@ -7,6 +7,11 @@ const meta: Meta<typeof InputCopy> = {
7
7
  tags: ["autodocs"],
8
8
  argTypes: {
9
9
  value: { control: "text" },
10
+ id: {
11
+ control: "text",
12
+ description: "Defaults to an auto-generated id — pass one only if something needs to target this input.",
13
+ },
14
+ name: { control: "text", description: "Defaults to the resolved id if not provided." },
10
15
  label: { control: "text" },
11
16
  description: { control: "text" },
12
17
  buttonText: { control: "text" },
@@ -101,6 +106,16 @@ export const NoDescription: Story = {
101
106
  },
102
107
  };
103
108
 
109
+ export const WithExplicitIdAndName: Story = {
110
+ args: {
111
+ value: "sk_live_abc123defgh456xyz789uvw",
112
+ id: "checkout-license-key",
113
+ name: "licenseKey",
114
+ label: "License key",
115
+ description: "Demonstrates passing explicit id/name instead of relying on the auto-generated one.",
116
+ },
117
+ };
118
+
104
119
  export const LongValue: Story = {
105
120
  args: {
106
121
  value: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POdHyP_DQkT3WTZhu9qLWSwVe8ZlHfHEPBOzzVI3vFA",
@@ -178,4 +178,45 @@ describe("InputCopy", () => {
178
178
  expect(wrapper.classes()).toContain("custom-class");
179
179
  expect(wrapper.classes()).toContain("another-class");
180
180
  });
181
+
182
+ it("auto-generates an id and mirrors it as the name when neither is provided", () => {
183
+ const wrapper = mount(InputCopy, {
184
+ props: {
185
+ value: "test-key",
186
+ },
187
+ });
188
+
189
+ const input = wrapper.find("input");
190
+ const id = input.attributes("id");
191
+
192
+ expect(id).toBeTruthy();
193
+ expect(input.attributes("name")).toBe(id);
194
+ });
195
+
196
+ it("uses the provided id prop instead of auto-generating one", () => {
197
+ const wrapper = mount(InputCopy, {
198
+ props: {
199
+ value: "test-key",
200
+ id: "license-key-input",
201
+ },
202
+ });
203
+
204
+ const input = wrapper.find("input");
205
+ expect(input.attributes("id")).toBe("license-key-input");
206
+ expect(input.attributes("name")).toBe("license-key-input");
207
+ });
208
+
209
+ it("uses the provided name prop instead of falling back to the id", () => {
210
+ const wrapper = mount(InputCopy, {
211
+ props: {
212
+ value: "test-key",
213
+ id: "license-key-input",
214
+ name: "licenseKey",
215
+ },
216
+ });
217
+
218
+ const input = wrapper.find("input");
219
+ expect(input.attributes("id")).toBe("license-key-input");
220
+ expect(input.attributes("name")).toBe("licenseKey");
221
+ });
181
222
  });
@@ -7,7 +7,7 @@
7
7
  `site-navigation--${navAlign}`,
8
8
  { 'is-collapsed': isCollapsed, 'is-loaded': isLoaded, 'menu-open': isMenuOpen, 'is-animated': isAnimated },
9
9
  ]"
10
- aria-label="Site navigation"
10
+ :aria-label="ariaLabel"
11
11
  >
12
12
  <ul
13
13
  v-if="!isCollapsed || !isLoaded"
@@ -106,11 +106,14 @@ interface Props {
106
106
  navItemData: NavItemData;
107
107
  navAlign?: "left" | "center" | "right";
108
108
  styleClassPassthrough?: string | string[];
109
+ /** aria-label on the nav landmark — override for localisation. */
110
+ ariaLabel?: string;
109
111
  }
110
112
 
111
113
  const props = withDefaults(defineProps<Props>(), {
112
114
  navAlign: "left",
113
115
  styleClassPassthrough: () => [],
116
+ ariaLabel: "Site navigation",
114
117
  });
115
118
 
116
119
  // ─── Animation gate — prevents indicator from transitioning on first paint ───
@@ -7,7 +7,7 @@
7
7
  `tab-navigation--${navAlign}`,
8
8
  { 'is-collapsed': isCollapsed, 'is-loaded': isLoaded, 'menu-open': isMenuOpen, 'is-animated': isAnimated },
9
9
  ]"
10
- aria-label="Site navigation"
10
+ :aria-label="ariaLabel"
11
11
  >
12
12
  <ul v-if="!isCollapsed || !isLoaded" ref="navListRef" class="tab-nav-list" @mouseleave="hoveredItemHref = null">
13
13
  <li
@@ -131,12 +131,15 @@ interface Props {
131
131
  navAlign?: "left" | "center" | "right";
132
132
  styleClassPassthrough?: string | string[];
133
133
  anchorScrollOffset?: number | (() => number);
134
+ /** aria-label on the nav landmark — override for localisation. */
135
+ ariaLabel?: string;
134
136
  }
135
137
 
136
138
  const props = withDefaults(defineProps<Props>(), {
137
139
  navAlign: "left",
138
140
  styleClassPassthrough: () => [],
139
141
  anchorScrollOffset: undefined,
142
+ ariaLabel: "Site navigation",
140
143
  });
141
144
 
142
145
  const { navRef, navListRef, isCollapsed, isLoaded, isMenuOpen, isActiveItem, toggleMenu, closeMenu } =
@@ -2,7 +2,7 @@
2
2
  <div ref="sliderGalleryWrapper" class="slider-gallery" :class="[elementClasses]">
3
3
  <div class="loading-state" :class="[{ galleryLoaded: !galleryLoaded }]">
4
4
  <div class="loading-spinner"></div>
5
- <p>Loading gallery...</p>
5
+ <p>{{ loadingText }}</p>
6
6
  </div>
7
7
 
8
8
  <div v-if="showGallery" class="gallery-content" :class="[{ galleryLoaded: !galleryLoaded }]">
@@ -17,7 +17,7 @@
17
17
  {{ item.description }}
18
18
  </div>
19
19
  <div class="buttons" :class="item.textBrightness">
20
- <button>SEE MORE</button>
20
+ <button>{{ seeMoreText }}</button>
21
21
  </div>
22
22
  </div>
23
23
  </div>
@@ -40,10 +40,10 @@
40
40
  </div>
41
41
 
42
42
  <div class="arrows">
43
- <button id="prev" ref="prevDom" aria-label="Previous image" @click.prevent="doPrevious()">
43
+ <button id="prev" ref="prevDom" :aria-label="prevAriaLabel" @click.prevent="doPrevious()">
44
44
  <Icon name="ic:outline-keyboard-arrow-left" class="arrows-icon" />
45
45
  </button>
46
- <button id="next" ref="nextDom" aria-label="Next image" @click.prevent="doNext()">
46
+ <button id="next" ref="nextDom" :aria-label="nextAriaLabel" @click.prevent="doNext()">
47
47
  <Icon name="ic:outline-keyboard-arrow-right" class="arrows-icon" />
48
48
  </button>
49
49
  </div>
@@ -60,6 +60,14 @@ interface Props {
60
60
  autoRun?: boolean;
61
61
  autoRunInterval?: number;
62
62
  animationDuration?: number;
63
+ /** Loading-state copy — override for localisation. */
64
+ loadingText?: string;
65
+ /** Per-slide call-to-action button copy — override for localisation. */
66
+ seeMoreText?: string;
67
+ /** aria-label on the previous-image button — override for localisation. */
68
+ prevAriaLabel?: string;
69
+ /** aria-label on the next-image button — override for localisation. */
70
+ nextAriaLabel?: string;
63
71
  styleClassPassthrough?: string | string[];
64
72
  }
65
73
 
@@ -67,6 +75,10 @@ const props = withDefaults(defineProps<Props>(), {
67
75
  autoRun: true,
68
76
  autoRunInterval: 7000,
69
77
  animationDuration: 3000,
78
+ loadingText: "Loading gallery...",
79
+ seeMoreText: "SEE MORE",
80
+ prevAriaLabel: "Previous image",
81
+ nextAriaLabel: "Next image",
70
82
  styleClassPassthrough: () => [],
71
83
  });
72
84
 
@@ -3,7 +3,7 @@
3
3
  class="overflow-navigation-wrapper"
4
4
  :class="[elementClasses, { 'is-panel-animating': isPanelAnimating }]"
5
5
  role="menu"
6
- aria-label="Overflow navigation menu"
6
+ :aria-label="ariaLabel"
7
7
  @mouseleave="
8
8
  hoveredItemKey = null;
9
9
  hoveredChildKey = null;
@@ -115,6 +115,8 @@ interface Props {
115
115
  mainNavigationState?: ResponsiveHeaderState;
116
116
  panelVariant?: "modern" | "classic";
117
117
  styleClassPassthrough?: string | string[];
118
+ /** aria-label on the overflow menu — override for localisation. */
119
+ ariaLabel?: string;
118
120
  }
119
121
 
120
122
  const props = withDefaults(defineProps<Props>(), {
@@ -124,6 +126,7 @@ const props = withDefaults(defineProps<Props>(), {
124
126
  // "modern" is opt-in.
125
127
  panelVariant: "classic",
126
128
  styleClassPassthrough: () => [],
129
+ ariaLabel: "Overflow navigation menu",
127
130
  });
128
131
 
129
132
  const panelComponent = computed(() => (props.panelVariant === "modern" ? ExpandingPanel : ExpandingPanelClassic));
@@ -9,7 +9,7 @@
9
9
  ref="mainNav"
10
10
  class="main-navigation"
11
11
  :class="{ 'is-animated': isAnimated }"
12
- aria-label="Main navigation"
12
+ :aria-label="mainNavAriaLabel"
13
13
  @mouseleave="hoveredItemKey = null"
14
14
  @focusout="handleNavFocusout"
15
15
  >
@@ -78,7 +78,7 @@
78
78
  <div aria-hidden="true" class="nav-indicator-hovered"></div>
79
79
  <div aria-hidden="true" class="nav-indicator-active"></div>
80
80
  </nav>
81
- <nav ref="secondaryNav" class="secondary-navigation" aria-label="Secondary navigation">
81
+ <nav ref="secondaryNav" class="secondary-navigation" :aria-label="secondaryNavAriaLabel">
82
82
  <details
83
83
  ref="overflowDetails"
84
84
  class="overflow-details"
@@ -100,7 +100,11 @@
100
100
  />
101
101
  </summary>
102
102
  <div class="overflow-details-nav" role="menu">
103
- <NavigationItems :main-navigation-state="mainNavigationState" :panel-variant="panelVariant" />
103
+ <NavigationItems
104
+ :main-navigation-state="mainNavigationState"
105
+ :panel-variant="panelVariant"
106
+ :aria-label="overflowMenuAriaLabel"
107
+ />
104
108
  </div>
105
109
  </details>
106
110
  <slot v-if="slots.secondaryNavigation" name="secondaryNavigation"></slot>
@@ -126,6 +130,12 @@ interface Props {
126
130
  styleClassPassthrough?: string | string[];
127
131
  allowExpandOnGesture?: boolean;
128
132
  panelVariant?: "modern" | "classic";
133
+ /** aria-label on the primary nav landmark — override for localisation. */
134
+ mainNavAriaLabel?: string;
135
+ /** aria-label on the secondary (overflow) nav landmark — override for localisation. */
136
+ secondaryNavAriaLabel?: string;
137
+ /** aria-label on the overflow menu itself (forwarded to NavigationItems) — override for localisation. */
138
+ overflowMenuAriaLabel?: string;
129
139
  }
130
140
 
131
141
  const props = withDefaults(defineProps<Props>(), {
@@ -142,6 +152,9 @@ const props = withDefaults(defineProps<Props>(), {
142
152
  // Forwarded to NavigationItems' overflow submenu panels — see its own default for why
143
153
  // "classic" is the safe default (CLAUDE.md pitfall #19).
144
154
  panelVariant: "classic",
155
+ mainNavAriaLabel: "Main navigation",
156
+ secondaryNavAriaLabel: "Secondary navigation",
157
+ overflowMenuAriaLabel: "Overflow navigation menu",
145
158
  });
146
159
 
147
160
  const collapseNavigationBelowWidth = computed(
@@ -2,7 +2,7 @@
2
2
  <PageRow tag="div" :variant="pageRowVariant" :style-class-passthrough="styleClassPassthrough">
3
3
  <template #default>
4
4
  <header class="site-header">
5
- <nav class="home-navigation" aria-label="Home Navigation">
5
+ <nav class="home-navigation" :aria-label="homeNavAriaLabel">
6
6
  <SkipLinks>
7
7
  <template #homeLink>
8
8
  <slot name="branding"></slot>
@@ -17,6 +17,9 @@
17
17
  :collapse-at-main-nav-intersection="collapseAtMainNavIntersection"
18
18
  :allow-expand-on-gesture="allowExpandOnGesture"
19
19
  :style-class-passthrough="navStyleClassPassthrough"
20
+ :main-nav-aria-label="mainNavAriaLabel"
21
+ :secondary-nav-aria-label="secondaryNavAriaLabel"
22
+ :overflow-menu-aria-label="overflowMenuAriaLabel"
20
23
  >
21
24
  <template v-if="slots.secondaryNavigation" #secondaryNavigation>
22
25
  <slot name="secondaryNavigation"></slot>
@@ -43,6 +46,14 @@ interface Props {
43
46
  pageRowVariant?: "full" | "popout" | "content" | "inset-content";
44
47
  styleClassPassthrough?: string | string[];
45
48
  navStyleClassPassthrough?: string | string[];
49
+ /** aria-label on the home/branding nav landmark — override for localisation. */
50
+ homeNavAriaLabel?: string;
51
+ /** Forwarded to ResponsiveHeader's mainNavAriaLabel — override for localisation. */
52
+ mainNavAriaLabel?: string;
53
+ /** Forwarded to ResponsiveHeader's secondaryNavAriaLabel — override for localisation. */
54
+ secondaryNavAriaLabel?: string;
55
+ /** Forwarded to ResponsiveHeader's overflowMenuAriaLabel — override for localisation. */
56
+ overflowMenuAriaLabel?: string;
46
57
  }
47
58
 
48
59
  withDefaults(defineProps<Props>(), {
@@ -58,6 +69,10 @@ withDefaults(defineProps<Props>(), {
58
69
  pageRowVariant: "content",
59
70
  styleClassPassthrough: () => [],
60
71
  navStyleClassPassthrough: () => [],
72
+ homeNavAriaLabel: "Home Navigation",
73
+ mainNavAriaLabel: "Main navigation",
74
+ secondaryNavAriaLabel: "Secondary navigation",
75
+ overflowMenuAriaLabel: "Overflow navigation menu",
61
76
  });
62
77
 
63
78
  const slots = useSlots();
@@ -9,7 +9,7 @@ exports[`SiteHeader > renders correct HTML structure with default props 1`] = `
9
9
  <nav class="skip-links-nav" aria-label="Skip navigation"><a href="#main-content" class="skip-link">Skip to main content</a><a href="#footer-content" class="skip-link">Skip to footer</a></nav>
10
10
  </div>
11
11
  </nav>
12
- <div class="responsive-header-stub"></div>
12
+ <div class="responsive-header-stub" main-nav-aria-label="Main navigation" secondary-nav-aria-label="Secondary navigation" overflow-menu-aria-label="Overflow navigation menu"></div>
13
13
  </header>
14
14
  </div>"
15
15
  `;
@@ -21,7 +21,7 @@
21
21
  v-for="item in fieldData.data"
22
22
  :key="item.id"
23
23
  :value="item.value"
24
- :selected="Number(item.value) === modelValue"
24
+ :selected="String(item.value) === String(modelValue)"
25
25
  class="input-select-core-option"
26
26
  >
27
27
  <Icon