srcdev-nuxt-components 9.0.18 → 9.1.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 (41) hide show
  1. package/.claude/settings.json +4 -2
  2. package/.claude/skills/component-inline-action-button.md +79 -0
  3. package/.claude/skills/components/input-copy-core.md +66 -0
  4. package/.claude/skills/components/page-hero-highlights.md +60 -0
  5. package/.claude/skills/components/site-navigation.md +120 -0
  6. package/.claude/skills/icon-sets.md +45 -0
  7. package/.claude/skills/index.md +7 -1
  8. package/.claude/skills/performance-review.md +105 -0
  9. package/.claude/skills/robots-env-aware.md +69 -0
  10. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_error.css +1 -1
  11. package/app/assets/styles/setup/02.colours/_amber.css +2 -2
  12. package/app/assets/styles/setup/03.theming/default/_dark.css +20 -2
  13. package/app/assets/styles/setup/03.theming/default/_light.css +11 -1
  14. package/app/assets/styles/setup/03.theming/error/_dark.css +1 -1
  15. package/app/components/01.atoms/text-blocks/eyebrow-text/EyebrowText.vue +15 -12
  16. package/app/components/01.atoms/text-blocks/hero-text/HeroText.vue +3 -1
  17. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +780 -0
  18. package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +335 -0
  19. package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +328 -0
  20. package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +30 -0
  21. package/app/components/04.templates/page-hero-highlights/PageHeroHighlights.vue +36 -21
  22. package/app/components/04.templates/page-hero-highlights/PageHeroHighlightsHeader.vue +66 -0
  23. package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlights.stories.ts +50 -3
  24. package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlightsHeader.stories.ts +77 -0
  25. package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlights.spec.ts +15 -7
  26. package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlightsHeader.spec.ts +51 -0
  27. package/app/components/04.templates/page-hero-highlights/tests/__snapshots__/PageHeroHighlights.spec.ts.snap +1 -1
  28. package/app/components/forms/form-errors/InputError.vue +104 -103
  29. package/app/components/forms/input-copy/InputCopyCore.vue +132 -0
  30. package/app/components/forms/input-copy/stories/InputCopyCore.stories.ts +89 -0
  31. package/app/components/forms/input-copy/tests/InputCopyCore.spec.ts +212 -0
  32. package/app/components/forms/input-copy/tests/__snapshots__/InputCopyCore.spec.ts.snap +28 -0
  33. package/app/layouts/default.vue +1 -0
  34. package/app/pages/index.vue +0 -5
  35. package/app/pages/page-hero-highlights.vue +15 -11
  36. package/modules/icon-sets.ts +53 -0
  37. package/nuxt.config.ts +8 -0
  38. package/package.json +49 -6
  39. package/app/components/03.organisms/treatment-consultant/TreatmentConsultant.vue +0 -2204
  40. package/app/components/03.organisms/treatment-consultant/stories/TreatmentConsultant.stories.ts +0 -38
  41. package/app/pages/ui/services/treatment-consultant.vue +0 -39
@@ -2,7 +2,7 @@
2
2
  <component
3
3
  :is="tag"
4
4
  class="page-hero-highlights"
5
- :class="[elementClasses, componentClasses]"
5
+ :class="[elementClasses, componentClasses, { 'has-content-panel': contentPanel }]"
6
6
  :aria-labelledby="ariaLabelledby"
7
7
  >
8
8
  <div class="header-row">
@@ -28,6 +28,7 @@ interface Props {
28
28
  highlightsJustify?: "start" | "center" | "end" | "space-between" | "space-around";
29
29
  maxWidth?: string;
30
30
  contentAlign?: "start" | "center";
31
+ contentPanel?: boolean;
31
32
  highlightTitleBaseline?: boolean;
32
33
  styleClassPassthrough?: string | string[];
33
34
  }
@@ -38,13 +39,14 @@ const props = withDefaults(defineProps<Props>(), {
38
39
  highlightsJustify: "start",
39
40
  maxWidth: undefined,
40
41
  contentAlign: "center",
42
+ contentPanel: true,
41
43
  highlightTitleBaseline: false,
42
44
  styleClassPassthrough: () => [],
43
45
  });
44
46
 
45
47
  const gridColumns = computed(() => {
46
48
  if (!props.maxWidth) return "16px 1fr 16px";
47
- if (props.contentAlign === "start") return `16px minmax(0, ${props.maxWidth}) 1fr`;
49
+ if (props.contentAlign === "start") return `16px minmax(0, ${props.maxWidth}) minmax(16px, 1fr)`;
48
50
  return `max(16px, (100% - ${props.maxWidth}) / 2) 1fr max(16px, (100% - ${props.maxWidth}) / 2)`;
49
51
  });
