srcdev-nuxt-components 9.3.11 → 9.3.13
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.
- package/.claude/skills/component-aria-landmark.md +3 -3
- package/.claude/skills/components/auto-grid.md +1 -1
- package/.claude/skills/components/page-hero-highlights.md +1 -1
- package/.claude/skills/components/service-detail.md +13 -14
- package/.claude/skills/components/service-summary-grid.md +141 -0
- package/.claude/skills/components/service-summary.md +165 -0
- package/.claude/skills/icon-sets.md +1 -1
- package/.claude/skills/index.md +3 -2
- package/.claude/skills/theming-component-token-pattern.md +126 -0
- package/.vscode/srcdev-component-service-summary.code-snippets +67 -0
- package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +10 -1
- package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +3 -3
- package/app/components/02.molecules/input-copy/InputCopy.vue +9 -3
- package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +3 -3
- package/app/components/02.molecules/pricing-card/PricingCard.vue +6 -3
- package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +7 -8
- package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +0 -1
- package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +2 -2
- package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +116 -0
- package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +183 -0
- package/app/components/03.organisms/services/{services-section/stories/ServicesSection.stories.ts → service-summary/stories/ServiceSummary.stories.ts} +78 -148
- package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +210 -0
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +17 -0
- package/app/components/03.organisms/services/services-grids/{ServicesSectionGrid.vue → ServiceSummaryGrid.vue} +8 -10
- package/app/components/03.organisms/services/services-grids/stories/{ServicesSectionGrid.stories.ts → ServiceSummaryGrid.stories.ts} +21 -21
- package/app/components/03.organisms/services/services-grids/tests/{ServicesSectionGrid.spec.ts → ServiceSummaryGrid.spec.ts} +28 -28
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +45 -0
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +117 -0
- package/app/components/05.forms/input-button/InputButtonCore.vue +83 -37
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-secondary-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-tertiary-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-primary-pending-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-secondary-pill-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-tertiary-readonly-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-default-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-error-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-success-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-warning-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-primary-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-secondary-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-tertiary-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/nuxt-icon-only-component-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pending-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pill-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/state-readonly-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/with-both-nuxt-icon-components-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/stories/InputButtonCore.stories.ts +35 -0
- package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +68 -0
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +19 -9
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioCore.vue +8 -5
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioButton.stories.ts +93 -0
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioCore.stories.ts +75 -0
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +41 -0
- package/app/components/05.forms/input-number/InputNumberCore.vue +12 -6
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +35 -29
- package/app/components/05.forms/input-select/InputSelectCore.vue +32 -20
- package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +69 -0
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +38 -48
- package/app/components/05.forms/input-text/InputTextCore.vue +35 -18
- package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +184 -0
- package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +125 -0
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +49 -0
- package/app/components/05.forms/toggle-switch/ToggleSwitchCore.vue +23 -16
- package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +72 -0
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +42 -20
- package/app/components/05.forms/triple-toggle-switch/stories/TripleToggleSwitchCore.stories.ts +63 -0
- package/app/components/display-theme-switch/DisplayThemeSwitch.vue +13 -6
- package/package.json +1 -1
- package/.claude/skills/components/services-section-grid.md +0 -130
- package/.claude/skills/components/services-section.md +0 -211
- package/.vscode/srcdev-component-services-section.code-snippets +0 -84
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesSectionGrid.spec.ts.snap +0 -87
- package/app/components/03.organisms/services/services-section/CONSUMER-STYLING.md +0 -149
- package/app/components/03.organisms/services/services-section/ServicesSection.vue +0 -345
- package/app/components/03.organisms/services/services-section/tests/ServicesSection.spec.ts +0 -297
- package/app/components/03.organisms/services/services-section/tests/__snapshots__/ServicesSection.spec.ts.snap +0 -87
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import ServiceSummaryGrid from "../ServiceSummaryGrid.vue";
|
|
2
2
|
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
3
|
import type { Service } from "~/types/types.services";
|
|
4
4
|
|
|
5
|
-
const meta: Meta<typeof
|
|
6
|
-
title: "Organisms/Services/
|
|
7
|
-
component:
|
|
5
|
+
const meta: Meta<typeof ServiceSummaryGrid> = {
|
|
6
|
+
title: "Organisms/Services/Service Summary Grid",
|
|
7
|
+
component: ServiceSummaryGrid,
|
|
8
8
|
argTypes: {
|
|
9
9
|
tag: {
|
|
10
10
|
control: { type: "select" },
|
|
@@ -15,7 +15,7 @@ const meta: Meta<typeof ServicesSectionGrid> = {
|
|
|
15
15
|
control: { type: "boolean" },
|
|
16
16
|
description: "Alternate image/content column order on every other section",
|
|
17
17
|
},
|
|
18
|
-
|
|
18
|
+
alignment: {
|
|
19
19
|
control: { type: "select" },
|
|
20
20
|
options: ["start", "center", "end"],
|
|
21
21
|
description: "Vertical alignment of the info column in each section",
|
|
@@ -28,7 +28,7 @@ const meta: Meta<typeof ServicesSectionGrid> = {
|
|
|
28
28
|
args: {
|
|
29
29
|
tag: "div",
|
|
30
30
|
useAlternateReverse: false,
|
|
31
|
-
|
|
31
|
+
alignment: "center",
|
|
32
32
|
styleClassPassthrough: [],
|
|
33
33
|
},
|
|
34
34
|
parameters: {
|
|
@@ -41,14 +41,14 @@ const meta: Meta<typeof ServicesSectionGrid> = {
|
|
|
41
41
|
docs: {
|
|
42
42
|
description: {
|
|
43
43
|
component:
|
|
44
|
-
"Renders a vertical stack of `
|
|
44
|
+
"Renders a vertical stack of `ServiceSummary` components from a `Service[]` array. Each summary includes a summary-link slot. Alternate image/content column order is controlled via `useAlternateReverse`.",
|
|
45
45
|
},
|
|
46
46
|
},
|
|
47
47
|
},
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
export default meta;
|
|
51
|
-
type Story = StoryObj<typeof
|
|
51
|
+
type Story = StoryObj<typeof ServiceSummaryGrid>;
|
|
52
52
|
|
|
53
53
|
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
54
54
|
|
|
@@ -82,12 +82,12 @@ const sampleServices: Service[] = [
|
|
|
82
82
|
|
|
83
83
|
export const Default: Story = {
|
|
84
84
|
render: (args) => ({
|
|
85
|
-
components: {
|
|
85
|
+
components: { ServiceSummaryGrid },
|
|
86
86
|
setup() {
|
|
87
87
|
return { args, sampleServices };
|
|
88
88
|
},
|
|
89
89
|
template: `
|
|
90
|
-
<
|
|
90
|
+
<ServiceSummaryGrid v-bind="args" :services-data="sampleServices">
|
|
91
91
|
<template #summary-link="{ serviceData }">
|
|
92
92
|
<a
|
|
93
93
|
:href="'/services/' + serviceData.slug"
|
|
@@ -96,7 +96,7 @@ export const Default: Story = {
|
|
|
96
96
|
More about {{ serviceData.title }} →
|
|
97
97
|
</a>
|
|
98
98
|
</template>
|
|
99
|
-
</
|
|
99
|
+
</ServiceSummaryGrid>
|
|
100
100
|
`,
|
|
101
101
|
}),
|
|
102
102
|
parameters: {
|
|
@@ -115,18 +115,18 @@ export const AlternateReverse: Story = {
|
|
|
115
115
|
useAlternateReverse: true,
|
|
116
116
|
},
|
|
117
117
|
render: (args) => ({
|
|
118
|
-
components: {
|
|
118
|
+
components: { ServiceSummaryGrid },
|
|
119
119
|
setup() {
|
|
120
120
|
return { args, sampleServices };
|
|
121
121
|
},
|
|
122
122
|
template: `
|
|
123
|
-
<
|
|
123
|
+
<ServiceSummaryGrid v-bind="args" :services-data="sampleServices">
|
|
124
124
|
<template #summary-link="{ serviceData }">
|
|
125
125
|
<a :href="'/services/' + serviceData.slug" style="display:inline-flex;margin-top:1.6rem;color:inherit;">
|
|
126
126
|
More about {{ serviceData.title }} →
|
|
127
127
|
</a>
|
|
128
128
|
</template>
|
|
129
|
-
</
|
|
129
|
+
</ServiceSummaryGrid>
|
|
130
130
|
`,
|
|
131
131
|
}),
|
|
132
132
|
parameters: {
|
|
@@ -142,11 +142,11 @@ export const AlternateReverse: Story = {
|
|
|
142
142
|
export const NoSlots: Story = {
|
|
143
143
|
name: "No Slots (empty sections)",
|
|
144
144
|
render: (args) => ({
|
|
145
|
-
components: {
|
|
145
|
+
components: { ServiceSummaryGrid },
|
|
146
146
|
setup() {
|
|
147
147
|
return { args, sampleServices };
|
|
148
148
|
},
|
|
149
|
-
template: `<
|
|
149
|
+
template: `<ServiceSummaryGrid v-bind="args" :services-data="sampleServices" />`,
|
|
150
150
|
}),
|
|
151
151
|
parameters: {
|
|
152
152
|
docs: {
|
|
@@ -161,18 +161,18 @@ export const NoSlots: Story = {
|
|
|
161
161
|
export const SingleService: Story = {
|
|
162
162
|
name: "Single Service",
|
|
163
163
|
render: (args) => ({
|
|
164
|
-
components: {
|
|
164
|
+
components: { ServiceSummaryGrid },
|
|
165
165
|
setup() {
|
|
166
166
|
return { args, sampleServices };
|
|
167
167
|
},
|
|
168
168
|
template: `
|
|
169
|
-
<
|
|
169
|
+
<ServiceSummaryGrid v-bind="args" :services-data="[sampleServices[0]]">
|
|
170
170
|
<template #summary-link="{ serviceData }">
|
|
171
171
|
<a :href="'/services/' + serviceData.slug" style="display:inline-flex;margin-top:1.6rem;color:inherit;">
|
|
172
172
|
More about {{ serviceData.title }} →
|
|
173
173
|
</a>
|
|
174
174
|
</template>
|
|
175
|
-
</
|
|
175
|
+
</ServiceSummaryGrid>
|
|
176
176
|
`,
|
|
177
177
|
}),
|
|
178
178
|
parameters: {
|
|
@@ -187,11 +187,11 @@ export const SingleService: Story = {
|
|
|
187
187
|
export const EmptyData: Story = {
|
|
188
188
|
name: "Empty Data",
|
|
189
189
|
render: (args) => ({
|
|
190
|
-
components: {
|
|
190
|
+
components: { ServiceSummaryGrid },
|
|
191
191
|
setup() {
|
|
192
192
|
return { args };
|
|
193
193
|
},
|
|
194
|
-
template: `<
|
|
194
|
+
template: `<ServiceSummaryGrid v-bind="args" :services-data="[]" />`,
|
|
195
195
|
}),
|
|
196
196
|
parameters: {
|
|
197
197
|
docs: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
-
import
|
|
3
|
+
import ServiceSummaryGrid from "../ServiceSummaryGrid.vue";
|
|
4
4
|
import type { Service } from "~/types/types.services";
|
|
5
5
|
|
|
6
6
|
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
@@ -33,18 +33,18 @@ const mockServices: Service[] = [
|
|
|
33
33
|
|
|
34
34
|
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
35
35
|
|
|
36
|
-
describe("
|
|
36
|
+
describe("ServiceSummaryGrid", () => {
|
|
37
37
|
// ─── Mount ─────────────────────────────────────────────────────────────
|
|
38
38
|
|
|
39
39
|
it("mounts without error", async () => {
|
|
40
|
-
const wrapper = await mountSuspended(
|
|
40
|
+
const wrapper = await mountSuspended(ServiceSummaryGrid, {
|
|
41
41
|
props: { servicesData: mockServices },
|
|
42
42
|
});
|
|
43
43
|
expect(wrapper.vm).toBeTruthy();
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
it("renders correct HTML structure", async () => {
|
|
47
|
-
const wrapper = await mountSuspended(
|
|
47
|
+
const wrapper = await mountSuspended(ServiceSummaryGrid, {
|
|
48
48
|
props: { servicesData: mockServices },
|
|
49
49
|
});
|
|
50
50
|
expect(wrapper.html()).toMatchSnapshot();
|
|
@@ -52,15 +52,15 @@ describe("ServicesSectionGrid", () => {
|
|
|
52
52
|
|
|
53
53
|
// ─── Root element ───────────────────────────────────────────────────────
|
|
54
54
|
|
|
55
|
-
it("has
|
|
56
|
-
const wrapper = await mountSuspended(
|
|
55
|
+
it("has service-summary-grid class on root", async () => {
|
|
56
|
+
const wrapper = await mountSuspended(ServiceSummaryGrid, {
|
|
57
57
|
props: { servicesData: mockServices },
|
|
58
58
|
});
|
|
59
|
-
expect(wrapper.classes()).toContain("
|
|
59
|
+
expect(wrapper.classes()).toContain("service-summary-grid");
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
it("renders as a div by default", async () => {
|
|
63
|
-
const wrapper = await mountSuspended(
|
|
63
|
+
const wrapper = await mountSuspended(ServiceSummaryGrid, {
|
|
64
64
|
props: { servicesData: mockServices },
|
|
65
65
|
});
|
|
66
66
|
expect(wrapper.element.tagName).toBe("DIV");
|
|
@@ -68,7 +68,7 @@ describe("ServicesSectionGrid", () => {
|
|
|
68
68
|
|
|
69
69
|
it("renders the tag prop as the root element", async () => {
|
|
70
70
|
for (const tag of ["section", "main"] as const) {
|
|
71
|
-
const wrapper = await mountSuspended(
|
|
71
|
+
const wrapper = await mountSuspended(ServiceSummaryGrid, {
|
|
72
72
|
props: { servicesData: mockServices, tag },
|
|
73
73
|
});
|
|
74
74
|
expect(wrapper.element.tagName).toBe(tag.toUpperCase());
|
|
@@ -78,51 +78,51 @@ describe("ServicesSectionGrid", () => {
|
|
|
78
78
|
// ─── Children ───────────────────────────────────────────────────────────
|
|
79
79
|
|
|
80
80
|
it("renders the correct number of service sections", async () => {
|
|
81
|
-
const wrapper = await mountSuspended(
|
|
81
|
+
const wrapper = await mountSuspended(ServiceSummaryGrid, {
|
|
82
82
|
props: { servicesData: mockServices },
|
|
83
83
|
});
|
|
84
|
-
expect(wrapper.findAll(".
|
|
84
|
+
expect(wrapper.findAll(".service-summary")).toHaveLength(mockServices.length);
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
it("renders no sections when servicesData is empty", async () => {
|
|
88
|
-
const wrapper = await mountSuspended(
|
|
88
|
+
const wrapper = await mountSuspended(ServiceSummaryGrid, {
|
|
89
89
|
props: { servicesData: [] },
|
|
90
90
|
});
|
|
91
|
-
expect(wrapper.findAll(".
|
|
91
|
+
expect(wrapper.findAll(".service-summary")).toHaveLength(0);
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
it("renders a single section correctly", async () => {
|
|
95
|
-
const wrapper = await mountSuspended(
|
|
95
|
+
const wrapper = await mountSuspended(ServiceSummaryGrid, {
|
|
96
96
|
props: { servicesData: [mockServices[0]!] },
|
|
97
97
|
});
|
|
98
|
-
expect(wrapper.findAll(".
|
|
98
|
+
expect(wrapper.findAll(".service-summary")).toHaveLength(1);
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
// ─── useAlternateReverse ────────────────────────────────────────────────
|
|
102
102
|
|
|
103
103
|
it("does not apply reverse to any section by default", async () => {
|
|
104
|
-
const wrapper = await mountSuspended(
|
|
104
|
+
const wrapper = await mountSuspended(ServiceSummaryGrid, {
|
|
105
105
|
props: { servicesData: mockServices },
|
|
106
106
|
});
|
|
107
|
-
wrapper.findAll(".
|
|
108
|
-
expect(grid.classes()).not.toContain("
|
|
107
|
+
wrapper.findAll(".service-summary__grid").forEach((grid) => {
|
|
108
|
+
expect(grid.classes()).not.toContain("service-summary__grid--reverse");
|
|
109
109
|
});
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
it("applies reverse to odd-indexed sections when useAlternateReverse is true", async () => {
|
|
113
|
-
const wrapper = await mountSuspended(
|
|
113
|
+
const wrapper = await mountSuspended(ServiceSummaryGrid, {
|
|
114
114
|
props: { servicesData: mockServices, useAlternateReverse: true },
|
|
115
115
|
});
|
|
116
|
-
const grids = wrapper.findAll(".
|
|
117
|
-
expect(grids[0]?.classes()).not.toContain("
|
|
118
|
-
expect(grids[1]?.classes()).toContain("
|
|
119
|
-
expect(grids[2]?.classes()).not.toContain("
|
|
116
|
+
const grids = wrapper.findAll(".service-summary__grid");
|
|
117
|
+
expect(grids[0]?.classes()).not.toContain("service-summary__grid--reverse"); // index 0 — even
|
|
118
|
+
expect(grids[1]?.classes()).toContain("service-summary__grid--reverse"); // index 1 — odd
|
|
119
|
+
expect(grids[2]?.classes()).not.toContain("service-summary__grid--reverse"); // index 2 — even
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
// ─── Slots ─────────────────────────────────────────────────────────────
|
|
123
123
|
|
|
124
124
|
it("renders summary-link slot content in each section", async () => {
|
|
125
|
-
const wrapper = await mountSuspended(
|
|
125
|
+
const wrapper = await mountSuspended(ServiceSummaryGrid, {
|
|
126
126
|
props: { servicesData: mockServices },
|
|
127
127
|
slots: {
|
|
128
128
|
"summary-link": '<a class="test-link" href="/services/test">View service</a>',
|
|
@@ -132,7 +132,7 @@ describe("ServicesSectionGrid", () => {
|
|
|
132
132
|
});
|
|
133
133
|
|
|
134
134
|
it("renders nothing in summary-link when slot is not provided", async () => {
|
|
135
|
-
const wrapper = await mountSuspended(
|
|
135
|
+
const wrapper = await mountSuspended(ServiceSummaryGrid, {
|
|
136
136
|
props: { servicesData: [mockServices[0]!] },
|
|
137
137
|
});
|
|
138
138
|
// isSummary=true, so summary-link slot is rendered — but empty since no content was provided
|
|
@@ -142,14 +142,14 @@ describe("ServicesSectionGrid", () => {
|
|
|
142
142
|
// ─── styleClassPassthrough ──────────────────────────────────────────────
|
|
143
143
|
|
|
144
144
|
it("applies a single styleClassPassthrough string", async () => {
|
|
145
|
-
const wrapper = await mountSuspended(
|
|
145
|
+
const wrapper = await mountSuspended(ServiceSummaryGrid, {
|
|
146
146
|
props: { servicesData: mockServices, styleClassPassthrough: "custom-grid" },
|
|
147
147
|
});
|
|
148
148
|
expect(wrapper.classes()).toContain("custom-grid");
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
152
|
-
const wrapper = await mountSuspended(
|
|
152
|
+
const wrapper = await mountSuspended(ServiceSummaryGrid, {
|
|
153
153
|
props: { servicesData: mockServices, styleClassPassthrough: ["class-a", "class-b"] },
|
|
154
154
|
});
|
|
155
155
|
expect(wrapper.classes()).toContain("class-a");
|
|
@@ -157,7 +157,7 @@ describe("ServicesSectionGrid", () => {
|
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
it("updates classes when styleClassPassthrough prop changes", async () => {
|
|
160
|
-
const wrapper = await mountSuspended(
|
|
160
|
+
const wrapper = await mountSuspended(ServiceSummaryGrid, {
|
|
161
161
|
props: { servicesData: mockServices, styleClassPassthrough: ["original"] },
|
|
162
162
|
});
|
|
163
163
|
expect(wrapper.classes()).toContain("original");
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`ServiceSummaryGrid > renders correct HTML structure 1`] = `
|
|
4
|
+
"<div class="service-summary-grid">
|
|
5
|
+
<div class="service-summary">
|
|
6
|
+
<div class="service-summary__grid">
|
|
7
|
+
<div class="service-summary__image-wrapper"><img data-nuxt-img="" srcset="/_ipx/_/images/locs-installation.jpg 1x, /_ipx/_/images/locs-installation.jpg 2x" alt="Locs Installation" loading="eager" fetchpriority="high" class="service-summary__image" src="/_ipx/_/images/locs-installation.jpg"></div>
|
|
8
|
+
<div class="service-summary__info-wrapper service-summary__info-wrapper--align-center">
|
|
9
|
+
<div class="eyebrow-text large">Subtitle for Locs Installation</div>
|
|
10
|
+
<h2 id="v-0-0-0" class="hero-text mb-20 title axis-horizontal">
|
|
11
|
+
<!--v-if--><span class="text-block-0 normal">Locs Installation</span>
|
|
12
|
+
</h2>
|
|
13
|
+
<div class="service-summary__pills"><span class="display-pill md neutral"><span class="pill-label">60 mins</span></span><span class="display-pill md neutral"><span class="pill-label">From £50</span></span></div>
|
|
14
|
+
<p class="page-body-normal">What this service is.</p>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="service-summary">
|
|
19
|
+
<div class="service-summary__grid">
|
|
20
|
+
<div class="service-summary__image-wrapper"><img data-nuxt-img="" srcset="/_ipx/_/images/locs-retwist.jpg 1x, /_ipx/_/images/locs-retwist.jpg 2x" alt="Locs Retwist" loading="eager" fetchpriority="auto" class="service-summary__image" src="/_ipx/_/images/locs-retwist.jpg"></div>
|
|
21
|
+
<div class="service-summary__info-wrapper service-summary__info-wrapper--align-center">
|
|
22
|
+
<div class="eyebrow-text large">Subtitle for Locs Retwist</div>
|
|
23
|
+
<h2 id="v-0-0-1" class="hero-text mb-20 title axis-horizontal">
|
|
24
|
+
<!--v-if--><span class="text-block-0 normal">Locs Retwist</span>
|
|
25
|
+
</h2>
|
|
26
|
+
<div class="service-summary__pills"><span class="display-pill md neutral"><span class="pill-label">60 mins</span></span><span class="display-pill md neutral"><span class="pill-label">From £50</span></span></div>
|
|
27
|
+
<p class="page-body-normal">What this service is.</p>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="service-summary">
|
|
32
|
+
<div class="service-summary__grid">
|
|
33
|
+
<div class="service-summary__image-wrapper"><img data-nuxt-img="" srcset="/_ipx/_/images/colour-treatment.jpg 1x, /_ipx/_/images/colour-treatment.jpg 2x" alt="Colour Treatment" loading="lazy" fetchpriority="auto" class="service-summary__image" src="/_ipx/_/images/colour-treatment.jpg"></div>
|
|
34
|
+
<div class="service-summary__info-wrapper service-summary__info-wrapper--align-center">
|
|
35
|
+
<div class="eyebrow-text large">Subtitle for Colour Treatment</div>
|
|
36
|
+
<h2 id="v-0-0-2" class="hero-text mb-20 title axis-horizontal">
|
|
37
|
+
<!--v-if--><span class="text-block-0 normal">Colour Treatment</span>
|
|
38
|
+
</h2>
|
|
39
|
+
<div class="service-summary__pills"><span class="display-pill md neutral"><span class="pill-label">60 mins</span></span><span class="display-pill md neutral"><span class="pill-label">From £50</span></span></div>
|
|
40
|
+
<p class="page-body-normal">What this service is.</p>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>"
|
|
45
|
+
`;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# InputButtonCore — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Public token API
|
|
4
|
+
|
|
5
|
+
Every colour token below is `var(--input-button-{variant}-{property}, {default})` — set the
|
|
6
|
+
`--input-button-*` name to override just this component's variant; leave it unset and it inherits
|
|
7
|
+
the shared `--theme-*` slot every other themed component (inputs, alerts, ...) also falls back to.
|
|
8
|
+
See `theming-component-token-pattern.md` for why this two-tier shape exists.
|
|
9
|
+
|
|
10
|
+
**Resting state is flat by design**: `border` defaults to the variant's own surface colour, so
|
|
11
|
+
there's no visible edge until you hover, focus, or override it. **`outline` defaults to fully
|
|
12
|
+
transparent** in both resting and hover state — outline is reserved for the focus-visible ring,
|
|
13
|
+
not used to reinforce the flat look. **Hover always darkens the resting surface** via `color-mix()`
|
|
14
|
+
rather than jumping to a different `--theme-*` step — it's reliably darker in both light and dark
|
|
15
|
+
mode. **`:focus-visible` is a separate token set**, not grouped with hover, since it's the one
|
|
16
|
+
state that gets a real, independently-themeable indicator (its `outline-width`/`-offset` come from
|
|
17
|
+
`--button-focus-ring-width`/`-offset`, not from here — see `theming-form-geometry-tokens.md`).
|
|
18
|
+
|
|
19
|
+
Geometry and typography (`--button-*`) are separate, already-global tokens with real defaults —
|
|
20
|
+
see `theming-form-geometry-tokens.md`. Not duplicated here. One geometry token worth calling out:
|
|
21
|
+
`--button-min-height` defaults to `var(--input-min-height)`, so a button lines up height-wise with
|
|
22
|
+
a text input/select/number field placed next to it (e.g. an inline "email + submit" pattern — see
|
|
23
|
+
the `InlineWithTextInput` story). `box-sizing: content-box` means it's a floor, not a fixed height.
|
|
24
|
+
|
|
25
|
+
### Primary
|
|
26
|
+
|
|
27
|
+
| Token | Falls back to | Controls |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `--input-button-primary-surface` | `var(--theme-surface)` | Background (resting) |
|
|
30
|
+
| `--input-button-primary-text` | `var(--theme-on-surface)` | Text colour (all states) |
|
|
31
|
+
| `--input-button-primary-border` | the resolved surface above | Border colour (resting) — flat by default |
|
|
32
|
+
| `--input-button-primary-ring` | `transparent` | Outline colour (resting) |
|
|
33
|
+
| `--input-button-primary-surface-hover` | `color-mix(in oklab, surface 85%, black)` | Background on hover |
|
|
34
|
+
| `--input-button-primary-border-hover` | the resolved hover surface above | Border colour on hover |
|
|
35
|
+
| `--input-button-primary-ring-hover` | `transparent` | Outline colour on hover |
|
|
36
|
+
| `--input-button-primary-border-focus` | `var(--theme-border-focus)` | Border colour on `:focus-visible` |
|
|
37
|
+
| `--input-button-primary-ring-focus` | `var(--theme-ring)` | Outline colour on `:focus-visible` |
|
|
38
|
+
|
|
39
|
+
### Secondary
|
|
40
|
+
|
|
41
|
+
Same shape, falls back to the inverted-surface slots instead:
|
|
42
|
+
|
|
43
|
+
| Token | Falls back to |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `--input-button-secondary-surface` | `var(--theme-surface-inverted)` |
|
|
46
|
+
| `--input-button-secondary-text` | `var(--theme-text-inverted)` |
|
|
47
|
+
| `--input-button-secondary-border` | the resolved surface above (flat) |
|
|
48
|
+
| `--input-button-secondary-ring` / `-ring-hover` | `transparent` |
|
|
49
|
+
| `--input-button-secondary-surface-hover` | `color-mix(in oklab, surface 85%, black)` |
|
|
50
|
+
| `--input-button-secondary-border-hover` | the resolved hover surface above |
|
|
51
|
+
| `--input-button-secondary-border-focus` | `var(--theme-border-focus)` |
|
|
52
|
+
| `--input-button-secondary-ring-focus` | `var(--theme-ring)` |
|
|
53
|
+
|
|
54
|
+
### Tertiary
|
|
55
|
+
|
|
56
|
+
Same shape again; resting surface is transparent by default in dark mode (see note below):
|
|
57
|
+
|
|
58
|
+
| Token | Falls back to |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `--input-button-tertiary-surface` | `light-dark(var(--slate-01), transparent)` |
|
|
61
|
+
| `--input-button-tertiary-text` | `var(--theme-text)` |
|
|
62
|
+
| `--input-button-tertiary-border` | the resolved surface above (flat) |
|
|
63
|
+
| `--input-button-tertiary-ring` / `-ring-hover` | `transparent` |
|
|
64
|
+
| `--input-button-tertiary-surface-hover` | `color-mix(in oklab, surface 85%, black)` |
|
|
65
|
+
| `--input-button-tertiary-border-hover` | the resolved hover surface above |
|
|
66
|
+
| `--input-button-tertiary-border-focus` | `var(--theme-border-focus)` |
|
|
67
|
+
| `--input-button-tertiary-ring-focus` | `var(--theme-ring)` |
|
|
68
|
+
|
|
69
|
+
> `--input-button-tertiary-surface`'s dark-mode default is literal `transparent`. `color-mix()`
|
|
70
|
+
> still produces a correct darker hover from that — mixing `transparent` with `black` lowers the
|
|
71
|
+
> alpha channel rather than the lightness, so hover reads as a faint dark tint over whatever's
|
|
72
|
+
> behind the button, not a flat fill swap.
|
|
73
|
+
|
|
74
|
+
`.is-pending` always dims the *primary* surface regardless of variant — a pre-existing quirk, not
|
|
75
|
+
something this token set changes.
|
|
76
|
+
|
|
77
|
+
`InputCopy` and `PricingCard` fall back through `--input-button-primary-*` for their own copy/CTA
|
|
78
|
+
buttons (see their own `CONSUMER-STYLING.md`) rather than duplicating this chain.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Global theming — app-level CSS file
|
|
83
|
+
|
|
84
|
+
Set tokens on `:where(html)` in the consuming app's own theming file (e.g.
|
|
85
|
+
`assets/styles/setup/03.theming/_button.css`). These apply to every `InputButtonCore` instance:
|
|
86
|
+
|
|
87
|
+
```css
|
|
88
|
+
:where(html) {
|
|
89
|
+
--input-button-primary-surface: var(--rose-09);
|
|
90
|
+
--input-button-primary-text: var(--rose-00);
|
|
91
|
+
/* Hover/border/ring all derive from --input-button-primary-surface automatically via
|
|
92
|
+
color-mix() — no need to also set -hover/-border/-ring unless you want a different
|
|
93
|
+
relationship than "same colour, darker". */
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Only declare the tokens you want to change — everything else keeps inheriting the shared
|
|
98
|
+
`--theme-*` slots (or, for hover, keeps darkening whatever surface you did set).
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Per-instance overrides
|
|
103
|
+
|
|
104
|
+
```vue
|
|
105
|
+
<InputButtonCore variant="primary" button-text="Book now" style="--input-button-primary-surface: var(--gold-06);" />
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
To make hover lighten instead of darken for one instance, override the hover token directly rather
|
|
109
|
+
than fighting the `color-mix()` default:
|
|
110
|
+
|
|
111
|
+
```vue
|
|
112
|
+
<InputButtonCore
|
|
113
|
+
variant="primary"
|
|
114
|
+
button-text="Book now"
|
|
115
|
+
style="--input-button-primary-surface-hover: var(--gold-04);"
|
|
116
|
+
/>
|
|
117
|
+
```
|
|
@@ -106,6 +106,7 @@ const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough)
|
|
|
106
106
|
justify-content: center;
|
|
107
107
|
align-items: center;
|
|
108
108
|
box-sizing: content-box;
|
|
109
|
+
min-height: var(--button-min-height);
|
|
109
110
|
border-radius: var(--button-border-radius);
|
|
110
111
|
font-family: var(--font-family);
|
|
111
112
|
padding-inline: var(--button-padding-inline);
|
|
@@ -121,60 +122,105 @@ const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough)
|
|
|
121
122
|
|
|
122
123
|
/*
|
|
123
124
|
* Theming
|
|
125
|
+
* Every colour is a public --input-button-{variant}-{property} token, inline-fallback to
|
|
126
|
+
* the shared theme slot (see theming-component-token-pattern.md for why: overriding one
|
|
127
|
+
* variant here never touches the --theme-* slots other components read).
|
|
128
|
+
*
|
|
129
|
+
* Flat by default: border defaults to the variant's own surface colour (so the resting state
|
|
130
|
+
* shows no visible edge) and outline defaults to fully transparent — outline is reserved for
|
|
131
|
+
* the focus-visible ring, not used to reinforce the flat look. Hover darkens the surface via
|
|
132
|
+
* color-mix (always darker, regardless of which direction the underlying theme ramp runs in
|
|
133
|
+
* light vs dark mode — no more "hover looks lighter" surprises) and carries the border along
|
|
134
|
+
* with it, still flat; outline stays transparent through hover too.
|
|
135
|
+
* focus-visible is deliberately its own block, not grouped with :hover — it's the one state
|
|
136
|
+
* that gets a real, separately-themeable a11y indicator, and its outline-width/-offset (set
|
|
137
|
+
* below, in Shared States) give it a visible ring regardless of the border/outline being
|
|
138
|
+
* invisible everywhere else.
|
|
139
|
+
* --_surface is a private local (not a public token) — it exists purely so -hover/-border/-ring
|
|
140
|
+
* can color-mix() off the SAME resolved value a consumer's --input-button-{variant}-surface
|
|
141
|
+
* override produces, without every derived token repeating that fallback chain itself.
|
|
124
142
|
**/
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
outline: var(--button-outline-width) solid var(--theme-ring);
|
|
143
|
+
&.primary {
|
|
144
|
+
--_surface: var(--input-button-primary-surface, var(--theme-surface));
|
|
145
|
+
--_surface-hover: var(--input-button-primary-surface-hover, color-mix(in oklab, var(--_surface) 85%, black));
|
|
129
146
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
background-color: var(--_surface);
|
|
148
|
+
color: var(--input-button-primary-text, var(--theme-on-surface));
|
|
149
|
+
border: var(--button-border-width) solid var(--input-button-primary-border, var(--_surface));
|
|
150
|
+
outline: var(--button-outline-width) solid var(--input-button-primary-ring, transparent);
|
|
151
|
+
|
|
152
|
+
&:hover {
|
|
153
|
+
background-color: var(--_surface-hover);
|
|
154
|
+
border-color: var(--input-button-primary-border-hover, var(--_surface-hover));
|
|
155
|
+
outline-color: var(--input-button-primary-ring-hover, transparent);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
&:focus-visible {
|
|
159
|
+
border-color: var(--input-button-primary-border-focus, var(--theme-border-focus));
|
|
160
|
+
outline-color: var(--input-button-primary-ring-focus, var(--theme-ring));
|
|
161
|
+
}
|
|
142
162
|
}
|
|
143
163
|
|
|
144
164
|
&.is-pending {
|
|
145
|
-
|
|
165
|
+
/* Always the primary surface regardless of variant — pre-existing behaviour, not
|
|
166
|
+
something this migration changes. */
|
|
167
|
+
background-color: color-mix(
|
|
168
|
+
in oklab,
|
|
169
|
+
var(--input-button-primary-surface, var(--theme-surface)) 50%,
|
|
170
|
+
transparent
|
|
171
|
+
);
|
|
146
172
|
}
|
|
147
173
|
|
|
148
174
|
&.secondary {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
175
|
+
--_surface: var(--input-button-secondary-surface, var(--theme-surface-inverted));
|
|
176
|
+
--_surface-hover: var(
|
|
177
|
+
--input-button-secondary-surface-hover,
|
|
178
|
+
color-mix(in oklab, var(--_surface) 85%, black)
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
background-color: var(--_surface);
|
|
182
|
+
color: var(--input-button-secondary-text, var(--theme-text-inverted));
|
|
183
|
+
border: var(--button-border-width) solid var(--input-button-secondary-border, var(--_surface));
|
|
184
|
+
outline: var(--button-outline-width) solid var(--input-button-secondary-ring, transparent);
|
|
185
|
+
|
|
186
|
+
&:hover {
|
|
187
|
+
background-color: var(--_surface-hover);
|
|
188
|
+
border-color: var(--input-button-secondary-border-hover, var(--_surface-hover));
|
|
189
|
+
outline-color: var(--input-button-secondary-ring-hover, transparent);
|
|
190
|
+
}
|
|
153
191
|
|
|
154
|
-
&:hover,
|
|
155
192
|
&:focus-visible {
|
|
156
|
-
|
|
157
|
-
color: var(--theme-
|
|
158
|
-
border-color: var(--theme-border-focus);
|
|
159
|
-
outline-color: var(--theme-surface);
|
|
193
|
+
border-color: var(--input-button-secondary-border-focus, var(--theme-border-focus));
|
|
194
|
+
outline-color: var(--input-button-secondary-ring-focus, var(--theme-ring));
|
|
160
195
|
}
|
|
161
196
|
}
|
|
162
197
|
|
|
163
198
|
&.tertiary {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
199
|
+
/* --_surface can resolve to literal transparent (dark mode default) — color-mix() still
|
|
200
|
+
darkens correctly here: mixing transparent with black lowers the alpha channel rather
|
|
201
|
+
than the lightness, so it reads as a faint dark tint over whatever's behind the button
|
|
202
|
+
instead of a flat fill. Same formula, no special-casing needed. */
|
|
203
|
+
--_surface: var(--input-button-tertiary-surface, light-dark(var(--slate-01), transparent));
|
|
204
|
+
--_surface-hover: var(
|
|
205
|
+
--input-button-tertiary-surface-hover,
|
|
206
|
+
color-mix(in oklab, var(--_surface) 85%, black)
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
background-color: var(--_surface);
|
|
210
|
+
color: var(--input-button-tertiary-text, var(--theme-text));
|
|
211
|
+
border: var(--button-border-width) solid var(--input-button-tertiary-border, var(--_surface));
|
|
212
|
+
outline: var(--button-outline-width) solid var(--input-button-tertiary-ring, transparent);
|
|
167
213
|
text-decoration: underline;
|
|
168
|
-
outline: var(--button-outline-width) solid var(--theme-border-focus);
|
|
169
214
|
|
|
170
|
-
&:hover
|
|
215
|
+
&:hover {
|
|
216
|
+
background-color: var(--_surface-hover);
|
|
217
|
+
border-color: var(--input-button-tertiary-border-hover, var(--_surface-hover));
|
|
218
|
+
outline-color: var(--input-button-tertiary-ring-hover, transparent);
|
|
219
|
+
}
|
|
220
|
+
|
|
171
221
|
&:focus-visible {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
needs to switch off --theme-text (dark in light mode) to stay visible. */
|
|
175
|
-
color: var(--theme-on-surface);
|
|
176
|
-
border-color: var(--theme-border-focus);
|
|
177
|
-
outline-color: var(--theme-border-focus);
|
|
222
|
+
border-color: var(--input-button-tertiary-border-focus, var(--theme-border-focus));
|
|
223
|
+
outline-color: var(--input-button-tertiary-ring-focus, var(--theme-ring));
|
|
178
224
|
}
|
|
179
225
|
}
|
|
180
226
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|