srcdev-nuxt-components 9.6.8 → 9.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/component-compliance-checklist.md +28 -1
- package/.claude/skills/component-inline-action-button.md +1 -1
- package/.claude/skills/component-local-style-override.md +1 -1
- package/.claude/skills/component-naming.md +17 -12
- package/.claude/skills/components/container-glow.md +2 -2
- package/.claude/skills/components/form-field.md +2 -2
- package/.claude/skills/components/form-wrapper.md +1 -1
- package/.claude/skills/components/glowing-border.md +1 -1
- package/.claude/skills/components/input-description.md +7 -7
- package/.claude/skills/components/input-error.md +3 -3
- package/.claude/skills/components/input-label.md +7 -8
- package/.claude/skills/components/input-number.md +17 -17
- package/.claude/skills/components/input-select.md +153 -0
- package/.claude/skills/components/input-text.md +220 -0
- package/.claude/skills/components/input-textarea.md +147 -0
- package/.claude/skills/components/select-menu.md +4 -4
- package/.claude/skills/components/toggle-switch.md +143 -0
- package/.claude/skills/composable-zod-validation.md +1 -1
- package/.claude/skills/css-nesting-conventions.md +22 -1
- package/.claude/skills/icon-sets.md +1 -1
- package/.claude/skills/icon-sizing.md +1 -1
- package/.claude/skills/index.md +5 -5
- package/.claude/skills/storybook-add-story.md +1 -1
- package/.claude/skills/theming-component-token-pattern.md +8 -8
- package/.claude/skills/theming-form-geometry-tokens.md +1 -1
- package/.vscode/srcdev-component-input-select.code-snippets +20 -14
- package/.vscode/srcdev-component-input-text.code-snippets +46 -40
- package/.vscode/srcdev-component-input-textarea.code-snippets +19 -14
- package/.vscode/srcdev-component-toggle-switch.code-snippets +19 -29
- package/app/assets/styles/setup/04.elements/forms/00.element-defaults.css +2 -2
- package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +1 -1
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +2 -2
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +4 -4
- package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +4 -4
- package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +1 -1
- package/app/components/05.forms/form-field/CONSUMER-STYLING.md +2 -2
- package/app/components/05.forms/input-button/stories/InputButton.stories.ts +3 -3
- package/app/components/05.forms/input-description/CONSUMER-STYLING.md +6 -6
- package/app/components/05.forms/input-label/CONSUMER-STYLING.md +7 -7
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +9 -5
- package/app/components/05.forms/input-number/InputNumber.vue +2 -2
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +80 -10
- package/app/components/05.forms/input-select/InputSelect.vue +198 -0
- package/app/components/05.forms/input-select/InputSelectField.vue +130 -0
- package/app/components/05.forms/input-select/stories/InputSelect.stories.ts +184 -0
- package/app/components/05.forms/input-select/stories/InputSelectField.stories.ts +187 -0
- package/app/components/05.forms/input-select/tests/{InputSelectCore.spec.ts → InputSelect.spec.ts} +75 -12
- package/app/components/05.forms/input-select/tests/InputSelectField.spec.ts +121 -0
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +85 -15
- package/app/components/05.forms/input-text/{InputPasswordWithLabel.vue → InputPasswordField.vue} +16 -19
- package/app/components/05.forms/input-text/{InputTextCore.vue → InputText.vue} +42 -78
- package/app/components/05.forms/input-text/{InputTextAsNumberWithLabel.vue → InputTextAsNumberField.vue} +66 -62
- package/app/components/05.forms/input-text/InputTextField.vue +168 -0
- package/app/components/05.forms/input-text/stories/InputPasswordField.stories.ts +163 -0
- package/app/components/05.forms/input-text/stories/InputText.stories.ts +194 -0
- package/app/components/05.forms/input-text/stories/InputTextAsNumberField.stories.ts +182 -0
- package/app/components/05.forms/input-text/stories/InputTextField.stories.ts +206 -0
- package/app/components/05.forms/input-text/tests/{InputPasswordWithLabel.spec.ts → InputPasswordField.spec.ts} +42 -3
- package/app/components/05.forms/input-text/tests/InputText.spec.ts +166 -0
- package/app/components/05.forms/input-text/tests/InputTextAsNumberField.spec.ts +135 -0
- package/app/components/05.forms/input-text/tests/InputTextField.spec.ts +130 -0
- package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +94 -12
- package/app/components/05.forms/input-textarea/InputTextarea.vue +165 -0
- package/app/components/05.forms/input-textarea/InputTextareaField.vue +159 -0
- package/app/components/05.forms/input-textarea/stories/InputTextarea.stories.ts +198 -0
- package/app/components/05.forms/input-textarea/stories/InputTextareaField.stories.ts +161 -0
- package/app/components/05.forms/input-textarea/tests/InputTextarea.spec.ts +149 -0
- package/app/components/05.forms/input-textarea/tests/InputTextareaField.spec.ts +122 -0
- package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +13 -13
- package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +6 -6
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +30 -32
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +71 -50
- package/app/components/05.forms/toggle-switch/{ToggleSwitchCore.vue → ToggleSwitch.vue} +58 -73
- package/app/components/05.forms/toggle-switch/{ToggleSwitchWithLabel.vue → ToggleSwitchField.vue} +65 -19
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitch.stories.ts +148 -0
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchField.stories.ts +176 -0
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitch.spec.ts +145 -0
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchField.spec.ts +155 -0
- package/package.json +1 -1
- package/.claude/skills/components/input-select-core.md +0 -151
- package/.claude/skills/components/input-text-core.md +0 -204
- package/.claude/skills/components/input-textarea-core.md +0 -154
- package/.claude/skills/components/toggle-switch-core.md +0 -169
- package/app/components/05.forms/input-select/InputSelectCore.vue +0 -188
- package/app/components/05.forms/input-select/InputSelectWithLabel.vue +0 -118
- package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +0 -69
- package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +0 -115
- package/app/components/05.forms/input-select/tests/InputSelectWithLabel.spec.ts +0 -87
- package/app/components/05.forms/input-text/InputTextWithLabel.vue +0 -154
- package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +0 -368
- package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -480
- package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +0 -267
- package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +0 -319
- package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +0 -75
- package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +0 -60
- package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +0 -71
- package/app/components/05.forms/input-textarea/InputTextareaCore.vue +0 -159
- package/app/components/05.forms/input-textarea/InputTextareaWithLabel.vue +0 -136
- package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +0 -247
- package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -458
- package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +0 -119
- package/app/components/05.forms/input-textarea/tests/InputTextareaWithLabel.spec.ts +0 -93
- package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabelInline.vue +0 -81
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -251
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -251
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -283
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +0 -101
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabel.spec.ts +0 -133
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabelInline.spec.ts +0 -66
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# InputText — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
> **Renamed 2026-10-16**: `InputTextCore` → `InputText`, `InputTextWithLabel` →
|
|
4
|
+
> `InputTextField`, `InputPasswordWithLabel` → `InputPasswordField`,
|
|
5
|
+
> `InputTextAsNumberWithLabel` → `InputTextAsNumberField`. Class changes are listed under "State
|
|
6
|
+
> hooks" below.
|
|
2
7
|
|
|
3
8
|
## Public token API
|
|
4
9
|
|
|
@@ -7,25 +12,69 @@ override just this component; leave it unset and it inherits the shared `--theme
|
|
|
7
12
|
`--theme-*` token every other themed component also falls back to. See
|
|
8
13
|
`theming-component-token-pattern.md` for why this two-tier shape exists.
|
|
9
14
|
|
|
10
|
-
|
|
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 |
|
|
15
|
+
| Token | Default | Controls |
|
|
16
16
|
|---|---|---|
|
|
17
|
-
| `--input-text-surface` | `var(--theme-input-surface)` | Wrapper background |
|
|
18
|
-
| `--input-text-surface-hover` | `var(--theme-input-surface-hover)` |
|
|
19
|
-
| `--input-text-border` | `var(--theme-border)` | Border colour (
|
|
20
|
-
| `--input-text-border-hover` | `var(--theme-border-focus)` | Outline colour on hover (
|
|
21
|
-
| `--input-text-border-focus` | `var(--theme-border-focus)` | Outline colour on `:focus-visible` (
|
|
22
|
-
| `--input-text-color` | `var(--theme-input-text-color-normal)` | Input text colour |
|
|
17
|
+
| `--input-text-surface` | `var(--theme-input-surface)` | Wrapper background, and slot `InputButton` background |
|
|
18
|
+
| `--input-text-surface-hover` | `var(--theme-input-surface-hover)` | Slot `InputButton` background on hover |
|
|
19
|
+
| `--input-text-border` | `var(--theme-border)` | Border colour (`normal`/`underlined`, resting), and the stepper's button dividers |
|
|
20
|
+
| `--input-text-border-hover` | `var(--theme-border-focus)` | Outline colour on hover (`normal` only) |
|
|
21
|
+
| `--input-text-border-focus` | `var(--theme-border-focus)` | Outline colour on `:focus-visible` (`normal` only), and a slot button's own focus outline |
|
|
22
|
+
| `--input-text-color` | `var(--theme-input-text-color-normal)` | Input text and slot button colour, and autofilled text (global rule) |
|
|
23
23
|
| `--input-text-placeholder-color` | `var(--theme-input-placeholder)` | Placeholder text colour |
|
|
24
24
|
|
|
25
|
-
Geometry (`--input-padding-*`, `--input-min-height`, `--input-font-size`,
|
|
25
|
+
Geometry (`--input-padding-*`, `--input-min-height`, `--input-font-size`, `--input-icon-slot-gap`,
|
|
26
26
|
`--form-element-*`, etc.) is shared across every `05.forms` component and untouched by this —
|
|
27
27
|
see `theming-form-geometry-tokens.md`.
|
|
28
28
|
|
|
29
|
+
Private (not public API): `--_surface`, `--_border` and `--_border-focus`, each resolved once and
|
|
30
|
+
read in several places.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## State hooks
|
|
35
|
+
|
|
36
|
+
| Attribute | On | When |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `data-input-variant="normal" \| "outlined" \| "underlined"` | `.input-text-wrapper`, each Field root | Always, mirrors `input-variant` |
|
|
39
|
+
| `data-theme` | `.input-text-wrapper`, each Field root | Always, mirrors `theme` |
|
|
40
|
+
| `data-invalid` | `.input-text-wrapper`, each Field root | `field-has-error` |
|
|
41
|
+
| `data-dirty` | `.input-text-wrapper`, each Field root | The Field has seen a non-empty value |
|
|
42
|
+
| `data-active` | `.input-text-wrapper`, each Field root | The input has focus |
|
|
43
|
+
| `data-readonly` | `.input-text-wrapper` | `readonly` |
|
|
44
|
+
| `data-inputmode` | `.input-text-wrapper` | Always, mirrors `inputmode` |
|
|
45
|
+
| `data-has-left-slot` / `data-has-right-slot` | `.input-text-wrapper` | The `left` / `right` slot is filled |
|
|
46
|
+
|
|
47
|
+
Field roots: `.input-text-field` (`InputTextField`, and `InputPasswordField`, which adds
|
|
48
|
+
`.input-password-field`), `.input-text-as-number-field` (`InputTextAsNumberField`).
|
|
49
|
+
|
|
50
|
+
Inner elements: `.input-text` (the native input), `.input-text-slot` (both slot holders),
|
|
51
|
+
`.input-text-slot-left`, `.input-text-slot-right`, `.input-text-label` (the Fields' `InputLabel`),
|
|
52
|
+
`.input-text-description` (the Fields' `InputDescription`), `.input-password-field-icon` (the
|
|
53
|
+
password toggle's icon).
|
|
54
|
+
|
|
55
|
+
`outlined` has no CSS of its own: the control renders with no border or outline. In
|
|
56
|
+
`InputTextField` it moves the description after the input and error.
|
|
57
|
+
|
|
58
|
+
> **Changed 2026-10-16**: state used to be bare classes, which collide with common consumer CSS
|
|
59
|
+
> (an unlayered `.error` or `.normal` rule would restyle the field). Select on the attributes
|
|
60
|
+
> above instead:
|
|
61
|
+
>
|
|
62
|
+
> - `.input-text-wrapper.normal` / `.underlined` / `.outlined` → `[data-input-variant="…"]`
|
|
63
|
+
> - `.input-text-wrapper.dirty` / `.active` / `.error` → `[data-dirty]` / `[data-active]` /
|
|
64
|
+
> `[data-invalid]`; `.has-left-slot` / `.has-right-slot` → `[data-has-left-slot]` /
|
|
65
|
+
> `[data-has-right-slot]`
|
|
66
|
+
> - `.slot.left-slot` / `.slot.right-slot` → `.input-text-slot-left` / `.input-text-slot-right`
|
|
67
|
+
> - `.input-text-core` → `.input-text` (it also no longer carries `dirty`/`active` classes)
|
|
68
|
+
> - `.input-text-with-label` → `.input-text-field`, now the Field's root (it used to sit inside
|
|
69
|
+
> an unclassed `<div>`); its variant/`dirty`/`active` classes → the attributes above
|
|
70
|
+
> - `InputTextAsNumberField`'s root was `.input-text-with-label.input-text-as-number`; it's now
|
|
71
|
+
> `.input-text-as-number-field`, and the input no longer gets `.input-text-as-number`
|
|
72
|
+
> - the password toggle icon's `.icon` → `.input-password-field-icon`
|
|
73
|
+
>
|
|
74
|
+
> Also: label, description and error copy wraps (`overflow-wrap: anywhere` on each Field root),
|
|
75
|
+
> and the input can shrink below its intrinsic width (`min-inline-size: 0`), so a field no longer
|
|
76
|
+
> overflows a narrow column.
|
|
77
|
+
|
|
29
78
|
---
|
|
30
79
|
|
|
31
80
|
## Global theming
|
|
@@ -51,9 +100,30 @@ Set the tokens above on an element you own (a page or section class, or a class
|
|
|
51
100
|
`<style scoped>`, tokens set on your element still work, but selectors that reach inside the component need
|
|
52
101
|
`:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
|
|
53
102
|
|
|
103
|
+
**Caveat**: `InputText`'s own passthrough lands on the `<input>`, which has no descendants, so
|
|
104
|
+
tokens set through it only reach the input itself (text and placeholder colour). Set the others on
|
|
105
|
+
a Field root or an ancestor.
|
|
106
|
+
|
|
54
107
|
### One instance
|
|
55
108
|
|
|
56
109
|
```vue
|
|
57
|
-
<
|
|
110
|
+
<InputTextField
|
|
111
|
+
v-model="email"
|
|
112
|
+
name="email"
|
|
113
|
+
type="email"
|
|
114
|
+
label="Email"
|
|
115
|
+
error-message=""
|
|
116
|
+
style="--input-text-border-focus: var(--gold-04);"
|
|
117
|
+
/>
|
|
58
118
|
```
|
|
59
119
|
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Class passthrough
|
|
123
|
+
|
|
124
|
+
`InputText`'s `:style-class-passthrough` lands on the native `<input class="input-text">`. Each
|
|
125
|
+
Field's lands on its own root only.
|
|
126
|
+
|
|
127
|
+
> **Changed 2026-10-16**: `InputTextField` and `InputTextAsNumberField` applied their passthrough
|
|
128
|
+
> classes to both their root and the inner input. If you styled the input that way, select
|
|
129
|
+
> `.your-class .input-text` instead.
|
package/app/components/05.forms/input-text/{InputPasswordWithLabel.vue → InputPasswordField.vue}
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<InputTextField
|
|
3
3
|
v-model="modelValue"
|
|
4
|
+
class="input-password-field"
|
|
4
5
|
:type="inputType"
|
|
5
6
|
:maxlength
|
|
6
7
|
:name
|
|
@@ -9,14 +10,15 @@
|
|
|
9
10
|
:error-message
|
|
10
11
|
:field-has-error
|
|
11
12
|
:required
|
|
13
|
+
:autocomplete
|
|
12
14
|
:style-class-passthrough
|
|
13
15
|
:theme
|
|
14
16
|
:input-variant
|
|
15
17
|
>
|
|
16
|
-
<template #descriptionHtml>
|
|
18
|
+
<template v-if="slots.descriptionHtml" #descriptionHtml>
|
|
17
19
|
<slot name="descriptionHtml"></slot>
|
|
18
20
|
</template>
|
|
19
|
-
<template #descriptionText>
|
|
21
|
+
<template v-if="slots.descriptionText" #descriptionText>
|
|
20
22
|
<slot name="descriptionText"></slot>
|
|
21
23
|
</template>
|
|
22
24
|
|
|
@@ -30,18 +32,19 @@
|
|
|
30
32
|
@click.stop.prevent="toggleDisplayPassword"
|
|
31
33
|
>
|
|
32
34
|
<template #iconOnly>
|
|
33
|
-
<Icon v-if="displayPassword" name="radix-icons:eye-none" class="icon" />
|
|
34
|
-
<Icon v-else name="radix-icons:eye-open" class="icon" />
|
|
35
|
+
<Icon v-if="displayPassword" name="radix-icons:eye-none" class="input-password-field-icon" />
|
|
36
|
+
<Icon v-else name="radix-icons:eye-open" class="input-password-field-icon" />
|
|
35
37
|
</template>
|
|
36
38
|
</InputButton>
|
|
37
39
|
</template>
|
|
38
|
-
</
|
|
40
|
+
</InputTextField>
|
|
39
41
|
</template>
|
|
40
42
|
|
|
41
43
|
<script setup lang="ts">
|
|
42
44
|
import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms";
|
|
43
45
|
|
|
44
46
|
interface Props {
|
|
47
|
+
/** Ignored: the toggle sets the type. Declared so a passed `type` can't override it. */
|
|
45
48
|
type?: "text" | "password";
|
|
46
49
|
maxlength?: number;
|
|
47
50
|
name: string;
|
|
@@ -50,12 +53,11 @@ interface Props {
|
|
|
50
53
|
errorMessage: object | string;
|
|
51
54
|
fieldHasError?: boolean;
|
|
52
55
|
required?: boolean;
|
|
56
|
+
autocomplete?: string;
|
|
53
57
|
styleClassPassthrough?: string | string[];
|
|
54
58
|
theme?: FormUiTheme;
|
|
55
59
|
inputVariant?: InputUiVariant;
|
|
56
|
-
/** Accessible name for the toggle button while the password is hidden. */
|
|
57
60
|
showPasswordText?: string;
|
|
58
|
-
/** Accessible name for the toggle button while the password is visible. */
|
|
59
61
|
hidePasswordText?: string;
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -65,6 +67,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
65
67
|
placeholder: "",
|
|
66
68
|
fieldHasError: false,
|
|
67
69
|
required: false,
|
|
70
|
+
autocomplete: "current-password",
|
|
68
71
|
styleClassPassthrough: () => [],
|
|
69
72
|
theme: "default",
|
|
70
73
|
inputVariant: "normal",
|
|
@@ -72,21 +75,15 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
72
75
|
hidePasswordText: "Hide password",
|
|
73
76
|
});
|
|
74
77
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
const { updateElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
78
|
+
const slots = useSlots();
|
|
78
79
|
|
|
79
|
-
const
|
|
80
|
-
return displayPassword.value ? "text" : "password";
|
|
81
|
-
});
|
|
80
|
+
const modelValue = defineModel<string>({ required: true });
|
|
82
81
|
|
|
83
82
|
const displayPassword = ref(false);
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
const inputType = computed<"text" | "password">(() => (displayPassword.value ? "text" : "password"));
|
|
84
|
+
const buttonText = computed(() => (displayPassword.value ? props.hidePasswordText : props.showPasswordText));
|
|
85
|
+
|
|
87
86
|
const toggleDisplayPassword = () => {
|
|
88
87
|
displayPassword.value = !displayPassword.value;
|
|
89
88
|
};
|
|
90
|
-
|
|
91
|
-
updateElementClasses(["has-right-button"]);
|
|
92
89
|
</script>
|
|
@@ -3,42 +3,42 @@
|
|
|
3
3
|
class="input-text-wrapper"
|
|
4
4
|
data-testid="input-text-wrapper"
|
|
5
5
|
:data-theme="theme"
|
|
6
|
+
:data-input-variant="inputVariant"
|
|
6
7
|
:data-invalid="fieldHasError ? '' : null"
|
|
8
|
+
:data-dirty="isDirty ? '' : null"
|
|
9
|
+
:data-active="isActive ? '' : null"
|
|
10
|
+
:data-readonly="readonly ? '' : null"
|
|
7
11
|
:data-inputmode="inputmode"
|
|
8
|
-
:
|
|
9
|
-
|
|
10
|
-
{ dirty: isDirty },
|
|
11
|
-
{ active: isActive },
|
|
12
|
-
{ error: fieldHasError },
|
|
13
|
-
{ 'has-left-slot': slots.left },
|
|
14
|
-
{ 'has-right-slot': slots.right },
|
|
15
|
-
]"
|
|
12
|
+
:data-has-left-slot="slots.left ? '' : null"
|
|
13
|
+
:data-has-right-slot="slots.right ? '' : null"
|
|
16
14
|
>
|
|
17
|
-
<div v-if="slots.left" class="slot
|
|
15
|
+
<div v-if="slots.left" class="input-text-slot input-text-slot-left">
|
|
18
16
|
<slot name="left"></slot>
|
|
19
17
|
</div>
|
|
20
18
|
|
|
21
19
|
<input
|
|
22
20
|
:id
|
|
23
|
-
ref="inputField"
|
|
24
21
|
v-model="modelValue"
|
|
25
22
|
:type
|
|
26
23
|
:placeholder
|
|
27
24
|
:name
|
|
28
25
|
:required
|
|
26
|
+
:readonly
|
|
27
|
+
:autocomplete
|
|
29
28
|
:maxlength
|
|
30
29
|
:min
|
|
31
30
|
:max
|
|
32
|
-
:class="['input-text
|
|
31
|
+
:class="['input-text', elementClasses]"
|
|
33
32
|
:aria-invalid="fieldHasError ? 'true' : undefined"
|
|
34
|
-
:aria-describedby
|
|
33
|
+
:aria-describedby="ariaDescribedby || undefined"
|
|
35
34
|
:pattern="inputPattern"
|
|
36
35
|
:inputmode
|
|
37
|
-
@
|
|
38
|
-
@
|
|
36
|
+
@beforeinput="filterInput"
|
|
37
|
+
@focusin="isActive = true"
|
|
38
|
+
@focusout="isActive = false"
|
|
39
39
|
/>
|
|
40
40
|
|
|
41
|
-
<div v-if="slots.right" class="slot
|
|
41
|
+
<div v-if="slots.right" class="input-text-slot input-text-slot-right">
|
|
42
42
|
<slot name="right"></slot>
|
|
43
43
|
</div>
|
|
44
44
|
</div>
|
|
@@ -51,13 +51,13 @@ interface Props {
|
|
|
51
51
|
type?: InputTypesText;
|
|
52
52
|
inputmode?: InputMode;
|
|
53
53
|
maxlength?: number;
|
|
54
|
-
/** Passed straight through to the native input — meaningful for type="date"/"number" etc. */
|
|
55
54
|
min?: string | number;
|
|
56
|
-
/** Passed straight through to the native input — meaningful for type="date"/"number" etc. */
|
|
57
55
|
max?: string | number;
|
|
58
56
|
id: string;
|
|
59
57
|
name: string;
|
|
60
58
|
required?: boolean;
|
|
59
|
+
readonly?: boolean;
|
|
60
|
+
autocomplete?: string;
|
|
61
61
|
placeholder?: string;
|
|
62
62
|
fieldHasError?: boolean;
|
|
63
63
|
styleClassPassthrough?: string | string[];
|
|
@@ -73,6 +73,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
73
73
|
min: undefined,
|
|
74
74
|
max: undefined,
|
|
75
75
|
required: false,
|
|
76
|
+
readonly: false,
|
|
77
|
+
autocomplete: undefined,
|
|
76
78
|
placeholder: "",
|
|
77
79
|
fieldHasError: false,
|
|
78
80
|
styleClassPassthrough: () => [],
|
|
@@ -83,64 +85,32 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
83
85
|
|
|
84
86
|
const slots = useSlots();
|
|
85
87
|
|
|
86
|
-
const FormUiTheme = computed(() => {
|
|
87
|
-
return props.fieldHasError ? "error" : props.theme;
|
|
88
|
-
});
|
|
89
|
-
|
|
90
88
|
const modelValue = defineModel<string>({ required: true });
|
|
91
89
|
const isDirty = defineModel<boolean>("isDirty");
|
|
92
90
|
const isActive = defineModel<boolean>("isActive");
|
|
93
91
|
|
|
94
|
-
const inputPattern = computed(() =>
|
|
95
|
-
return props.inputmode === "numeric" ? "[0-9]+" : undefined;
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const updateFocus = (isFocused: boolean) => {
|
|
99
|
-
isActive.value = isFocused;
|
|
100
|
-
};
|
|
92
|
+
const inputPattern = computed(() => (props.inputmode === "numeric" ? "[0-9]+" : undefined));
|
|
101
93
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// TODO: Move this to a utility function to allow removeEventListener on unmounted
|
|
107
|
-
// Leaving like this could lead to memory leaks
|
|
108
|
-
const validateInput = () => {
|
|
109
|
-
if (inputField.value !== null) {
|
|
110
|
-
inputField.value.addEventListener("beforeinput", (event: InputEvent) => {
|
|
111
|
-
const beforeValue = modelValue.value;
|
|
112
|
-
if (event.data === null) {
|
|
113
|
-
// Handle deletion case
|
|
114
|
-
return;
|
|
115
|
-
} else if (event.target) {
|
|
116
|
-
event.target.addEventListener(
|
|
117
|
-
"input",
|
|
118
|
-
() => {
|
|
119
|
-
if (inputField.value !== null && inputField.value.validity.patternMismatch) {
|
|
120
|
-
inputField.value.value = beforeValue as string;
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
{ once: true }
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
});
|
|
94
|
+
const filterInput = (event: Event) => {
|
|
95
|
+
const data = (event as InputEvent).data;
|
|
96
|
+
if (props.inputmode === "numeric" && data !== null && !/^[0-9]*$/.test(data)) {
|
|
97
|
+
event.preventDefault();
|
|
127
98
|
}
|
|
128
99
|
};
|
|
129
100
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
101
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
102
|
+
|
|
103
|
+
watch(
|
|
104
|
+
() => props.styleClassPassthrough,
|
|
105
|
+
() => {
|
|
106
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
107
|
+
}
|
|
108
|
+
);
|
|
133
109
|
</script>
|
|
134
110
|
|
|
135
111
|
<style lang="css">
|
|
136
112
|
@layer components {
|
|
137
113
|
.input-text-wrapper {
|
|
138
|
-
/* Public --input-text-* tokens, inline-fallback to the shared --theme-* tokens (see
|
|
139
|
-
theming-component-token-pattern.md) — overriding one here doesn't touch every other themed
|
|
140
|
-
input/control that also reads --theme-input-surface/--theme-border. --_surface etc. are
|
|
141
|
-
private locals purely so the rest of this rule can reuse the resolved value without
|
|
142
|
-
repeating the fallback chain at every property — they are not themselves an override point,
|
|
143
|
-
see CONSUMER-STYLING.md. */
|
|
144
114
|
--_surface: var(--input-text-surface, var(--theme-input-surface));
|
|
145
115
|
--_border: var(--input-text-border, var(--theme-border));
|
|
146
116
|
--_border-focus: var(--input-text-border-focus, var(--theme-border-focus));
|
|
@@ -148,6 +118,8 @@ onMounted(() => {
|
|
|
148
118
|
display: flex;
|
|
149
119
|
align-items: center;
|
|
150
120
|
gap: var(--input-icon-slot-gap);
|
|
121
|
+
min-inline-size: 0;
|
|
122
|
+
max-inline-size: 100%;
|
|
151
123
|
background-color: var(--_surface);
|
|
152
124
|
overflow: hidden;
|
|
153
125
|
transition: all var(--theme-form-transition-duration) ease-in-out;
|
|
@@ -155,7 +127,7 @@ onMounted(() => {
|
|
|
155
127
|
z-index: 2;
|
|
156
128
|
position: relative;
|
|
157
129
|
|
|
158
|
-
|
|
130
|
+
&[data-input-variant="normal"] {
|
|
159
131
|
border: var(--form-element-border-width) solid var(--_border);
|
|
160
132
|
border-radius: var(--form-input-border-radius);
|
|
161
133
|
outline: var(--form-element-outline-width) solid transparent;
|
|
@@ -173,30 +145,24 @@ onMounted(() => {
|
|
|
173
145
|
}
|
|
174
146
|
}
|
|
175
147
|
|
|
176
|
-
|
|
148
|
+
&[data-input-variant="underlined"] {
|
|
177
149
|
border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--_border);
|
|
178
150
|
padding-inline: var(--input-padding-inline);
|
|
179
151
|
}
|
|
180
152
|
|
|
181
|
-
|
|
153
|
+
&[data-has-right-slot]:has(.input-text-slot-right .input-button) {
|
|
182
154
|
padding-inline-end: 0;
|
|
183
155
|
}
|
|
184
156
|
|
|
185
|
-
.slot {
|
|
157
|
+
.input-text-slot {
|
|
186
158
|
display: flex;
|
|
187
159
|
align-items: center;
|
|
188
160
|
justify-content: center;
|
|
161
|
+
flex-shrink: 0;
|
|
189
162
|
box-sizing: content-box;
|
|
190
163
|
|
|
164
|
+
/* variant="inline" InputButton ships unstyled; the slot supplies its border, colour and outline. */
|
|
191
165
|
.input-button {
|
|
192
|
-
/* variant="inline" ships with zero border/colour styling by design (see
|
|
193
|
-
component-inline-action-button.md) — the parent supplies all of it. This rule was
|
|
194
|
-
always missing the border/colour half of that contract, only ever setting
|
|
195
|
-
background-color; harmless before InputButton's flat-button redesign, which
|
|
196
|
-
removed the unscoped base rule that used to accidentally give every variant a real
|
|
197
|
-
border regardless. Without its own border/color/outline reset, this button now falls
|
|
198
|
-
through to the browser's UA default <button> chrome (a stark outset border and
|
|
199
|
-
black/white text) instead of sitting flush with the input. */
|
|
200
166
|
background-color: var(--_surface);
|
|
201
167
|
color: var(--input-text-color, var(--theme-input-text-color-normal));
|
|
202
168
|
border: none;
|
|
@@ -222,20 +188,18 @@ onMounted(() => {
|
|
|
222
188
|
}
|
|
223
189
|
}
|
|
224
190
|
|
|
225
|
-
.input-text
|
|
191
|
+
.input-text {
|
|
226
192
|
all: unset;
|
|
227
193
|
touch-action: manipulation;
|
|
228
194
|
flex-grow: 1;
|
|
195
|
+
min-inline-size: 0;
|
|
229
196
|
|
|
230
197
|
color: var(--input-text-color, var(--theme-input-text-color-normal));
|
|
231
198
|
font-family: var(--font-family);
|
|
232
199
|
font-size: var(--input-font-size);
|
|
233
200
|
|
|
234
|
-
/* line-height: var(--input-element-line-height); */
|
|
235
|
-
|
|
236
201
|
padding-block: var(--input-padding-block);
|
|
237
202
|
padding-inline: var(--input-padding-inline);
|
|
238
|
-
/* height: stretch; */
|
|
239
203
|
min-height: var(--input-min-height);
|
|
240
204
|
|
|
241
205
|
&::placeholder {
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
class="input-text-
|
|
3
|
+
class="input-text-as-number-field"
|
|
4
|
+
:class="elementClasses"
|
|
4
5
|
:data-theme="theme"
|
|
6
|
+
:data-input-variant="inputVariant"
|
|
5
7
|
:data-invalid="fieldHasError ? '' : null"
|
|
6
|
-
:
|
|
8
|
+
:data-dirty="isDirty ? '' : null"
|
|
9
|
+
:data-active="isActive ? '' : null"
|
|
7
10
|
>
|
|
8
11
|
<InputLabel
|
|
9
12
|
:id
|
|
@@ -29,7 +32,7 @@
|
|
|
29
32
|
</template>
|
|
30
33
|
</InputDescription>
|
|
31
34
|
|
|
32
|
-
<
|
|
35
|
+
<InputText
|
|
33
36
|
:id
|
|
34
37
|
v-model="internalStringValue"
|
|
35
38
|
v-model:is-dirty="isDirty"
|
|
@@ -38,14 +41,11 @@
|
|
|
38
41
|
:maxlength
|
|
39
42
|
:name
|
|
40
43
|
:placeholder
|
|
41
|
-
:label
|
|
42
|
-
:error-message
|
|
43
44
|
:field-has-error
|
|
44
45
|
:required
|
|
45
|
-
:style-class-passthrough="elementClasses"
|
|
46
46
|
:theme
|
|
47
|
-
inputmode
|
|
48
|
-
:aria-describedby="ariaDescribedby()"
|
|
47
|
+
:inputmode
|
|
48
|
+
:aria-describedby="ariaDescribedby() ?? undefined"
|
|
49
49
|
:input-variant
|
|
50
50
|
>
|
|
51
51
|
<template v-if="slots.left" #left>
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
:readonly="(modelValue ?? 0) <= min"
|
|
55
55
|
:is-pending="false"
|
|
56
56
|
:button-text="stepDownText"
|
|
57
|
-
theme
|
|
57
|
+
:theme
|
|
58
58
|
variant="inline"
|
|
59
|
-
@click.stop.prevent="
|
|
59
|
+
@click.stop.prevent="stepBy(-1)"
|
|
60
60
|
>
|
|
61
61
|
<template #iconOnly>
|
|
62
62
|
<slot name="left"></slot>
|
|
@@ -69,17 +69,17 @@
|
|
|
69
69
|
:readonly="(modelValue ?? 0) >= max"
|
|
70
70
|
:is-pending="false"
|
|
71
71
|
:button-text="stepUpText"
|
|
72
|
-
theme
|
|
72
|
+
:theme
|
|
73
73
|
variant="inline"
|
|
74
|
-
@click.stop.prevent="
|
|
74
|
+
@click.stop.prevent="stepBy(1)"
|
|
75
75
|
>
|
|
76
76
|
<template #iconOnly>
|
|
77
77
|
<slot name="right"></slot>
|
|
78
78
|
</template>
|
|
79
79
|
</InputButton>
|
|
80
80
|
</template>
|
|
81
|
-
</
|
|
82
|
-
<InputError :id="errorId" :error-message :show-error="fieldHasError" :is-detached="true" />
|
|
81
|
+
</InputText>
|
|
82
|
+
<InputError :id="errorId" :error-message :show-error="fieldHasError" :is-detached="true" :input-variant />
|
|
83
83
|
</div>
|
|
84
84
|
</template>
|
|
85
85
|
|
|
@@ -100,9 +100,7 @@ interface Props {
|
|
|
100
100
|
max: number;
|
|
101
101
|
step?: number;
|
|
102
102
|
inputVariant?: InputUiVariant;
|
|
103
|
-
/** Accessible name for the decrement button. */
|
|
104
103
|
stepDownText?: string;
|
|
105
|
-
/** Accessible name for the increment button. */
|
|
106
104
|
stepUpText?: string;
|
|
107
105
|
}
|
|
108
106
|
|
|
@@ -131,71 +129,77 @@ const modelValue = defineModel<number | undefined>({ default: undefined });
|
|
|
131
129
|
const isActive = ref<boolean>(false);
|
|
132
130
|
const isDirty = ref<boolean>(false);
|
|
133
131
|
|
|
134
|
-
// Convert between string (for input) and number (for modelValue)
|
|
135
132
|
const internalStringValue = computed({
|
|
136
133
|
get: () => modelValue.value?.toString() ?? "",
|
|
137
134
|
set: (value: string) => {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
modelValue.value = numericValue;
|
|
135
|
+
if (value === "") {
|
|
136
|
+
modelValue.value = undefined;
|
|
137
|
+
return;
|
|
142
138
|
}
|
|
139
|
+
const numericValue = Number(value);
|
|
140
|
+
if (Number.isFinite(numericValue)) modelValue.value = numericValue;
|
|
143
141
|
},
|
|
144
142
|
});
|
|
145
143
|
|
|
146
|
-
const { elementClasses,
|
|
147
|
-
const minLength = computed(() => `${props.max.toString().length + 1}em`);
|
|
144
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
148
145
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
146
|
+
watch(
|
|
147
|
+
() => props.styleClassPassthrough,
|
|
148
|
+
() => {
|
|
149
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
153
150
|
}
|
|
154
|
-
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const decimalPlaces = (value: number) => (String(value).split(".")[1] ?? "").length;
|
|
154
|
+
const clamp = (value: number) => Math.min(Math.max(value, props.min), props.max);
|
|
155
|
+
|
|
156
|
+
const inputmode = computed(() => {
|
|
157
|
+
if (props.min < 0) return "text";
|
|
158
|
+
return Number.isInteger(props.step) ? "numeric" : "decimal";
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const minLength = computed(() => {
|
|
162
|
+
const longest = Math.max(String(props.min).length, String(props.max).length) + decimalPlaces(props.step);
|
|
163
|
+
return `${Math.min(longest + 1, 12)}em`;
|
|
164
|
+
});
|
|
155
165
|
|
|
156
|
-
|
|
166
|
+
const stepBy = (direction: 1 | -1) => {
|
|
167
|
+
const current = modelValue.value;
|
|
168
|
+
if (typeof current !== "number" || !Number.isFinite(current)) {
|
|
169
|
+
modelValue.value = clamp(0);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const stepSize = Math.abs(props.step) || 1;
|
|
173
|
+
const precision = Math.max(decimalPlaces(stepSize), decimalPlaces(current), decimalPlaces(props.min));
|
|
174
|
+
const next = clamp(Number((current + direction * stepSize).toFixed(precision)));
|
|
175
|
+
if (next !== current) modelValue.value = next;
|
|
176
|
+
};
|
|
157
177
|
</script>
|
|
158
178
|
|
|
159
179
|
<style lang="css">
|
|
160
180
|
@layer components {
|
|
161
|
-
.input-text-as-number {
|
|
162
|
-
|
|
181
|
+
.input-text-as-number-field {
|
|
182
|
+
min-inline-size: 0;
|
|
183
|
+
overflow-wrap: anywhere;
|
|
184
|
+
|
|
185
|
+
.input-text-wrapper[data-input-variant] {
|
|
163
186
|
width: fit-content;
|
|
187
|
+
max-inline-size: 100%;
|
|
164
188
|
padding-inline: 0;
|
|
165
189
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
&.icon-only {
|
|
178
|
-
.btn-icon {
|
|
179
|
-
margin: 0;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
&.left-slot {
|
|
185
|
-
.input-button {
|
|
186
|
-
border-right: var(--form-element-border-width) solid var(--theme-border);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
&.right-slot {
|
|
191
|
-
.input-button {
|
|
192
|
-
border-left: var(--form-element-border-width) solid var(--theme-border);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
190
|
+
.input-text-slot {
|
|
191
|
+
margin-inline: 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.input-text-slot-left .input-button {
|
|
195
|
+
border-inline-end: var(--form-element-border-width) solid var(--input-text-border, var(--theme-border));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.input-text-slot-right .input-button {
|
|
199
|
+
border-inline-start: var(--form-element-border-width) solid var(--input-text-border, var(--theme-border));
|
|
196
200
|
}
|
|
197
201
|
|
|
198
|
-
.input-text
|
|
202
|
+
.input-text {
|
|
199
203
|
flex-grow: initial;
|
|
200
204
|
text-align: center;
|
|
201
205
|
width: v-bind(minLength);
|