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.
Files changed (109) hide show
  1. package/.claude/skills/component-compliance-checklist.md +28 -1
  2. package/.claude/skills/component-inline-action-button.md +1 -1
  3. package/.claude/skills/component-local-style-override.md +1 -1
  4. package/.claude/skills/component-naming.md +17 -12
  5. package/.claude/skills/components/container-glow.md +2 -2
  6. package/.claude/skills/components/form-field.md +2 -2
  7. package/.claude/skills/components/form-wrapper.md +1 -1
  8. package/.claude/skills/components/glowing-border.md +1 -1
  9. package/.claude/skills/components/input-description.md +7 -7
  10. package/.claude/skills/components/input-error.md +3 -3
  11. package/.claude/skills/components/input-label.md +7 -8
  12. package/.claude/skills/components/input-number.md +17 -17
  13. package/.claude/skills/components/input-select.md +153 -0
  14. package/.claude/skills/components/input-text.md +220 -0
  15. package/.claude/skills/components/input-textarea.md +147 -0
  16. package/.claude/skills/components/select-menu.md +4 -4
  17. package/.claude/skills/components/toggle-switch.md +143 -0
  18. package/.claude/skills/composable-zod-validation.md +1 -1
  19. package/.claude/skills/css-nesting-conventions.md +22 -1
  20. package/.claude/skills/icon-sets.md +1 -1
  21. package/.claude/skills/icon-sizing.md +1 -1
  22. package/.claude/skills/index.md +5 -5
  23. package/.claude/skills/storybook-add-story.md +1 -1
  24. package/.claude/skills/theming-component-token-pattern.md +8 -8
  25. package/.claude/skills/theming-form-geometry-tokens.md +1 -1
  26. package/.vscode/srcdev-component-input-select.code-snippets +20 -14
  27. package/.vscode/srcdev-component-input-text.code-snippets +46 -40
  28. package/.vscode/srcdev-component-input-textarea.code-snippets +19 -14
  29. package/.vscode/srcdev-component-toggle-switch.code-snippets +19 -29
  30. package/app/assets/styles/setup/04.elements/forms/00.element-defaults.css +2 -2
  31. package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +1 -1
  32. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +2 -2
  33. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +4 -4
  34. package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +4 -4
  35. package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +1 -1
  36. package/app/components/05.forms/form-field/CONSUMER-STYLING.md +2 -2
  37. package/app/components/05.forms/input-button/stories/InputButton.stories.ts +3 -3
  38. package/app/components/05.forms/input-description/CONSUMER-STYLING.md +6 -6
  39. package/app/components/05.forms/input-label/CONSUMER-STYLING.md +7 -7
  40. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +9 -5
  41. package/app/components/05.forms/input-number/InputNumber.vue +2 -2
  42. package/app/components/05.forms/input-select/CONSUMER-STYLING.md +80 -10
  43. package/app/components/05.forms/input-select/InputSelect.vue +198 -0
  44. package/app/components/05.forms/input-select/InputSelectField.vue +130 -0
  45. package/app/components/05.forms/input-select/stories/InputSelect.stories.ts +184 -0
  46. package/app/components/05.forms/input-select/stories/InputSelectField.stories.ts +187 -0
  47. package/app/components/05.forms/input-select/tests/{InputSelectCore.spec.ts → InputSelect.spec.ts} +75 -12
  48. package/app/components/05.forms/input-select/tests/InputSelectField.spec.ts +121 -0
  49. package/app/components/05.forms/input-text/CONSUMER-STYLING.md +85 -15
  50. package/app/components/05.forms/input-text/{InputPasswordWithLabel.vue → InputPasswordField.vue} +16 -19
  51. package/app/components/05.forms/input-text/{InputTextCore.vue → InputText.vue} +42 -78
  52. package/app/components/05.forms/input-text/{InputTextAsNumberWithLabel.vue → InputTextAsNumberField.vue} +66 -62
  53. package/app/components/05.forms/input-text/InputTextField.vue +168 -0
  54. package/app/components/05.forms/input-text/stories/InputPasswordField.stories.ts +163 -0
  55. package/app/components/05.forms/input-text/stories/InputText.stories.ts +194 -0
  56. package/app/components/05.forms/input-text/stories/InputTextAsNumberField.stories.ts +182 -0
  57. package/app/components/05.forms/input-text/stories/InputTextField.stories.ts +206 -0
  58. package/app/components/05.forms/input-text/tests/{InputPasswordWithLabel.spec.ts → InputPasswordField.spec.ts} +42 -3
  59. package/app/components/05.forms/input-text/tests/InputText.spec.ts +166 -0
  60. package/app/components/05.forms/input-text/tests/InputTextAsNumberField.spec.ts +135 -0
  61. package/app/components/05.forms/input-text/tests/InputTextField.spec.ts +130 -0
  62. package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +94 -12
  63. package/app/components/05.forms/input-textarea/InputTextarea.vue +165 -0
  64. package/app/components/05.forms/input-textarea/InputTextareaField.vue +159 -0
  65. package/app/components/05.forms/input-textarea/stories/InputTextarea.stories.ts +198 -0
  66. package/app/components/05.forms/input-textarea/stories/InputTextareaField.stories.ts +161 -0
  67. package/app/components/05.forms/input-textarea/tests/InputTextarea.spec.ts +149 -0
  68. package/app/components/05.forms/input-textarea/tests/InputTextareaField.spec.ts +122 -0
  69. package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +13 -13
  70. package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +6 -6
  71. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +30 -32
  72. package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +71 -50
  73. package/app/components/05.forms/toggle-switch/{ToggleSwitchCore.vue → ToggleSwitch.vue} +58 -73
  74. package/app/components/05.forms/toggle-switch/{ToggleSwitchWithLabel.vue → ToggleSwitchField.vue} +65 -19
  75. package/app/components/05.forms/toggle-switch/stories/ToggleSwitch.stories.ts +148 -0
  76. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchField.stories.ts +176 -0
  77. package/app/components/05.forms/toggle-switch/tests/ToggleSwitch.spec.ts +145 -0
  78. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchField.spec.ts +155 -0
  79. package/package.json +1 -1
  80. package/.claude/skills/components/input-select-core.md +0 -151
  81. package/.claude/skills/components/input-text-core.md +0 -204
  82. package/.claude/skills/components/input-textarea-core.md +0 -154
  83. package/.claude/skills/components/toggle-switch-core.md +0 -169
  84. package/app/components/05.forms/input-select/InputSelectCore.vue +0 -188
  85. package/app/components/05.forms/input-select/InputSelectWithLabel.vue +0 -118
  86. package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +0 -69
  87. package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +0 -115
  88. package/app/components/05.forms/input-select/tests/InputSelectWithLabel.spec.ts +0 -87
  89. package/app/components/05.forms/input-text/InputTextWithLabel.vue +0 -154
  90. package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +0 -368
  91. package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -480
  92. package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +0 -267
  93. package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +0 -319
  94. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +0 -75
  95. package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +0 -60
  96. package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +0 -71
  97. package/app/components/05.forms/input-textarea/InputTextareaCore.vue +0 -159
  98. package/app/components/05.forms/input-textarea/InputTextareaWithLabel.vue +0 -136
  99. package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +0 -247
  100. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -458
  101. package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +0 -119
  102. package/app/components/05.forms/input-textarea/tests/InputTextareaWithLabel.spec.ts +0 -93
  103. package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabelInline.vue +0 -81
  104. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -251
  105. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -251
  106. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -283
  107. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +0 -101
  108. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabel.spec.ts +0 -133
  109. 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 (`InputTextWithLabel`,
4
- `InputTextAsNumberWithLabel`, `InputNumberField`, `InputSelectWithLabel`,
5
- `InputTextareaWithLabel`, `InputRangeField`, `ToggleSwitchWithLabel`,
6
- `ToggleSwitchWithLabelInline`).
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 (not passed by `ToggleSwitchWithLabelInline`) |
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`, `input-switch-label`) and don't forward yours.
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 `InputTextCore`'s token set (see its own CONSUMER-STYLING.md) — `InputNumber`
11
- is a native `<input type="number">` sibling to the text-based `InputTextAsNumberWithLabel` variant,
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 `InputTextCore`.
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
- `InputTextCore`. Only `normal` (bordered box, with hover/focus outline) and `underlined`
66
- (bottom-border only) have CSS today; `outlined` has no styling in `InputTextCore` either, so passing it renders with no border/outline at all — a
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 InputTextCore's
104
- token shape so a native number input and the text-based InputTextAsNumberWithLabel variant
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
- # InputSelectCore — Consumer Styling Guide
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
- (Before 2026-08-25 this component used a private `--_input-select-*` naming scheme presented as a
11
- "local override path" — that's gone. A private, underscore-prefixed custom property was never a
12
- real override point; renaming it public with a real fallback is the actual fix, not a second
13
- documented path to reach for.)
13
+ ### Control
14
14
 
15
- | Token | Falls back to | Controls |
15
+ | Token | Default | Controls |
16
16
  |---|---|---|
17
17
  | `--input-select-surface` | `var(--theme-input-surface)` | Wrapper/select background |
18
- | `--input-select-surface-hover` | `var(--theme-input-surface-hover)` | Option row background on hover |
19
- | `--input-select-border` | `var(--theme-border)` | Border colour (`.normal`/`.underlined`/open picker, resting) |
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
- <InputSelectCore id="country" name="country" style="--input-select-border-focus: var(--gold-04);" />
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>