srcdev-nuxt-components 9.6.10 → 9.6.12
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 +14 -3
- package/.claude/skills/component-inline-action-button.md +5 -4
- package/.claude/skills/component-naming.md +2 -1
- 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/form-field.md +15 -0
- package/.claude/skills/components/form-fieldset.md +18 -2
- 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/marquee-scroller.md +51 -14
- 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/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-form-field.code-snippets +7 -6
- package/.vscode/srcdev-component-form-fieldset.code-snippets +12 -12
- package/.vscode/srcdev-component-marquee-scroller.code-snippets +31 -28
- 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/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +77 -54
- package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +103 -91
- package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +132 -3
- package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +190 -102
- package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +17 -13
- 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/form-field/CONSUMER-STYLING.md +7 -0
- package/app/components/05.forms/form-field/stories/FormField.stories.ts +116 -2
- package/app/components/05.forms/form-fieldset/CONSUMER-STYLING.md +11 -1
- package/app/components/05.forms/form-fieldset/FormFieldset.vue +4 -2
- package/app/components/05.forms/form-fieldset/stories/FormFieldset.stories.ts +68 -4
- package/app/components/05.forms/form-fieldset/tests/FormFieldset.spec.ts +10 -0
- 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 +8 -0
- 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
|
@@ -114,6 +114,7 @@ watch(
|
|
|
114
114
|
border: var(--form-element-border-width) solid var(--input-checkbox-button-border, var(--theme-border));
|
|
115
115
|
border-radius: var(--input-checkbox-button-border-radius, 0.4rem);
|
|
116
116
|
outline: var(--form-element-outline-width) solid transparent;
|
|
117
|
+
box-shadow: 0 0 0 var(--form-element-shadow-width) var(--input-checkbox-button-shadow, var(--form-element-shadow-colour, var(--colour-theme-2)));
|
|
117
118
|
|
|
118
119
|
transition: all var(--input-checkbox-transition-duration, var(--theme-form-transition-duration)) ease-in-out;
|
|
119
120
|
|
|
@@ -127,8 +128,7 @@ watch(
|
|
|
127
128
|
|
|
128
129
|
&:hover {
|
|
129
130
|
background-color: var(--input-checkbox-button-surface-hover, var(--theme-input-surface-hover));
|
|
130
|
-
outline-color: var(--input-checkbox-button-ring-hover, var(--theme-
|
|
131
|
-
outline-offset: var(--form-element-outline-offset-focus);
|
|
131
|
+
outline-color: var(--input-checkbox-button-ring-hover, var(--input-checkbox-button-border, var(--theme-border)));
|
|
132
132
|
cursor: pointer;
|
|
133
133
|
}
|
|
134
134
|
|
|
@@ -36,7 +36,7 @@ These match `FormFieldset`'s `--form-fieldset-*` panel tokens.
|
|
|
36
36
|
| `--input-description-slot-margin-block-end` | `0.8rem` | Space below each slot's wrapper |
|
|
37
37
|
| `--input-description-slot-margin-inline` | `0` | Inline margin of each slot's wrapper |
|
|
38
38
|
| `--input-description-font-size` | `var(--step-4)` | Font size of the `descriptionText` paragraph |
|
|
39
|
-
| `--input-description-line-height` | `
|
|
39
|
+
| `--input-description-line-height` | `1.5` | Line height of the `descriptionText` paragraph |
|
|
40
40
|
|
|
41
41
|
With default (unpadded) panel tokens, the slot margins collapse through the root, so they are what
|
|
42
42
|
spaces the description from the label and control. Beyond these the `descriptionHtml` wrapper
|
|
@@ -67,7 +67,7 @@ Every field's description at once, from an app-level stylesheet (e.g. `app/asset
|
|
|
67
67
|
```css
|
|
68
68
|
:where(html) {
|
|
69
69
|
--input-description-color: var(--theme-input-placeholder);
|
|
70
|
-
--input-description-
|
|
70
|
+
--input-description-font-size: 1.4rem;
|
|
71
71
|
}
|
|
72
72
|
```
|
|
73
73
|
|
|
@@ -175,3 +175,15 @@ show as uneven space inside the panel. Zero them and space the panel with the ro
|
|
|
175
175
|
`.input-description` element. It's only reachable when you render `InputDescription` directly in
|
|
176
176
|
a custom field wrapper: the library's own wrappers pass `input-text-description` (no styles
|
|
177
177
|
attached) and don't forward yours. Use a `class` on the wrapper instead (see **One field** above).
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Notes
|
|
182
|
+
|
|
183
|
+
- The root sets `overflow-wrap: anywhere`, so a long URL or unbroken word wraps inside a narrow
|
|
184
|
+
column. Copy arrives through slots, so there is no line-clamp token: clamp your own slot markup
|
|
185
|
+
if you need to.
|
|
186
|
+
|
|
187
|
+
> **Changed 2026-10-17**: `--input-description-line-height` defaults to `1.5` (was `var(--step-4)`,
|
|
188
|
+
> the same as the font size, so wrapped lines touched). If you relied on the tighter spacing, set
|
|
189
|
+
> it back with `--input-description-line-height: var(--step-4)`.
|
|
@@ -59,6 +59,7 @@ watch(
|
|
|
59
59
|
border-radius: var(--input-description-border-radius, 0);
|
|
60
60
|
outline: var(--input-description-outline, 0);
|
|
61
61
|
outline-offset: var(--input-description-outline-offset, 0);
|
|
62
|
+
overflow-wrap: anywhere;
|
|
62
63
|
|
|
63
64
|
.input-description-html,
|
|
64
65
|
.input-description-text {
|
|
@@ -69,7 +70,7 @@ watch(
|
|
|
69
70
|
|
|
70
71
|
.input-description-text {
|
|
71
72
|
font-size: var(--input-description-font-size, var(--step-4));
|
|
72
|
-
line-height: var(--input-description-line-height,
|
|
73
|
+
line-height: var(--input-description-line-height, 1.5);
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
-
import { computed } from "vue";
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
3
|
import StorybookComponent from "../InputDescription.vue";
|
|
4
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
4
5
|
import type { InputUiVariant } from "~/types/forms/types.forms";
|
|
6
|
+
import type { MediaCanvas } from "~/types/components";
|
|
5
7
|
|
|
6
8
|
interface InputDescriptionStoryArgs {
|
|
7
9
|
descriptionId: string;
|
|
@@ -52,8 +54,43 @@ export default {
|
|
|
52
54
|
descriptionText: "We'll only use this to send your booking confirmation.",
|
|
53
55
|
descriptionHtml: "",
|
|
54
56
|
},
|
|
57
|
+
decorators: [
|
|
58
|
+
(story, context) => ({
|
|
59
|
+
components: { story, CanvasSwitcher },
|
|
60
|
+
setup() {
|
|
61
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
62
|
+
return { canvasName };
|
|
63
|
+
},
|
|
64
|
+
template: `
|
|
65
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
66
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
67
|
+
</div>
|
|
68
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
69
|
+
<story />
|
|
70
|
+
</div>
|
|
71
|
+
`,
|
|
72
|
+
}),
|
|
73
|
+
],
|
|
74
|
+
parameters: {
|
|
75
|
+
layout: "fullscreen",
|
|
76
|
+
docs: {
|
|
77
|
+
description: {
|
|
78
|
+
component:
|
|
79
|
+
"Help text for a form field. Copy goes in the `descriptionText` slot (a paragraph) and the `descriptionHtml` slot (rich markup, rendered first); with neither filled nothing renders. `description-id` becomes the root id, for the control's `aria-describedby`. `field-has-error` and `input-variant` are styling hooks only. Every labelled field renders it from its own description slots: see [InputTextField](?path=/story/components-forms-input-text-inputtextfield--with-description).",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
55
83
|
} as Meta<InputDescriptionStoryArgs>;
|
|
56
84
|
|
|
85
|
+
// Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
|
|
86
|
+
const storyNoteStyle = "margin: 0 0 1.6rem; font-size: 1.4rem;";
|
|
87
|
+
const fieldNote = `
|
|
88
|
+
<p style="${storyNoteStyle}">
|
|
89
|
+
Every labelled field renders this from its own description slots. See it in a real field in the
|
|
90
|
+
<a href="/?path=/story/components-forms-input-text-inputtextfield--with-description" target="_top">InputTextField</a> story.
|
|
91
|
+
</p>
|
|
92
|
+
`;
|
|
93
|
+
|
|
57
94
|
const Template: StoryFn<InputDescriptionStoryArgs> = (args) => ({
|
|
58
95
|
components: { StorybookComponent },
|
|
59
96
|
setup() {
|
|
@@ -64,7 +101,8 @@ const Template: StoryFn<InputDescriptionStoryArgs> = (args) => ({
|
|
|
64
101
|
return { args, componentArgs };
|
|
65
102
|
},
|
|
66
103
|
template: `
|
|
67
|
-
<div style="max-width: 48rem;
|
|
104
|
+
<div style="max-width: 48rem; font-size: 1.6rem;">
|
|
105
|
+
${fieldNote}
|
|
68
106
|
<label for="story-field" style="display: block; font-weight: 700;">Email address</label>
|
|
69
107
|
<StorybookComponent v-bind="componentArgs">
|
|
70
108
|
<template v-if="args.descriptionHtml" #descriptionHtml><span v-html="args.descriptionHtml"></span></template>
|
|
@@ -81,17 +119,26 @@ const Template: StoryFn<InputDescriptionStoryArgs> = (args) => ({
|
|
|
81
119
|
});
|
|
82
120
|
|
|
83
121
|
export const TextOnly = Template.bind({});
|
|
122
|
+
TextOnly.parameters = {
|
|
123
|
+
docs: { description: { story: "Plain help text in the `descriptionText` slot." } },
|
|
124
|
+
};
|
|
84
125
|
|
|
85
126
|
export const HtmlOnly = Template.bind({});
|
|
86
127
|
HtmlOnly.args = {
|
|
87
128
|
descriptionText: "",
|
|
88
129
|
descriptionHtml: "Password must contain:<ul><li>At least 12 characters</li><li>One number</li></ul>",
|
|
89
130
|
};
|
|
131
|
+
HtmlOnly.parameters = {
|
|
132
|
+
docs: { description: { story: "Rich markup, here a list, in the `descriptionHtml` slot." } },
|
|
133
|
+
};
|
|
90
134
|
|
|
91
135
|
export const TextAndHtml = Template.bind({});
|
|
92
136
|
TextAndHtml.args = {
|
|
93
137
|
descriptionHtml: "<strong>Tip:</strong> use your work address if you have one.",
|
|
94
138
|
};
|
|
139
|
+
TextAndHtml.parameters = {
|
|
140
|
+
docs: { description: { story: "Both slots: `descriptionHtml` renders above `descriptionText`." } },
|
|
141
|
+
};
|
|
95
142
|
|
|
96
143
|
export const Empty = Template.bind({});
|
|
97
144
|
Empty.args = { descriptionText: "", descriptionHtml: "" };
|
|
@@ -101,6 +148,9 @@ Empty.parameters = {
|
|
|
101
148
|
|
|
102
149
|
export const CustomTokens = Template.bind({});
|
|
103
150
|
CustomTokens.args = { styleClassPassthrough: "input-description-story-tokens" };
|
|
151
|
+
CustomTokens.parameters = {
|
|
152
|
+
docs: { description: { story: "Colour, size and slot spacing set as tokens through a passthrough class." } },
|
|
153
|
+
};
|
|
104
154
|
CustomTokens.decorators = [
|
|
105
155
|
() => ({
|
|
106
156
|
template: `
|
|
@@ -109,7 +159,6 @@ CustomTokens.decorators = [
|
|
|
109
159
|
.input-description-story-tokens {
|
|
110
160
|
--input-description-color: var(--theme-input-placeholder);
|
|
111
161
|
--input-description-font-size: 1.4rem;
|
|
112
|
-
--input-description-line-height: 1.5;
|
|
113
162
|
--input-description-slot-margin-block-start: 0.2rem;
|
|
114
163
|
--input-description-slot-margin-block-end: 1.2rem;
|
|
115
164
|
}
|
|
@@ -147,7 +196,6 @@ Panel.decorators = [
|
|
|
147
196
|
--input-description-padding-inline: 1.2rem;
|
|
148
197
|
--input-description-slot-margin-block-start: 0;
|
|
149
198
|
--input-description-slot-margin-block-end: 0;
|
|
150
|
-
--input-description-line-height: 1.5;
|
|
151
199
|
}
|
|
152
200
|
.input-description-story-panel .input-description-html + .input-description-text {
|
|
153
201
|
margin-block-start: 0.4rem;
|
|
@@ -158,3 +206,21 @@ Panel.decorators = [
|
|
|
158
206
|
`,
|
|
159
207
|
}),
|
|
160
208
|
];
|
|
209
|
+
|
|
210
|
+
export const StressTest = Template.bind({});
|
|
211
|
+
StressTest.storyName = "Stress Test (Worst-Case Data)";
|
|
212
|
+
StressTest.args = {
|
|
213
|
+
descriptionText:
|
|
214
|
+
"📧 Wir verwenden Ihre Adresse ausschließlich für Rückfragen zu Ihrer Bestellung. Datenschutzgrundverordnungskonformitätsbestätigung https://example.com/a/very/long/unbroken/url/for/the/privacy/policy <b>not bold</b> يرجى إدخال بريدك الإلكتروني",
|
|
215
|
+
descriptionHtml:
|
|
216
|
+
"<strong>Hinweis:</strong> Elektronischepostadressefürdiebenachrichtigungüberbestellungen <a href=\"https://example.com/a/very/long/unbroken/url/for/the/terms\">https://example.com/a/very/long/unbroken/url/for/the/terms</a>",
|
|
217
|
+
};
|
|
218
|
+
StressTest.parameters = {
|
|
219
|
+
initialCanvas: "mobileCanvas",
|
|
220
|
+
docs: {
|
|
221
|
+
description: {
|
|
222
|
+
story:
|
|
223
|
+
"Long German copy, unbroken compound words, long URLs (one as a link), emoji, HTML-like and right-to-left text in both slots. Check at every canvas width: every line wraps inside the dashed outline and the wrapped lines don't touch. Copy arrives through slots, so there is no line-clamp token: clamp your own slot markup if you need to.",
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
2
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
3
|
import { nextTick } from "vue";
|
|
4
4
|
import InputDescription from "../InputDescription.vue";
|
|
@@ -19,6 +19,7 @@ describe("InputDescription", () => {
|
|
|
19
19
|
afterEach(() => {
|
|
20
20
|
wrapper?.unmount();
|
|
21
21
|
wrapper = undefined;
|
|
22
|
+
vi.restoreAllMocks();
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
describe("rendering", () => {
|
|
@@ -132,3 +132,11 @@ ancestor of the label).
|
|
|
132
132
|
reachable when you render `InputLabel` directly in a custom field wrapper: the library's wrappers
|
|
133
133
|
pass their own hook classes (`input-text-label`, `input-number-label`, `input-range-label`, `input-select-label`,
|
|
134
134
|
`input-textarea-label`, `toggle-switch-label`) and don't forward yours.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Notes
|
|
139
|
+
|
|
140
|
+
- The label sets `overflow-wrap: anywhere`, so a long URL or unbroken word (and an unbroken
|
|
141
|
+
marker text) wraps inside a narrow column. Label copy arrives through slots, so there is no
|
|
142
|
+
line-clamp token: clamp your own slot markup if you need to.
|
|
@@ -95,6 +95,7 @@ watch(
|
|
|
95
95
|
line-height: var(--input-label-line-height, 1.5);
|
|
96
96
|
margin-block: var(--input-label-margin-block, 0.8rem);
|
|
97
97
|
margin-inline: var(--input-label-margin-inline, 0);
|
|
98
|
+
overflow-wrap: anywhere;
|
|
98
99
|
|
|
99
100
|
.input-label-indicator {
|
|
100
101
|
--_indicator-color: var(--input-label-indicator-color, inherit);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
-
import { computed } from "vue";
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
3
|
import StorybookComponent from "../InputLabel.vue";
|
|
4
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
4
5
|
import type { InputUiVariant, InputLabelIndicator } from "~/types/forms/types.forms";
|
|
6
|
+
import type { MediaCanvas } from "~/types/components";
|
|
5
7
|
|
|
6
8
|
interface InputLabelStoryArgs {
|
|
7
9
|
id: string;
|
|
@@ -109,8 +111,43 @@ export default {
|
|
|
109
111
|
textLabel: "Email address",
|
|
110
112
|
htmlLabel: "",
|
|
111
113
|
},
|
|
114
|
+
decorators: [
|
|
115
|
+
(story, context) => ({
|
|
116
|
+
components: { story, CanvasSwitcher },
|
|
117
|
+
setup() {
|
|
118
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
119
|
+
return { canvasName };
|
|
120
|
+
},
|
|
121
|
+
template: `
|
|
122
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
123
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
124
|
+
</div>
|
|
125
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
126
|
+
<story />
|
|
127
|
+
</div>
|
|
128
|
+
`,
|
|
129
|
+
}),
|
|
130
|
+
],
|
|
131
|
+
parameters: {
|
|
132
|
+
layout: "fullscreen",
|
|
133
|
+
docs: {
|
|
134
|
+
description: {
|
|
135
|
+
component:
|
|
136
|
+
"The `<label>` every labelled field wrapper renders. Label copy goes in the `textLabel` and `htmlLabel` slots (html first), `id` becomes its `for`, and `indicator` with `required` adds a required or optional marker (falling back to `app.config` `srcdev.inputLabel`). `field-has-error` and `input-variant` are styling hooks only. The control in these stories is a plain input; see [InputTextField](?path=/story/components-forms-input-text-inputtextfield--required) for it inside a real field.",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
112
140
|
} as Meta<InputLabelStoryArgs>;
|
|
113
141
|
|
|
142
|
+
// Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
|
|
143
|
+
const storyNoteStyle = "margin: 0 0 1.6rem; font-size: 1.4rem;";
|
|
144
|
+
const fieldNote = `
|
|
145
|
+
<p style="${storyNoteStyle}">
|
|
146
|
+
Every labelled field wrapper renders this label and forwards its own <code>required</code> prop. See it in a real field in the
|
|
147
|
+
<a href="/?path=/story/components-forms-input-text-inputtextfield--required" target="_top">InputTextField</a> story.
|
|
148
|
+
</p>
|
|
149
|
+
`;
|
|
150
|
+
|
|
114
151
|
const Template: StoryFn<InputLabelStoryArgs> = (args) => ({
|
|
115
152
|
components: { StorybookComponent },
|
|
116
153
|
setup() {
|
|
@@ -133,7 +170,8 @@ const Template: StoryFn<InputLabelStoryArgs> = (args) => ({
|
|
|
133
170
|
return { args, componentArgs, tokenStyles };
|
|
134
171
|
},
|
|
135
172
|
template: `
|
|
136
|
-
<div style="max-width: 48rem;
|
|
173
|
+
<div style="max-width: 48rem; font-size: 1.6rem;" :style="tokenStyles">
|
|
174
|
+
${fieldNote}
|
|
137
175
|
<StorybookComponent v-bind="componentArgs">
|
|
138
176
|
<template v-if="args.htmlLabel" #htmlLabel><span v-html="args.htmlLabel"></span></template>
|
|
139
177
|
<template v-if="args.textLabel" #textLabel>{{ args.textLabel }}</template>
|
|
@@ -150,17 +188,26 @@ const Template: StoryFn<InputLabelStoryArgs> = (args) => ({
|
|
|
150
188
|
});
|
|
151
189
|
|
|
152
190
|
export const TextLabel = Template.bind({});
|
|
191
|
+
TextLabel.parameters = {
|
|
192
|
+
docs: { description: { story: "Plain label text through the `textLabel` slot." } },
|
|
193
|
+
};
|
|
153
194
|
|
|
154
195
|
export const HtmlLabel = Template.bind({});
|
|
155
196
|
HtmlLabel.args = {
|
|
156
197
|
textLabel: "",
|
|
157
198
|
htmlLabel: 'Email address <small style="font-weight: normal;">(work or personal)</small>',
|
|
158
199
|
};
|
|
200
|
+
HtmlLabel.parameters = {
|
|
201
|
+
docs: { description: { story: "Rich markup through the `htmlLabel` slot." } },
|
|
202
|
+
};
|
|
159
203
|
|
|
160
204
|
export const TextAndHtml = Template.bind({});
|
|
161
205
|
TextAndHtml.args = {
|
|
162
206
|
htmlLabel: "<strong>Work</strong> ",
|
|
163
207
|
};
|
|
208
|
+
TextAndHtml.parameters = {
|
|
209
|
+
docs: { description: { story: "Both slots filled: `htmlLabel` renders first, then `textLabel`." } },
|
|
210
|
+
};
|
|
164
211
|
|
|
165
212
|
export const MarkRequired = Template.bind({});
|
|
166
213
|
MarkRequired.args = { indicator: "required", required: true };
|
|
@@ -234,3 +281,22 @@ CustomTokens.decorators = [
|
|
|
234
281
|
`,
|
|
235
282
|
}),
|
|
236
283
|
];
|
|
284
|
+
|
|
285
|
+
export const StressTest = Template.bind({});
|
|
286
|
+
StressTest.storyName = "Stress Test (Worst-Case Data)";
|
|
287
|
+
StressTest.args = {
|
|
288
|
+
textLabel:
|
|
289
|
+
"📧 Elektronischepostadressefürdiebenachrichtigungüberbestellungen https://example.com/a/very/long/unbroken/url/for/the/label <b>not bold</b> البريد الإلكتروني",
|
|
290
|
+
indicator: "optional",
|
|
291
|
+
required: false,
|
|
292
|
+
optionalText: "(freiwilligeAngabekeinePflichtfeldbitteleerlassenwennnichtzutreffend)",
|
|
293
|
+
};
|
|
294
|
+
StressTest.parameters = {
|
|
295
|
+
initialCanvas: "mobileCanvas",
|
|
296
|
+
docs: {
|
|
297
|
+
description: {
|
|
298
|
+
story:
|
|
299
|
+
"Long unbroken German, a long URL, emoji, HTML-like and right-to-left text in the label, plus an unbroken optional marker. Check at every canvas width: the label and the marker wrap inside the dashed outline, markup renders as text, and the marker stays after the text in RTL. Switch `indicator` to required and turn `required` on to check the required marker. Label copy arrives through slots, so there is no line-clamp token: clamp your own slot markup if you need to.",
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
};
|
|
@@ -1,14 +1,24 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
2
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
3
|
import InputLabel from "../InputLabel.vue";
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
5
|
+
const mounted: { unmount: () => void }[] = [];
|
|
6
|
+
|
|
7
|
+
const createWrapper = async (props = {}, slots = {}) => {
|
|
8
|
+
const wrapper = await mountSuspended(InputLabel, {
|
|
7
9
|
props: { id: "test-input", ...props },
|
|
8
10
|
slots,
|
|
9
11
|
});
|
|
12
|
+
mounted.push(wrapper);
|
|
13
|
+
return wrapper;
|
|
14
|
+
};
|
|
10
15
|
|
|
11
16
|
describe("InputLabel", () => {
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
19
|
+
vi.restoreAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
12
22
|
describe("rendering", () => {
|
|
13
23
|
it("renders a label linked to the control id", async () => {
|
|
14
24
|
const wrapper = await createWrapper({ id: "email-input" });
|
|
@@ -18,7 +18,8 @@ token, same as `InputText`.
|
|
|
18
18
|
| `--input-number-surface` | `var(--theme-input-surface)` | Wrapper, input, and embedded-button background |
|
|
19
19
|
| `--input-number-surface-hover` | `var(--theme-input-surface-hover)` | Embedded left/right button background on hover |
|
|
20
20
|
| `--input-number-border` | `var(--theme-border)` | Border/divider colour (`normal`/`underlined` variant border, and the divider between the input and an embedded button) |
|
|
21
|
-
| `--input-number-border-hover` |
|
|
21
|
+
| `--input-number-border-hover` | the resolved border | 1px wrapper outline colour on mouse hover over the input or either step button (`normal` variant only) |
|
|
22
|
+
| `--input-number-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Resting shadow ring outside the border (`normal` only). Width is the global `--form-element-shadow-width` (`0.2rem`; `0` removes it) |
|
|
22
23
|
| `--input-number-border-focus` | `var(--theme-border-focus)` | Wrapper outline colour on `:focus-visible` (`normal` variant), and the embedded button's own `:focus-visible` outline |
|
|
23
24
|
| `--input-number-text-color` | `var(--theme-input-text-color-normal)` | Input text colour |
|
|
24
25
|
| `--input-number-placeholder-color` | `var(--theme-input-placeholder)` | Placeholder text colour |
|
|
@@ -33,6 +34,11 @@ and `step` and capped at `12em`).
|
|
|
33
34
|
|
|
34
35
|
---
|
|
35
36
|
|
|
37
|
+
> **Changed 2026-10-17**: bordered controls carry a light shadow ring (`box-shadow`, theme step 2,
|
|
38
|
+
> so red when invalid), and hover draws a 1px outline in the border colour with no offset, over the
|
|
39
|
+
> ring's inner edge. Hover used to draw a 2px outline at a 2px offset in the focus colour. Focus is
|
|
40
|
+
> unchanged and sits just outside the ring.
|
|
41
|
+
|
|
36
42
|
## State hooks
|
|
37
43
|
|
|
38
44
|
| Attribute | On | When |
|
|
@@ -123,11 +123,10 @@ const inputmode = computed(() => {
|
|
|
123
123
|
border: var(--form-element-border-width) solid var(--_border);
|
|
124
124
|
border-radius: var(--form-input-border-radius);
|
|
125
125
|
outline: var(--form-element-outline-width) solid transparent;
|
|
126
|
+
box-shadow: 0 0 0 var(--form-element-shadow-width) var(--input-number-shadow, var(--form-element-shadow-colour, var(--colour-theme-2)));
|
|
126
127
|
|
|
127
128
|
&:has(input:hover, button:hover) {
|
|
128
|
-
outline: var(--
|
|
129
|
-
var(--input-number-border-hover, var(--theme-border-focus));
|
|
130
|
-
outline-offset: var(--form-element-outline-offset-focus);
|
|
129
|
+
outline-color: var(--input-number-border-hover, var(--_border));
|
|
131
130
|
}
|
|
132
131
|
|
|
133
132
|
&:has(input:focus-visible, button:focus-visible) {
|
|
@@ -167,8 +166,8 @@ const inputmode = computed(() => {
|
|
|
167
166
|
outline-offset: -4px;
|
|
168
167
|
}
|
|
169
168
|
|
|
170
|
-
|
|
171
|
-
.
|
|
169
|
+
&[data-icon-only] {
|
|
170
|
+
& .input-button-icon {
|
|
172
171
|
margin: 0;
|
|
173
172
|
}
|
|
174
173
|
}
|
|
@@ -18,7 +18,8 @@ for the two-tier shape.
|
|
|
18
18
|
| `--input-otp-surface` | `var(--theme-input-surface)` | Box background |
|
|
19
19
|
| `--input-otp-text-color` | `var(--theme-input-text-color-normal)` | Digit colour |
|
|
20
20
|
| `--input-otp-border` | `var(--theme-border)` | Box border (`normal`) or bottom border (`underlined`) |
|
|
21
|
-
| `--input-otp-border-hover` |
|
|
21
|
+
| `--input-otp-border-hover` | the resolved border | 1px outline colour on hover (`normal` variant) |
|
|
22
|
+
| `--input-otp-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Resting shadow ring outside the border (each box, `normal` only). Width is the global `--form-element-shadow-width` (`0.2rem`; `0` removes it) |
|
|
22
23
|
| `--input-otp-border-focus` | `var(--theme-border-focus)` | Outline colour on `:focus-visible`, and the bottom border colour on focus (`underlined`) |
|
|
23
24
|
|
|
24
25
|
### Field (`InputOtpField`)
|
|
@@ -35,6 +36,11 @@ Border/outline widths and transition duration are the shared form geometry token
|
|
|
35
36
|
Private tokens (not public API): `--_border` and `--_border-focus` on `.input-otp`, which only
|
|
36
37
|
resolve the two public border tokens once for reuse across the variant rules.
|
|
37
38
|
|
|
39
|
+
> **Changed 2026-10-17**: bordered controls carry a light shadow ring (`box-shadow`, theme step 2,
|
|
40
|
+
> so red when invalid), and hover draws a 1px outline in the border colour with no offset, over the
|
|
41
|
+
> ring's inner edge. Hover used to draw a 2px outline at a 2px offset in the focus colour. Focus is
|
|
42
|
+
> unchanged and sits just outside the ring.
|
|
43
|
+
|
|
38
44
|
## State hooks
|
|
39
45
|
|
|
40
46
|
| Hook | Where | Meaning |
|
|
@@ -230,10 +230,10 @@ function onFocus(event: FocusEvent) {
|
|
|
230
230
|
border: var(--form-element-border-width) solid var(--_border);
|
|
231
231
|
border-radius: var(--input-otp-border-radius, var(--form-input-border-radius));
|
|
232
232
|
outline: var(--form-element-outline-width) solid transparent;
|
|
233
|
+
box-shadow: 0 0 0 var(--form-element-shadow-width) var(--input-otp-shadow, var(--form-element-shadow-colour, var(--colour-theme-2)));
|
|
233
234
|
|
|
234
235
|
&:hover {
|
|
235
|
-
outline: var(--
|
|
236
|
-
outline-offset: var(--form-element-outline-offset-focus);
|
|
236
|
+
outline-color: var(--input-otp-border-hover, var(--_border));
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
&:focus-visible {
|
|
@@ -17,7 +17,8 @@ to override just this component; leave it unset and it inherits the shared `--th
|
|
|
17
17
|
| `--input-select-surface` | `var(--theme-input-surface)` | Wrapper/select background |
|
|
18
18
|
| `--input-select-text-color` | `var(--theme-input-text-color-normal)` | Select text colour (added 2026-10-16; it inherited the page colour before) |
|
|
19
19
|
| `--input-select-border` | `var(--theme-border)` | Border colour (`normal`/`underlined` variants and the open picker, resting) |
|
|
20
|
-
| `--input-select-border-hover` | `var(--theme-border-focus)` | Outline colour on hover (mouse) |
|
|
20
|
+
| `--input-select-border-hover` | the resolved border (`normal`), `var(--theme-border-focus)` (`underlined`) | Outline colour on hover (mouse): 1px with no offset on `normal`, 2px at a 2px offset on `underlined` |
|
|
21
|
+
| `--input-select-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Resting shadow ring outside the border (`normal` only). Width is the global `--form-element-shadow-width` (`0.2rem`; `0` removes it) |
|
|
21
22
|
| `--input-select-border-focus` | `var(--theme-border-focus)` | Outline colour on `:focus-visible` (keyboard/assistive), and the open picker's outline |
|
|
22
23
|
|
|
23
24
|
### Options
|
|
@@ -41,6 +42,11 @@ shows and ignores them.
|
|
|
41
42
|
|
|
42
43
|
---
|
|
43
44
|
|
|
45
|
+
> **Changed 2026-10-17**: bordered controls carry a light shadow ring (`box-shadow`, theme step 2,
|
|
46
|
+
> so red when invalid), and hover draws a 1px outline in the border colour with no offset, over the
|
|
47
|
+
> ring's inner edge. Hover used to draw a 2px outline at a 2px offset in the focus colour. Focus is
|
|
48
|
+
> unchanged and sits just outside the ring.
|
|
49
|
+
|
|
44
50
|
## State hooks
|
|
45
51
|
|
|
46
52
|
| Attribute | On | When |
|
|
@@ -112,16 +112,25 @@ watch(
|
|
|
112
112
|
border: var(--form-element-border-width) solid var(--_border);
|
|
113
113
|
border-radius: var(--form-input-border-radius);
|
|
114
114
|
outline: var(--form-element-outline-width) solid transparent;
|
|
115
|
+
box-shadow: 0 0 0 var(--form-element-shadow-width) var(--input-select-shadow, var(--form-element-shadow-colour, var(--colour-theme-2)));
|
|
116
|
+
|
|
117
|
+
&:has(:hover) {
|
|
118
|
+
outline-color: var(--input-select-border-hover, var(--_border));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
&:has(select:focus-visible) {
|
|
122
|
+
outline-color: var(--_border-focus);
|
|
123
|
+
}
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
&[data-input-variant="underlined"] {
|
|
118
127
|
border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--_border);
|
|
119
|
-
}
|
|
120
128
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
129
|
+
&:has(:hover) {
|
|
130
|
+
outline: var(--form-element-outline-width-focus) solid
|
|
131
|
+
var(--input-select-border-hover, var(--theme-border-focus));
|
|
132
|
+
outline-offset: var(--form-element-outline-offset-focus);
|
|
133
|
+
}
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
&:has(select:focus-visible) {
|
|
@@ -17,7 +17,8 @@ override just this component; leave it unset and it inherits the shared `--theme
|
|
|
17
17
|
| `--input-text-surface` | `var(--theme-input-surface)` | Wrapper background, and slot `InputButton` background |
|
|
18
18
|
| `--input-text-surface-hover` | `var(--theme-input-surface-hover)` | Slot `InputButton` background on hover |
|
|
19
19
|
| `--input-text-border` | `var(--theme-border)` | Border colour (`normal`/`underlined`, resting), and the stepper's button dividers |
|
|
20
|
-
| `--input-text-border-hover` |
|
|
20
|
+
| `--input-text-border-hover` | the resolved border | 1px outline colour on hover (`normal` only) |
|
|
21
|
+
| `--input-text-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Resting shadow ring outside the border (`normal` only). Width is the global `--form-element-shadow-width` (`0.2rem`; `0` removes it) |
|
|
21
22
|
| `--input-text-border-focus` | `var(--theme-border-focus)` | Outline colour on `:focus-visible` (`normal` only), and a slot button's own focus outline |
|
|
22
23
|
| `--input-text-color` | `var(--theme-input-text-color-normal)` | Input text and slot button colour, and autofilled text (global rule) |
|
|
23
24
|
| `--input-text-placeholder-color` | `var(--theme-input-placeholder)` | Placeholder text colour |
|
|
@@ -31,6 +32,11 @@ read in several places.
|
|
|
31
32
|
|
|
32
33
|
---
|
|
33
34
|
|
|
35
|
+
> **Changed 2026-10-17**: bordered controls carry a light shadow ring (`box-shadow`, theme step 2,
|
|
36
|
+
> so red when invalid), and hover draws a 1px outline in the border colour with no offset, over the
|
|
37
|
+
> ring's inner edge. Hover used to draw a 2px outline at a 2px offset in the focus colour. Focus is
|
|
38
|
+
> unchanged and sits just outside the ring.
|
|
39
|
+
|
|
34
40
|
## State hooks
|
|
35
41
|
|
|
36
42
|
| Attribute | On | When |
|
|
@@ -131,12 +131,12 @@ watch(
|
|
|
131
131
|
border: var(--form-element-border-width) solid var(--_border);
|
|
132
132
|
border-radius: var(--form-input-border-radius);
|
|
133
133
|
outline: var(--form-element-outline-width) solid transparent;
|
|
134
|
+
box-shadow: 0 0 0 var(--form-element-shadow-width) var(--input-text-shadow, var(--form-element-shadow-colour, var(--colour-theme-2)));
|
|
134
135
|
|
|
135
136
|
padding-inline: var(--input-padding-inline);
|
|
136
137
|
|
|
137
138
|
&:has(input:is(:hover), button:is(:hover)) {
|
|
138
|
-
outline: var(--
|
|
139
|
-
outline-offset: var(--form-element-outline-offset-focus);
|
|
139
|
+
outline-color: var(--input-text-border-hover, var(--_border));
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
&:has(input:focus-visible, button:focus-visible) {
|
|
@@ -180,8 +180,8 @@ watch(
|
|
|
180
180
|
outline-offset: -4px;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
|
|
184
|
-
.
|
|
183
|
+
&[data-icon-only] {
|
|
184
|
+
& .input-button-icon {
|
|
185
185
|
margin: 0;
|
|
186
186
|
}
|
|
187
187
|
}
|
|
@@ -18,7 +18,8 @@ This shape mirrors `InputText`'s token set (see its own CONSUMER-STYLING.md).
|
|
|
18
18
|
|---|---|---|
|
|
19
19
|
| `--input-textarea-surface` | `var(--theme-input-surface)` | Wrapper background |
|
|
20
20
|
| `--input-textarea-border` | `var(--theme-border)` | Border colour (`normal`/`underlined` variant border) |
|
|
21
|
-
| `--input-textarea-border-hover` |
|
|
21
|
+
| `--input-textarea-border-hover` | the resolved border | 1px wrapper outline colour on mouse hover (`normal` only) |
|
|
22
|
+
| `--input-textarea-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Resting shadow ring outside the border (`normal` only). Width is the global `--form-element-shadow-width` (`0.2rem`; `0` removes it) |
|
|
22
23
|
| `--input-textarea-border-focus` | `var(--theme-border-focus)` | Wrapper outline colour on `:focus-visible` (`normal` only) |
|
|
23
24
|
| `--input-textarea-text-color` | `var(--theme-input-text-color-normal)` | Textarea text colour |
|
|
24
25
|
| `--input-textarea-placeholder-color` | `var(--theme-input-placeholder)` | Placeholder text colour |
|
|
@@ -41,6 +42,11 @@ Private (not public API): `--_border`, resolved once and read by both border var
|
|
|
41
42
|
|
|
42
43
|
---
|
|
43
44
|
|
|
45
|
+
> **Changed 2026-10-17**: bordered controls carry a light shadow ring (`box-shadow`, theme step 2,
|
|
46
|
+
> so red when invalid), and hover draws a 1px outline in the border colour with no offset, over the
|
|
47
|
+
> ring's inner edge. Hover used to draw a 2px outline at a 2px offset in the focus colour. Focus is
|
|
48
|
+
> unchanged and sits just outside the ring.
|
|
49
|
+
|
|
44
50
|
## State hooks
|
|
45
51
|
|
|
46
52
|
| Attribute | On | When |
|
|
@@ -105,11 +105,10 @@ watch(
|
|
|
105
105
|
border: var(--form-element-border-width) solid var(--_border);
|
|
106
106
|
border-radius: var(--form-input-border-radius);
|
|
107
107
|
outline: var(--form-element-outline-width) solid transparent;
|
|
108
|
+
box-shadow: 0 0 0 var(--form-element-shadow-width) var(--input-textarea-shadow, var(--form-element-shadow-colour, var(--colour-theme-2)));
|
|
108
109
|
|
|
109
110
|
&:has(textarea:is(:hover)) {
|
|
110
|
-
outline: var(--
|
|
111
|
-
var(--input-textarea-border-hover, var(--theme-border-focus));
|
|
112
|
-
outline-offset: var(--form-element-outline-offset-focus);
|
|
111
|
+
outline-color: var(--input-textarea-border-hover, var(--_border));
|
|
113
112
|
}
|
|
114
113
|
|
|
115
114
|
&:has(textarea:focus-visible) {
|