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,9 +1,9 @@
|
|
|
1
1
|
# InputLabel — Consumer Styling Guide
|
|
2
2
|
|
|
3
|
-
`InputLabel` is the `<label>` rendered by every labelled field wrapper (`
|
|
4
|
-
`
|
|
5
|
-
`
|
|
6
|
-
`
|
|
3
|
+
`InputLabel` is the `<label>` rendered by every labelled field wrapper (`InputTextField`,
|
|
4
|
+
`InputTextAsNumberField`, `InputNumberField`, `InputSelectField`,
|
|
5
|
+
`InputTextareaField`, `InputRangeField`,
|
|
6
|
+
`ToggleSwitchField`).
|
|
7
7
|
|
|
8
8
|
## Public token API
|
|
9
9
|
|
|
@@ -51,7 +51,7 @@ shared fallback so the per-state tokens don't repeat it.
|
|
|
51
51
|
| Attribute on `.input-label` | When |
|
|
52
52
|
|---|---|
|
|
53
53
|
| `data-input-variant="normal" \| "outlined" \| "underlined"` | Always, mirrors the field's `input-variant` (`InputRangeField` and the toggle switches always pass `normal`) |
|
|
54
|
-
| `data-invalid` | The field has an error (
|
|
54
|
+
| `data-invalid` | The field has an error (passed by every Field) |
|
|
55
55
|
|
|
56
56
|
| Attribute on `.input-label-indicator` | When |
|
|
57
57
|
|---|---|
|
|
@@ -130,5 +130,5 @@ ancestor of the label).
|
|
|
130
130
|
|
|
131
131
|
`:style-class-passthrough` (string or string array) adds classes to the `<label>`. It's only
|
|
132
132
|
reachable when you render `InputLabel` directly in a custom field wrapper: the library's wrappers
|
|
133
|
-
pass their own hook classes (`input-text-label`, `input-number-label`, `input-select-label`,
|
|
134
|
-
`input-textarea-label`, `
|
|
133
|
+
pass their own hook classes (`input-text-label`, `input-number-label`, `input-range-label`, `input-select-label`,
|
|
134
|
+
`input-textarea-label`, `toggle-switch-label`) and don't forward yours.
|
|
@@ -7,11 +7,11 @@ to override just this component; leave it unset and it inherits the shared `--th
|
|
|
7
7
|
`--theme-*` token every other themed component also falls back to. See
|
|
8
8
|
`theming-component-token-pattern.md` for why this two-tier shape exists.
|
|
9
9
|
|
|
10
|
-
This shape mirrors `
|
|
11
|
-
is a native `<input type="number">` sibling to the text-based `
|
|
10
|
+
This shape mirrors `InputText`'s token set (see its own CONSUMER-STYLING.md) — `InputNumber`
|
|
11
|
+
is a native `<input type="number">` sibling to the text-based `InputTextAsNumberField` variant,
|
|
12
12
|
and both are meant to look and behave the same way. Border colour also doubles as the divider
|
|
13
13
|
colour between the input and an embedded left/right `InputButton` — there's no separate divider
|
|
14
|
-
token, same as `
|
|
14
|
+
token, same as `InputText`.
|
|
15
15
|
|
|
16
16
|
| Token | Falls back to | Controls |
|
|
17
17
|
|---|---|---|
|
|
@@ -27,6 +27,10 @@ Geometry (`--input-padding-block`, `--input-padding-inline`, `--input-min-height
|
|
|
27
27
|
`--form-input-border-radius`, `--input-font-size`, etc.) is shared across every `05.forms`
|
|
28
28
|
component and untouched by this — see `theming-form-geometry-tokens.md`.
|
|
29
29
|
|
|
30
|
+
Private (not public API): `--_surface`, `--_border` and `--_border-focus` (each resolved once and
|
|
31
|
+
read in several places) and `--_min-width` (the input's minimum width, worked out from `min`, `max`
|
|
32
|
+
and `step` and capped at `12em`).
|
|
33
|
+
|
|
30
34
|
---
|
|
31
35
|
|
|
32
36
|
## State hooks
|
|
@@ -62,8 +66,8 @@ Inner elements: `.input-number-slot` (both slot holders), `.input-number-slot-le
|
|
|
62
66
|
## Input variant
|
|
63
67
|
|
|
64
68
|
`:input-variant="'normal' | 'outlined' | 'underlined'"` (default `"normal"`) — same prop as
|
|
65
|
-
`
|
|
66
|
-
(bottom-border only) have CSS today; `outlined` has no styling in `
|
|
69
|
+
`InputText`. Only `normal` (bordered box, with hover/focus outline) and `underlined`
|
|
70
|
+
(bottom-border only) have CSS today; `outlined` has no styling in `InputText` either, so passing it renders with no border/outline at all — a
|
|
67
71
|
pre-existing gap in the type, not something specific to `InputNumber`. Declared for prop-shape
|
|
68
72
|
consistency with the rest of `05.forms`.
|
|
69
73
|
|
|
@@ -100,8 +100,8 @@ const inputmode = computed(() => {
|
|
|
100
100
|
|
|
101
101
|
/* Public --input-number-* tokens, inline-fallback to the shared --theme-* tokens (see
|
|
102
102
|
theming-component-token-pattern.md) — overriding one here doesn't touch every other themed
|
|
103
|
-
input/control that also reads --theme-input-surface/--theme-border. Mirrors
|
|
104
|
-
token shape so a native number input and the text-based
|
|
103
|
+
input/control that also reads --theme-input-surface/--theme-border. Mirrors InputText's
|
|
104
|
+
token shape so a native number input and the text-based InputTextAsNumberField variant
|
|
105
105
|
look and behave the same way (see input-number.md's "Fixed 2026-09-22" note). */
|
|
106
106
|
--_surface: var(--input-number-surface, var(--theme-input-surface));
|
|
107
107
|
--_border: var(--input-number-border, var(--theme-border));
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
#
|
|
1
|
+
# InputSelect — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
> **Renamed 2026-10-16**: `InputSelectCore` → `InputSelect`, `InputSelectWithLabel` →
|
|
4
|
+
> `InputSelectField`. Class changes are listed under "State hooks" below.
|
|
2
5
|
|
|
3
6
|
## Public token API
|
|
4
7
|
|
|
@@ -7,23 +10,73 @@ to override just this component; leave it unset and it inherits the shared `--th
|
|
|
7
10
|
`--theme-*` token every other themed component also falls back to. See
|
|
8
11
|
`theming-component-token-pattern.md` for why this two-tier shape exists.
|
|
9
12
|
|
|
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.)
|
|
13
|
+
### Control
|
|
14
14
|
|
|
15
|
-
| Token |
|
|
15
|
+
| Token | Default | Controls |
|
|
16
16
|
|---|---|---|
|
|
17
17
|
| `--input-select-surface` | `var(--theme-input-surface)` | Wrapper/select background |
|
|
18
|
-
| `--input-select-
|
|
19
|
-
| `--input-select-border` | `var(--theme-border)` | Border colour (
|
|
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
|
+
| `--input-select-border` | `var(--theme-border)` | Border colour (`normal`/`underlined` variants and the open picker, resting) |
|
|
20
20
|
| `--input-select-border-hover` | `var(--theme-border-focus)` | Outline colour on hover (mouse) |
|
|
21
|
-
| `--input-select-border-focus` | `var(--theme-border-focus)` | Outline colour on `:focus-visible` (keyboard/assistive) |
|
|
21
|
+
| `--input-select-border-focus` | `var(--theme-border-focus)` | Outline colour on `:focus-visible` (keyboard/assistive), and the open picker's outline |
|
|
22
|
+
|
|
23
|
+
### Options
|
|
24
|
+
|
|
25
|
+
| Token | Default | Controls |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `--input-select-surface-hover` | `var(--theme-input-surface-hover)` | Option row background on hover |
|
|
28
|
+
| `--input-select-option-gap` | `1.5rem` | Gap between an option's icon and its label |
|
|
29
|
+
| `--input-select-option-icon-size` | `1.5rem` | Option icon width and height |
|
|
22
30
|
|
|
23
31
|
Geometry (`--input-padding-*`, `--input-min-height`, `--input-select-option-min-height`,
|
|
24
32
|
`--form-element-*`, etc.) is shared across every `05.forms` component and untouched by this — see
|
|
25
33
|
`theming-form-geometry-tokens.md`.
|
|
26
34
|
|
|
35
|
+
Private (not public API): `--_border` and `--_border-focus`, each resolved once and read in several
|
|
36
|
+
places.
|
|
37
|
+
|
|
38
|
+
Option rows (`--input-select-surface-hover`, the gap and the icon) and the open picker's border are
|
|
39
|
+
only styled where the browser supports `appearance: base-select`. Elsewhere the native dropdown
|
|
40
|
+
shows and ignores them.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## State hooks
|
|
45
|
+
|
|
46
|
+
| Attribute | On | When |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `data-input-variant="normal" \| "outlined" \| "underlined"` | `.input-select-wrapper`, `.input-select-field` | Always, mirrors `input-variant` |
|
|
49
|
+
| `data-theme` | `.input-select-wrapper`, `.input-select-field` | Always. The Field's own is the `theme` prop; the inner wrapper's switches to `error` while `field-has-error` is set |
|
|
50
|
+
| `data-invalid` | `.input-select-wrapper`, `.input-select-field` | `field-has-error` |
|
|
51
|
+
| `data-dirty` | `.input-select-wrapper` | The value has changed (the `is-dirty` model) |
|
|
52
|
+
| `data-active` | `.input-select-wrapper` | The select has focus (the `is-active` model) |
|
|
53
|
+
|
|
54
|
+
Inner elements: `.input-select` (the native select), `.input-select-option` (each option),
|
|
55
|
+
`.input-select-option-placeholder` (the placeholder option), `.input-select-option-icon`,
|
|
56
|
+
`.input-select-label` (the Field's `InputLabel`), `.input-text-description` (the Field's
|
|
57
|
+
`InputDescription`, shared with the other Field components).
|
|
58
|
+
|
|
59
|
+
`outlined` has no CSS of its own: the control renders with no border or outline. In the Field it
|
|
60
|
+
moves the description after the control and error.
|
|
61
|
+
|
|
62
|
+
> **Changed 2026-10-16**: state used to be bare classes, which collide with common consumer CSS
|
|
63
|
+
> (an unlayered `.error` or `.normal` rule would restyle the field). Select on the attributes
|
|
64
|
+
> above instead:
|
|
65
|
+
>
|
|
66
|
+
> - `.input-select-wrapper.normal` / `.underlined` / `.outlined` → `[data-input-variant="…"]`
|
|
67
|
+
> - `.input-select-wrapper.dirty` / `.active` / `.error` → `[data-dirty]` / `[data-active]` /
|
|
68
|
+
> `[data-invalid]`
|
|
69
|
+
> - `.input-select-core` → `.input-select`; `.input-select-core-option` → `.input-select-option`;
|
|
70
|
+
> `option.placeholder` → `.input-select-option-placeholder`;
|
|
71
|
+
> `.input-select-core-option-decorator-icon` → `.input-select-option-icon`
|
|
72
|
+
> - `.input-select-with-label` → `.input-select-field`, which is now the root (it used to sit
|
|
73
|
+
> inside an unclassed `<div>`); its `.error`, variant, `dirty` and `active` classes are gone, use
|
|
74
|
+
> `[data-invalid]` / `[data-input-variant]`, or the inner wrapper's attributes
|
|
75
|
+
> - the Field's default `data-testid` is `input-select-field` (was `input-select-with-label`)
|
|
76
|
+
>
|
|
77
|
+
> Also: long label, description and error copy now wraps (`overflow-wrap: anywhere` on
|
|
78
|
+
> `.input-select-field`), and long option labels wrap in the open picker.
|
|
79
|
+
|
|
27
80
|
---
|
|
28
81
|
|
|
29
82
|
## Global theming
|
|
@@ -52,6 +105,23 @@ Set the tokens above on an element you own (a page or section class, or a class
|
|
|
52
105
|
### One instance
|
|
53
106
|
|
|
54
107
|
```vue
|
|
55
|
-
<
|
|
108
|
+
<InputSelectField
|
|
109
|
+
v-model="country"
|
|
110
|
+
v-model:field-data="countries"
|
|
111
|
+
name="country"
|
|
112
|
+
label="Country"
|
|
113
|
+
:error-message="errors.country"
|
|
114
|
+
style="--input-select-border-focus: var(--gold-04);"
|
|
115
|
+
/>
|
|
56
116
|
```
|
|
57
117
|
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Class passthrough
|
|
121
|
+
|
|
122
|
+
`InputSelect`'s `:style-class-passthrough` lands on the native `<select class="input-select">`.
|
|
123
|
+
`InputSelectField`'s lands on its root `.input-select-field` only.
|
|
124
|
+
|
|
125
|
+
> **Changed 2026-10-16**: `InputSelectWithLabel` forwarded its passthrough classes to the inner
|
|
126
|
+
> select instead of its own root. If you styled the select through the Field's passthrough, select
|
|
127
|
+
> `.your-class .input-select` instead.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="input-select-wrapper"
|
|
4
|
+
:data-theme="theme"
|
|
5
|
+
:data-input-variant="inputVariant"
|
|
6
|
+
:data-invalid="fieldHasError ? '' : null"
|
|
7
|
+
:data-dirty="isDirty ? '' : null"
|
|
8
|
+
:data-active="isActive ? '' : null"
|
|
9
|
+
>
|
|
10
|
+
<select
|
|
11
|
+
:id
|
|
12
|
+
v-model="modelValue"
|
|
13
|
+
:aria-invalid="fieldHasError ? 'true' : undefined"
|
|
14
|
+
:class="['input-select', elementClasses]"
|
|
15
|
+
:name
|
|
16
|
+
:title
|
|
17
|
+
:required
|
|
18
|
+
:aria-describedby="ariaDescribedby || undefined"
|
|
19
|
+
@focusin="isActive = true"
|
|
20
|
+
@focusout="isActive = false"
|
|
21
|
+
@change="isDirty = true"
|
|
22
|
+
>
|
|
23
|
+
<option
|
|
24
|
+
v-if="placeholder"
|
|
25
|
+
value=""
|
|
26
|
+
disabled
|
|
27
|
+
:selected="isEmpty"
|
|
28
|
+
class="input-select-option input-select-option-placeholder"
|
|
29
|
+
>
|
|
30
|
+
{{ placeholder }}
|
|
31
|
+
</option>
|
|
32
|
+
<option
|
|
33
|
+
v-for="item in fieldData.data"
|
|
34
|
+
:key="item.id"
|
|
35
|
+
:value="item.value"
|
|
36
|
+
:selected="String(item.value) === String(modelValue)"
|
|
37
|
+
class="input-select-option"
|
|
38
|
+
>
|
|
39
|
+
<Icon
|
|
40
|
+
v-if="item.icon"
|
|
41
|
+
:name="item.icon"
|
|
42
|
+
class="input-select-option-icon"
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
focusable="false"
|
|
45
|
+
/>
|
|
46
|
+
{{ item.label }}
|
|
47
|
+
</option>
|
|
48
|
+
</select>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<script setup lang="ts">
|
|
53
|
+
import type { IFormMultipleOptions, FormUiTheme, InputUiVariant } from "~/types/forms/types.forms";
|
|
54
|
+
|
|
55
|
+
interface Props {
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
placeholder?: string;
|
|
59
|
+
title?: string;
|
|
60
|
+
required?: boolean;
|
|
61
|
+
fieldHasError?: boolean;
|
|
62
|
+
styleClassPassthrough?: string | string[];
|
|
63
|
+
theme?: FormUiTheme;
|
|
64
|
+
inputVariant?: InputUiVariant;
|
|
65
|
+
ariaDescribedby?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
69
|
+
placeholder: "",
|
|
70
|
+
title: "Please select an option",
|
|
71
|
+
required: false,
|
|
72
|
+
fieldHasError: false,
|
|
73
|
+
styleClassPassthrough: () => [],
|
|
74
|
+
theme: "default",
|
|
75
|
+
inputVariant: "normal",
|
|
76
|
+
ariaDescribedby: "",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const modelValue = defineModel<string | number | readonly number[]>({ required: true });
|
|
80
|
+
const isDirty = defineModel<boolean>("isDirty");
|
|
81
|
+
const isActive = defineModel<boolean>("isActive");
|
|
82
|
+
const fieldData = defineModel<IFormMultipleOptions>("fieldData", { required: true });
|
|
83
|
+
|
|
84
|
+
const isEmpty = computed(() => modelValue.value === "" || modelValue.value == null);
|
|
85
|
+
|
|
86
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
87
|
+
|
|
88
|
+
watch(
|
|
89
|
+
() => props.styleClassPassthrough,
|
|
90
|
+
() => {
|
|
91
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<style lang="css">
|
|
97
|
+
@layer components {
|
|
98
|
+
.input-select-wrapper {
|
|
99
|
+
--_border: var(--input-select-border, var(--theme-border));
|
|
100
|
+
--_border-focus: var(--input-select-border-focus, var(--theme-border-focus));
|
|
101
|
+
|
|
102
|
+
min-inline-size: 0;
|
|
103
|
+
max-inline-size: 100%;
|
|
104
|
+
background-color: var(--input-select-surface, var(--theme-input-surface));
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
|
|
107
|
+
z-index: 2;
|
|
108
|
+
position: relative;
|
|
109
|
+
transition: all var(--theme-form-transition-duration) ease-in-out;
|
|
110
|
+
|
|
111
|
+
&[data-input-variant="normal"] {
|
|
112
|
+
border: var(--form-element-border-width) solid var(--_border);
|
|
113
|
+
border-radius: var(--form-input-border-radius);
|
|
114
|
+
outline: var(--form-element-outline-width) solid transparent;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
&[data-input-variant="underlined"] {
|
|
118
|
+
border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--_border);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
&:has(:hover) {
|
|
122
|
+
outline: var(--form-element-outline-width-focus) solid
|
|
123
|
+
var(--input-select-border-hover, var(--theme-border-focus));
|
|
124
|
+
outline-offset: var(--form-element-outline-offset-focus);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
&:has(select:focus-visible) {
|
|
128
|
+
outline: var(--form-element-outline-width-focus) solid var(--_border-focus);
|
|
129
|
+
outline-offset: var(--form-element-outline-offset-focus);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.input-select {
|
|
133
|
+
appearance: none;
|
|
134
|
+
background-color: transparent;
|
|
135
|
+
color: var(--input-select-text-color, var(--theme-input-text-color-normal));
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
|
|
139
|
+
&,
|
|
140
|
+
&::picker(select) {
|
|
141
|
+
appearance: base-select;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
&::picker(select) {
|
|
145
|
+
transition:
|
|
146
|
+
display allow-discrete var(--theme-form-transition-duration),
|
|
147
|
+
opacity var(--theme-form-transition-duration),
|
|
148
|
+
overlay var(--theme-form-transition-duration) allow-discrete;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
&:not(:open)::picker(select) {
|
|
152
|
+
opacity: 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
&:open::picker(select) {
|
|
156
|
+
opacity: 1;
|
|
157
|
+
border: var(--form-element-border-width) solid var(--_border);
|
|
158
|
+
outline: var(--form-element-outline-width) solid var(--_border-focus);
|
|
159
|
+
|
|
160
|
+
@starting-style {
|
|
161
|
+
opacity: 0;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
border: none;
|
|
166
|
+
outline: none;
|
|
167
|
+
border-radius: 0;
|
|
168
|
+
width: 100%;
|
|
169
|
+
min-inline-size: 0;
|
|
170
|
+
|
|
171
|
+
font-family: var(--font-family);
|
|
172
|
+
font-size: var(--input-font-size);
|
|
173
|
+
padding-block: var(--input-padding-block);
|
|
174
|
+
padding-inline: var(--input-padding-inline);
|
|
175
|
+
min-height: var(--input-min-height);
|
|
176
|
+
|
|
177
|
+
.input-select-option {
|
|
178
|
+
min-height: var(--input-select-option-min-height);
|
|
179
|
+
gap: var(--input-select-option-gap, 1.5rem);
|
|
180
|
+
overflow-wrap: anywhere;
|
|
181
|
+
white-space: normal;
|
|
182
|
+
transition: all var(--theme-form-transition-duration) ease-in-out;
|
|
183
|
+
|
|
184
|
+
&:hover {
|
|
185
|
+
background-color: var(--input-select-surface-hover, var(--theme-input-surface-hover));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.input-select-option-icon {
|
|
189
|
+
display: inline-block;
|
|
190
|
+
flex-shrink: 0;
|
|
191
|
+
width: var(--input-select-option-icon-size, 1.5rem);
|
|
192
|
+
height: var(--input-select-option-icon-size, 1.5rem);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
</style>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="input-select-field"
|
|
4
|
+
:class="elementClasses"
|
|
5
|
+
:data-testid
|
|
6
|
+
:data-theme="theme"
|
|
7
|
+
:data-input-variant="inputVariant"
|
|
8
|
+
:data-invalid="fieldHasError ? '' : null"
|
|
9
|
+
>
|
|
10
|
+
<InputLabel
|
|
11
|
+
:id
|
|
12
|
+
:required
|
|
13
|
+
:input-variant
|
|
14
|
+
:field-has-error
|
|
15
|
+
:style-class-passthrough="['input-select-label']"
|
|
16
|
+
>
|
|
17
|
+
<template #textLabel>{{ label }}</template>
|
|
18
|
+
</InputLabel>
|
|
19
|
+
|
|
20
|
+
<InputDescription
|
|
21
|
+
v-if="inputVariant !== 'outlined'"
|
|
22
|
+
:description-id
|
|
23
|
+
:input-variant
|
|
24
|
+
:field-has-error="fieldHasError"
|
|
25
|
+
:style-class-passthrough="['input-text-description']"
|
|
26
|
+
>
|
|
27
|
+
<template v-if="slots.descriptionHtml" #descriptionHtml>
|
|
28
|
+
<slot name="descriptionHtml"></slot>
|
|
29
|
+
</template>
|
|
30
|
+
<template v-if="slots.descriptionText" #descriptionText>
|
|
31
|
+
<slot name="descriptionText"></slot>
|
|
32
|
+
</template>
|
|
33
|
+
</InputDescription>
|
|
34
|
+
|
|
35
|
+
<InputSelect
|
|
36
|
+
:id
|
|
37
|
+
v-model="modelValue"
|
|
38
|
+
v-model:field-data="fieldData"
|
|
39
|
+
v-model:is-dirty="isDirty"
|
|
40
|
+
v-model:is-active="isActive"
|
|
41
|
+
:name
|
|
42
|
+
:placeholder
|
|
43
|
+
:title
|
|
44
|
+
:field-has-error
|
|
45
|
+
:required
|
|
46
|
+
:theme="selectTheme"
|
|
47
|
+
:aria-describedby="ariaDescribedby()"
|
|
48
|
+
:input-variant
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
<InputError :id="errorId" :error-message :show-error="fieldHasError" :is-detached="false" :input-variant />
|
|
52
|
+
|
|
53
|
+
<InputDescription
|
|
54
|
+
v-if="inputVariant === 'outlined'"
|
|
55
|
+
:description-id
|
|
56
|
+
:input-variant
|
|
57
|
+
:field-has-error="fieldHasError"
|
|
58
|
+
:style-class-passthrough="['input-text-description']"
|
|
59
|
+
>
|
|
60
|
+
<template v-if="slots.descriptionHtml" #descriptionHtml>
|
|
61
|
+
<slot name="descriptionHtml"></slot>
|
|
62
|
+
</template>
|
|
63
|
+
<template v-if="slots.descriptionText" #descriptionText>
|
|
64
|
+
<slot name="descriptionText"></slot>
|
|
65
|
+
</template>
|
|
66
|
+
</InputDescription>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<script setup lang="ts">
|
|
71
|
+
import type { FormUiTheme, InputUiVariant, IFormMultipleOptions } from "~/types/forms/types.forms";
|
|
72
|
+
|
|
73
|
+
interface Props {
|
|
74
|
+
dataTestid?: string;
|
|
75
|
+
name: string;
|
|
76
|
+
label: string;
|
|
77
|
+
placeholder?: string;
|
|
78
|
+
title?: string;
|
|
79
|
+
errorMessage: object | string;
|
|
80
|
+
required?: boolean;
|
|
81
|
+
fieldHasError?: boolean;
|
|
82
|
+
styleClassPassthrough?: string | string[];
|
|
83
|
+
theme?: FormUiTheme;
|
|
84
|
+
inputVariant?: InputUiVariant;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
88
|
+
dataTestid: "input-select-field",
|
|
89
|
+
placeholder: "",
|
|
90
|
+
title: "Please select an option",
|
|
91
|
+
required: false,
|
|
92
|
+
fieldHasError: false,
|
|
93
|
+
styleClassPassthrough: () => [],
|
|
94
|
+
theme: "default",
|
|
95
|
+
inputVariant: "normal",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const slots = useSlots();
|
|
99
|
+
|
|
100
|
+
const selectTheme = computed(() => (props.fieldHasError ? "error" : props.theme));
|
|
101
|
+
|
|
102
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
103
|
+
|
|
104
|
+
watch(
|
|
105
|
+
() => props.styleClassPassthrough,
|
|
106
|
+
() => {
|
|
107
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const { id, errorId, descriptionId, ariaDescribedby } = useAriaDescribedById(
|
|
112
|
+
props.name,
|
|
113
|
+
toRef(props, "fieldHasError"),
|
|
114
|
+
slots
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const modelValue = defineModel<string | number | readonly number[]>({ required: true });
|
|
118
|
+
const isDirty = defineModel<boolean>("isDirty");
|
|
119
|
+
const isActive = defineModel<boolean>("isActive");
|
|
120
|
+
const fieldData = defineModel<IFormMultipleOptions>("fieldData", { required: true });
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<style lang="css">
|
|
124
|
+
@layer components {
|
|
125
|
+
.input-select-field {
|
|
126
|
+
min-inline-size: 0;
|
|
127
|
+
overflow-wrap: anywhere;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
</style>
|