srcdev-nuxt-forms 0.1.0 → 1.0.0

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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/assets/styles/brand/_brand.css +150 -0
  3. package/assets/styles/brand/_brand_dark.css +152 -0
  4. package/assets/styles/brand/_palette_dark.css +148 -0
  5. package/assets/styles/brand/_palette_light.css +148 -0
  6. package/assets/styles/brand/_typography.css +176 -0
  7. package/assets/styles/brand/index.css +1 -0
  8. package/assets/styles/forms/index.css +1 -1
  9. package/assets/styles/forms/themes/_default.css +3 -0
  10. package/assets/styles/forms/themes/_error.css +45 -11
  11. package/assets/styles/forms/themes/_ghost.css +42 -10
  12. package/assets/styles/forms/themes/_primary.css +42 -10
  13. package/assets/styles/forms/themes/_secondary.css +42 -10
  14. package/assets/styles/forms/themes/_success.css +42 -11
  15. package/assets/styles/forms/themes/_tertiary.css +42 -10
  16. package/assets/styles/forms/themes/_warning.css +42 -10
  17. package/assets/styles/forms/themes/index.css +1 -0
  18. package/assets/styles/forms/variables/_palette.css +104 -0
  19. package/assets/styles/forms/variables/_theme.css +12 -18
  20. package/assets/styles/forms/variables/index.css +2 -0
  21. package/assets/styles/main.css +2 -0
  22. package/assets/styles/scaffolding/_margin-helpers.css +308 -0
  23. package/assets/styles/scaffolding/_padding-helpers.css +308 -0
  24. package/assets/styles/scaffolding/_page.css +23 -0
  25. package/assets/styles/scaffolding/index.css +3 -0
  26. package/assets/styles/variables/colors/_blue.css +2 -2
  27. package/assets/styles/variables/colors/_gray.css +2 -1
  28. package/assets/styles/variables/colors/_green.css +2 -2
  29. package/assets/styles/variables/colors/_orange.css +2 -2
  30. package/assets/styles/variables/colors/_red.css +2 -2
  31. package/assets/styles/variables/colors/_yellow.css +1 -1
  32. package/components/forms/c12/prop-validators/index.ts +8 -20
  33. package/components/forms/c12/utils.ts +14 -0
  34. package/components/forms/c12/validation-patterns/en.json +12 -0
  35. package/components/forms/form-errors/InputError.vue +177 -0
  36. package/components/forms/input-button/InputButtonCore.vue +33 -109
  37. package/components/forms/input-button/variants/InputButtonConfirm.vue +1 -1
  38. package/components/forms/input-button/variants/InputButtonSubmit.vue +1 -1
  39. package/components/forms/input-checkbox/InputCheckboxCore.vue +263 -0
  40. package/components/forms/input-checkbox/InputCheckboxWithLabel.vue +116 -0
  41. package/components/forms/input-checkbox/variants/MultipleCheckboxes.vue +167 -0
  42. package/components/forms/input-checkbox/variants/SingleCheckbox.vue +172 -0
  43. package/components/forms/input-number/InputNumberCore.vue +184 -0
  44. package/components/forms/input-number/variants/InputNumberDefault.vue +155 -0
  45. package/components/forms/input-radio/InputRadiobuttonCore.vue +212 -0
  46. package/components/forms/input-radio/InputRadiobuttonWithLabel.vue +103 -0
  47. package/components/forms/input-radio/variants/MultipleRadiobuttons.vue +166 -0
  48. package/components/forms/input-range/InputRangeCore.vue +153 -0
  49. package/components/forms/input-range/variants/InputRangeDefault.vue +159 -0
  50. package/components/forms/input-text/InputTextCore.vue +149 -87
  51. package/components/forms/input-text/variants/material/InputPasswordWithLabel.vue +99 -0
  52. package/components/forms/input-text/variants/material/InputTextAsNumberWithLabel.vue +142 -0
  53. package/components/forms/input-text/variants/material/InputTextWithLabel.vue +125 -0
  54. package/components/forms/input-textarea/InputTextareaCore.vue +161 -0
  55. package/components/forms/input-textarea/variants/InputTextareaWithLabel.vue +106 -0
  56. package/components/scaffolding/footer/NavFooter.vue +62 -0
  57. package/components/ui/content-grid/ContentGrid.vue +85 -0
  58. package/composables/useApiRequest.ts +25 -0
  59. package/composables/useErrorMessages.ts +17 -5
  60. package/composables/useFormControl.ts +149 -37
  61. package/composables/useSleep.ts +2 -2
  62. package/composables/useStyleClassPassthrough.ts +30 -0
  63. package/composables/useZodValidation.ts +120 -0
  64. package/layouts/default.vue +26 -16
  65. package/nuxt.config.ts +22 -0
  66. package/package.json +13 -8
  67. package/pages/forms/examples/buttons/index.vue +14 -13
  68. package/pages/forms/examples/material/cssbattle.vue +60 -0
  69. package/pages/forms/examples/material/text-fields.vue +551 -93
  70. package/pages/index.vue +2 -2
  71. package/pages/limit-text.vue +43 -0
  72. package/pages/typography.vue +83 -0
  73. package/server/api/places/list.get.ts +23 -0
  74. package/server/api/textFields.post.ts +37 -0
  75. package/server/api/utils/index.get.ts +20 -0
  76. package/server/data/places/cities.json +43 -0
  77. package/server/data/places/countries.json +55 -0
  78. package/server/data/utils/title.json +49 -0
  79. package/types/types.forms.ts +135 -3
  80. package/types/types.places.ts +8 -0
  81. package/types/types.zodFormControl.ts +21 -0
  82. package/components/forms/input-text/variants/material/InputEmailMaterial.vue +0 -72
  83. package/components/forms/input-text/variants/material/InputPasswordMaterial.vue +0 -88
  84. package/components/forms/input-text/variants/material/InputTextMaterial.vue +0 -75
  85. package/components/forms/input-text/variants/material/InputTextMaterialCore.vue +0 -258
  86. package/composables/useUpdateStyleClassPassthrough.ts +0 -29
  87. package/pages/forms/examples/material/text-fields-compact.vue +0 -136
