srcdev-nuxt-components 9.5.6 → 9.6.0
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-compliance-checklist.md +6 -2
- package/.claude/skills/components/breadcrumb.md +5 -0
- package/.claude/skills/components/display-pill.md +2 -0
- package/.claude/skills/components/input-button.md +3 -0
- package/.claude/skills/components/navigation-items.md +1 -0
- package/.claude/skills/components/opening-hours.md +12 -0
- package/.claude/skills/components/price-list.md +10 -1
- package/.claude/skills/components/responsive-header.md +74 -0
- package/.claude/skills/components/service-detail.md +32 -0
- package/.claude/skills/components/service-summary.md +8 -0
- package/.claude/skills/components/services-card-grid.md +5 -3
- package/.claude/skills/components/services-card.md +22 -12
- package/.claude/skills/components/site-header.md +2 -0
- package/.claude/skills/components/slider-gallery.md +10 -4
- package/.claude/skills/components/tab-navigation.md +25 -9
- package/.claude/skills/index.md +1 -2
- package/.claude/skills/storybook-add-story.md +14 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +13 -1
- package/.vscode/srcdev-component-service-detail.code-snippets +1 -0
- package/.vscode/srcdev-component-service-summary.code-snippets +3 -1
- package/.vscode/srcdev-component-services-card.code-snippets +38 -36
- package/.vscode/srcdev-component-services-grids.code-snippets +2 -1
- package/.vscode/srcdev-component-site-header.code-snippets +3 -1
- package/.vscode/srcdev-component-tab-navigation.code-snippets +1 -1
- package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +3 -0
- package/app/components/01.atoms/display-pill/DisplayPill.vue +5 -2
- package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +16 -4
- package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +28 -5
- package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +94 -0
- package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +27 -0
- package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +52 -11
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +55 -23
- package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +176 -257
- package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +42 -2
- package/app/components/02.molecules/opening-hours/CONSUMER-STYLING.md +11 -1
- package/app/components/02.molecules/opening-hours/OpeningHours.vue +71 -43
- package/app/components/02.molecules/opening-hours/stories/OpeningHours.stories.ts +138 -6
- package/app/components/02.molecules/opening-hours/tests/OpeningHours.spec.ts +53 -0
- package/app/components/02.molecules/opening-hours/tests/__snapshots__/OpeningHours.spec.ts.snap +1 -1
- package/app/components/02.molecules/opening-hours/tests/buildOpeningHoursSpecification.spec.ts +37 -1
- package/app/components/02.molecules/price-list/CONSUMER-STYLING.md +17 -5
- package/app/components/02.molecules/price-list/PriceList.vue +32 -9
- package/app/components/02.molecules/price-list/stories/PriceList.stories.ts +125 -29
- package/app/components/02.molecules/price-list/tests/PriceList.spec.ts +20 -0
- package/app/components/03.organisms/google-reviews/stories/GoogleReviews.stories.ts +9 -4
- package/app/components/03.organisms/image-galleries/slider-gallery/CONSUMER-STYLING.md +19 -1
- package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +65 -7
- package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +201 -63
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +41 -2
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +6 -6
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +18 -0
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +56 -18
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +106 -31
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +201 -18
- package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +21 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +66 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +30 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +1 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +35 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +23 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +1 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +71 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +386 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +11 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +79 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +17 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +64 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +29 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +32 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +10 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
- package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +36 -0
- package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +131 -6
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +9 -9
- package/app/components/03.organisms/responsive-header/tests/node_modules/.cache/nuxt/chrome-workspace.json +1 -0
- package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +92 -41
- package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +198 -73
- package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +157 -9
- package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +58 -0
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +4 -3
- package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +30 -0
- package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +32 -8
- package/app/components/03.organisms/services/service-summary/stories/ServiceSummary.stories.ts +147 -0
- package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +26 -0
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +1 -1
- package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +104 -63
- package/app/components/03.organisms/services/services-card/ServicesCard.vue +92 -66
- package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +194 -27
- package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +34 -8
- package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +8 -8
- package/app/components/03.organisms/services/services-grids/CONSUMER-STYLING.md +9 -0
- package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +2 -1
- package/app/components/03.organisms/services/services-grids/stories/ServiceSummaryGrid.stories.ts +46 -6
- package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +89 -11
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +8 -0
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +3 -3
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +24 -24
- package/app/components/03.organisms/site-header/SiteHeader.vue +8 -0
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +8 -0
- package/app/components/05.forms/input-button/InputButton.vue +3 -1
- package/app/composables/tests/useAnchorScroll.spec.ts +8 -0
- package/app/composables/useAnchorScroll.ts +1 -1
- package/app/utils/opening-hours.ts +23 -2
- package/nuxt.config.ts +1 -1
- package/package.json +1 -1
- package/.claude/skills/components/site-navigation.md +0 -145
- package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
- package/app/components/02.molecules/navigation/site-navigation/CONSUMER-STYLING.md +0 -109
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +0 -751
- package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +0 -356
- package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +0 -367
- package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +0 -30
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
1
2
|
import PriceList from "../PriceList.vue";
|
|
3
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
2
4
|
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
5
|
+
import type { MediaCanvas, PriceListData } from "~/types/components";
|
|
3
6
|
|
|
4
7
|
const priceListData = [
|
|
5
8
|
{
|
|
@@ -26,10 +29,26 @@ const priceListData = [
|
|
|
26
29
|
},
|
|
27
30
|
];
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
type LineClamp = "none" | "1" | "2" | "3" | "4";
|
|
33
|
+
|
|
34
|
+
// Story-only arg: drives a CSS token, not a PriceList prop. See the argTypes entry.
|
|
35
|
+
type StoryArgs = InstanceType<typeof PriceList>["$props"] & {
|
|
36
|
+
descriptionLineClamp: LineClamp;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const meta: Meta<StoryArgs> = {
|
|
30
40
|
title: "Molecules/PriceList",
|
|
31
41
|
component: PriceList,
|
|
32
42
|
argTypes: {
|
|
43
|
+
descriptionLineClamp: {
|
|
44
|
+
control: "select",
|
|
45
|
+
options: ["none", "1", "2", "3", "4"] satisfies LineClamp[],
|
|
46
|
+
description:
|
|
47
|
+
"**Story control, not a prop.** Sets the `--price-list-description-line-clamp` CSS token on a wrapper so you can try it here. " +
|
|
48
|
+
"To use it in an app, set the token in your own CSS, e.g. `.salon-menu { --price-list-description-line-clamp: 2; }`. " +
|
|
49
|
+
"`1` is single-line ellipsis, `none` (the default) shows everything.",
|
|
50
|
+
table: { category: "CSS tokens (story only, set in your CSS)", defaultValue: { summary: "none" } },
|
|
51
|
+
},
|
|
33
52
|
priceListData: {
|
|
34
53
|
control: "object",
|
|
35
54
|
description: "Array of columns, each with a heading and list of items",
|
|
@@ -52,10 +71,52 @@ const meta: Meta<typeof PriceList> = {
|
|
|
52
71
|
table: { category: "Styling" },
|
|
53
72
|
},
|
|
54
73
|
},
|
|
74
|
+
args: {
|
|
75
|
+
descriptionLineClamp: "none",
|
|
76
|
+
},
|
|
77
|
+
decorators: [
|
|
78
|
+
(story, context) => ({
|
|
79
|
+
components: { story, CanvasSwitcher },
|
|
80
|
+
setup() {
|
|
81
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
82
|
+
return { canvasName };
|
|
83
|
+
},
|
|
84
|
+
template: `
|
|
85
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
86
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
87
|
+
</div>
|
|
88
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
89
|
+
<story />
|
|
90
|
+
</div>
|
|
91
|
+
`,
|
|
92
|
+
}),
|
|
93
|
+
],
|
|
94
|
+
parameters: {
|
|
95
|
+
layout: "fullscreen",
|
|
96
|
+
},
|
|
55
97
|
};
|
|
56
98
|
|
|
57
99
|
export default meta;
|
|
58
|
-
type Story = StoryObj<
|
|
100
|
+
type Story = StoryObj<StoryArgs>;
|
|
101
|
+
|
|
102
|
+
function useStorySetup(args: StoryArgs) {
|
|
103
|
+
const componentArgs = computed(() => {
|
|
104
|
+
const { descriptionLineClamp: _descriptionLineClamp, ...rest } = args;
|
|
105
|
+
return rest;
|
|
106
|
+
});
|
|
107
|
+
const tokenStyles = computed(() => ({
|
|
108
|
+
"--price-list-description-line-clamp": args.descriptionLineClamp,
|
|
109
|
+
}));
|
|
110
|
+
return { componentArgs, tokenStyles };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const render = (args: StoryArgs) => ({
|
|
114
|
+
components: { PriceList },
|
|
115
|
+
setup() {
|
|
116
|
+
return useStorySetup(args);
|
|
117
|
+
},
|
|
118
|
+
template: `<div :style="tokenStyles"><PriceList v-bind="componentArgs" /></div>`,
|
|
119
|
+
});
|
|
59
120
|
|
|
60
121
|
export const Default: Story = {
|
|
61
122
|
args: {
|
|
@@ -64,13 +125,7 @@ export const Default: Story = {
|
|
|
64
125
|
fromLabel: "from",
|
|
65
126
|
styleClassPassthrough: [],
|
|
66
127
|
},
|
|
67
|
-
render
|
|
68
|
-
components: { PriceList },
|
|
69
|
-
setup() {
|
|
70
|
-
return { args };
|
|
71
|
-
},
|
|
72
|
-
template: `<PriceList v-bind="args" />`,
|
|
73
|
-
}),
|
|
128
|
+
render,
|
|
74
129
|
};
|
|
75
130
|
|
|
76
131
|
export const SingleColumn: Story = {
|
|
@@ -78,13 +133,7 @@ export const SingleColumn: Story = {
|
|
|
78
133
|
args: {
|
|
79
134
|
priceListData: [priceListData[0]!],
|
|
80
135
|
},
|
|
81
|
-
render
|
|
82
|
-
components: { PriceList },
|
|
83
|
-
setup() {
|
|
84
|
-
return { args };
|
|
85
|
-
},
|
|
86
|
-
template: `<PriceList v-bind="args" />`,
|
|
87
|
-
}),
|
|
136
|
+
render,
|
|
88
137
|
};
|
|
89
138
|
|
|
90
139
|
export const EmptyState: Story = {
|
|
@@ -92,13 +141,7 @@ export const EmptyState: Story = {
|
|
|
92
141
|
args: {
|
|
93
142
|
priceListData: priceListData.map(({ headingtext, headingIcon }) => ({ headingtext, headingIcon, items: [] })),
|
|
94
143
|
},
|
|
95
|
-
render
|
|
96
|
-
components: { PriceList },
|
|
97
|
-
setup() {
|
|
98
|
-
return { args };
|
|
99
|
-
},
|
|
100
|
-
template: `<PriceList v-bind="args" />`,
|
|
101
|
-
}),
|
|
144
|
+
render,
|
|
102
145
|
};
|
|
103
146
|
|
|
104
147
|
export const NoIcons: Story = {
|
|
@@ -106,11 +149,64 @@ export const NoIcons: Story = {
|
|
|
106
149
|
args: {
|
|
107
150
|
priceListData: priceListData.map(({ headingtext, items }) => ({ headingtext, items })),
|
|
108
151
|
},
|
|
109
|
-
render
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
152
|
+
render,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// ─── Stress test ──────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
const stressPriceListData: PriceListData[] = [
|
|
158
|
+
{
|
|
159
|
+
headingtext: "Haarschnitte, Pflegebehandlungen und Kopfhautmassagen für jeden Haartyp und jede Länge",
|
|
160
|
+
headingIcon: "lucide:sparkles",
|
|
161
|
+
items: [
|
|
162
|
+
{ description: "Pneumonoultramicroscopicsilicovolcanoconiosisbehandlungohneleerzeichen", price: "£45" },
|
|
163
|
+
{
|
|
164
|
+
description: `${"Cut, wash, condition, blow dry and finish with a heat protectant serum. ".repeat(5)}`,
|
|
165
|
+
price: "£1,234.56 – £9,999.99 pro Sitzung inklusive Beratung",
|
|
166
|
+
from: true,
|
|
167
|
+
},
|
|
168
|
+
{ description: "Price with no spaces", price: "£123456789012345678901234567890.00" },
|
|
169
|
+
{ description: "From, but the price is empty", price: "", from: true },
|
|
170
|
+
{ description: "", price: "£10" },
|
|
171
|
+
{ description: "💇♀️💅🏽 Emoji first", price: "£0" },
|
|
172
|
+
{ description: "<script>alert('xss')</script> rendered as text & entities", price: "<b>£5</b>" },
|
|
173
|
+
{ description: "قص الشعر وتصفيفه", price: "٤٥ £" },
|
|
174
|
+
{ description: "X", price: "1" },
|
|
175
|
+
{ description: "See https://example.com/a/very/long/path/that/never/breaks/segmentsegmentsegment", price: "-£20" },
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
headingtext: "",
|
|
180
|
+
items: Array.from({ length: 30 }, (_, i) => ({ description: `Item ${i + 1} in a column with no heading`, price: `£${(i + 1) * 5}` })),
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
headingtext: "Broken icon, no items",
|
|
184
|
+
headingIcon: "lucide:this-icon-does-not-exist",
|
|
185
|
+
items: [],
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
export const StressTest: Story = {
|
|
190
|
+
name: "Stress Test (Worst-Case Data)",
|
|
191
|
+
args: {
|
|
192
|
+
priceListData: stressPriceListData,
|
|
193
|
+
headingTag: "h2",
|
|
194
|
+
fromLabel: "ab einem Mindestpreis von",
|
|
195
|
+
},
|
|
196
|
+
render,
|
|
197
|
+
parameters: {
|
|
198
|
+
initialCanvas: "mobileCanvas",
|
|
199
|
+
docs: {
|
|
200
|
+
description: {
|
|
201
|
+
story:
|
|
202
|
+
"Deliberately hostile data to find breakage: a long German heading, unbroken description and price strings, " +
|
|
203
|
+
"a long price range with a long German 'from' label, a 'from' item with an empty price, an empty description, " +
|
|
204
|
+
"emoji, HTML-like text (must render as text), right-to-left Arabic, single characters, a negative price, " +
|
|
205
|
+
"a column with no heading (no empty heading is rendered) and 30 items, a broken icon name, and a column with no items. " +
|
|
206
|
+
"Check at every canvas width: nothing overflows its column, the price column never takes more than half the row " +
|
|
207
|
+
"and wraps inside it, columns go two-up only when the container is wide enough, and a third column wraps to a new row. " +
|
|
208
|
+
"Try the Description line clamp control too: it's a CSS token set by the story, not a prop.",
|
|
209
|
+
},
|
|
113
210
|
},
|
|
114
|
-
|
|
115
|
-
}),
|
|
211
|
+
},
|
|
116
212
|
};
|
|
@@ -191,6 +191,26 @@ describe("PriceList", () => {
|
|
|
191
191
|
expect(classes).not.toContain("first-class");
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
+
it("does not render an empty heading when headingtext is empty", async () => {
|
|
195
|
+
wrapper = await createWrapper([
|
|
196
|
+
{ headingtext: "", items: [{ description: "Cut", price: "£45" }] },
|
|
197
|
+
{ headingtext: "Colour", items: [] },
|
|
198
|
+
]);
|
|
199
|
+
const columns = wrapper.findAll(".price-list__column");
|
|
200
|
+
expect(columns[0]!.find(".price-list__heading").exists()).toBe(false);
|
|
201
|
+
expect(columns[0]!.findAll(".price-list__row").length).toBe(1);
|
|
202
|
+
expect(columns[1]!.find(".price-list__heading").text()).toBe("Colour");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("renders HTML-like text as text", async () => {
|
|
206
|
+
wrapper = await createWrapper([
|
|
207
|
+
{ headingtext: "Cuts", items: [{ description: "<script>alert(1)</script>", price: "<b>£5</b>" }] },
|
|
208
|
+
]);
|
|
209
|
+
expect(wrapper.find("script").exists()).toBe(false);
|
|
210
|
+
expect(wrapper.find("dd b").exists()).toBe(false);
|
|
211
|
+
expect(wrapper.find(".price-list__description").text()).toBe("<script>alert(1)</script>");
|
|
212
|
+
});
|
|
213
|
+
|
|
194
214
|
it("does not render a 'from' label when item.from is omitted", async () => {
|
|
195
215
|
wrapper = await createWrapper();
|
|
196
216
|
expect(wrapper.find(".price-list__from").exists()).toBe(false);
|
|
@@ -54,14 +54,19 @@ const meta: Meta<StoryArgs> = {
|
|
|
54
54
|
authorLineClamp: {
|
|
55
55
|
control: "select",
|
|
56
56
|
options: lineClampOptions,
|
|
57
|
-
description:
|
|
58
|
-
|
|
57
|
+
description:
|
|
58
|
+
"**Story control, not a prop.** Sets the `--google-reviews-author-line-clamp` CSS token on a wrapper. " +
|
|
59
|
+
"To use it in an app, set the token in your own CSS, e.g. `.reviews { --google-reviews-author-line-clamp: 1; }`. " +
|
|
60
|
+
"`1` is single-line ellipsis.",
|
|
61
|
+
table: { category: "CSS tokens (story only, set in your CSS)" },
|
|
59
62
|
},
|
|
60
63
|
textLineClamp: {
|
|
61
64
|
control: "select",
|
|
62
65
|
options: lineClampOptions,
|
|
63
|
-
description:
|
|
64
|
-
|
|
66
|
+
description:
|
|
67
|
+
"**Story control, not a prop.** Sets the `--google-reviews-text-line-clamp` CSS token on a wrapper. " +
|
|
68
|
+
"To use it in an app, set the token in your own CSS, e.g. `.reviews { --google-reviews-text-line-clamp: 3; }`.",
|
|
69
|
+
table: { category: "CSS tokens (story only, set in your CSS)" },
|
|
65
70
|
},
|
|
66
71
|
data: { control: "object", description: "GoogleReviewsData, usually from useGoogleReviews()" },
|
|
67
72
|
minRating: { control: { type: "range", min: 0, max: 5, step: 1 }, description: "Hide reviews below this rating; shows the filter notice" },
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
| Token | Default | Controls |
|
|
8
8
|
|---|---|---|
|
|
9
|
-
| `--slider-gallery-height` | `100svh` | Gallery height
|
|
9
|
+
| `--slider-gallery-height` | `100svh` | Gallery height |
|
|
10
|
+
| `--slider-gallery-width` | `100vw` | Gallery width. Set `100%` to fill a positioned container instead of the viewport |
|
|
10
11
|
| `--slider-gallery-z-index` | `9999` | Stacking order (full-screen takeover) |
|
|
11
12
|
| `--slider-gallery-accent` | `#f1683a` | Loading spinner and progress bar colour |
|
|
12
13
|
| `--slider-gallery-loading-background` | `var(--page-bg)` | Loading overlay background |
|
|
@@ -25,6 +26,15 @@
|
|
|
25
26
|
| `--slider-gallery-cta-background` | `#99999975` | See-more link background |
|
|
26
27
|
| `--slider-gallery-cta-border-colour` | `#fff` | See-more link border |
|
|
27
28
|
| `--slider-gallery-cta-colour` | `#fff` | See-more link text |
|
|
29
|
+
| `--slider-gallery-author-line-clamp` | `none` | Max lines of the stylist line |
|
|
30
|
+
| `--slider-gallery-title-line-clamp` | `none` | Max lines of the title |
|
|
31
|
+
| `--slider-gallery-topic-line-clamp` | `none` | Max lines of the category line |
|
|
32
|
+
| `--slider-gallery-description-line-clamp` | `none` | Max lines of the description |
|
|
33
|
+
|
|
34
|
+
Line-clamp tokens: `1` is single-line ellipsis, any other number caps the lines with an ellipsis
|
|
35
|
+
on the last, `none` shows everything. All slide text wraps long unbroken strings (`overflow-wrap: anywhere`).
|
|
36
|
+
The slide text is absolutely positioned, so very long copy can run into the thumbnails and arrows:
|
|
37
|
+
clamp the description if your CMS copy is unbounded.
|
|
28
38
|
|
|
29
39
|
### Thumbnails
|
|
30
40
|
|
|
@@ -43,6 +53,11 @@
|
|
|
43
53
|
| `--slider-gallery-thumbnail-outline-offset-hover` | the resting offset | Outline gap on hover |
|
|
44
54
|
| `--slider-gallery-thumbnail-border-radius` | `20px` | Thumbnail rounding |
|
|
45
55
|
| `--slider-gallery-thumbnail-overlay` | `#0004` | Tint over thumbnail images |
|
|
56
|
+
| `--slider-gallery-thumbnail-title-line-clamp` | `2` | Max lines of a thumbnail title |
|
|
57
|
+
| `--slider-gallery-thumbnail-description-line-clamp` | `2` | Max lines of a thumbnail description |
|
|
58
|
+
|
|
59
|
+
The thumbnail clamps default to `2` because the card is a fixed size with its text anchored to the
|
|
60
|
+
bottom: uncapped, long text grows upwards and its first lines are cut off.
|
|
46
61
|
|
|
47
62
|
### Arrows
|
|
48
63
|
|
|
@@ -83,6 +98,9 @@ Private (not public API): `--_animation-duration` (from the `animationDuration`
|
|
|
83
98
|
`--_arrow-border-*`/`--_arrow-outline-*` (resting values the hover tokens fall back to) and
|
|
84
99
|
`--_arrows-top`/`-right`/`-width`/`-max-width` (the resolved, breakpoint-switched row position).
|
|
85
100
|
|
|
101
|
+
> Changed 2026-10-06: added `--slider-gallery-width` (was a fixed `100vw`) and the line-clamp tokens;
|
|
102
|
+
> slide, thumbnail, see-more and loading text now wrap unbroken strings.
|
|
103
|
+
|
|
86
104
|
> Changed 2026-09-28: the thumbnail sizing/border/radius overrides used to be private names
|
|
87
105
|
> (`--_thumbnailMobileWidth`, `--_thumbnailBorderRadius`, etc.) with no public token; colours were
|
|
88
106
|
> hardcoded. The arrow icon is now sized by `--slider-gallery-arrow-icon-size` (width/height), separately from the button, and the `--_translate-x` icon nudge was removed. The wide thumbnail size now also applies to the image
|
|
@@ -25,7 +25,14 @@
|
|
|
25
25
|
:data-text-brightness="item.textBrightness"
|
|
26
26
|
:data-has-text="hasSlideText(item) ? '' : undefined"
|
|
27
27
|
>
|
|
28
|
-
<NuxtImg
|
|
28
|
+
<NuxtImg
|
|
29
|
+
:src="item.src"
|
|
30
|
+
:alt="item.alt"
|
|
31
|
+
width="1920"
|
|
32
|
+
height="1080"
|
|
33
|
+
@load="handleImageLoad(index)"
|
|
34
|
+
@error="handleImageError(index)"
|
|
35
|
+
/>
|
|
29
36
|
<div class="slider-gallery-item-content" :class="item.textBrightness">
|
|
30
37
|
<div v-if="item.stylist" class="slider-gallery-author">{{ item.stylist }}</div>
|
|
31
38
|
<div v-if="item.title" class="slider-gallery-title">{{ item.title }}</div>
|
|
@@ -41,7 +48,7 @@
|
|
|
41
48
|
<div ref="sliderGalleryThumbnailsList" class="slider-gallery-thumbnails" aria-hidden="true">
|
|
42
49
|
<div v-for="(item, index) in galleryData" :key="index" class="slider-gallery-item">
|
|
43
50
|
<div class="slider-gallery-thumbnail-overlay">
|
|
44
|
-
<NuxtImg :src="item.src" alt="" loading="lazy" />
|
|
51
|
+
<NuxtImg :src="item.src" alt="" width="640" height="940" loading="lazy" />
|
|
45
52
|
<div class="slider-gallery-item-content" :class="item.textBrightness">
|
|
46
53
|
<div v-if="item.thumbnail?.title" class="slider-gallery-title">{{ item.thumbnail.title }}</div>
|
|
47
54
|
<div v-if="item.thumbnail?.description" class="slider-gallery-description">
|
|
@@ -138,6 +145,12 @@ const prefersReducedMotion = () =>
|
|
|
138
145
|
typeof window !== "undefined" && !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
139
146
|
const shouldAutoRun = () => props.autoRun && !prefersReducedMotion();
|
|
140
147
|
|
|
148
|
+
const finiteOr = (value: number, fallback: number) => (Number.isFinite(value) ? value : fallback);
|
|
149
|
+
const safeAnimationDuration = computed(() => Math.max(0, finiteOr(props.animationDuration, 3000)));
|
|
150
|
+
const safeAutoRunInterval = computed(() =>
|
|
151
|
+
Math.max(1000, safeAnimationDuration.value, finiteOr(props.autoRunInterval, 7000))
|
|
152
|
+
);
|
|
153
|
+
|
|
141
154
|
onMounted(async () => {
|
|
142
155
|
await nextTick();
|
|
143
156
|
|
|
@@ -203,12 +216,12 @@ function scheduleAutoRun() {
|
|
|
203
216
|
if (runNextAuto) clearTimeout(runNextAuto);
|
|
204
217
|
runNextAuto = setTimeout(() => {
|
|
205
218
|
if (!shouldAutoRun() || isLoading.value) return;
|
|
206
|
-
if (isPaused.value) {
|
|
219
|
+
if (isPaused.value || transitionRunning.value) {
|
|
207
220
|
scheduleAutoRun();
|
|
208
221
|
return;
|
|
209
222
|
}
|
|
210
223
|
doNext();
|
|
211
|
-
},
|
|
224
|
+
}, safeAutoRunInterval.value);
|
|
212
225
|
}
|
|
213
226
|
|
|
214
227
|
function showSlider(type: "next" | "prev") {
|
|
@@ -249,7 +262,7 @@ function showSlider(type: "next" | "prev") {
|
|
|
249
262
|
sliderGalleryWrapper.value.querySelectorAll(".is-prepended").forEach((el) => el.classList.remove("is-prepended"));
|
|
250
263
|
}
|
|
251
264
|
transitionRunning.value = false;
|
|
252
|
-
},
|
|
265
|
+
}, safeAnimationDuration.value);
|
|
253
266
|
|
|
254
267
|
scheduleAutoRun();
|
|
255
268
|
}
|
|
@@ -286,13 +299,13 @@ onBeforeUnmount(() => {
|
|
|
286
299
|
<style lang="css">
|
|
287
300
|
@layer components {
|
|
288
301
|
.slider-gallery {
|
|
289
|
-
--_animation-duration: v-bind(
|
|
302
|
+
--_animation-duration: v-bind(safeAnimationDuration + "ms");
|
|
290
303
|
--_accent: var(--slider-gallery-accent, #f1683a);
|
|
291
304
|
--_thumbnail-width: var(--slider-gallery-thumbnail-width, 100px);
|
|
292
305
|
--_thumbnail-height: var(--slider-gallery-thumbnail-height, 165px);
|
|
293
306
|
|
|
294
307
|
height: var(--slider-gallery-height, 100svh);
|
|
295
|
-
width: 100vw;
|
|
308
|
+
width: var(--slider-gallery-width, 100vw);
|
|
296
309
|
overflow: hidden;
|
|
297
310
|
position: absolute;
|
|
298
311
|
inset: 0;
|
|
@@ -333,6 +346,10 @@ onBeforeUnmount(() => {
|
|
|
333
346
|
p {
|
|
334
347
|
font-size: 1.2em;
|
|
335
348
|
font-weight: 500;
|
|
349
|
+
max-inline-size: 100%;
|
|
350
|
+
padding-inline: 1.6rem;
|
|
351
|
+
text-align: center;
|
|
352
|
+
overflow-wrap: anywhere;
|
|
336
353
|
}
|
|
337
354
|
}
|
|
338
355
|
|
|
@@ -435,14 +452,41 @@ onBeforeUnmount(() => {
|
|
|
435
452
|
padding-right: 30%;
|
|
436
453
|
box-sizing: border-box;
|
|
437
454
|
text-shadow: 0 5px 10px #0004;
|
|
455
|
+
overflow-wrap: anywhere;
|
|
438
456
|
|
|
439
457
|
@container (width < 678px) {
|
|
440
458
|
padding-right: 0;
|
|
441
459
|
}
|
|
442
460
|
|
|
461
|
+
.slider-gallery-author,
|
|
462
|
+
.slider-gallery-title,
|
|
463
|
+
.slider-gallery-topic,
|
|
464
|
+
.slider-gallery-description {
|
|
465
|
+
display: -webkit-box;
|
|
466
|
+
-webkit-box-orient: vertical;
|
|
467
|
+
overflow: hidden;
|
|
468
|
+
}
|
|
469
|
+
|
|
443
470
|
.slider-gallery-author {
|
|
444
471
|
font-weight: bold;
|
|
445
472
|
letter-spacing: 10px;
|
|
473
|
+
-webkit-line-clamp: var(--slider-gallery-author-line-clamp, none);
|
|
474
|
+
line-clamp: var(--slider-gallery-author-line-clamp, none);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.slider-gallery-title {
|
|
478
|
+
-webkit-line-clamp: var(--slider-gallery-title-line-clamp, none);
|
|
479
|
+
line-clamp: var(--slider-gallery-title-line-clamp, none);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.slider-gallery-topic {
|
|
483
|
+
-webkit-line-clamp: var(--slider-gallery-topic-line-clamp, none);
|
|
484
|
+
line-clamp: var(--slider-gallery-topic-line-clamp, none);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.slider-gallery-description {
|
|
488
|
+
-webkit-line-clamp: var(--slider-gallery-description-line-clamp, none);
|
|
489
|
+
line-clamp: var(--slider-gallery-description-line-clamp, none);
|
|
446
490
|
}
|
|
447
491
|
|
|
448
492
|
.slider-gallery-title,
|
|
@@ -466,7 +510,9 @@ onBeforeUnmount(() => {
|
|
|
466
510
|
place-items: center;
|
|
467
511
|
min-width: 130px;
|
|
468
512
|
min-height: 40px;
|
|
513
|
+
max-inline-size: 100%;
|
|
469
514
|
padding-inline: 1.2rem;
|
|
515
|
+
box-sizing: border-box;
|
|
470
516
|
background-color: var(--slider-gallery-cta-background, #99999975);
|
|
471
517
|
border: 1px solid var(--slider-gallery-cta-border-colour, #fff);
|
|
472
518
|
color: var(--slider-gallery-cta-colour, #fff);
|
|
@@ -528,13 +574,25 @@ onBeforeUnmount(() => {
|
|
|
528
574
|
bottom: 10px;
|
|
529
575
|
left: 10px;
|
|
530
576
|
right: 10px;
|
|
577
|
+
overflow-wrap: anywhere;
|
|
578
|
+
|
|
579
|
+
.slider-gallery-title,
|
|
580
|
+
.slider-gallery-description {
|
|
581
|
+
display: -webkit-box;
|
|
582
|
+
-webkit-box-orient: vertical;
|
|
583
|
+
overflow: hidden;
|
|
584
|
+
}
|
|
531
585
|
|
|
532
586
|
.slider-gallery-title {
|
|
533
587
|
font-weight: 500;
|
|
588
|
+
-webkit-line-clamp: var(--slider-gallery-thumbnail-title-line-clamp, 2);
|
|
589
|
+
line-clamp: var(--slider-gallery-thumbnail-title-line-clamp, 2);
|
|
534
590
|
}
|
|
535
591
|
|
|
536
592
|
.slider-gallery-description {
|
|
537
593
|
font-weight: 300;
|
|
594
|
+
-webkit-line-clamp: var(--slider-gallery-thumbnail-description-line-clamp, 2);
|
|
595
|
+
line-clamp: var(--slider-gallery-thumbnail-description-line-clamp, 2);
|
|
538
596
|
}
|
|
539
597
|
}
|
|
540
598
|
}
|