srcdev-nuxt-components 9.3.12 → 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/index.md +1 -0
- package/.claude/skills/theming-component-token-pattern.md +126 -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/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
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import { ref } from "vue";
|
|
3
|
+
import StorybookComponent from "../InputSelectCore.vue";
|
|
4
|
+
import type { IFormMultipleOptions } from "~/types/forms/types.forms.d";
|
|
5
|
+
|
|
6
|
+
interface InputSelectCoreStoryArgs {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
placeholder: string;
|
|
10
|
+
required: boolean;
|
|
11
|
+
fieldHasError: boolean;
|
|
12
|
+
inputVariant: "normal" | "outlined" | "underlined";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
title: "Components/Forms/Input Select/InputSelectCore",
|
|
17
|
+
component: StorybookComponent,
|
|
18
|
+
argTypes: {
|
|
19
|
+
id: { control: "text", table: { category: "Basic" } },
|
|
20
|
+
name: { control: "text", table: { category: "Basic" } },
|
|
21
|
+
placeholder: { control: "text", table: { category: "Basic" } },
|
|
22
|
+
required: { control: "boolean", table: { category: "Basic" } },
|
|
23
|
+
fieldHasError: { control: "boolean", table: { category: "States" } },
|
|
24
|
+
inputVariant: {
|
|
25
|
+
control: { type: "select" },
|
|
26
|
+
options: ["normal", "outlined", "underlined"],
|
|
27
|
+
table: { category: "Styling" },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
args: {
|
|
31
|
+
id: "select-default",
|
|
32
|
+
name: "selectDefault",
|
|
33
|
+
placeholder: "Choose a service",
|
|
34
|
+
required: false,
|
|
35
|
+
fieldHasError: false,
|
|
36
|
+
inputVariant: "normal",
|
|
37
|
+
},
|
|
38
|
+
} as Meta<InputSelectCoreStoryArgs>;
|
|
39
|
+
|
|
40
|
+
const Template: StoryFn<InputSelectCoreStoryArgs> = (args) => ({
|
|
41
|
+
components: { StorybookComponent },
|
|
42
|
+
setup() {
|
|
43
|
+
const modelValue = ref<string | number>("");
|
|
44
|
+
const fieldData = ref<IFormMultipleOptions>({
|
|
45
|
+
data: [
|
|
46
|
+
{ id: "1", name: "balayage", value: "balayage", label: "Balayage" },
|
|
47
|
+
{ id: "2", name: "highlights", value: "highlights", label: "Highlights" },
|
|
48
|
+
{ id: "3", name: "full-colour", value: "full-colour", label: "Full Colour" },
|
|
49
|
+
],
|
|
50
|
+
total: 3,
|
|
51
|
+
skip: 0,
|
|
52
|
+
limit: 10,
|
|
53
|
+
});
|
|
54
|
+
return { args, modelValue, fieldData };
|
|
55
|
+
},
|
|
56
|
+
template: `
|
|
57
|
+
<div style="margin: 36px; max-width: 320px;">
|
|
58
|
+
<StorybookComponent v-bind="args" v-model="modelValue" v-model:field-data="fieldData" />
|
|
59
|
+
</div>
|
|
60
|
+
`,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const Default = Template.bind({});
|
|
64
|
+
|
|
65
|
+
export const ErrorState = Template.bind({});
|
|
66
|
+
ErrorState.args = { fieldHasError: true };
|
|
67
|
+
|
|
68
|
+
export const Underlined = Template.bind({});
|
|
69
|
+
Underlined.args = { inputVariant: "underlined" };
|
|
@@ -1,60 +1,50 @@
|
|
|
1
1
|
# InputTextCore — Consumer Styling Guide
|
|
2
2
|
|
|
3
|
-
##
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
3
|
+
## Public token API
|
|
4
|
+
|
|
5
|
+
Every colour token below is `var(--input-text-*, {default})` — set the `--input-text-*` name to
|
|
6
|
+
override just this component; leave it unset and it inherits the shared `--theme-input-*`/
|
|
7
|
+
`--theme-*` token every other themed component also falls back to. See
|
|
8
|
+
`theming-component-token-pattern.md` for why this two-tier shape exists.
|
|
9
|
+
|
|
10
|
+
(Before 2026-08-25 this component used a private `--_input-text-*` naming scheme presented as a
|
|
11
|
+
"local override path" — that's gone. A private, underscore-prefixed custom property was never a
|
|
12
|
+
real override point; renaming it public with a real fallback is the actual fix, not a second
|
|
13
|
+
documented path to reach for.)
|
|
14
|
+
|
|
15
|
+
| Token | Falls back to | Controls |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `--input-text-surface` | `var(--theme-input-surface)` | Wrapper background |
|
|
18
|
+
| `--input-text-surface-hover` | `var(--theme-input-surface-hover)` | Background of an embedded `InputButtonCore` slot on hover |
|
|
19
|
+
| `--input-text-border` | `var(--theme-border)` | Border colour (`.normal`/`.underlined`, resting) |
|
|
20
|
+
| `--input-text-border-hover` | `var(--theme-border-focus)` | Outline colour on hover (mouse) |
|
|
21
|
+
| `--input-text-border-focus` | `var(--theme-border-focus)` | Outline colour on `:focus-visible` (keyboard/assistive) |
|
|
22
|
+
| `--input-text-color` | `var(--theme-input-text-color-normal)` | Input text colour |
|
|
23
|
+
| `--input-text-placeholder-color` | `var(--theme-input-placeholder)` | Placeholder text colour |
|
|
24
|
+
|
|
25
|
+
Geometry (`--input-padding-*`, `--input-min-height`, `--input-font-size`,
|
|
26
|
+
`--form-element-*`, etc.) is shared across every `05.forms` component and untouched by this —
|
|
27
|
+
see `theming-form-geometry-tokens.md`.
|
|
20
28
|
|
|
21
|
-
|
|
22
|
-
--_input-text-surface: var(--theme-input-surface);
|
|
23
|
-
--_input-text-surface-hover: var(--theme-input-surface-hover);
|
|
24
|
-
--_input-text-border: var(--theme-border);
|
|
25
|
-
--_input-text-border-focus: var(--theme-border-focus);
|
|
26
|
-
```
|
|
29
|
+
---
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
`--theme-input-surface` / `--theme-border` / `--theme-border-focus` / `--theme-input-surface-hover`
|
|
30
|
-
at whatever scope you'd normally set theme tokens (`:root`, `[data-theme]`, a page wrapper).
|
|
31
|
-
- **Local override** (this component only, guaranteed to win — it's the last declaration before
|
|
32
|
-
the property that consumes it, no global-token indirection to reason about):
|
|
31
|
+
## Global theming — app-level CSS file
|
|
33
32
|
|
|
34
33
|
```css
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
--
|
|
38
|
-
--
|
|
39
|
-
--_input-text-surface: var(--slate-01);
|
|
34
|
+
:where(html) {
|
|
35
|
+
--input-text-surface: var(--rose-09);
|
|
36
|
+
--input-text-border: var(--rose-05);
|
|
37
|
+
--input-text-border-focus: var(--rose-03);
|
|
40
38
|
}
|
|
41
39
|
```
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
to dig into why — the local path is a strictly simpler cascade to reason about.
|
|
41
|
+
Only declare the tokens you want to change — everything else keeps inheriting the shared
|
|
42
|
+
`--theme-input-*`/`--theme-*` tokens.
|
|
46
43
|
|
|
47
44
|
---
|
|
48
45
|
|
|
49
|
-
##
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
`--form-element-outline-offset-focus` — all generic form-geometry tokens shared across every
|
|
55
|
-
`05.forms` component, declared in `setup/04.elements/forms/*.css`. Not component-local, so
|
|
56
|
-
overriding one affects every form element, not just `InputTextCore`.
|
|
57
|
-
- `--theme-input-text-color-normal`, `--theme-input-placeholder`,
|
|
58
|
-
`--theme-input-placeholder-font-size` — still read directly from the global theme tokens, no
|
|
59
|
-
local indirection yet. Follow the same pattern above (`--_input-text-*`) if a future consumer
|
|
60
|
-
needs a local override for these.
|
|
46
|
+
## Per-instance overrides
|
|
47
|
+
|
|
48
|
+
```vue
|
|
49
|
+
<InputTextCore id="email" name="email" style="--input-text-border-focus: var(--gold-04);" />
|
|
50
|
+
```
|
|
@@ -127,19 +127,25 @@ onMounted(() => {
|
|
|
127
127
|
<style lang="css">
|
|
128
128
|
@layer components {
|
|
129
129
|
.input-text-wrapper {
|
|
130
|
-
/*
|
|
131
|
-
—
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
--
|
|
137
|
-
--
|
|
130
|
+
/* Public --input-text-* tokens, inline-fallback to the shared --theme-* tokens (see
|
|
131
|
+
theming-component-token-pattern.md) — overriding one here doesn't touch every other themed
|
|
132
|
+
input/control that also reads --theme-input-surface/--theme-border. --_surface etc. are
|
|
133
|
+
private locals purely so the rest of this rule can reuse the resolved value without
|
|
134
|
+
repeating the fallback chain at every property — they are not themselves an override point,
|
|
135
|
+
see CONSUMER-STYLING.md. */
|
|
136
|
+
--_surface: var(--input-text-surface, var(--theme-input-surface));
|
|
137
|
+
--_surface-hover: var(--input-text-surface-hover, var(--theme-input-surface-hover));
|
|
138
|
+
--_border: var(--input-text-border, var(--theme-border));
|
|
139
|
+
/* Split so hover (mouse) and :focus-visible (keyboard/assistive) can diverge — both default
|
|
140
|
+
to the same --theme-border-focus today, identical appearance to before this token existed,
|
|
141
|
+
but each now has its own override point. */
|
|
142
|
+
--_border-hover: var(--input-text-border-hover, var(--theme-border-focus));
|
|
143
|
+
--_border-focus: var(--input-text-border-focus, var(--theme-border-focus));
|
|
138
144
|
|
|
139
145
|
display: flex;
|
|
140
146
|
align-items: center;
|
|
141
147
|
gap: var(--input-icon-slot-gap);
|
|
142
|
-
background-color: var(--
|
|
148
|
+
background-color: var(--_surface);
|
|
143
149
|
overflow: hidden;
|
|
144
150
|
transition: all var(--theme-form-transition-duration) ease-in-out;
|
|
145
151
|
|
|
@@ -147,25 +153,25 @@ onMounted(() => {
|
|
|
147
153
|
position: relative;
|
|
148
154
|
|
|
149
155
|
&.normal {
|
|
150
|
-
border: var(--form-element-border-width) solid var(--
|
|
156
|
+
border: var(--form-element-border-width) solid var(--_border);
|
|
151
157
|
border-radius: var(--form-input-border-radius);
|
|
152
158
|
outline: var(--form-element-outline-width) solid transparent;
|
|
153
159
|
|
|
154
160
|
padding-inline: var(--input-padding-inline);
|
|
155
161
|
|
|
156
162
|
&:has(input:is(:hover), button:is(:hover)) {
|
|
157
|
-
outline: var(--form-element-outline-width-focus) solid var(--
|
|
163
|
+
outline: var(--form-element-outline-width-focus) solid var(--_border-hover);
|
|
158
164
|
outline-offset: var(--form-element-outline-offset-focus);
|
|
159
165
|
}
|
|
160
166
|
|
|
161
167
|
&:has(input:focus-visible, button:focus-visible) {
|
|
162
|
-
outline: var(--form-element-outline-width-focus) solid var(--
|
|
168
|
+
outline: var(--form-element-outline-width-focus) solid var(--_border-focus);
|
|
163
169
|
outline-offset: var(--form-element-outline-offset-focus);
|
|
164
170
|
}
|
|
165
171
|
}
|
|
166
172
|
|
|
167
173
|
&.underlined {
|
|
168
|
-
border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--
|
|
174
|
+
border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--_border);
|
|
169
175
|
padding-inline: var(--input-padding-inline);
|
|
170
176
|
}
|
|
171
177
|
|
|
@@ -180,17 +186,28 @@ onMounted(() => {
|
|
|
180
186
|
box-sizing: content-box;
|
|
181
187
|
|
|
182
188
|
.input-button-core {
|
|
183
|
-
|
|
189
|
+
/* variant="inline" ships with zero border/colour styling by design (see
|
|
190
|
+
component-inline-action-button.md) — the parent supplies all of it. This rule was
|
|
191
|
+
always missing the border/colour half of that contract, only ever setting
|
|
192
|
+
background-color; harmless before InputButtonCore's flat-button redesign, which
|
|
193
|
+
removed the unscoped base rule that used to accidentally give every variant a real
|
|
194
|
+
border regardless. Without its own border/color/outline reset, this button now falls
|
|
195
|
+
through to the browser's UA default <button> chrome (a stark outset border and
|
|
196
|
+
black/white text) instead of sitting flush with the input. */
|
|
197
|
+
background-color: var(--_surface);
|
|
198
|
+
color: var(--input-text-color, var(--theme-input-text-color-normal));
|
|
199
|
+
border: none;
|
|
200
|
+
outline: none;
|
|
184
201
|
aspect-ratio: 1;
|
|
185
202
|
border-radius: 0;
|
|
186
203
|
width: var(--input-min-height);
|
|
187
204
|
|
|
188
205
|
&:hover {
|
|
189
|
-
background-color: var(--
|
|
206
|
+
background-color: var(--_surface-hover);
|
|
190
207
|
}
|
|
191
208
|
|
|
192
209
|
&:is(:focus-visible) {
|
|
193
|
-
outline: var(--form-element-outline-width-focus) solid var(--
|
|
210
|
+
outline: var(--form-element-outline-width-focus) solid var(--_border-focus);
|
|
194
211
|
outline-offset: -4px;
|
|
195
212
|
}
|
|
196
213
|
|
|
@@ -207,7 +224,7 @@ onMounted(() => {
|
|
|
207
224
|
touch-action: manipulation;
|
|
208
225
|
flex-grow: 1;
|
|
209
226
|
|
|
210
|
-
color: var(--theme-input-text-color-normal);
|
|
227
|
+
color: var(--input-text-color, var(--theme-input-text-color-normal));
|
|
211
228
|
font-family: var(--font-family);
|
|
212
229
|
font-size: var(--input-font-size);
|
|
213
230
|
|
|
@@ -219,7 +236,7 @@ onMounted(() => {
|
|
|
219
236
|
min-height: var(--input-min-height);
|
|
220
237
|
|
|
221
238
|
&::placeholder {
|
|
222
|
-
color: var(--theme-input-placeholder);
|
|
239
|
+
color: var(--input-text-placeholder-color, var(--theme-input-placeholder));
|
|
223
240
|
font-size: var(--theme-input-placeholder-font-size);
|
|
224
241
|
font-style: italic;
|
|
225
242
|
line-height: 1;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import { reactive, ref } from "vue";
|
|
3
|
+
import InputTextWithLabel from "../../input-text/variants/InputTextWithLabel.vue";
|
|
4
|
+
import InputSelectWithLabel from "../../input-select/variants/InputSelectWithLabel.vue";
|
|
5
|
+
import InputTextareaWithLabel from "../../input-textarea/variants/InputTextareaWithLabel.vue";
|
|
6
|
+
import MultipleCheckboxes from "../../input-checkbox/MultipleCheckboxes.vue";
|
|
7
|
+
import InputButtonCore from "../../input-button/InputButtonCore.vue";
|
|
8
|
+
import FormField from "../../form-field/FormField.vue";
|
|
9
|
+
import HeroText from "../../../01.atoms/text-blocks/hero-text/HeroText.vue";
|
|
10
|
+
import type { IFormMultipleOptions, InputUiVariant } from "~/types/forms/types.forms.d";
|
|
11
|
+
|
|
12
|
+
interface ContactFormStoryArgs {
|
|
13
|
+
inputVariant: InputUiVariant;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Demo composition, not a component of its own — a richer variant of the pattern the real
|
|
17
|
+
// luxury-locs-by-natasha-nuxt3 /contact page uses (see that repo's app/pages/contact.vue): same
|
|
18
|
+
// name/phone/email + multi-select services + comments shape, plus an InputSelectWithLabel
|
|
19
|
+
// "How did you hear about us?" field that page doesn't have, to also exercise the select
|
|
20
|
+
// component alongside the others in one composed form. No validation wiring
|
|
21
|
+
// (useZodValidation/zod) here — that's the consuming app's concern, this is a visual/composition
|
|
22
|
+
// reference only.
|
|
23
|
+
export default {
|
|
24
|
+
title: "Patterns/Contact Form",
|
|
25
|
+
argTypes: {
|
|
26
|
+
inputVariant: {
|
|
27
|
+
control: { type: "select" },
|
|
28
|
+
options: ["normal", "outlined", "underlined"],
|
|
29
|
+
description: "Applied to every field in the form at once",
|
|
30
|
+
table: { category: "Styling" },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
args: {
|
|
34
|
+
inputVariant: "normal",
|
|
35
|
+
},
|
|
36
|
+
} as Meta<ContactFormStoryArgs>;
|
|
37
|
+
|
|
38
|
+
const Template: StoryFn<ContactFormStoryArgs> = (args) => ({
|
|
39
|
+
components: {
|
|
40
|
+
InputTextWithLabel,
|
|
41
|
+
InputSelectWithLabel,
|
|
42
|
+
InputTextareaWithLabel,
|
|
43
|
+
MultipleCheckboxes,
|
|
44
|
+
InputButtonCore,
|
|
45
|
+
FormField,
|
|
46
|
+
HeroText,
|
|
47
|
+
},
|
|
48
|
+
setup() {
|
|
49
|
+
const state = reactive({
|
|
50
|
+
fullName: "",
|
|
51
|
+
telNumber: "",
|
|
52
|
+
emailAddress: "",
|
|
53
|
+
hearAboutUs: "",
|
|
54
|
+
services: [] as string[],
|
|
55
|
+
comments: "",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const hearAboutUsOptions = ref<IFormMultipleOptions>({
|
|
59
|
+
data: [
|
|
60
|
+
{ id: "search", name: "hearAboutUs", value: "search", label: "Search engine" },
|
|
61
|
+
{ id: "social", name: "hearAboutUs", value: "social", label: "Social media" },
|
|
62
|
+
{ id: "referral", name: "hearAboutUs", value: "referral", label: "Friend referral" },
|
|
63
|
+
{ id: "other", name: "hearAboutUs", value: "other", label: "Other" },
|
|
64
|
+
],
|
|
65
|
+
total: 4,
|
|
66
|
+
skip: 0,
|
|
67
|
+
limit: 10,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const servicesOptions = ref<IFormMultipleOptions>({
|
|
71
|
+
data: [
|
|
72
|
+
{ id: "balayage", name: "services", value: "balayage", label: "Balayage" },
|
|
73
|
+
{ id: "highlights", name: "services", value: "highlights", label: "Highlights" },
|
|
74
|
+
{ id: "full-colour", name: "services", value: "full-colour", label: "Full Colour" },
|
|
75
|
+
{ id: "cut-style", name: "services", value: "cut-style", label: "Cut & Style" },
|
|
76
|
+
],
|
|
77
|
+
total: 4,
|
|
78
|
+
skip: 0,
|
|
79
|
+
limit: 10,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return { args, state, hearAboutUsOptions, servicesOptions };
|
|
83
|
+
},
|
|
84
|
+
template: `
|
|
85
|
+
<div style="margin: 36px; max-width: 480px;">
|
|
86
|
+
<HeroText
|
|
87
|
+
tag="h2"
|
|
88
|
+
font-size="heading"
|
|
89
|
+
:text-content="[{ text: 'Get in touch', styleClass: 'normal' }]"
|
|
90
|
+
:style-class-passthrough="['mbe-20']"
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
<form @submit.prevent>
|
|
94
|
+
<FormField width="wide" :has-gutter="false">
|
|
95
|
+
<InputTextWithLabel
|
|
96
|
+
v-model="state.fullName"
|
|
97
|
+
type="text"
|
|
98
|
+
name="fullName"
|
|
99
|
+
label="Full name"
|
|
100
|
+
placeholder="eg. Jane Smith"
|
|
101
|
+
error-message=""
|
|
102
|
+
:required="true"
|
|
103
|
+
:input-variant="args.inputVariant"
|
|
104
|
+
/>
|
|
105
|
+
</FormField>
|
|
106
|
+
|
|
107
|
+
<FormField width="wide" :has-gutter="false">
|
|
108
|
+
<InputTextWithLabel
|
|
109
|
+
v-model="state.telNumber"
|
|
110
|
+
type="tel"
|
|
111
|
+
inputmode="tel"
|
|
112
|
+
name="telNumber"
|
|
113
|
+
label="Telephone number"
|
|
114
|
+
placeholder="eg. 07700 900000"
|
|
115
|
+
error-message=""
|
|
116
|
+
:required="true"
|
|
117
|
+
:input-variant="args.inputVariant"
|
|
118
|
+
/>
|
|
119
|
+
</FormField>
|
|
120
|
+
|
|
121
|
+
<FormField width="wide" :has-gutter="false">
|
|
122
|
+
<InputTextWithLabel
|
|
123
|
+
v-model="state.emailAddress"
|
|
124
|
+
type="email"
|
|
125
|
+
inputmode="email"
|
|
126
|
+
name="emailAddress"
|
|
127
|
+
label="Email address"
|
|
128
|
+
placeholder="eg. name@domain.com"
|
|
129
|
+
error-message=""
|
|
130
|
+
:required="true"
|
|
131
|
+
:input-variant="args.inputVariant"
|
|
132
|
+
/>
|
|
133
|
+
</FormField>
|
|
134
|
+
|
|
135
|
+
<FormField width="wide" :has-gutter="false">
|
|
136
|
+
<InputSelectWithLabel
|
|
137
|
+
v-model="state.hearAboutUs"
|
|
138
|
+
v-model:field-data="hearAboutUsOptions"
|
|
139
|
+
name="hearAboutUs"
|
|
140
|
+
label="How did you hear about us?"
|
|
141
|
+
placeholder="Select an option"
|
|
142
|
+
error-message=""
|
|
143
|
+
:input-variant="args.inputVariant"
|
|
144
|
+
/>
|
|
145
|
+
</FormField>
|
|
146
|
+
|
|
147
|
+
<FormField width="wide" :has-gutter="false">
|
|
148
|
+
<MultipleCheckboxes
|
|
149
|
+
v-model="state.services"
|
|
150
|
+
v-model:field-data="servicesOptions"
|
|
151
|
+
name="services"
|
|
152
|
+
legend="Services of interest"
|
|
153
|
+
label="Select all that apply"
|
|
154
|
+
error-message=""
|
|
155
|
+
options-layout="inline"
|
|
156
|
+
:is-button="true"
|
|
157
|
+
:input-variant="args.inputVariant"
|
|
158
|
+
>
|
|
159
|
+
<template #descriptionText>Choose one or more services you're interested in.</template>
|
|
160
|
+
</MultipleCheckboxes>
|
|
161
|
+
</FormField>
|
|
162
|
+
|
|
163
|
+
<FormField width="wide" :has-gutter="false">
|
|
164
|
+
<InputTextareaWithLabel
|
|
165
|
+
v-model="state.comments"
|
|
166
|
+
name="comments"
|
|
167
|
+
label="Comments"
|
|
168
|
+
placeholder="Tell us a bit more about what you're looking for…"
|
|
169
|
+
error-message=""
|
|
170
|
+
:input-variant="args.inputVariant"
|
|
171
|
+
>
|
|
172
|
+
<template #descriptionText>Optional — any extra information that might help us.</template>
|
|
173
|
+
</InputTextareaWithLabel>
|
|
174
|
+
</FormField>
|
|
175
|
+
|
|
176
|
+
<FormField width="wide" :has-gutter="false">
|
|
177
|
+
<InputButtonCore type="submit" variant="primary" button-text="Send message" />
|
|
178
|
+
</FormField>
|
|
179
|
+
</form>
|
|
180
|
+
</div>
|
|
181
|
+
`,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
export const Default = Template.bind({});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import { reactive } from "vue";
|
|
3
|
+
import InputTextWithLabel from "../../input-text/variants/InputTextWithLabel.vue";
|
|
4
|
+
import InputPasswordWithLabel from "../../input-text/variants/InputPasswordWithLabel.vue";
|
|
5
|
+
import InputCheckboxRadioWithLabel from "../../input-checkbox-radio/InputCheckboxRadioWithLabel.vue";
|
|
6
|
+
import InputButtonCore from "../../input-button/InputButtonCore.vue";
|
|
7
|
+
import FormField from "../../form-field/FormField.vue";
|
|
8
|
+
import GlassPanel from "../../../01.atoms/glass-panel/GlassPanel.vue";
|
|
9
|
+
import HeroText from "../../../01.atoms/text-blocks/hero-text/HeroText.vue";
|
|
10
|
+
import type { InputUiVariant } from "~/types/forms/types.forms.d";
|
|
11
|
+
|
|
12
|
+
interface LoginFormStoryArgs {
|
|
13
|
+
inputVariant: InputUiVariant;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Demo composition, not a component of its own — exercises the real-world "login form" pattern
|
|
17
|
+
// (labelled text/password inputs, a single checkbox, primary + tertiary buttons) using the
|
|
18
|
+
// existing WithLabel wrapper components rather than the bare *Core components other stories use,
|
|
19
|
+
// since that's what a consuming app actually reaches for. No validation wiring (useZodValidation)
|
|
20
|
+
// here — that's the consuming app's concern, this is a visual/composition reference only.
|
|
21
|
+
export default {
|
|
22
|
+
title: "Patterns/Login Form",
|
|
23
|
+
argTypes: {
|
|
24
|
+
inputVariant: {
|
|
25
|
+
control: { type: "select" },
|
|
26
|
+
options: ["normal", "outlined", "underlined"],
|
|
27
|
+
description: "Applied to every field in the form at once",
|
|
28
|
+
table: { category: "Styling" },
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
args: {
|
|
32
|
+
inputVariant: "normal",
|
|
33
|
+
},
|
|
34
|
+
} as Meta<LoginFormStoryArgs>;
|
|
35
|
+
|
|
36
|
+
const Template: StoryFn<LoginFormStoryArgs> = (args) => ({
|
|
37
|
+
components: {
|
|
38
|
+
InputTextWithLabel,
|
|
39
|
+
InputPasswordWithLabel,
|
|
40
|
+
InputCheckboxRadioWithLabel,
|
|
41
|
+
InputButtonCore,
|
|
42
|
+
FormField,
|
|
43
|
+
GlassPanel,
|
|
44
|
+
HeroText,
|
|
45
|
+
},
|
|
46
|
+
setup() {
|
|
47
|
+
const state = reactive({
|
|
48
|
+
email: "",
|
|
49
|
+
password: "",
|
|
50
|
+
rememberMe: false,
|
|
51
|
+
});
|
|
52
|
+
return { args, state };
|
|
53
|
+
},
|
|
54
|
+
template: `
|
|
55
|
+
<div style="margin: 36px; max-width: 420px;">
|
|
56
|
+
<GlassPanel :style-class-passthrough="['login-panel']">
|
|
57
|
+
<div style="padding: 3.2rem;">
|
|
58
|
+
<HeroText
|
|
59
|
+
tag="h2"
|
|
60
|
+
font-size="heading"
|
|
61
|
+
:text-content="[{ text: 'Sign in', styleClass: 'normal' }]"
|
|
62
|
+
:style-class-passthrough="['mbe-20']"
|
|
63
|
+
/>
|
|
64
|
+
|
|
65
|
+
<form @submit.prevent>
|
|
66
|
+
<FormField width="wide" :has-gutter="false">
|
|
67
|
+
<InputTextWithLabel
|
|
68
|
+
v-model="state.email"
|
|
69
|
+
type="email"
|
|
70
|
+
inputmode="email"
|
|
71
|
+
name="email"
|
|
72
|
+
label="Email address"
|
|
73
|
+
placeholder="you@example.com"
|
|
74
|
+
error-message=""
|
|
75
|
+
:required="true"
|
|
76
|
+
:input-variant="args.inputVariant"
|
|
77
|
+
/>
|
|
78
|
+
</FormField>
|
|
79
|
+
|
|
80
|
+
<FormField width="wide" :has-gutter="false">
|
|
81
|
+
<InputPasswordWithLabel
|
|
82
|
+
v-model="state.password"
|
|
83
|
+
name="password"
|
|
84
|
+
label="Password"
|
|
85
|
+
placeholder="Enter your password"
|
|
86
|
+
error-message=""
|
|
87
|
+
:required="true"
|
|
88
|
+
:input-variant="args.inputVariant"
|
|
89
|
+
/>
|
|
90
|
+
</FormField>
|
|
91
|
+
|
|
92
|
+
<FormField width="wide" :has-gutter="false">
|
|
93
|
+
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem;">
|
|
94
|
+
<InputCheckboxRadioWithLabel
|
|
95
|
+
v-model="state.rememberMe"
|
|
96
|
+
type="checkbox"
|
|
97
|
+
name="rememberMe"
|
|
98
|
+
label="Remember me"
|
|
99
|
+
:input-variant="args.inputVariant"
|
|
100
|
+
/>
|
|
101
|
+
<InputButtonCore type="button" variant="tertiary" button-text="Forgot password?" />
|
|
102
|
+
</div>
|
|
103
|
+
</FormField>
|
|
104
|
+
|
|
105
|
+
<FormField width="wide" :has-gutter="false">
|
|
106
|
+
<!-- InputButtonCore keeps box-sizing: content-box, so width: 100% alone sizes only
|
|
107
|
+
the content box and padding/border overflow past it — box-sizing: border-box
|
|
108
|
+
here makes width: 100% mean the actual rendered width, not a demo-only quirk
|
|
109
|
+
worth fixing in the component itself since content-box is relied on elsewhere
|
|
110
|
+
(e.g. icon-only's aspect-ratio: 1). -->
|
|
111
|
+
<InputButtonCore
|
|
112
|
+
type="submit"
|
|
113
|
+
variant="primary"
|
|
114
|
+
button-text="Sign in"
|
|
115
|
+
style="width: 100%; box-sizing: border-box;"
|
|
116
|
+
/>
|
|
117
|
+
</FormField>
|
|
118
|
+
</form>
|
|
119
|
+
</div>
|
|
120
|
+
</GlassPanel>
|
|
121
|
+
</div>
|
|
122
|
+
`,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export const Default = Template.bind({});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# ToggleSwitchCore — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Public token API
|
|
4
|
+
|
|
5
|
+
Every colour token below is `var(--toggle-switch-*, {default})` — set the `--toggle-switch-*`
|
|
6
|
+
name to override just this component; leave it unset and it inherits the shared `--theme-*` token
|
|
7
|
+
every other themed component also falls back to. See `theming-component-token-pattern.md` for why
|
|
8
|
+
this two-tier shape exists. Defaults are unchanged from before this migration — this is a
|
|
9
|
+
token-rename pass, not a redesign.
|
|
10
|
+
|
|
11
|
+
| Token | Falls back to | Controls |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `--toggle-switch-surface` | `var(--theme-checkbox-symbol-surface)` | Track background (resting) |
|
|
14
|
+
| `--toggle-switch-surface-hover` | `var(--theme-surface-subtle)` | Track background on hover |
|
|
15
|
+
| `--toggle-switch-border` | `var(--theme-border)` | Track border colour |
|
|
16
|
+
| `--toggle-switch-border-focus` | `var(--theme-border-focus)` | Track outline colour on `:focus-visible` |
|
|
17
|
+
| `--toggle-switch-symbol-border` | `var(--theme-text)` | Thumb border colour |
|
|
18
|
+
| `--toggle-switch-symbol-surface-off` | `var(--theme-on-surface)` | Thumb background, unchecked |
|
|
19
|
+
| `--toggle-switch-symbol-surface-on` | `var(--theme-surface)` | Thumb background, checked |
|
|
20
|
+
| `--toggle-switch-icon-on-color` | `var(--theme-on-surface)` | "On" icon colour |
|
|
21
|
+
| `--toggle-switch-icon-off-color` | `var(--theme-surface)` | "Off" icon colour |
|
|
22
|
+
|
|
23
|
+
Geometry (`--input-toggle-*`, `--form-element-*`) is shared across every `05.forms` component and
|
|
24
|
+
untouched by this — see `theming-form-geometry-tokens.md`.
|
|
25
|
+
|
|
26
|
+
> Five private locals (`--_icon-on-opacity`, `--_icon-off-opacity`, `--_symbol-background-color`,
|
|
27
|
+
> `--_symbol-margin-inline-start`, `--_symbol-checked-offset`) were removed during this migration —
|
|
28
|
+
> declared but never actually read by any property in the component, confirmed by grep. Not part
|
|
29
|
+
> of any public API, so nothing to update if you were relying on them (you weren't — they had zero
|
|
30
|
+
> effect).
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Global theming — app-level CSS file
|
|
35
|
+
|
|
36
|
+
```css
|
|
37
|
+
:where(html) {
|
|
38
|
+
--toggle-switch-symbol-surface-on: var(--rose-05);
|
|
39
|
+
--toggle-switch-border-focus: var(--rose-03);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Per-instance overrides
|
|
46
|
+
|
|
47
|
+
```vue
|
|
48
|
+
<ToggleSwitchCore id="notifications" name="notifications" style="--toggle-switch-symbol-surface-on: var(--gold-06);" />
|
|
49
|
+
```
|