@@ -0,0 +1,159 @@
1
+ <template>
2
+ <div class="input-range-with-label" :data-form-theme="formTheme" :class="[elementClasses, { error: fieldHasError }]">
3
+ <label class="input-range-label body-normal-bold" :for="id">{{ label }}</label>
4
+ <template v-if="hasDescription">
5
+ <slot name="description"></slot>
6
+ </template>
7
+
8
+ <InputRangeCore v-model="modelValue" :id :name :min :max :step :theme :required :size :weight :fieldHasError>
9
+ <template v-if="hasDataList" #datalist>
10
+ <slot name="datalist"></slot>
11
+ </template>
12
+ <template v-if="hasLeftContent" #left>
13
+ <InputButtonCore
14
+ type="button"
15
+ @click.stop.prevent="updateRange(-step, Number(modelValue) > min)"
16
+ :readonly="Number(modelValue) === min"
17
+ :is-pending="false"
18
+ buttonText="Step down"
19
+ theme="ghost"
20
+ size="x-small"
21
+ >
22
+ <template #iconOnly>
23
+ <slot name="left"></slot>
24
+ </template>
25
+ </InputButtonCore>
26
+ </template>
27
+ <template v-if="hasRightContent" #right>
28
+ <InputButtonCore
29
+ type="button"
30
+ @click.stop.prevent="updateRange(step, Number(modelValue) < max)"
31
+ :readonly="Number(modelValue) === max"
32
+ :is-pending="false"
33
+ buttonText="Step up"
34
+ theme="ghost"
35
+ size="x-small"
36
+ >
37
+ <template #iconOnly>
38
+ <slot name="right"></slot>
39
+ </template>
40
+ </InputButtonCore>
41
+ </template>
42
+ </InputRangeCore>
43
+ <InputError :errorMessage :fieldHasError :id :isDetached="true" :styleClassPassthrough="['mbe-20']" />
44
+ </div>
45
+ </template>
46
+
47
+ <script setup lang="ts">
48
+ import propValidators from '../../c12/prop-validators';
49
+
50
+ const { id, name, label, required, min, max, step, theme, size, weight, styleClassPassthrough, errorMessage, fieldHasError } = defineProps({
51
+ id: {
52
+ type: String,
53
+ required: true,
54
+ },
55
+ name: {
56
+ type: String,
57
+ required: true,
58
+ },
59
+ label: {
60
+ type: String,
61
+ required: true,
62
+ },
63
+ min: {
64
+ type: Number,
65
+ required: true,
66
+ },
67
+ max: {
68
+ type: Number,
69
+ required: true,
70
+ },
71
+ step: {
72
+ type: Number,
73
+ default: 1,
74
+ },
75
+ placeholder: {
76
+ type: String,
77
+ default: '',
78
+ },
79
+ errorMessage: {
80
+ type: [Object, String],
81
+ required: true,
82
+ },
83
+ fieldHasError: {
84
+ type: Boolean,
85
+ default: false,
86
+ },
87
+ required: {
88
+ type: Boolean,
89
+ default: false,
90
+ },
91
+ theme: {
92
+ type: String as PropType<string>,
93
+ default: 'primary',
94
+ validator(value: string) {
95
+ return propValidators.theme.includes(value);
96
+ },
97
+ },
98
+ size: {
99
+ type: String as PropType<string>,
100
+ default: 'medium',
101
+ validator(value: string) {
102
+ return propValidators.size.includes(value);
103
+ },
104
+ },
105
+ weight: {
106
+ type: String as PropType<string>,
107
+ default: 'wght-400',
108
+ validator(value: string) {
109
+ return propValidators.weight.includes(value);
110
+ },
111
+ },
112
+ styleClassPassthrough: {
113
+ type: Array as PropType<string[]>,
114
+ default: () => [],
115
+ },
116
+ deepCssClassPassthrough: {
117
+ type: String,
118
+ default: '',
119
+ },
120
+ });
121
+
122
+ const slots = useSlots();
123
+ const hasDescription = computed(() => slots.description !== undefined);
124
+ const hasDataList = computed(() => slots.datalist !== undefined);
125
+ const hasLeftContent = computed(() => slots.left !== undefined);
126
+ const hasRightContent = computed(() => slots.right !== undefined);
127
+ const { elementClasses, updateElementClasses } = useStyleClassPassthrough(styleClassPassthrough);
128
+
129
+ const formTheme = computed(() => {
130
+ return fieldHasError ? 'error' : theme;
131
+ });
132
+
133
+ const modelValue = defineModel<number | readonly number[]>();
134
+
135
+ const updateRange = (step: number, withinRangeLimit: boolean) => {
136
+ if (withinRangeLimit) {
137
+ modelValue.value = (Number(modelValue.value) + step) as number;
138
+ }
139
+ };
140
+ </script>
141
+
142
+ <style lang="css">
143
+ .input-range-with-label {
144
+ .input-range-label {
145
+ display: block;
146
+ margin-block: 8px;
147
+
148
+ &:hover {
149
+ cursor: pointer;
150
+ }
151
+ }
152
+
153
+ .label-description {
154
+ font-family: var(--font-family);
155
+ font-size: 16px;
156
+ margin-top: 12px;
157
+ }
158
+ }
159
+ </style>
@@ -1,49 +1,57 @@
1
1
  <template>