50
52
 
@@ -122,6 +124,7 @@ watch(
122
124
  .header-slot {
123
125
  grid-column: 2;
124
126
  grid-row: 1;
127
+ container-type: inline-size;
125
128
  }
126
129
  }
127
130
 
@@ -135,7 +138,7 @@ watch(
135
138
 
136
139
  /* Element theme */
137
140
  gap: var(--highlights-row-item-gap);
138
- margin-inline-start: var(--highlights-row-initial-item-offset);
141
+ margin-inline-start: 0;
139
142
 
140
143
  &.equal-widths {
141
144
  display: grid;
@@ -211,28 +214,40 @@ watch(
211
214
  background-color: var(--content-row-background-color);
212
215
  padding-block-end: var(--content-row-end-gap);
213
216
 
214
- &:before {
215
- /* Element geometry */
216
- content: "";
217
- grid-template-columns: subgrid;
218
- grid-template-rows: subgrid;
219
- display: grid;
220
- grid-column: 2;
221
- grid-row: 1 / span 2;
222
-
223
- /* Element theme */
224
- margin-top: var(--content-row-start-gap);
225
-
226
- background-color: var(--content-slot-background-color);
227
- border: var(--content-slot-border);
228
- outline: var(--content-slot-outline);
229
- border-radius: var(--content-slot-border-radius);
230
- }
231
217
  .content-slot {
232
218
  grid-column: 2;
233
219
  grid-row: 2;
234
220
  margin-block: var(--content-slot-margin-block-start) var(--content-slot-margin);
235
- margin-inline: var(--content-slot-margin);
221
+ margin-inline: 0;
222
+ }
223
+ }
224
+
225
+ &.has-content-panel {
226
+ .highlights-row {
227
+ margin-inline: var(--highlights-row-initial-item-offset);
228
+ }
229
+ .content-row {
230
+ &:before {
231
+ /* Element geometry */
232
+ content: "";
233
+ grid-template-columns: subgrid;
234
+ grid-template-rows: subgrid;
235
+ display: grid;
236
+ grid-column: 2;
237
+ grid-row: 1 / span 2;
238
+
239
+ /* Element theme */
240
+ margin-top: var(--content-row-start-gap);
241
+
242
+ background-color: var(--content-slot-background-color);
243
+ border: var(--content-slot-border);
244
+ outline: var(--content-slot-outline);
245
+ border-radius: var(--content-slot-border-radius);
246
+ }
247
+
248
+ .content-slot {
249
+ margin-inline: var(--content-slot-margin);
250
+ }
236
251
  }
237
252
  }
238
253
  }
@@ -0,0 +1,66 @@
1
+ <template>
2
+ <div class="page-hero-highlights-header" :class="elementClasses">
3
+ <div class="phh-start">
4
+ <slot name="start"></slot>
5
+ </div>
6
+ <div v-if="hasEndSlot" class="phh-end">
7
+ <slot name="end"></slot>
8
+ </div>
9
+ </div>
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ interface Props {
14
+ styleClassPassthrough?: string | string[];
15
+ }
16
+
17
+ const props = withDefaults(defineProps<Props>(), {
18
+ styleClassPassthrough: () => [],
19
+ });
20
+
21
+ const slots = useSlots();
22
+ const hasEndSlot = computed(() => Boolean(slots.end));
23
+
24
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
25
+
26
+ watch(
27
+ () => props.styleClassPassthrough,
28
+ () => {
29
+ resetElementClasses(props.styleClassPassthrough);
30
+ }
31
+ );
32
+ </script>
33
+
34
+ <style lang="css">
35
+ .page-hero-highlights-header {
36
+ /* User themable tokens */
37
+ --phh-padding-block: 1.6rem;
38
+ --phh-gap: 1.6rem;
39
+ --phh-end-gap: 0.8rem;
40
+
41
+ display: flex;
42
+ flex-direction: column;
43
+ gap: var(--phh-gap);
44
+ padding-block: var(--phh-padding-block);
45
+
46
+ &:has(.phh-end) {
47
+ @container (width >= 768px) {
48
+ display: grid;
49
+ grid-template-columns: repeat(2, 1fr);
50
+ align-items: flex-end;
51
+ justify-content: space-between;
52
+ }
53
+ }
54
+
55
+ .phh-start {
56
+ /* flex: 1; */
57
+ }
58
+
59
+ .phh-end {
60
+ display: flex;
61
+ align-items: center;
62
+ gap: var(--phh-end-gap);
63
+ /* flex-shrink: 0; */
64
+ }
65
+ }
66
+ </style>
@@ -8,6 +8,7 @@ type StoryArgs = {
8
8
  highlightsJustify?: "start" | "center" | "end" | "space-between" | "space-around";
9
9
  maxWidth?: string;
10
10
  contentAlign?: "start" | "center";
11
+ contentPanel?: boolean;
11
12
  highlightTitleBaseline?: boolean;
12
13
  headerBackground?: string;
13
14
  contentBackground?: string;
@@ -35,7 +36,7 @@ const meta: Meta<StoryArgs> = {
35
36
  },
36
37
  maxWidth: {
37
38
  control: { type: "select" },
38
- options: ["", "600px", "800px", "1024px", "1200px", "1440px"],
39
+ options: ["", "600px", "800px", "1024px", "1064px", "1200px", "1440px"],
39
40
  description:
40
41
  "Max width of the central content column. Gutters grow to enforce the constraint; below this width they hold at 16px.",
41
42
  },
@@ -45,6 +46,11 @@ const meta: Meta<StoryArgs> = {
45
46
  description:
46
47
  "Align the content column to the start (left gutter stays 16px, right takes remaining space) or center (equal gutters). Only meaningful when maxWidth is set.",
47
48
  },
49
+ contentPanel: {
50
+ control: "boolean",
51
+ description:
52
+ "When true (default), renders a decorative panel behind the content slot and offsets the highlights strip. Set to false for a flat layout with no backdrop.",
53
+ },
48
54
  highlightTitleBaseline: {
49
55
  control: "boolean",
50
56
  description:
@@ -74,6 +80,7 @@ const meta: Meta<StoryArgs> = {
74
80
  highlightsJustify: "start",
75
81
  maxWidth: "",
76
82
  contentAlign: "center",
83
+ contentPanel: true,
77
84
  highlightTitleBaseline: false,
78
85
  headerBackground: "",
79
86
  contentBackground: "",
@@ -162,8 +169,8 @@ All layout and visual properties are customisable via CSS custom properties. Set
162
169
  <PageHeroHighlights v-bind="componentArgs" :style="bgStyles">
163
170
  <template #header>
164
171
  <div style="color: white; padding-block: 1.6rem;">
165
- <p class="page-heading-1">Dashboard</p>
166
- <p class="page-body-normal">Overview of your account activity and key metrics.</p>
172
+ <h1 class="page-heading-1" style="color: white;">Dashboard</h1>
173
+ <p class="page-body-normal" style="color: white;">Overview of your account activity and key metrics.</p>
167
174
  </div>
168
175
  </template>
169
176
 
@@ -402,3 +409,43 @@ export const MaxWidthStart: Story = {
402
409
  `,
403
410
  }),
404
411
  };
412
+
413
+ /** No content panel — flat layout with no backdrop or highlight offset behind the content slot. */
414
+ export const NoContentPanel: Story = {
415
+ name: "No Content Panel",
416
+ args: { contentPanel: false },
417
+ render: (args: StoryArgs) => ({
418
+ components: { PageHeroHighlights },
419
+ setup() {
420
+ return useStorySetup(args);
421
+ },
422
+ template: `
423
+ <PageHeroHighlights v-bind="componentArgs" :style="bgStyles">
424
+ <template #header>
425
+ <p class="page-heading-1">Dashboard</p>
426
+ <p class="page-body-normal">Flat layout — no decorative panel behind the content slot.</p>
427
+ </template>
428
+
429
+ <template #highlights>
430
+ <div style="border-radius: 12px; background: #1a1a2e; color: white; padding: 1.6rem;">
431
+ <p class="page-heading-2">Total Revenue</p>
432
+ <p class="page-body-normal">£24,500</p>
433
+ </div>
434
+ <div v-if="highlightCount >= 2" style="border-radius: 12px; background: #1a1a2e; color: white; padding: 1.6rem;">
435
+ <p class="page-heading-2">Active Users</p>
436
+ <p class="page-body-normal">1,284</p>
437
+ </div>
438
+ <div v-if="highlightCount >= 3" style="border-radius: 12px; background: #1a1a2e; color: white; padding: 1.6rem;">
439
+ <p class="page-heading-2">Open Tasks</p>
440
+ <p class="page-body-normal">37</p>
441
+ </div>
442
+ </template>
443
+
444
+ <template #content>
445
+ <p class="page-heading-2">Recent Activity</p>
446
+ <p class="page-body-normal">Your most recent transactions and events will appear here.</p>
447
+ </template>
448
+ </PageHeroHighlights>
449
+ `,
450
+ }),
451
+ };
@@ -0,0 +1,77 @@
1
+ import PageHeroHighlights from "../PageHeroHighlights.vue";
2
+ import PageHeroHighlightsHeader from "../PageHeroHighlightsHeader.vue";
3
+ import type { Meta, StoryObj } from "@nuxtjs/storybook";
4
+
5
+ type StoryArgs = {
6
+ styleClassPassthrough?: string | string[];
7
+ };
8
+
9
+ const meta: Meta<StoryArgs> = {
10
+ title: "Templates/PageHeroHighlightsHeader",
11
+ component: PageHeroHighlightsHeader,
12
+ argTypes: {
13
+ styleClassPassthrough: {
14
+ control: "object",
15
+ description: "Additional CSS classes applied to the root element",
16
+ },
17
+ },
18
+ args: {
19
+ styleClassPassthrough: [],
20
+ },
21
+ };
22
+
23
+ export default meta;
24
+ type Story = StoryObj<typeof PageHeroHighlightsHeader>;
25
+
26
+ /** Default — start slot only, fills full width. */
27
+ export const Default: Story = {
28
+ render: (args: StoryArgs) => ({
29
+ components: { PageHeroHighlights, PageHeroHighlightsHeader },
30
+ setup() {
31
+ return { args };
32
+ },
33
+ template: `
34
+ <PageHeroHighlights tag="section" style="--header-row-background-colour: #2d4a35;">
35
+ <template #header>
36
+ <PageHeroHighlightsHeader v-bind="args">
37
+ <template #start>
38
+ <div style="color: white; padding-block: 1.6rem;">
39
+ <h1 class="page-heading-1" style="color: white;">Dashboard</h1>
40
+ <p class="page-body-normal" style="color: white;">Overview of your account activity and key metrics.</p>
41
+ </div>
42
+ </template>
43
+ </PageHeroHighlightsHeader>
44
+ </template>
45
+ </PageHeroHighlights>
46
+ `,
47
+ }),
48
+ };
49
+
50
+ /** With actions — start slot for title/description, end slot for action buttons. */
51
+ export const WithActions: Story = {
52
+ name: "With Actions",
53
+ render: (args: StoryArgs) => ({
54
+ components: { PageHeroHighlights, PageHeroHighlightsHeader },
55
+ setup() {
56
+ return { args };
57
+ },
58
+ template: `
59
+ <PageHeroHighlights tag="section" style="--header-row-background-colour: #2d4a35;">
60
+ <template #header>
61
+ <PageHeroHighlightsHeader v-bind="args">
62
+ <template #start>
63
+ <div style="color: white; padding-block: 1.6rem;">
64
+ <h1 class="page-heading-1" style="color: white;">Dashboard</h1>
65
+ <p class="page-body-normal" style="color: white;">Overview of your account activity and key metrics.</p>
66
+ </div>
67
+ </template>
68
+ <template #end>
69
+ <button style="border: 2px solid white; color: white; background: transparent; border-radius: 50%; width: 4rem; height: 4rem; cursor: pointer;">?</button>
70
+ <button style="border: 2px solid white; color: white; background: transparent; border-radius: 0.8rem; padding: 1rem 1.6rem; cursor: pointer;">Create something new</button>
71
+ </template>
72
+ </PageHeroHighlightsHeader>
73
+ </template>
74
+ </PageHeroHighlights>
75
+ `,
76
+ }),
77
+ };
@@ -45,10 +45,7 @@ describe("PageHeroHighlights", () => {
45
45
  it("renders highlights slot content", async () => {
46
46
  const wrapper = await mountSuspended(PageHeroHighlights, {
47
47
  slots: {
48
- highlights: [
49
- "<div class='highlight-card'>Card 1</div>",
50
- "<div class='highlight-card'>Card 2</div>",
51
- ].join(""),
48
+ highlights: ["<div class='highlight-card'>Card 1</div>", "<div class='highlight-card'>Card 2</div>"].join(""),
52
49
  },
53
50
  });
54
51
  expect(wrapper.findAll(".highlight-card").length).toBe(2);
@@ -101,8 +98,7 @@ describe("PageHeroHighlights", () => {
101
98
  const wrapper = await mountSuspended(PageHeroHighlights, {
102
99
  props: { tag: "section" },
103
100
  slots: {
104
- header: (props: Record<string, unknown>) =>
105
- h("h1", { id: props.headingId, class: "page-title" }, "Dashboard"),
101
+ header: (props: Record<string, unknown>) => h("h1", { id: props.headingId, class: "page-title" }, "Dashboard"),
106
102
  },
107
103
  });
108
104
  const labelledBy = wrapper.find(".page-hero-highlights").attributes("aria-labelledby");
@@ -139,6 +135,18 @@ describe("PageHeroHighlights", () => {
139
135
  }
140
136
  });
141
137
 
138
+ it("applies has-content-panel class by default", async () => {
139
+ const wrapper = await mountSuspended(PageHeroHighlights);
140
+ expect(wrapper.find(".page-hero-highlights").classes()).toContain("has-content-panel");
141
+ });
142
+
143
+ it("does not apply has-content-panel class when contentPanel is false", async () => {
144
+ const wrapper = await mountSuspended(PageHeroHighlights, {
145
+ props: { contentPanel: false },
146
+ });
147
+ expect(wrapper.find(".page-hero-highlights").classes()).not.toContain("has-content-panel");
148
+ });
149
+
142
150
  it("does not apply highlight-title-baseline class by default", async () => {
143
151
  const wrapper = await mountSuspended(PageHeroHighlights);
144
152
  expect(wrapper.find(".page-hero-highlights").classes()).not.toContain("highlight-title-baseline");
@@ -184,7 +192,7 @@ describe("PageHeroHighlights", () => {
184
192
  props: { maxWidth: "1064px", contentAlign: "start" },
185
193
  });
186
194
  const vm = wrapper.vm as unknown as ComponentInstance;
187
- expect(vm.gridColumns).toBe("16px minmax(0, 1064px) 1fr");
195
+ expect(vm.gridColumns).toBe("16px minmax(0, 1064px) minmax(16px, 1fr)");
188
196
  });
189
197
 
190
198
  it("ignores contentAlign when maxWidth is not set", async () => {
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import PageHeroHighlightsHeader from "../PageHeroHighlightsHeader.vue";
4
+
5
+ describe("PageHeroHighlightsHeader", () => {
6
+ it("mounts without error", async () => {
7
+ const wrapper = await mountSuspended(PageHeroHighlightsHeader);
8
+ expect(wrapper.vm).toBeTruthy();
9
+ });
10
+
11
+ it("renders start slot content", async () => {
12
+ const wrapper = await mountSuspended(PageHeroHighlightsHeader, {
13
+ slots: { start: "<h1 class='page-title'>Surplus needs</h1>" },
14
+ });
15
+ expect(wrapper.find(".phh-start .page-title").exists()).toBe(true);
16
+ expect(wrapper.html()).toContain("Surplus needs");
17
+ });
18
+
19
+ it("renders end slot content when provided", async () => {
20
+ const wrapper = await mountSuspended(PageHeroHighlightsHeader, {
21
+ slots: {
22
+ start: "<h1>Title</h1>",
23
+ end: "<button class='cta'>Create new need</button>",
24
+ },
25
+ });
26
+ expect(wrapper.find(".phh-end .cta").exists()).toBe(true);
27
+ });
28
+
29
+ it("does not render .phh-end when no end slot is provided", async () => {
30
+ const wrapper = await mountSuspended(PageHeroHighlightsHeader, {
31
+ slots: { start: "<h1>Title</h1>" },
32
+ });
33
+ expect(wrapper.find(".phh-end").exists()).toBe(false);
34
+ });
35
+
36
+ it("renders .phh-start as full width when no end slot is provided", async () => {
37
+ const wrapper = await mountSuspended(PageHeroHighlightsHeader, {
38
+ slots: { start: "<h1>Title</h1>" },
39
+ });
40
+ expect(wrapper.find(".phh-start").exists()).toBe(true);
41
+ expect(wrapper.find(".phh-end").exists()).toBe(false);
42
+ });
43
+
44
+ it("applies styleClassPassthrough classes", async () => {
45
+ const wrapper = await mountSuspended(PageHeroHighlightsHeader, {
46
+ props: { styleClassPassthrough: ["extra-class", "another-class"] },
47
+ });
48
+ expect(wrapper.find(".page-hero-highlights-header").classes()).toContain("extra-class");
49
+ expect(wrapper.find(".page-hero-highlights-header").classes()).toContain("another-class");
50
+ });
51
+ });
@@ -1,7 +1,7 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`PageHeroHighlights > renders correct HTML structure 1`] = `
4
- "<div class="page-hero-highlights">
4
+ "<div class="page-hero-highlights has-content-panel">
5
5
  <div class="header-row">
6
6
  <div class="header-slot">
7
7
  <h1>Page Title</h1>