srcdev-nuxt-components 9.6.9 → 9.6.11
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/rules/consumer/ids-from-useid.md +4 -2
- package/.claude/skills/component-compliance-checklist.md +24 -2
- package/.claude/skills/component-inline-action-button.md +5 -4
- package/.claude/skills/component-naming.md +8 -2
- package/.claude/skills/components/{alert-mask-core.md → alert-mask.md} +38 -23
- package/.claude/skills/components/alert-masked-content.md +3 -3
- package/.claude/skills/components/container-glow.md +1 -1
- package/.claude/skills/components/display-theme-switch.md +8 -8
- package/.claude/skills/components/input-button.md +26 -7
- package/.claude/skills/components/input-checkbox-radio.md +2 -0
- package/.claude/skills/components/input-description.md +5 -0
- package/.claude/skills/components/input-error.md +9 -2
- package/.claude/skills/components/input-label.md +8 -2
- package/.claude/skills/components/input-number.md +2 -0
- package/.claude/skills/components/input-otp.md +2 -0
- package/.claude/skills/components/input-range.md +2 -0
- package/.claude/skills/components/input-select.md +2 -0
- package/.claude/skills/components/input-text.md +2 -0
- package/.claude/skills/components/input-textarea.md +2 -0
- package/.claude/skills/components/pending-effect.md +5 -4
- package/.claude/skills/components/{triple-toggle-switch.md → segmented-control.md} +34 -19
- package/.claude/skills/components/services-card.md +1 -1
- package/.claude/skills/components/tabbed-content.md +1 -1
- package/.claude/skills/components/toggle-switch.md +2 -0
- package/.claude/skills/css-nesting-conventions.md +22 -1
- package/.claude/skills/index.md +5 -5
- package/.claude/skills/storybook-add-story.md +3 -1
- package/.claude/skills/theming-component-token-pattern.md +24 -0
- package/.claude/skills/theming-form-geometry-tokens.md +1 -1
- package/.claude/skills/theming-partial-override.md +1 -1
- package/.vscode/srcdev-component-alert-mask.code-snippets +41 -0
- package/.vscode/{srcdev-component-triple-toggle-switch.code-snippets → srcdev-component-segmented-control.code-snippets} +6 -7
- package/app/assets/styles/setup/03.theming/_theme-slots.css +1 -1
- package/app/assets/styles/setup/04.elements/forms/03.generic-input-geometry.css +4 -0
- package/app/components/01.atoms/alert-mask/AlertMask.vue +194 -0
- package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +57 -42
- package/app/components/01.atoms/alert-mask/stories/AlertMask.stories.ts +198 -0
- package/app/components/01.atoms/alert-mask/tests/AlertMask.spec.ts +233 -0
- package/app/components/02.molecules/alert-masked-content/AlertMaskedContent.vue +2 -2
- package/app/components/02.molecules/alert-masked-content/CONSUMER-STYLING.md +4 -4
- package/app/components/02.molecules/alert-masked-content/stories/AlertMaskedContent.stories.ts +60 -0
- package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +12 -12
- package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +3 -3
- package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +1 -1
- package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +1 -1
- package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +9 -9
- package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +16 -16
- package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +53 -1
- package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +8 -8
- package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +4 -4
- package/app/components/02.molecules/input-copy/InputCopy.vue +2 -2
- package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +2 -2
- package/app/components/03.organisms/google-reviews/tests/GoogleReviews.spec.ts +4 -4
- package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +2 -2
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +2 -2
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +6 -6
- package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +8 -0
- package/app/components/05.forms/form-errors/InputError.vue +43 -37
- package/app/components/05.forms/form-errors/stories/InputError.stories.ts +79 -1
- package/app/components/05.forms/form-errors/tests/InputError.spec.ts +29 -1
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +80 -47
- package/app/components/05.forms/input-button/InputButton.vue +52 -100
- package/app/components/05.forms/input-button/playwright/input-button.playwright.ts +1 -1
- package/app/components/05.forms/input-button/stories/InputButton.stories.ts +145 -8
- package/app/components/05.forms/input-button/tests/InputButton.spec.ts +76 -46
- package/app/components/05.forms/input-button/tests/__snapshots__/InputButton.spec.ts.snap +17 -17
- package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +9 -1
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +8 -0
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +2 -2
- package/app/components/05.forms/input-description/CONSUMER-STYLING.md +14 -2
- package/app/components/05.forms/input-description/InputDescription.vue +2 -1
- package/app/components/05.forms/input-description/stories/InputDescription.stories.ts +70 -4
- package/app/components/05.forms/input-description/tests/InputDescription.spec.ts +2 -1
- package/app/components/05.forms/input-label/CONSUMER-STYLING.md +10 -2
- package/app/components/05.forms/input-label/InputLabel.vue +1 -0
- package/app/components/05.forms/input-label/stories/InputLabel.stories.ts +68 -2
- package/app/components/05.forms/input-label/tests/InputLabel.spec.ts +13 -3
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-number/InputNumber.vue +4 -5
- package/app/components/05.forms/input-otp/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-otp/InputOtp.vue +2 -2
- package/app/components/05.forms/input-range/InputRange.vue +1 -1
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-select/InputSelect.vue +14 -5
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-text/InputText.vue +4 -4
- package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-textarea/InputTextarea.vue +2 -3
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +4 -4
- package/app/components/05.forms/pending-effect/CONSUMER-STYLING.md +7 -3
- package/app/components/05.forms/pending-effect/PendingEffect.vue +3 -3
- package/app/components/05.forms/pending-effect/tests/PendingEffect.spec.ts +1 -1
- package/app/components/05.forms/segmented-control/CONSUMER-STYLING.md +125 -0
- package/app/components/05.forms/segmented-control/SegmentedControl.vue +231 -0
- package/app/components/05.forms/segmented-control/stories/SegmentedControl.stories.ts +215 -0
- package/app/components/05.forms/segmented-control/tests/SegmentedControl.spec.ts +202 -0
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +7 -0
- package/app/components/05.forms/toggle-switch/ToggleSwitch.vue +2 -0
- package/app/types/components/index.ts +1 -1
- package/package.json +1 -1
- package/.vscode/srcdev-component-alert-mask-core.code-snippets +0 -31
- package/app/components/01.atoms/alert-mask/AlertMaskCore.vue +0 -166
- package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +0 -105
- package/app/components/01.atoms/alert-mask/tests/AlertMaskCore.spec.ts +0 -342
- package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +0 -99
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitch.vue +0 -251
- package/app/components/05.forms/triple-toggle-switch/stories/TripleToggleSwitch.stories.ts +0 -104
- package/app/components/05.forms/triple-toggle-switch/tests/TripleToggleSwitch.spec.ts +0 -138
- /package/app/types/components/{alert-mask-core.d.ts → alert-mask.d.ts} +0 -0
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
<Icon :name="icon" class="input-error-message-icon" aria-hidden="true" />
|
|
17
17
|
</div>
|
|
18
18
|
<div class="input-error-message-text">
|
|
19
|
-
<ul v-if="isArray" class="input-error-message-list">
|
|
20
|
-
<li v-for="(message, index) in
|
|
19
|
+
<ul v-if="isArray" class="input-error-message-list" role="list">
|
|
20
|
+
<li v-for="(message, index) in messages" :key="index" class="input-error-message-list-item">
|
|
21
21
|
{{ message }}
|
|
22
22
|
</li>
|
|
23
23
|
</ul>
|
|
@@ -52,6 +52,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
const isArray = computed(() => Array.isArray(props.errorMessage));
|
|
55
|
+
const messages = computed(() =>
|
|
56
|
+
Array.isArray(props.errorMessage) ? props.errorMessage.filter((message) => String(message).trim() !== "") : []
|
|
57
|
+
);
|
|
55
58
|
|
|
56
59
|
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
57
60
|
|
|
@@ -141,48 +144,51 @@ watch(
|
|
|
141
144
|
outline-color: transparent;
|
|
142
145
|
}
|
|
143
146
|
}
|
|
144
|
-
}
|
|
145
147
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
& .input-error-message-inner {
|
|
149
|
+
align-items: center;
|
|
150
|
+
overflow: hidden;
|
|
151
|
+
}
|
|
150
152
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
& .input-error-message-content {
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
}
|
|
155
157
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
& .input-error-message-icon-wrapper {
|
|
159
|
+
display: inline-block;
|
|
160
|
+
flex-shrink: 0;
|
|
161
|
+
padding-inline-start: var(--_padding-inline);
|
|
162
|
+
}
|
|
160
163
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
& .input-error-message-icon {
|
|
165
|
+
color: var(--input-error-icon-color, var(--_text-color));
|
|
166
|
+
font-size: var(--input-error-icon-size, 1em);
|
|
167
|
+
transform: translateY(3px);
|
|
168
|
+
}
|
|
166
169
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
var(--input-error-
|
|
175
|
-
|
|
176
|
-
|
|
170
|
+
& .input-error-message-text {
|
|
171
|
+
display: inline-block;
|
|
172
|
+
flex-grow: 1;
|
|
173
|
+
min-inline-size: 0;
|
|
174
|
+
overflow-wrap: anywhere;
|
|
175
|
+
font-family: var(--input-error-font-family, var(--font-family));
|
|
176
|
+
font-size: var(--input-error-font-size, 1.6rem);
|
|
177
|
+
font-weight: var(--input-error-font-weight, 500);
|
|
178
|
+
padding-block: var(--input-error-padding-block-start, var(--_padding-block-start))
|
|
179
|
+
var(--input-error-padding-block-end, 1rem);
|
|
180
|
+
padding-inline: var(--_padding-inline);
|
|
181
|
+
}
|
|
177
182
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
+
& .input-error-message-list {
|
|
184
|
+
list-style-type: none;
|
|
185
|
+
padding-inline-start: 0;
|
|
186
|
+
margin-block: 0;
|
|
187
|
+
}
|
|
183
188
|
|
|
184
|
-
|
|
185
|
-
|
|
189
|
+
& .input-error-message-list-item + .input-error-message-list-item {
|
|
190
|
+
margin-block-start: var(--input-error-list-gap, 0.6rem);
|
|
191
|
+
}
|
|
186
192
|
}
|
|
187
193
|
}
|
|
188
194
|
</style>
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
1
2
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
3
|
import StorybookComponent from "../InputError.vue";
|
|
4
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
5
|
+
import type { MediaCanvas } from "~/types/components";
|
|
3
6
|
|
|
4
7
|
interface InputErrorStoryArgs {
|
|
5
8
|
id: string;
|
|
@@ -44,15 +47,52 @@ export default {
|
|
|
44
47
|
icon: "radix-icons:circle-backslash",
|
|
45
48
|
styleClassPassthrough: "",
|
|
46
49
|
},
|
|
50
|
+
decorators: [
|
|
51
|
+
(story, context) => ({
|
|
52
|
+
components: { story, CanvasSwitcher },
|
|
53
|
+
setup() {
|
|
54
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
55
|
+
return { canvasName };
|
|
56
|
+
},
|
|
57
|
+
template: `
|
|
58
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
59
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
60
|
+
</div>
|
|
61
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
62
|
+
<story />
|
|
63
|
+
</div>
|
|
64
|
+
`,
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
parameters: {
|
|
68
|
+
layout: "fullscreen",
|
|
69
|
+
docs: {
|
|
70
|
+
description: {
|
|
71
|
+
component:
|
|
72
|
+
"The error strip every `*Field` wrapper renders under its control, in grid row 2. `error-message` takes a string or an array (rendered as a list), `show-error` opens it, `is-detached` chooses between tucked under the control and sitting below it with a gap, and `input-variant` matches the control's variant. The control in these stories is a stand-in; see [InputTextField](?path=/story/components-forms-input-text-inputtextfield--with-error) (attached) and [MultipleCheckboxes](?path=/story/components-forms-input-checkbox-multiplecheckboxes--error-state) (detached) for it in real use.",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
47
76
|
} as Meta<InputErrorStoryArgs>;
|
|
48
77
|
|
|
78
|
+
// Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
|
|
79
|
+
const storyNoteStyle = "margin: 0 0 1.6rem; font-size: 1.4rem;";
|
|
80
|
+
const fieldNote = `
|
|
81
|
+
<p style="${storyNoteStyle}">
|
|
82
|
+
Every field wrapper renders this strip for you. See it attached under a real control in the
|
|
83
|
+
<a href="/?path=/story/components-forms-input-text-inputtextfield--with-error" target="_top">InputTextField</a>
|
|
84
|
+
story, and detached under a group in the
|
|
85
|
+
<a href="/?path=/story/components-forms-input-checkbox-multiplecheckboxes--error-state" target="_top">MultipleCheckboxes</a>
|
|
86
|
+
story.
|
|
87
|
+
</p>
|
|
88
|
+
`;
|
|
89
|
+
|
|
49
90
|
const demoStyles = `
|
|
50
91
|
<component is="style">
|
|
51
92
|
.input-error-story-field {
|
|
52
93
|
display: grid;
|
|
53
94
|
grid-template-rows: auto auto;
|
|
54
95
|
max-width: 48rem;
|
|
55
|
-
margin: 3.6rem;
|
|
56
96
|
}
|
|
57
97
|
.input-error-story-control {
|
|
58
98
|
grid-row: 1;
|
|
@@ -84,6 +124,7 @@ const Template: StoryFn<InputErrorStoryArgs> = (args) => ({
|
|
|
84
124
|
template: `
|
|
85
125
|
<div>
|
|
86
126
|
${demoStyles}
|
|
127
|
+
${fieldNote}
|
|
87
128
|
<div
|
|
88
129
|
class="input-error-story-field"
|
|
89
130
|
:data-invalid="args.showError ? '' : undefined"
|
|
@@ -105,21 +146,58 @@ const Template: StoryFn<InputErrorStoryArgs> = (args) => ({
|
|
|
105
146
|
});
|
|
106
147
|
|
|
107
148
|
export const Attached = Template.bind({});
|
|
149
|
+
Attached.parameters = {
|
|
150
|
+
docs: { description: { story: "The default: tucked under the control's bottom edge, sharing its border." } },
|
|
151
|
+
};
|
|
108
152
|
|
|
109
153
|
export const Detached = Template.bind({});
|
|
110
154
|
Detached.args = { isDetached: true };
|
|
155
|
+
Detached.parameters = {
|
|
156
|
+
docs: { description: { story: "Sits below the control with a gap (`--input-error-detached-offset`) and fully rounded corners, as used under checkbox and radio groups." } },
|
|
157
|
+
};
|
|
111
158
|
|
|
112
159
|
export const MultipleMessages = Template.bind({});
|
|
113
160
|
MultipleMessages.args = {
|
|
114
161
|
isDetached: true,
|
|
115
162
|
errorMessage: ["Password must be at least 12 characters", "Password must contain a number"],
|
|
116
163
|
};
|
|
164
|
+
MultipleMessages.parameters = {
|
|
165
|
+
docs: { description: { story: "An array `error-message` renders as a list, spaced by `--input-error-list-gap`." } },
|
|
166
|
+
};
|
|
117
167
|
|
|
118
168
|
export const Underlined = Template.bind({});
|
|
119
169
|
Underlined.args = { inputVariant: "underlined" };
|
|
170
|
+
Underlined.parameters = {
|
|
171
|
+
docs: { description: { story: "Square corners, and no outline while attached, to match an underlined control." } },
|
|
172
|
+
};
|
|
120
173
|
|
|
121
174
|
export const Hidden = Template.bind({});
|
|
122
175
|
Hidden.args = { showError: false };
|
|
123
176
|
Hidden.parameters = {
|
|
124
177
|
docs: { description: { story: "Toggle `showError` to see the open/close animation." } },
|
|
125
178
|
};
|
|
179
|
+
|
|
180
|
+
export const StressTest = Template.bind({});
|
|
181
|
+
StressTest.storyName = "Stress Test (Worst-Case Data)";
|
|
182
|
+
StressTest.args = {
|
|
183
|
+
isDetached: true,
|
|
184
|
+
errorMessage: [
|
|
185
|
+
"DieseEingabeistleiderungültigbitteüberprüfenSiedieSchreibweiseundversuchenSieeserneut",
|
|
186
|
+
"https://example.com/a/very/long/unbroken/url/that/should/wrap/inside/the/strip?with=query&and=more",
|
|
187
|
+
"<b>not bold</b> <script>alert(1)</script>",
|
|
188
|
+
"⚠️ Bitte geben Sie eine gültige E-Mail-Adresse ein. Wir verwenden Ihre Adresse ausschließlich für Rückfragen zu Ihrer Bestellung bei Alex Morgan in Northtown.",
|
|
189
|
+
"يرجى إدخال عنوان بريد إلكتروني صالح",
|
|
190
|
+
"x",
|
|
191
|
+
"",
|
|
192
|
+
],
|
|
193
|
+
};
|
|
194
|
+
StressTest.parameters = {
|
|
195
|
+
initialCanvas: "mobileCanvas",
|
|
196
|
+
docs: {
|
|
197
|
+
story: { inline: false, height: "720px" },
|
|
198
|
+
description: {
|
|
199
|
+
story:
|
|
200
|
+
"Long unbroken German, a long URL, HTML-like text, emoji, right-to-left text, a single character and an empty string in one array. Check at every canvas width: every message wraps inside the strip (no horizontal overflow past the dashed outline), the icon keeps its size, markup renders as text, and the empty item takes no visible space. Switch `errorMessage` to a single long string to check the non-list path too. Error text is never line-clamped: a truncated validation message can't be acted on.",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
2
|
import type { VueWrapper } from "@vue/test-utils";
|
|
3
3
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
4
4
|
import { nextTick } from "vue";
|
|
@@ -22,6 +22,11 @@ const wrapperFactory = (propsData = {}) => {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
describe("InputError", () => {
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
wrapper?.unmount();
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
25
30
|
it("renders the root with id, testid, error theme and passthrough classes", async () => {
|
|
26
31
|
wrapper = await wrapperFactory();
|
|
27
32
|
const root = wrapper.find(".input-error-message");
|
|
@@ -92,6 +97,29 @@ describe("InputError", () => {
|
|
|
92
97
|
expect(items[1]!.text()).toBe("Hello World 2");
|
|
93
98
|
});
|
|
94
99
|
|
|
100
|
+
it("keeps list semantics on the unstyled list", async () => {
|
|
101
|
+
wrapper = await wrapperFactory({ errorMessage: ["Hello World", "Hello World 2"] });
|
|
102
|
+
|
|
103
|
+
expect(wrapper.find(".input-error-message-list").attributes("role")).toBe("list");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("drops blank entries from an array of messages", async () => {
|
|
107
|
+
wrapper = await wrapperFactory({ errorMessage: ["First", "", " ", "Second"] });
|
|
108
|
+
const items = wrapper.findAll(".input-error-message-list-item");
|
|
109
|
+
|
|
110
|
+
expect(items).toHaveLength(2);
|
|
111
|
+
expect(items[0]!.text()).toBe("First");
|
|
112
|
+
expect(items[1]!.text()).toBe("Second");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("renders HTML-like message text as text", async () => {
|
|
116
|
+
wrapper = await wrapperFactory({ errorMessage: "<b>not bold</b>" });
|
|
117
|
+
const message = wrapper.find(".input-error-message-single");
|
|
118
|
+
|
|
119
|
+
expect(message.find("b").exists()).toBe(false);
|
|
120
|
+
expect(message.text()).toBe("<b>not bold</b>");
|
|
121
|
+
});
|
|
122
|
+
|
|
95
123
|
it("renders a decorative icon that can be overridden", async () => {
|
|
96
124
|
wrapper = await wrapperFactory();
|
|
97
125
|
const icon = wrapper.find(".input-error-message-icon");
|
|
@@ -7,14 +7,21 @@ Every colour token below is `var(--input-button-{variant}-{property}, {default})
|
|
|
7
7
|
the shared `--theme-*` slot every other themed component (inputs, alerts, ...) also falls back to.
|
|
8
8
|
See `theming-component-token-pattern.md` for why this two-tier shape exists.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
`--button-focus-ring-width`/`-offset`,
|
|
10
|
+
The variants follow the usual hierarchy: **primary** is filled, **secondary** is outlined
|
|
11
|
+
(transparent background, border and text in the theme colour), **tertiary** is text-only
|
|
12
|
+
(no background or border until hover), and **inline** has no colours at all, for wrapper
|
|
13
|
+
components that style the button themselves. Primary and secondary also carry a light **shadow ring** (`box-shadow`, 2px of theme step 2)
|
|
14
|
+
outside the border, and on **hover** their 1px outline (no offset) takes the hover border colour,
|
|
15
|
+
so it draws over the inner edge of that ring. Tertiary has neither. The `outline` is transparent
|
|
16
|
+
at rest. **`:focus-visible` is a separate token set**
|
|
17
|
+
(its `outline-width`/`-offset` come from `--button-focus-ring-width`/`-offset`, see
|
|
18
|
+
`theming-form-geometry-tokens.md`).
|
|
19
|
+
|
|
20
|
+
> **Changed 2026-10-17**: secondary was filled with `--theme-surface-inverted`, which read as a
|
|
21
|
+
> darker primary; tertiary was a grey `--slate-01` fill with an underlined label. To bring back
|
|
22
|
+
> the old look for one site, set the secondary or tertiary tokens below (e.g.
|
|
23
|
+
> `--input-button-secondary-surface: var(--theme-surface-inverted)` and
|
|
24
|
+
> `--input-button-secondary-text: var(--theme-text-inverted)`).
|
|
18
25
|
|
|
19
26
|
Geometry and typography (`--button-*`) are separate, already-global tokens with real defaults —
|
|
20
27
|
see `theming-form-geometry-tokens.md`. Not duplicated here. One geometry token worth calling out:
|
|
@@ -32,60 +39,78 @@ the `InlineWithTextInput` story). `box-sizing: content-box` means it's a floor,
|
|
|
32
39
|
| `--input-button-primary-ring` | `transparent` | Outline colour (resting) |
|
|
33
40
|
| `--input-button-primary-surface-hover` | `color-mix(in oklab, surface 85%, black)` | Background on hover |
|
|
34
41
|
| `--input-button-primary-border-hover` | the resolved hover surface above | Border colour on hover |
|
|
35
|
-
| `--input-button-primary-ring-hover` |
|
|
42
|
+
| `--input-button-primary-ring-hover` | the resolved hover border above | Outline colour on hover (1px, no offset) |
|
|
43
|
+
| `--input-button-primary-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Shadow ring colour (all states) |
|
|
36
44
|
| `--input-button-primary-border-focus` | `var(--theme-border-focus)` | Border colour on `:focus-visible` |
|
|
37
45
|
| `--input-button-primary-ring-focus` | `var(--theme-ring)` | Outline colour on `:focus-visible` |
|
|
38
46
|
|
|
39
|
-
### Secondary
|
|
47
|
+
### Secondary (outlined)
|
|
40
48
|
|
|
41
|
-
|
|
49
|
+
| Token | Falls back to | Controls |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `--input-button-secondary-surface` | `transparent` | Background (resting) |
|
|
52
|
+
| `--input-button-secondary-text` | `var(--theme-surface)` | Text colour (all states) |
|
|
53
|
+
| `--input-button-secondary-border` | `var(--theme-surface)` | Border colour (resting) |
|
|
54
|
+
| `--input-button-secondary-ring` | `transparent` | Outline colour (resting) |
|
|
55
|
+
| `--input-button-secondary-ring-hover` | the resolved hover border | Outline colour on hover (1px, no offset) |
|
|
56
|
+
| `--input-button-secondary-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Shadow ring colour (all states) |
|
|
57
|
+
| `--input-button-secondary-surface-hover` | `var(--theme-surface-subtle)` | Background on hover |
|
|
58
|
+
| `--input-button-secondary-border-hover` | the resolved border above | Border colour on hover |
|
|
59
|
+
| `--input-button-secondary-border-focus` | `var(--theme-border-focus)` | Border colour on `:focus-visible` |
|
|
60
|
+
| `--input-button-secondary-ring-focus` | `var(--theme-ring)` | Outline colour on `:focus-visible` |
|
|
61
|
+
|
|
62
|
+
### Tertiary (text-only)
|
|
42
63
|
|
|
43
|
-
| Token | Falls back to |
|
|
44
|
-
|
|
45
|
-
| `--input-button-
|
|
46
|
-
| `--input-button-
|
|
47
|
-
| `--input-button-
|
|
48
|
-
| `--input-button-
|
|
49
|
-
| `--input-button-
|
|
50
|
-
| `--input-button-
|
|
51
|
-
| `--input-button-
|
|
52
|
-
| `--input-button-
|
|
64
|
+
| Token | Falls back to | Controls |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `--input-button-tertiary-surface` | `transparent` | Background (resting) |
|
|
67
|
+
| `--input-button-tertiary-text` | `var(--theme-surface)` | Text colour (all states) |
|
|
68
|
+
| `--input-button-tertiary-border` | `transparent` | Border colour (resting) |
|
|
69
|
+
| `--input-button-tertiary-ring` / `-ring-hover` | `transparent` | Outline colour |
|
|
70
|
+
| `--input-button-tertiary-surface-hover` | `var(--theme-surface-subtle)` | Background on hover |
|
|
71
|
+
| `--input-button-tertiary-border-hover` | `transparent` | Border colour on hover |
|
|
72
|
+
| `--input-button-tertiary-border-focus` | `var(--theme-border-focus)` | Border colour on `:focus-visible` |
|
|
73
|
+
| `--input-button-tertiary-ring-focus` | `var(--theme-ring)` | Outline colour on `:focus-visible` |
|
|
74
|
+
|
|
75
|
+
| Token | Default | Controls |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `--input-button-shadow-width` | `var(--form-element-shadow-width)` (`0.2rem`) | Width of the primary and secondary shadow ring. Set `0` to remove it from buttons only; set the global token to `0` to remove it from every form control |
|
|
53
78
|
|
|
54
|
-
|
|
79
|
+
The focus ring's offset (`--button-focus-ring-offset`, `0.2rem`) puts it just outside the shadow
|
|
80
|
+
ring; if you widen the ring, raise the offset to match or the focus ring will draw over it. An
|
|
81
|
+
ancestor with `overflow: hidden` and no spare room clips the ring.
|
|
55
82
|
|
|
56
|
-
|
|
83
|
+
Secondary and tertiary text sits on the page background, so on a dark section set their `-text`
|
|
84
|
+
and `-border` tokens to a light colour.
|
|
57
85
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
| `--input-button-tertiary-text` | `var(--theme-text)` |
|
|
62
|
-
| `--input-button-tertiary-border` | the resolved surface above (flat) |
|
|
63
|
-
| `--input-button-tertiary-ring` / `-ring-hover` | `transparent` |
|
|
64
|
-
| `--input-button-tertiary-surface-hover` | `color-mix(in oklab, surface 85%, black)` |
|
|
65
|
-
| `--input-button-tertiary-border-hover` | the resolved hover surface above |
|
|
66
|
-
| `--input-button-tertiary-border-focus` | `var(--theme-border-focus)` |
|
|
67
|
-
| `--input-button-tertiary-ring-focus` | `var(--theme-ring)` |
|
|
68
|
-
|
|
69
|
-
`.is-pending` always dims the *primary* surface regardless of variant — a pre-existing quirk, not
|
|
70
|
-
something this token set changes. The animated border added by `has-pending-effect` has its own
|
|
71
|
-
`--pending-effect-*` tokens, see `../pending-effect/CONSUMER-STYLING.md`.
|
|
86
|
+
`[data-pending]` dims a primary button's surface to 50%; secondary and tertiary keep their own
|
|
87
|
+
background. The animated border added by `has-pending-effect` has its own `--pending-effect-*`
|
|
88
|
+
tokens, see `../pending-effect/CONSUMER-STYLING.md`.
|
|
72
89
|
|
|
73
90
|
### Text wrapping
|
|
74
91
|
|
|
75
92
|
| Token | Default | Controls |
|
|
76
93
|
|---|---|---|
|
|
77
|
-
| `--input-button-text-white-space` | `
|
|
94
|
+
| `--input-button-text-white-space` | `normal` | Long labels wrap onto several lines inside the button, and unbroken strings break anywhere. Set `nowrap` to keep a label on one line; the button then grows to fit it and can overflow a narrow container |
|
|
78
95
|
|
|
79
|
-
> Added 2026-10-06.
|
|
96
|
+
> Added 2026-10-06. **Changed 2026-10-17**: the default was `nowrap`, which pushed a long label
|
|
97
|
+
> past a narrow column. If you relied on one-line labels in a tight row, set
|
|
98
|
+
> `--input-button-text-white-space: nowrap` on that row.
|
|
99
|
+
|
|
100
|
+
Button text has no line-clamp token on purpose: a cut-off action label can't be trusted.
|
|
101
|
+
|
|
102
|
+
Private tokens (not public API): `--_surface` and `--_surface-hover` (primary's resolved
|
|
103
|
+
surfaces, which its border, hover and contrast colours derive from) and `--_primary-border-hover`, `--_secondary-border` and `--_secondary-border-hover`.
|
|
80
104
|
|
|
81
105
|
`InputCopy` and `PricingCard` fall back through `--input-button-primary-*` for their own copy/CTA
|
|
82
106
|
buttons (see their own `CONSUMER-STYLING.md`) rather than duplicating this chain.
|
|
83
107
|
|
|
84
108
|
> **Trial, 2026-09-27: automatic text contrast.** In browsers that support CSS `contrast-color()`
|
|
85
|
-
> (Safari/iOS 26, Firefox 146, Chrome/Edge 147), an unset `--input-button-
|
|
109
|
+
> (Safari/iOS 26, Firefox 146, Chrome/Edge 147), an unset `--input-button-primary-text` resolves
|
|
86
110
|
> to `contrast-color()` of that state's own surface: pure black or pure white, whichever has more
|
|
87
111
|
> contrast, re-evaluated on hover. Older browsers keep the defaults in the tables above. Setting
|
|
88
|
-
> the `-text` token explicitly always wins, so you can opt
|
|
112
|
+
> the `-text` token explicitly always wins, so you can opt out. Secondary and tertiary aren't part of the trial since
|
|
113
|
+
> 2026-10-17: their surface is transparent. The whole behaviour is
|
|
89
114
|
> one `@supports` block in `InputButton.vue` and may be removed if the trial doesn't hold up.
|
|
90
115
|
|
|
91
116
|
---
|
|
@@ -101,15 +126,23 @@ buttons (see their own `CONSUMER-STYLING.md`) rather than duplicating this chain
|
|
|
101
126
|
|
|
102
127
|
| Hook | When |
|
|
103
128
|
|---|---|
|
|
104
|
-
|
|
|
129
|
+
| `[data-variant="primary" \| "secondary" \| "tertiary" \| "inline"]` | The `variant` prop |
|
|
105
130
|
| `[data-theme]` | The `theme` prop (re-declares the `--theme-*` palette on the button) |
|
|
106
|
-
|
|
|
107
|
-
|
|
|
108
|
-
|
|
|
109
|
-
|
|
|
131
|
+
| `[data-icon-only]` | The `#iconOnly` slot is used (square, text becomes `sr-only`) |
|
|
132
|
+
| `[data-pill]` | `is-pill` |
|
|
133
|
+
| `[data-pending]`, `[data-pending-effect]` | `is-pending` / `has-pending-effect` |
|
|
134
|
+
| `[data-link]` | `href` is set (renders `NuxtLink` or `<a>`) |
|
|
110
135
|
| `[readonly]` + `aria-disabled="true"` | `readonly` (dimmed, not operable by mouse or keyboard) |
|
|
111
136
|
|
|
112
|
-
Inner
|
|
137
|
+
Inner elements: `.input-button-text`, and `.input-button-icon` with `data-slot="left"`,
|
|
138
|
+
`"right"` or `"icon-only"`.
|
|
139
|
+
|
|
140
|
+
> **Changed 2026-10-17**: these were bare classes, which collided with consumer CSS. Update
|
|
141
|
+
> selectors: `.input-button.primary` → `.input-button[data-variant="primary"]`, `.pill` →
|
|
142
|
+
> `[data-pill]`, `.is-pending` → `[data-pending]`, `.pending-effect` → `[data-pending-effect]`,
|
|
143
|
+
> `.is-link` → `[data-link]`, `.icon-only` → `[data-icon-only]`, `.button-text` →
|
|
144
|
+
> `.input-button-text`, `.btn-icon.left` → `.input-button-icon[data-slot="left"]`. Same
|
|
145
|
+
> specificity, so overrides keep winning where they did.
|
|
113
146
|
|
|
114
147
|
|
|
115
148
|
## Global theming
|