srcdev-nuxt-components 9.5.3 → 9.5.5
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/colour-scheme-disable.md +1 -1
- package/.claude/skills/component-compliance-checklist.md +214 -0
- package/.claude/skills/component-inline-action-button.md +2 -2
- package/.claude/skills/components/carousel-flip.md +3 -3
- package/.claude/skills/components/clipped-panel.md +2 -2
- package/.claude/skills/components/dashboard-quad-grid.md +2 -2
- package/.claude/skills/components/dashboard-stats-grid.md +1 -1
- package/.claude/skills/components/deep-expanding-menu-classic.md +1 -1
- package/.claude/skills/components/deep-expanding-menu.md +1 -1
- package/.claude/skills/components/google-reviews.md +193 -0
- package/.claude/skills/components/input-number.md +4 -0
- package/.claude/skills/components/input-otp.md +120 -0
- package/.claude/skills/components/section-parallax.md +1 -1
- package/.claude/skills/components/select-menu.md +19 -5
- package/.claude/skills/components/triple-toggle-switch.md +7 -1
- package/.claude/skills/composable-colour-scheme.md +2 -0
- package/.claude/skills/index.md +5 -1
- package/.claude/skills/new-app-scaffold.md +1 -1
- package/.claude/skills/theming-colour-ramps.md +6 -33
- package/.claude/skills/theming-component-token-pattern.md +30 -0
- package/.claude/skills/theming-dark-mode.md +243 -0
- package/.claude/skills/theming-override-default.md +6 -7
- package/.claude/skills/theming-partial-override.md +11 -11
- package/.vscode/srcdev-component-google-reviews.code-snippets +78 -0
- package/.vscode/srcdev-component-input-otp.code-snippets +47 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +13 -0
- package/README.md +81 -41
- package/app/assets/styles/setup/01.config/_head.css +0 -9
- package/app/assets/styles/setup/03.theming/_default.css +29 -29
- package/app/assets/styles/setup/03.theming/_error.css +3 -3
- package/app/assets/styles/setup/03.theming/_theme-slots.css +13 -16
- package/app/assets/styles/setup/a11y/_variables.css +2 -2
- package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +7 -7
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +19 -1
- package/app/components/02.molecules/select-menu/SelectMenu.vue +54 -3
- package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +35 -0
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +9 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +12 -12
- package/app/components/03.organisms/google-reviews/CONSUMER-STYLING.md +110 -0
- package/app/components/03.organisms/google-reviews/GoogleReviewCard.vue +161 -0
- package/app/components/03.organisms/google-reviews/GoogleReviews.vue +275 -0
- package/app/components/03.organisms/google-reviews/stories/GoogleReviews.stories.ts +323 -0
- package/app/components/03.organisms/google-reviews/tests/GoogleReviewCard.spec.ts +104 -0
- package/app/components/03.organisms/google-reviews/tests/GoogleReviews.spec.ts +232 -0
- package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviewCard.spec.ts.snap +9 -0
- package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviews.spec.ts.snap +43 -0
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +1 -1
- package/app/components/05.forms/input-number/InputNumber.vue +2 -2
- package/app/components/05.forms/input-otp/CONSUMER-STYLING.md +59 -0
- package/app/components/05.forms/input-otp/InputOtp.vue +254 -0
- package/app/components/05.forms/input-otp/InputOtpField.vue +106 -0
- package/app/components/05.forms/input-otp/stories/InputOtp.stories.ts +100 -0
- package/app/components/05.forms/input-otp/stories/InputOtpField.stories.ts +163 -0
- package/app/components/05.forms/input-otp/tests/InputOtp.spec.ts +275 -0
- package/app/components/05.forms/input-otp/tests/InputOtpField.spec.ts +76 -0
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +151 -33
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitch.vue +11 -3
- package/app/composables/useGoogleReviews.ts +4 -0
- package/app/types/components/google-reviews.d.ts +18 -0
- package/app/types/components/index.ts +1 -0
- package/nuxt.config.ts +9 -9
- package/package.json +9 -8
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
-
import { reactive } from "vue";
|
|
2
|
+
import { reactive, ref } from "vue";
|
|
3
3
|
import InputTextWithLabel from "../../input-text/InputTextWithLabel.vue";
|
|
4
|
+
import InputPasswordWithLabel from "../../input-text/InputPasswordWithLabel.vue";
|
|
5
|
+
import InputTextAsNumberWithLabel from "../../input-text/InputTextAsNumberWithLabel.vue";
|
|
4
6
|
import InputRangeDefault from "../../input-range/InputRangeDefault.vue";
|
|
5
7
|
import InputNumberField from "../../input-number/InputNumberField.vue";
|
|
8
|
+
import InputOtpField from "../../input-otp/InputOtpField.vue";
|
|
6
9
|
import InputTextareaWithLabel from "../../input-textarea/InputTextareaWithLabel.vue";
|
|
7
10
|
import InputSelectWithLabel from "../../input-select/InputSelectWithLabel.vue";
|
|
8
11
|
import ToggleSwitchWithLabel from "../../toggle-switch/ToggleSwitchWithLabel.vue";
|
|
12
|
+
import ToggleSwitchWithLabelInline from "../../toggle-switch/ToggleSwitchWithLabelInline.vue";
|
|
9
13
|
import MultipleCheckboxes from "../../input-checkbox/MultipleCheckboxes.vue";
|
|
10
14
|
import SingleCheckbox from "../../input-checkbox/SingleCheckbox.vue";
|
|
11
15
|
import MultipleRadiobuttons from "../../input-radio/MultipleRadiobuttons.vue";
|
|
@@ -14,35 +18,31 @@ import InputButton from "../../input-button/InputButton.vue";
|
|
|
14
18
|
import FormField from "../../form-field/FormField.vue";
|
|
15
19
|
import FormWrapper from "../../form-wrapper/FormWrapper.vue";
|
|
16
20
|
import HeroText from "../../../01.atoms/text-blocks/hero-text/HeroText.vue";
|
|
17
|
-
import type { InputUiVariant, IFormMultipleOptions } from "~/types/forms/types.forms.d";
|
|
21
|
+
import type { InputUiVariant, IFormMultipleOptions, InputTypesText, InputMode } from "~/types/forms/types.forms.d";
|
|
18
22
|
|
|
19
23
|
interface MigratedFieldsFormStoryArgs {
|
|
20
24
|
inputVariant: InputUiVariant;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
// Living reference, not a component of its own
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// Add a field here each time /migrate-component brings another 05.forms component up to 5/5, so
|
|
43
|
-
// this story doubles as a visible migration-progress tracker rather than living only in the
|
|
44
|
-
// ledger's HTML output. No validation wiring (useZodValidation/zod) here — that's the consuming
|
|
45
|
-
// app's concern, this is a visual/composition reference only.
|
|
27
|
+
// Living reference, not a component of its own: every 05.forms component that scores 5/5 in the
|
|
28
|
+
// Component Ledger (.claude/component-ledger/audit.json) appears here, either as its own field or
|
|
29
|
+
// rendered inside one. As of 2026-10-04 every 05.forms component is 5/5, so all of them are in:
|
|
30
|
+
// - Own field: InputTextWithLabel (InputTextCore; plus one per type/inputmode pair for checking
|
|
31
|
+
// on-device keyboards), InputPasswordWithLabel,
|
|
32
|
+
// InputTextAsNumberWithLabel, InputRangeDefault (InputRangeCore), InputNumberField
|
|
33
|
+
// (InputNumber), InputOtpField (InputOtp), InputTextareaWithLabel (InputTextareaCore), InputSelectWithLabel
|
|
34
|
+
// (InputSelectCore), ToggleSwitchWithLabel and ToggleSwitchWithLabelInline (ToggleSwitchCore),
|
|
35
|
+
// MultipleCheckboxes, SingleCheckbox, MultipleRadiobuttons, TripleToggleSwitch ("Theme
|
|
36
|
+
// preference": no Field wrapper or error state, so it sits under a plain caption).
|
|
37
|
+
// - Rendered inside other fields: FormWrapper (wraps the form), FormField (wraps every field),
|
|
38
|
+
// FormFieldset (checkbox/radio groups), InputLabel, InputDescription (the "Full name" and
|
|
39
|
+
// "Password" help text), InputError (every error strip), the input-checkbox-radio family
|
|
40
|
+
// (every checkbox and radio), InputButton (Continue/Clear errors, and the inline buttons inside
|
|
41
|
+
// the password and number fields), PendingEffect (Continue's simulated 1.5s submit).
|
|
42
|
+
// Add a field here each time a new 05.forms component reaches 5/5, so this story stays a visible
|
|
43
|
+
// completeness check rather than living only in the ledger's HTML output. No validation wiring
|
|
44
|
+
// (useZodValidation/zod) here — that's the consuming app's concern, this is a visual/composition
|
|
45
|
+
// reference only.
|
|
46
46
|
export default {
|
|
47
47
|
title: "Patterns/Migrated Fields Form",
|
|
48
48
|
argTypes: {
|
|
@@ -61,11 +61,15 @@ export default {
|
|
|
61
61
|
const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
62
62
|
components: {
|
|
63
63
|
InputTextWithLabel,
|
|
64
|
+
InputPasswordWithLabel,
|
|
65
|
+
InputTextAsNumberWithLabel,
|
|
64
66
|
InputRangeDefault,
|
|
65
67
|
InputNumberField,
|
|
68
|
+
InputOtpField,
|
|
66
69
|
InputTextareaWithLabel,
|
|
67
70
|
InputSelectWithLabel,
|
|
68
71
|
ToggleSwitchWithLabel,
|
|
72
|
+
ToggleSwitchWithLabelInline,
|
|
69
73
|
MultipleCheckboxes,
|
|
70
74
|
SingleCheckbox,
|
|
71
75
|
MultipleRadiobuttons,
|
|
@@ -78,17 +82,36 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
78
82
|
setup() {
|
|
79
83
|
const state = reactive({
|
|
80
84
|
fullName: "",
|
|
85
|
+
password: "",
|
|
86
|
+
guests: 2 as number | undefined,
|
|
81
87
|
budget: 50,
|
|
82
88
|
quantity: 1,
|
|
89
|
+
code: "",
|
|
83
90
|
notes: "",
|
|
84
91
|
colour: "",
|
|
85
92
|
subscribe: false,
|
|
93
|
+
reminders: true,
|
|
86
94
|
services: [] as string[],
|
|
87
95
|
contactMethod: "",
|
|
88
96
|
themePreference: "system",
|
|
89
97
|
terms: false,
|
|
90
98
|
});
|
|
91
99
|
|
|
100
|
+
// One InputTextWithLabel per type/inputmode pair, for checking each on-screen keyboard on a real device.
|
|
101
|
+
const textVariants: { name: string; label: string; type: InputTypesText; inputmode: InputMode; placeholder: string }[] = [
|
|
102
|
+
{ name: "variantEmail", label: "Email", type: "email", inputmode: "email", placeholder: "eg. jane@example.com" },
|
|
103
|
+
{ name: "variantTel", label: "Phone", type: "tel", inputmode: "tel", placeholder: "eg. 07700 900123" },
|
|
104
|
+
{ name: "variantUrl", label: "Website", type: "url", inputmode: "url", placeholder: "eg. https://example.com" },
|
|
105
|
+
{ name: "variantSearch", label: "Search", type: "text", inputmode: "search", placeholder: "Search treatments" },
|
|
106
|
+
{ name: "variantNumber", label: "Age", type: "number", inputmode: "numeric", placeholder: "eg. 34" },
|
|
107
|
+
{ name: "variantNumeric", label: "One-time code", type: "text", inputmode: "numeric", placeholder: "eg. 123456" },
|
|
108
|
+
{ name: "variantDecimal", label: "Hair length (cm)", type: "text", inputmode: "decimal", placeholder: "eg. 12.5" },
|
|
109
|
+
{ name: "variantDate", label: "Appointment date", type: "date", inputmode: "text", placeholder: "" },
|
|
110
|
+
];
|
|
111
|
+
const textVariantValues = reactive<Record<string, string>>(
|
|
112
|
+
Object.fromEntries(textVariants.map((variant) => [variant.name, ""]))
|
|
113
|
+
);
|
|
114
|
+
|
|
92
115
|
const serviceOptions = reactive<IFormMultipleOptions>({
|
|
93
116
|
data: [
|
|
94
117
|
{ id: "1", name: "services", value: "cut", label: "Cut" },
|
|
@@ -142,12 +165,14 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
142
165
|
// Demo-only "validation" — a real consuming app would wire this through something like
|
|
143
166
|
// useZodValidation instead (see this file's top comment). Kept deliberately simple so the
|
|
144
167
|
// Continue button has an obvious, reliable way to trigger each field's error state for
|
|
145
|
-
// exercising
|
|
146
|
-
// InputSelectWithLabel/MultipleCheckboxes/MultipleRadiobuttons/SingleCheckbox's error UI in Storybook.
|
|
168
|
+
// exercising every field's error UI in Storybook.
|
|
147
169
|
const errors = reactive({
|
|
148
170
|
fullName: "",
|
|
171
|
+
password: "",
|
|
172
|
+
guests: "",
|
|
149
173
|
budget: "",
|
|
150
174
|
quantity: "",
|
|
175
|
+
code: "",
|
|
151
176
|
notes: "",
|
|
152
177
|
colour: "",
|
|
153
178
|
services: "",
|
|
@@ -157,8 +182,11 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
157
182
|
|
|
158
183
|
const validate = () => {
|
|
159
184
|
errors.fullName = state.fullName.trim() ? "" : "Full name is required";
|
|
185
|
+
errors.password = state.password.length >= 8 ? "" : "Password must be at least 8 characters";
|
|
186
|
+
errors.guests = (state.guests ?? 0) >= 1 ? "" : "At least 1 guest is required";
|
|
160
187
|
errors.budget = state.budget >= 100 ? "" : "Budget must be at least £100";
|
|
161
188
|
errors.quantity = state.quantity >= 1 ? "" : "Quantity must be at least 1";
|
|
189
|
+
errors.code = /^\d{6}$/.test(state.code) ? "" : "Enter the 6-digit code";
|
|
162
190
|
errors.notes = state.notes.trim() ? "" : "Notes are required";
|
|
163
191
|
errors.colour = state.colour ? "" : "Please choose a colour";
|
|
164
192
|
errors.services = state.services.length ? "" : "Choose at least one service";
|
|
@@ -166,10 +194,23 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
166
194
|
errors.terms = state.terms ? "" : "You must agree to the terms";
|
|
167
195
|
};
|
|
168
196
|
|
|
197
|
+
// Simulated round trip so the Continue button shows PendingEffect before the errors land.
|
|
198
|
+
const isSubmitting = ref(false);
|
|
199
|
+
const submit = () => {
|
|
200
|
+
isSubmitting.value = true;
|
|
201
|
+
setTimeout(() => {
|
|
202
|
+
isSubmitting.value = false;
|
|
203
|
+
validate();
|
|
204
|
+
}, 1500);
|
|
205
|
+
};
|
|
206
|
+
|
|
169
207
|
const clearErrors = () => {
|
|
170
208
|
errors.fullName = "";
|
|
209
|
+
errors.password = "";
|
|
210
|
+
errors.guests = "";
|
|
171
211
|
errors.budget = "";
|
|
172
212
|
errors.quantity = "";
|
|
213
|
+
errors.code = "";
|
|
173
214
|
errors.notes = "";
|
|
174
215
|
errors.colour = "";
|
|
175
216
|
errors.services = "";
|
|
@@ -181,11 +222,14 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
181
222
|
args,
|
|
182
223
|
state,
|
|
183
224
|
errors,
|
|
184
|
-
|
|
225
|
+
submit,
|
|
226
|
+
isSubmitting,
|
|
185
227
|
clearErrors,
|
|
186
228
|
colourOptions,
|
|
187
229
|
serviceOptions,
|
|
188
230
|
contactMethodOptions,
|
|
231
|
+
textVariants,
|
|
232
|
+
textVariantValues,
|
|
189
233
|
themeOptions,
|
|
190
234
|
};
|
|
191
235
|
},
|
|
@@ -198,13 +242,14 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
198
242
|
:style-class-passthrough="['mbe-20']"
|
|
199
243
|
/>
|
|
200
244
|
<p style="margin: 0 0 2rem 0; color: #475569; font-size: 1.4rem;">
|
|
201
|
-
|
|
202
|
-
Click Continue
|
|
203
|
-
|
|
245
|
+
Every 05.forms component that is fully migrated (5/5 on the Component Ledger).
|
|
246
|
+
Click Continue to see the pending state, then the error states for any empty or
|
|
247
|
+
out-of-range field: name, password under 8 characters, no guests, budget under £100,
|
|
248
|
+
quantity under 1, verification code, notes, colour, services, contact method or terms.
|
|
204
249
|
</p>
|
|
205
250
|
|
|
206
251
|
<FormWrapper width="medium">
|
|
207
|
-
<form novalidate @submit.prevent="
|
|
252
|
+
<form novalidate @submit.prevent="submit">
|
|
208
253
|
<FormField width="wide" :has-gutter="false">
|
|
209
254
|
<InputTextWithLabel
|
|
210
255
|
v-model="state.fullName"
|
|
@@ -221,6 +266,35 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
221
266
|
</InputTextWithLabel>
|
|
222
267
|
</FormField>
|
|
223
268
|
|
|
269
|
+
<FormField width="wide" :has-gutter="false">
|
|
270
|
+
<InputPasswordWithLabel
|
|
271
|
+
v-model="state.password"
|
|
272
|
+
name="password"
|
|
273
|
+
label="Password"
|
|
274
|
+
:error-message="errors.password"
|
|
275
|
+
:field-has-error="!!errors.password"
|
|
276
|
+
:required="true"
|
|
277
|
+
:input-variant="args.inputVariant"
|
|
278
|
+
>
|
|
279
|
+
<template #descriptionText>At least 8 characters</template>
|
|
280
|
+
</InputPasswordWithLabel>
|
|
281
|
+
</FormField>
|
|
282
|
+
|
|
283
|
+
<FormField v-for="variant in textVariants" :key="variant.name" width="wide" :has-gutter="false">
|
|
284
|
+
<InputTextWithLabel
|
|
285
|
+
v-model="textVariantValues[variant.name]"
|
|
286
|
+
:type="variant.type"
|
|
287
|
+
:inputmode="variant.inputmode"
|
|
288
|
+
:name="variant.name"
|
|
289
|
+
:label="variant.label"
|
|
290
|
+
:placeholder="variant.placeholder"
|
|
291
|
+
error-message=""
|
|
292
|
+
:input-variant="args.inputVariant"
|
|
293
|
+
>
|
|
294
|
+
<template #descriptionText>type="{{ variant.type }}", inputmode="{{ variant.inputmode }}"</template>
|
|
295
|
+
</InputTextWithLabel>
|
|
296
|
+
</FormField>
|
|
297
|
+
|
|
224
298
|
<FormField width="wide" :has-gutter="false">
|
|
225
299
|
<InputRangeDefault
|
|
226
300
|
v-model="state.budget"
|
|
@@ -253,6 +327,35 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
253
327
|
</InputNumberField>
|
|
254
328
|
</FormField>
|
|
255
329
|
|
|
330
|
+
<FormField width="wide" :has-gutter="false">
|
|
331
|
+
<InputOtpField
|
|
332
|
+
v-model="state.code"
|
|
333
|
+
name="code"
|
|
334
|
+
label="Verification code"
|
|
335
|
+
:error-message="errors.code"
|
|
336
|
+
:field-has-error="!!errors.code"
|
|
337
|
+
:input-variant="args.inputVariant"
|
|
338
|
+
>
|
|
339
|
+
<template #descriptionText>Enter the 6-digit code we sent to your email</template>
|
|
340
|
+
</InputOtpField>
|
|
341
|
+
</FormField>
|
|
342
|
+
|
|
343
|
+
<FormField width="wide" :has-gutter="false">
|
|
344
|
+
<InputTextAsNumberWithLabel
|
|
345
|
+
v-model="state.guests"
|
|
346
|
+
name="guests"
|
|
347
|
+
label="Guests"
|
|
348
|
+
:min="1"
|
|
349
|
+
:max="8"
|
|
350
|
+
:error-message="errors.guests"
|
|
351
|
+
:field-has-error="!!errors.guests"
|
|
352
|
+
:input-variant="args.inputVariant"
|
|
353
|
+
>
|
|
354
|
+
<template #left><span aria-hidden="true">−</span></template>
|
|
355
|
+
<template #right><span aria-hidden="true">+</span></template>
|
|
356
|
+
</InputTextAsNumberWithLabel>
|
|
357
|
+
</FormField>
|
|
358
|
+
|
|
256
359
|
<FormField width="wide" :has-gutter="false">
|
|
257
360
|
<InputTextareaWithLabel
|
|
258
361
|
v-model="state.notes"
|
|
@@ -282,6 +385,10 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
282
385
|
<ToggleSwitchWithLabel v-model="state.subscribe" name="subscribe" label="Subscribe to updates" />
|
|
283
386
|
</FormField>
|
|
284
387
|
|
|
388
|
+
<FormField width="wide" :has-gutter="false">
|
|
389
|
+
<ToggleSwitchWithLabelInline v-model="state.reminders" name="reminders" label="Email me appointment reminders" />
|
|
390
|
+
</FormField>
|
|
391
|
+
|
|
285
392
|
<FormField width="wide" :has-gutter="false">
|
|
286
393
|
<MultipleCheckboxes
|
|
287
394
|
v-model="state.services"
|
|
@@ -337,7 +444,14 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
337
444
|
|
|
338
445
|
<FormField width="wide" :has-gutter="false">
|
|
339
446
|
<div style="display: flex; gap: 1.2rem;">
|
|
340
|
-
<InputButton
|
|
447
|
+
<InputButton
|
|
448
|
+
type="submit"
|
|
449
|
+
variant="primary"
|
|
450
|
+
button-text="Continue"
|
|
451
|
+
:has-pending-effect="isSubmitting"
|
|
452
|
+
:is-pending="isSubmitting"
|
|
453
|
+
:readonly="isSubmitting"
|
|
454
|
+
/>
|
|
341
455
|
<InputButton type="button" variant="tertiary" button-text="Clear errors" @click="clearErrors" />
|
|
342
456
|
</div>
|
|
343
457
|
</FormField>
|
|
@@ -348,11 +462,15 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
348
462
|
style="margin-top: 2rem; padding: 1.6rem; border-radius: 0.8rem; background: #f8fafc; font-family: monospace; font-size: 1.3rem;"
|
|
349
463
|
>
|
|
350
464
|
<div>fullName: {{ state.fullName || '""' }}</div>
|
|
465
|
+
<div>password: {{ state.password ? "•".repeat(state.password.length) : '""' }}</div>
|
|
466
|
+
<div v-for="variant in textVariants" :key="variant.name">{{ variant.name }}: {{ textVariantValues[variant.name] || '""' }}</div>
|
|
467
|
+
<div>guests: {{ state.guests ?? "undefined" }}</div>
|
|
351
468
|
<div>budget: £{{ state.budget }}</div>
|
|
352
469
|
<div>quantity: {{ state.quantity }}</div>
|
|
353
470
|
<div>notes: {{ state.notes || '""' }}</div>
|
|
354
471
|
<div>colour: {{ state.colour || '""' }}</div>
|
|
355
472
|
<div>subscribe: {{ state.subscribe }}</div>
|
|
473
|
+
<div>reminders: {{ state.reminders }}</div>
|
|
356
474
|
<div>services: {{ JSON.stringify(state.services) }}</div>
|
|
357
475
|
<div>contactMethod: {{ state.contactMethod || '""' }}</div>
|
|
358
476
|
<div>themePreference: {{ state.themePreference }}</div>
|
|
@@ -186,10 +186,9 @@ onMounted(() => {
|
|
|
186
186
|
position: relative;
|
|
187
187
|
|
|
188
188
|
.option-group {
|
|
189
|
-
aspect-ratio: 1;
|
|
190
189
|
display: grid;
|
|
191
190
|
grid-template-areas: "icon-stack";
|
|
192
|
-
place-
|
|
191
|
+
place-items: center;
|
|
193
192
|
background: transparent;
|
|
194
193
|
border: var(--form-element-border-width) solid var(--triple-toggle-switch-option-border, #00000025);
|
|
195
194
|
outline: var(--form-element-outline-width) solid transparent;
|
|
@@ -214,10 +213,18 @@ onMounted(() => {
|
|
|
214
213
|
grid-area: icon-stack;
|
|
215
214
|
display: block;
|
|
216
215
|
font-size: var(--_scheme-icon-font-size);
|
|
216
|
+
width: 1em;
|
|
217
|
+
height: 1em;
|
|
217
218
|
color: var(--triple-toggle-switch-option-icon-color, var(--slate-10));
|
|
219
|
+
transition: color calc(v-bind(stepAnimationDuration) / 2) ease-in-out;
|
|
218
220
|
|
|
219
221
|
&.active {
|
|
220
222
|
color: var(--triple-toggle-switch-option-icon-color-active, var(--slate-00));
|
|
223
|
+
transition-delay: calc(v-bind(stepAnimationDuration) / 2);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@media (prefers-reduced-motion: reduce) {
|
|
227
|
+
transition: none;
|
|
221
228
|
}
|
|
222
229
|
|
|
223
230
|
&:hover {
|
|
@@ -227,9 +234,10 @@ onMounted(() => {
|
|
|
227
234
|
|
|
228
235
|
.option-input {
|
|
229
236
|
grid-area: icon-stack;
|
|
237
|
+
appearance: none;
|
|
230
238
|
opacity: 0;
|
|
231
|
-
aspect-ratio: 1;
|
|
232
239
|
width: var(--_scheme-icon-font-size);
|
|
240
|
+
height: var(--_scheme-icon-font-size);
|
|
233
241
|
|
|
234
242
|
&:hover {
|
|
235
243
|
cursor: pointer;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface GoogleReview {
|
|
2
|
+
authorName: string;
|
|
3
|
+
authorUri?: string;
|
|
4
|
+
authorPhotoUri?: string;
|
|
5
|
+
rating: number;
|
|
6
|
+
text: string;
|
|
7
|
+
relativeTime: string;
|
|
8
|
+
publishTime: string;
|
|
9
|
+
reviewUri?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GoogleReviewsData {
|
|
13
|
+
placeName: string;
|
|
14
|
+
rating: number;
|
|
15
|
+
totalReviews: number;
|
|
16
|
+
mapsUri: string;
|
|
17
|
+
reviews: GoogleReview[];
|
|
18
|
+
}
|
package/nuxt.config.ts
CHANGED
|
@@ -19,6 +19,13 @@ export default defineNuxtConfig({
|
|
|
19
19
|
resendApiKey: "", // NUXT_RESEND_API_KEY
|
|
20
20
|
contactEmailTo: "", // NUXT_CONTACT_EMAIL_TO — inbox that receives enquiries
|
|
21
21
|
contactEmailFrom: "", // NUXT_CONTACT_EMAIL_FROM — must be a verified Resend domain
|
|
22
|
+
// Used by server/api/google-reviews.get.ts (GoogleReviews component)
|
|
23
|
+
googleReviews: {
|
|
24
|
+
apiKey: "", // NUXT_GOOGLE_REVIEWS_API_KEY — Places API (New) key
|
|
25
|
+
placeId: "", // NUXT_GOOGLE_REVIEWS_PLACE_ID
|
|
26
|
+
languageCode: "", // NUXT_GOOGLE_REVIEWS_LANGUAGE_CODE — optional, e.g. en-GB
|
|
27
|
+
cacheMaxAge: 3600, // NUXT_GOOGLE_REVIEWS_CACHE_MAX_AGE — seconds
|
|
28
|
+
},
|
|
22
29
|
public: {
|
|
23
30
|
whatsappNumber: "", // NUXT_PUBLIC_WHATSAPP_NUMBER — in international format, no + or spaces, e.g. 447700900000
|
|
24
31
|
// Consumer apps that don't support dark/light mode can opt out entirely:
|
|
@@ -127,15 +134,8 @@ export default defineNuxtConfig({
|
|
|
127
134
|
// In Storybook, use 'none' provider so images render as plain /images/... paths
|
|
128
135
|
// (avoids /_vercel/image which has no source images in storybook-static/)
|
|
129
136
|
provider: process.env.STORYBOOK ? "none" : undefined,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
externals: {
|
|
133
|
-
// Nitro's dependency tracer only copies vue's CJS build but Node.js 22 (ESM)
|
|
134
|
-
// resolves the "import"+"node" export condition → ./index.mjs, which doesn't exist
|
|
135
|
-
// in the output. Inlining forces Nitro/Rollup to bundle Vue directly, avoiding the
|
|
136
|
-
// runtime resolution entirely.
|
|
137
|
-
inline: ["vue", "@vue/runtime-core", "@vue/runtime-dom", "@vue/reactivity", "@vue/shared", "@vue/server-renderer"],
|
|
138
|
-
},
|
|
137
|
+
// GoogleReviews reviewer photos
|
|
138
|
+
domains: ["lh3.googleusercontent.com"],
|
|
139
139
|
},
|
|
140
140
|
vite: {
|
|
141
141
|
optimizeDeps: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "srcdev-nuxt-components",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "9.5.
|
|
4
|
+
"version": "9.5.5",
|
|
5
5
|
"main": "nuxt.config.ts",
|
|
6
6
|
"types": "types.d.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
91
|
"devDependencies": {
|
|
92
|
-
"@chromatic-com/storybook": "4.
|
|
92
|
+
"@chromatic-com/storybook": "5.4.0",
|
|
93
93
|
"@iconify-json/akar-icons": "1.2.7",
|
|
94
94
|
"@iconify-json/bi": "1.2.7",
|
|
95
95
|
"@iconify-json/bitcoin-icons": "1.2.5",
|
|
@@ -103,21 +103,22 @@
|
|
|
103
103
|
"@nuxt/eslint": "1.17.0",
|
|
104
104
|
"@nuxt/schema": "4.4.6",
|
|
105
105
|
"@nuxt/test-utils": "4.1.0",
|
|
106
|
-
"@nuxtjs/storybook": "
|
|
106
|
+
"@nuxtjs/storybook": "10.0.0",
|
|
107
107
|
"@playwright/test": "1.62.1",
|
|
108
|
-
"@storybook/addon-a11y": "10.
|
|
109
|
-
"@storybook/addon-docs": "10.
|
|
110
|
-
"@storybook/
|
|
108
|
+
"@storybook/addon-a11y": "10.6.1",
|
|
109
|
+
"@storybook/addon-docs": "10.6.1",
|
|
110
|
+
"@storybook/addon-mcp": "10.6.1",
|
|
111
|
+
"@storybook/builder-vite": "10.6.1",
|
|
111
112
|
"@vue/test-utils": "2.4.11",
|
|
112
113
|
"@vueuse/core": "14.4.0",
|
|
113
114
|
"eslint": "10.8.1",
|
|
114
|
-
"eslint-plugin-storybook": "10.
|
|
115
|
+
"eslint-plugin-storybook": "10.6.1",
|
|
115
116
|
"happy-dom": "20.11.2",
|
|
116
117
|
"nuxt-qrcode": "0.4.10",
|
|
117
118
|
"oxc-parser": "0.131.0",
|
|
118
119
|
"patch-package": "8.0.1",
|
|
119
120
|
"release-it": "21.0.2",
|
|
120
|
-
"storybook": "10.
|
|
121
|
+
"storybook": "10.6.1",
|
|
121
122
|
"typescript": "5.9.3",
|
|
122
123
|
"vitest": "4.1.11",
|
|
123
124
|
"vue-router": "5.2.0",
|