2
- <div class="input-text-wrapper" :class="[{ 'has-left-content': hasLeftContent }]">
3
- <template v-if="hasLeftContent">
4
- <span class="left-content">
5
- <slot name="left"></slot>
6
- </span>
7
- </template>
2
+ <div
3
+ class="input-text-wrapper"
4
+ :data-form-theme="formTheme"
5
+ :class="[{ dirty: isDirty }, { active: isActive }, { error: fieldHasError }, { 'has-left-slot': hasLeftSlot }, { 'has-right-slot': hasRightSlot }]"
6
+ >
7
+ <span v-if="hasLeftSlot" class="slot left-slot">
8
+ <slot name="left"></slot>
9
+ </span>
8
10
 
9
11
  <input
10
12
  :type
11
- :placeholder="c12.placeholder"
13
+ :placeholder
12
14
  :id
13
15
  :name
14
- :pattern="componentValidation.pattern"
15
- :maxlength="componentValidation.maxlength"
16
16
  :required
17
- :class="['input-text', 'text-normal', styleClassPassthrough, { active: isFocused }, { dirty: isDirty }, { error: fieldHasError() }]"
18
- v-model="modelValue.data[name]"
17
+ :maxlength
18
+ :class="['input-text-core', 'text-normal', elementClasses, { dirty: isDirty }, { active: isActive }]"
19
+ v-model="modelValue"
19
20
  ref="inputField"
20
- :readonly="isPending"
21
- @focusin="updateFocus(name, true)"
22
- @focusout="updateFocus(name, false)"
21
+ :aria-invalid="fieldHasError"
22
+ :aria-describedby="`${id}-error-message`"
23
+ :pattern="inputPattern"
24
+ :inputmode
25
+ @focusin="updateFocus(true)"
26
+ @focusout="updateFocus(false)"
23
27
  />
24
28
 
25
- <template v-if="hasRightContent">
26
- <span class="right-content">
27
- <slot name="right"></slot>
28
- </span>
29
- </template>
29
+ <span v-if="hasRightSlot" class="slot right-slot">
30
+ <slot name="right"></slot>
31
+ </span>
30
32
  </div>
31
33
  </template>
32
34
 
33
35
  <script setup lang="ts">
34
- import type { InpuTextC12, IFormData } from '@/types/types.forms';
35
- import { validationConfig } from '@/components/forms/c12/validation-patterns';
36
+ import propValidators from '../c12/prop-validators';
36
37
 
37
- const props = defineProps({
38
+ const { type, inputmode, maxlength, id, name, placeholder, required, fieldHasError, styleClassPassthrough, theme } = defineProps({
38
39
  type: {
39
- // type: String as PropType<"text" | "password" | "tel" | "number" | "email" | "url">, // This breaks props setup in unit tests
40
- type: String,
40
+ type: String as PropType<'text' | 'email' | 'password' | 'number' | 'tel' | 'url'>,
41
+ default: 'text',
41
42
  validator(value: string) {
42
- return ['text', 'password', 'tel', 'number', 'email', 'url'].includes(value);
43
+ return propValidators.inputTypesText.includes(value);
43
44
  },
44
45
  },
46
+ inputmode: {
47
+ type: String as PropType<'text' | 'email' | 'tel' | 'url' | 'search' | 'numeric' | 'none' | 'decimal'>,
48
+ default: 'text',
49
+ },
50
+ maxlength: {
51
+ type: Number,
52
+ default: 255,
53
+ },
45
54
  id: {
46
- // type: String as PropType<string>,
47
55
  type: String,
48
56
  required: true,
49
57
  },
@@ -51,101 +59,155 @@ const props = defineProps({
51
59
  type: String,
52
60
  required: true,
53
61
  },
54
- validation: {
55
- type: String,
56
- default: null,
57
- },
58
62
  required: {
59
63
  type: Boolean,
60
- value: false,
61
- },
62
- c12: {
63
- type: Object as PropType<InpuTextC12>,
64
- required: true,
64
+ default: false,
65
65
  },
66
- styleClassPassthrough: {
66
+ placeholder: {
67
67
  type: String,
68
68
  default: '',
69
69
  },
70
+ fieldHasError: {
71
+ type: Boolean,
72
+ default: false,
73
+ },
74
+ styleClassPassthrough: {
75
+ type: Array as PropType<string[]>,
76
+ default: () => [],
77
+ },
78
+ theme: {
79
+ type: String as PropType<string>,
80
+ default: 'primary',
81
+ validator(value: string) {
82
+ return propValidators.theme.includes(value);
83
+ },
84
+ },
70
85
  });
71
86
 
72
87
  const slots = useSlots();
73
- const hasLeftContent = computed(() => slots.left !== undefined);
74
- const hasRightContent = computed(() => slots.right !== undefined);
75
-
76
- const modelValue = defineModel() as Ref<IFormData>;
77
-
78
- const updateFocus = (name: string, isFocused: boolean) => {
79
- modelValue.value.focusedField = isFocused ? name : '';
80
- };
88
+ const hasLeftSlot = computed(() => slots.left !== undefined);
89
+ const hasRightSlot = computed(() => slots.right !== undefined);
81
90
 
82
- const isPending = computed(() => {
83
- return modelValue.value.isPending;
91
+ const formTheme = computed(() => {
92
+ return fieldHasError ? 'error' : theme;
84
93
  });
85
94
 
86
- const isFocused = computed(() => {
87
- return modelValue.value.focusedField == name.value;
88
- });
89
-
90
- const isDirty = computed(() => {
91
- return modelValue.value.dirtyFields[name.value];
92
- });
95
+ const modelValue = defineModel();
96
+ const isDirty = defineModel('isDirty');
97
+ const isActive = defineModel('isActive');
93
98
 
94
- const name = computed(() => {
95
- return props.name !== null ? props.name : props.id;
99
+ const inputPattern = computed(() => {
100
+ return inputmode === 'numeric' ? '[0-9]+' : undefined;
96
101
  });
97
102
 
98
- const validatorLocale = toRef(useRuntimeConfig().public.validatorLocale);
99
-
100
- const componentValidation = validationConfig[validatorLocale.value][props.validation];
101
- const inputField = ref<HTMLInputElement | null>(null);
102
-
103
- const { hasCustomError, removeCustomError } = useErrorMessage(name.value, modelValue);
104
-
105
- const fieldHasError = () => {
106
- const hasApiErrorMessage = hasCustomError();
107
- const inputBad = !inputField.value?.validity.valid;
108
-
109
- if (modelValue.value.isPending) {
110
- modelValue.value!.validityState[name.value] = inputField.value?.validity.valid ?? false;
111
- return hasApiErrorMessage ? hasApiErrorMessage : inputBad;
112
- }
113
- return false;
103
+ const updateFocus = (isFocused: boolean) => {
104
+ isActive.value = isFocused;
114
105
  };
115
106
 
116
- watchEffect(() => {
117
- console.log('watchEffect()');
118
- modelValue.value.dirtyFields[name.value] = modelValue.value.data[name.value] !== '';
107
+ const inputField = ref<HTMLInputElement | null>(null);
119
108
 
120
- modelValue.value!.validityState[name.value] = inputField.value?.validity.valid ?? false;
121
- if (hasCustomError()) {
122
- removeCustomError(inputField.value?.validity.valid);
109
+ const { elementClasses, updateElementClasses } = useStyleClassPassthrough(styleClassPassthrough);
110
+
111
+ // TODO: Move this to a utility function to allow removeEventListener on unmounted
112
+ // Leaving like this could lead to memory leaks
113
+ const validateInput = () => {
114
+ if (inputField.value !== null) {
115
+ inputField.value.addEventListener('beforeinput', (event: any) => {
116
+ let beforeValue = modelValue.value;
117
+ event.target.addEventListener(
118
+ 'input',
119
+ () => {
120
+ if (inputField.value !== null && inputField.value.validity.patternMismatch) {
121
+ inputField.value.value = beforeValue as string;
122
+ console.log('you did some bad chars');
123
+ }
124
+ },
125
+ { once: true }
126
+ );
127
+ });
123
128
  }
124
- });
125
-
126
- const isValid = () => {
127
- setTimeout(() => {
128
- modelValue.value!.validityState[name.value] = inputField.value?.validity.valid ?? false;
129
- }, 0);
130
129
  };
131
130
 
132
131
  onMounted(() => {
133
- isValid();
132
+ updateElementClasses(['deep-bristol', 'deep-london', 'deep-bath']);
133
+ if (inputmode === 'numeric') validateInput();
134
134
  });
135
135
  </script>
136
136
 
137
137
  <style lang="css">
138
138
  .input-text-wrapper {
139
+ --_gutter: 12px;
140
+ --_border-width: var(--input-border-width-thin);
141
+ --_outline-width: var(--input-border-width-thin);
142
+
139
143
  display: flex;
140
144
  align-items: center;
141
145
 
142
- &.has-left-content {
143
- margin-left: var(--_gutter);
146
+ background-color: var(--theme-form-input-bg);
147
+ border-radius: var(--input-border-width-default);
148
+ border: var(--_border-width) solid var(--theme-form-input-border);
149
+
150
+ &:focus-within {
151
+ border: var(--_border-width) solid var(--theme-form-input-border-focus);
152
+ outline: var(--_outline-width) solid hsl(from var(--theme-form-input-outline-focus) h s 50%);
153
+ box-shadow: var(--theme-form-focus-box-shadow);
154
+ }
155
+
156
+ .slot {
157
+ display: inline-block;
158
+ padding-inline: 8px;
144
159
 
145
- .left-content {
160
+ .icon {
161
+ color: var(--theme-form-input-text);
162
+ }
163
+ }
164
+
165
+ &.has-left-slot {
166
+ .left-slot {
167
+ display: flex;
168
+ align-items: center;
169
+ }
170
+ }
171
+
172
+ &.has-right-slot {
173
+ .right-slot {
146
174
  display: flex;
147
175
  align-items: center;
148
176
  }
149
177
  }
178
+
179
+ .input-text-core {
180
+ background-color: transparent;
181
+ border: none;
182
+ outline: none;
183
+ box-shadow: none;
184
+ flex-grow: 1;
185
+
186
+ color: var(--theme-form-input-text);
187
+ font-family: var(--font-family);
188
+ font-size: var(--theme-form-button-font-size-normal);
189
+ line-height: var(--line-height);
190
+ padding: 8px 12px;
191
+
192
+ &::placeholder,
193
+ &::-webkit-input-placeholder {
194
+ font-family: var(--font-family);
195
+ font-size: var(--font-size);
196
+ font-style: italic;
197
+ font-weight: 400;
198
+ }
199
+ }
200
+ }
201
+
202
+ input:autofill,
203
+ input:-webkit-autofill-strong-password,
204
+ input:-webkit-autofill-strong-password-viewable,
205
+ input:-webkit-autofill-and-obscured {
206
+ background-color: var(--theme-form-input-bg) !important;
207
+ background-image: none !important;
208
+ color: var(--_input-text-color) !important;
209
+ -webkit-box-shadow: 0 0 0px 1000px var(--theme-form-input-bg) inset;
210
+ /* -webkit-text-fill-color: black; */
211
+ transition: background-color 5000s ease-in-out 0s;
150
212
  }
151
213
  </style>
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <InputTextWithLabel v-model="modelValue" :data-form-theme="formTheme" :type="inputType" :maxlength :id :name :placeholder :label :errorMessage :fieldHasError :required :styleClassPassthrough :theme>
3
+ <template #right>
4
+ <InputButtonCore
5
+ type="button"
6
+ @click.stop.prevent="toggleDisplayPassword"
7
+ :is-pending="false"
8
+ :buttonText
9
+ theme="ghost"
10
+ size="x-small"
11
+ @focusin="updateFocus(name, true)"
12
+ @focusout="updateFocus(name, false)"
13
+ >
14
+ <template #iconOnly>
15
+ <Icon v-if="displayPassword" name="radix-icons:eye-none" class="icon" />
16
+ <Icon v-else name="radix-icons:eye-open" class="icon" />
17
+ </template>
18
+ </InputButtonCore>
19
+ </template>
20
+ </InputTextWithLabel>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ import propValidators from '../../../c12/prop-validators';
25
+
26
+ const { type, maxlength, id, name, placeholder, label, errorMessage, fieldHasError, required, styleClassPassthrough, theme } = defineProps({
27
+ type: {
28
+ type: String,
29
+ default: 'password',
30
+ },
31
+ maxlength: {
32
+ type: Number,
33
+ default: 255,
34
+ },
35
+ id: {
36
+ type: String,
37
+ required: true,
38
+ },
39
+ name: {
40
+ type: String,
41
+ required: true,
42
+ },
43
+ placeholder: {
44
+ type: String,
45
+ default: '',
46
+ },
47
+ label: {
48
+ type: String,
49
+ required: true,
50
+ },
51
+ errorMessage: {
52
+ type: [Object, String],
53
+ required: true,
54
+ },
55
+ fieldHasError: {
56
+ type: Boolean,
57
+ default: false,
58
+ },
59
+ required: {
60
+ type: Boolean,
61
+ default: false,
62
+ },
63
+ styleClassPassthrough: {
64
+ type: Array as PropType<string[]>,
65
+ default: () => [],
66
+ },
67
+ theme: {
68
+ type: String as PropType<string>,
69
+ default: 'primary',
70
+ validator(value: string) {
71
+ return propValidators.theme.includes(value);
72
+ },
73
+ },
74
+ });
75
+
76
+ const formTheme = computed(() => {
77
+ return fieldHasError ? 'error' : theme;
78
+ });
79
+
80
+ const modelValue = defineModel();
81
+
82
+ const updateFocus = (name: string, isFocused: boolean) => {
83
+ // console.log('updateFocus', name, isFocused);
84
+ // modelValue.value.focusedField = isFocused ? name : '';
85
+ };
86
+
87
+ const inputType = ref(type);
88
+
89
+ const displayPassword = ref(false);
90
+ const buttonText = computed(() => {
91
+ inputType.value = displayPassword.value ? 'text' : 'password';
92
+ return displayPassword.value ? 'Hide password' : 'Show password';
93
+ });
94
+ const toggleDisplayPassword = () => {
95
+ displayPassword.value = !displayPassword.value;
96
+ };
97
+ </script>
98
+
99
+ <style lang="css"></style>
@@ -0,0 +1,142 @@
1
+ <template>
2
+ <div class="input-text-with-label" :data-form-theme="formTheme" :class="[elementClasses, { dirty: isDirty }, { active: isActive }]">
3
+ <label :for="id" class="input-text-label body-normal-bold">{{ label }}</label>
4
+ <template v-if="hasDescription">
5
+ <slot name="description"></slot>
6
+ </template>
7
+
8
+ <InputTextCore
9
+ v-model="modelValue"
10
+ v-model:isDirty="isDirty"
11
+ v-model:isActive="isActive"
12
+ type="text"
13
+ :maxlength
14
+ :id
15
+ :name
16
+ :placeholder
17
+ :label
18
+ :errorMessage
19
+ :fieldHasError
20
+ :required
21
+ :styleClassPassthrough
22
+ :theme
23
+ inputmode="numeric"
24
+ >
25
+ <template v-if="hasLeftSlot" #left>
26
+ <InputButtonCore type="button" @click.stop.prevent="updateValue(-step, Number(modelValue) > min)" :is-pending="false" buttonText="Step down" theme="ghost" size="x-small">
27
+ <template #iconOnly>
28
+ <slot name="left"></slot>
29
+ </template>
30
+ </InputButtonCore>
31
+ </template>
32
+ <template v-if="hasRightSlot" #right>
33
+ <InputButtonCore type="button" @click.stop.prevent="updateValue(step, Number(modelValue) < max)" :is-pending="false" buttonText="Step up" theme="ghost" size="x-small">
34
+ <template #iconOnly>
35
+ <slot name="right"></slot>
36
+ </template>
37
+ </InputButtonCore>
38
+ </template>
39
+ </InputTextCore>
40
+ <InputError :errorMessage="errorMessage" :fieldHasError :id :isDetached="true" />
41
+ </div>
42
+ </template>
43
+
44
+ <script setup lang="ts">
45
+ import propValidators from '../../../c12/prop-validators';
46
+ const { maxlength, id, name, placeholder, label, errorMessage, fieldHasError, required, styleClassPassthrough, theme, step, min, max } = defineProps({
47
+ maxlength: {
48
+ type: Number,
49
+ default: 255,
50
+ },
51
+ id: {
52
+ type: String,
53
+ required: true,
54
+ },
55
+ name: {
56
+ type: String,
57
+ required: true,
58
+ },
59
+ placeholder: {
60
+ type: String,
61
+ default: '',
62
+ },
63
+ label: {
64
+ type: String,
65
+ required: true,
66
+ },
67
+ errorMessage: {
68
+ type: [Object, String],
69
+ required: true,
70
+ },
71
+ fieldHasError: {
72
+ type: Boolean,
73
+ default: false,
74
+ },
75
+ required: {
76
+ type: Boolean,
77
+ default: false,
78
+ },
79
+ styleClassPassthrough: {
80
+ type: Array as PropType<string[]>,
81
+ default: () => [],
82
+ },
83
+ theme: {
84
+ type: String as PropType<string>,
85
+ default: 'primary',
86
+ validator(value: string) {
87
+ return propValidators.theme.includes(value);
88
+ },
89
+ },
90
+ min: {
91
+ type: Number,
92
+ required: true,
93
+ },
94
+ max: {
95
+ type: Number,
96
+ required: true,
97
+ },
98
+ step: {
99
+ type: Number,
100
+ default: 1,
101
+ },
102
+ });
103
+
104
+ const slots = useSlots();
105
+ const hasDescription = computed(() => slots.description !== undefined);
106
+ const hasLeftSlot = computed(() => slots.left !== undefined);
107
+ const hasRightSlot = computed(() => slots.right !== undefined);
108
+
109
+ const formTheme = computed(() => {
110
+ return fieldHasError ? 'error' : theme;
111
+ });
112
+
113
+ const modelValue = defineModel();
114
+ const isActive = ref<boolean>(false);
115
+ const isDirty = ref<boolean>(false);
116
+
117
+ const { elementClasses, updateElementClasses } = useStyleClassPassthrough(styleClassPassthrough);
118
+ const minLength = computed(() => `${max.toString().length + 3}ch`);
119
+
120
+ const updateValue = (step: number, withinRangeLimit: boolean) => {
121
+ if (withinRangeLimit) {
122
+ modelValue.value = (Number(modelValue.value) + step) as number;
123
+ }
124
+ };
125
+
126
+ onMounted(() => {
127
+ updateElementClasses(['input-text-as-number']);
128
+ });
129
+ </script>
130
+
131
+ <style lang="css">
132
+ .input-text-as-number {
133
+ .input-text-wrapper {
134
+ width: fit-content;
135
+ .input-text-core.input-text-as-number {
136
+ flex-grow: initial;
137
+ text-align: center;
138
+ width: v-bind(minLength);
139
+ }
140
+ }
141
+ }
142
+ </style>