srcdev-nuxt-forms 0.2.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 (77) hide show
  1. package/assets/styles/brand/_brand.css +150 -0
  2. package/assets/styles/brand/_brand_dark.css +152 -0
  3. package/assets/styles/brand/_palette_dark.css +148 -0
  4. package/assets/styles/brand/_palette_light.css +148 -0
  5. package/assets/styles/brand/_typography.css +176 -0
  6. package/assets/styles/brand/index.css +1 -0
  7. package/assets/styles/forms/index.css +1 -2
  8. package/assets/styles/forms/themes/_default.css +3 -0
  9. package/assets/styles/forms/themes/_error.css +45 -11
  10. package/assets/styles/forms/themes/_ghost.css +42 -10
  11. package/assets/styles/forms/themes/_primary.css +42 -12
  12. package/assets/styles/forms/themes/_secondary.css +42 -12
  13. package/assets/styles/forms/themes/_success.css +42 -11
  14. package/assets/styles/forms/themes/_tertiary.css +42 -10
  15. package/assets/styles/forms/themes/_warning.css +42 -10
  16. package/assets/styles/forms/themes/index.css +1 -0
  17. package/assets/styles/forms/variables/_palette.css +104 -0
  18. package/assets/styles/forms/variables/_theme.css +1 -1
  19. package/assets/styles/forms/variables/index.css +2 -0
  20. package/assets/styles/main.css +2 -0
  21. package/assets/styles/scaffolding/_margin-helpers.css +308 -0
  22. package/assets/styles/scaffolding/_padding-helpers.css +308 -0
  23. package/assets/styles/scaffolding/_page.css +23 -0
  24. package/assets/styles/scaffolding/index.css +3 -0
  25. package/assets/styles/variables/colors/_blue.css +2 -2
  26. package/assets/styles/variables/colors/_gray.css +1 -1
  27. package/assets/styles/variables/colors/_green.css +2 -2
  28. package/assets/styles/variables/colors/_orange.css +2 -2
  29. package/assets/styles/variables/colors/_red.css +2 -2
  30. package/assets/styles/variables/colors/_yellow.css +1 -1
  31. package/components/forms/form-errors/InputError.vue +82 -37
  32. package/components/forms/input-button/InputButtonCore.vue +25 -104
  33. package/components/forms/input-checkbox/InputCheckboxCore.vue +37 -181
  34. package/components/forms/input-checkbox/InputCheckboxWithLabel.vue +42 -51
  35. package/components/forms/input-checkbox/variants/MultipleCheckboxes.vue +42 -69
  36. package/components/forms/input-checkbox/variants/SingleCheckbox.vue +126 -111
  37. package/components/forms/input-number/InputNumberCore.vue +184 -0
  38. package/components/forms/input-number/variants/InputNumberDefault.vue +155 -0
  39. package/components/forms/input-radio/InputRadiobuttonCore.vue +212 -0
  40. package/components/forms/input-radio/InputRadiobuttonWithLabel.vue +103 -0
  41. package/components/forms/input-radio/variants/MultipleRadiobuttons.vue +166 -0
  42. package/components/forms/input-range/InputRangeCore.vue +70 -88
  43. package/components/forms/input-range/variants/InputRangeDefault.vue +74 -46
  44. package/components/forms/input-text/InputTextCore.vue +141 -109
  45. package/components/forms/input-text/variants/material/InputPasswordWithLabel.vue +99 -0
  46. package/components/forms/input-text/variants/material/InputTextAsNumberWithLabel.vue +142 -0
  47. package/components/forms/input-text/variants/material/InputTextWithLabel.vue +125 -0
  48. package/components/forms/input-textarea/InputTextareaCore.vue +96 -105
  49. package/components/forms/input-textarea/variants/InputTextareaWithLabel.vue +106 -0
  50. package/components/scaffolding/footer/NavFooter.vue +62 -0
  51. package/composables/useApiRequest.ts +25 -0
  52. package/composables/useFormControl.ts +2 -0
  53. package/composables/useSleep.ts +2 -2
  54. package/composables/useStyleClassPassthrough.ts +30 -0
  55. package/composables/useZodValidation.ts +120 -0
  56. package/layouts/default.vue +21 -5
  57. package/package.json +13 -9
  58. package/pages/forms/examples/material/cssbattle.vue +60 -0
  59. package/pages/forms/examples/material/text-fields.vue +375 -153
  60. package/pages/index.vue +2 -2
  61. package/pages/typography.vue +83 -0
  62. package/server/data/places/cities.json +7 -1
  63. package/types/types.forms.ts +102 -0
  64. package/types/types.zodFormControl.ts +21 -0
  65. package/assets/styles/forms/utils/_a11y.css +0 -5
  66. package/assets/styles/forms/utils/index.css +0 -1
  67. package/components/forms/input-radio/InputRadioCore.vue +0 -226
  68. package/components/forms/input-radio/InputRadioWithLabel.vue +0 -118
  69. package/components/forms/input-radio/variants/MultipleRadio.vue +0 -183
  70. package/components/forms/input-radio/variants/SingleRadio.vue +0 -131
  71. package/components/forms/input-text/variants/material/InputEmailMaterial.vue +0 -72
  72. package/components/forms/input-text/variants/material/InputPasswordMaterial.vue +0 -114
  73. package/components/forms/input-text/variants/material/InputTextMaterial.vue +0 -68
  74. package/components/forms/input-text/variants/material/InputTextMaterialCore.vue +0 -313
  75. package/components/forms/input-textarea/variants/material/InputTextareaMaterial.vue +0 -75
  76. package/components/forms/input-textarea/variants/material/InputTextareaMaterialCore.vue +0 -290
  77. package/composables/useUpdateStyleClassPassthrough.ts +0 -29
@@ -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>
@@ -0,0 +1,125 @@
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
+
5
+ <InputTextCore
6
+ v-model="modelValue"
7
+ v-model:isDirty="isDirty"
8
+ v-model:isActive="isActive"
9
+ :type
10
+ :maxlength
11
+ :id
12
+ :name
13
+ :placeholder
14
+ :label
15
+ :errorMessage
16
+ :fieldHasError
17
+ :required
18
+ :styleClassPassthrough
19
+ :theme
20
+ >
21
+ <template v-if="hasLeftSlot" #left>
22
+ <slot name="left"></slot>
23
+ </template>
24
+ <template v-if="hasRightSlot" #right>
25
+ <slot name="right"></slot>
26
+ </template>
27
+ </InputTextCore>
28
+ <InputError :errorMessage="errorMessage" :fieldHasError :id :isDetached="false" />
29
+ </div>
30
+ </template>
31
+
32
+ <script setup lang="ts">
33
+ import propValidators from '../../../c12/prop-validators';
34
+ const { type, maxlength, id, name, placeholder, label, errorMessage, fieldHasError, required, styleClassPassthrough, theme } = defineProps({
35
+ maxlength: {
36
+ type: Number,
37
+ default: 255,
38
+ },
39
+ type: {
40
+ type: String,
41
+ required: true,
42
+ },
43
+ id: {
44
+ type: String,
45
+ required: true,
46
+ },
47
+ name: {
48
+ type: String,
49
+ required: true,
50
+ },
51
+ placeholder: {
52
+ type: String,
53
+ default: '',
54
+ },
55
+ label: {
56
+ type: String,
57
+ required: true,
58
+ },
59
+ errorMessage: {
60
+ type: [Object, String],
61
+ required: true,
62
+ },
63
+ fieldHasError: {
64
+ type: Boolean,
65
+ default: false,
66
+ },
67
+ required: {
68
+ type: Boolean,
69
+ default: false,
70
+ },
71
+ styleClassPassthrough: {
72
+ type: Array as PropType<string[]>,
73
+ default: () => [],
74
+ },
75
+ theme: {
76
+ type: String as PropType<string>,
77
+ default: 'primary',
78
+ validator(value: string) {
79
+ return propValidators.theme.includes(value);
80
+ },
81
+ },
82
+ });
83
+
84
+ const slots = useSlots();
85
+ const hasLeftSlot = computed(() => slots.left !== undefined);
86
+ const hasRightSlot = computed(() => slots.right !== undefined);
87
+
88
+ const formTheme = computed(() => {
89
+ return fieldHasError ? 'error' : theme;
90
+ });
91
+
92
+ const modelValue = defineModel();
93
+ const isActive = ref<boolean>(false);
94
+ const isDirty = ref<boolean>(false);
95
+
96
+ const { elementClasses, updateElementClasses } = useStyleClassPassthrough(styleClassPassthrough);
97
+
98
+ const testDirty = () => {
99
+ const watchValue = modelValue.value ?? '';
100
+
101
+ if (!isDirty.value && typeof watchValue === 'string' && watchValue.length > 0) {
102
+ isDirty.value = true;
103
+ }
104
+ };
105
+
106
+ onMounted(() => {
107
+ testDirty();
108
+ });
109
+
110
+ watch(
111
+ () => modelValue.value,
112
+ () => {
113
+ testDirty();
114
+ }
115
+ );
116
+ </script>
117
+
118
+ <style lang="css">
119
+ .input-text-with-label {
120
+ .input-text-label {
121
+ display: block;
122
+ margin-block: 8px;
123
+ }
124
+ }
125
+ </style>
@@ -1,36 +1,41 @@
1
1
  <template>
2
- <div class="input-textarea-wrapper" :class="[{ 'has-left-content': hasLeftContent }, { 'has-right-content': hasRightContent }]">
3
- <span v-if="hasLeftContent" class="left-content">
2
+ <div
3
+ class="input-textarea-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">
4
8
  <slot name="left"></slot>
5
9
  </span>
6
10
 
7
11
  <textarea
12
+ :maxlength
13
+ :placeholder
8
14
  :id
9
15
  :name
10
- :pattern="componentValidation.pattern"
11
- :maxlength="componentValidation.maxlength"
12
16
  :required
13
- :class="['input-textarea-core', 'text-normal', styleClassPassthrough, { active: isFocused }, { dirty: fieldIsDirty }, { error: fieldHasError }]"
14
- v-model="<string>modelValue.data[name]"
17
+ :class="['input-text-core', 'text-normal', elementClasses, { dirty: isDirty }, { active: isActive }]"
18
+ v-model="modelValue"
15
19
  ref="inputField"
16
- :placeholder="c12.placeholder"
17
20
  :aria-invalid="fieldHasError"
18
21
  :aria-describedby="`${id}-error-message`"
19
- @focusin="updateFocus(name, true)"
20
- @focusout="updateFocus(name, false)"
22
+ @focusin="updateFocus(true)"
23
+ @focusout="updateFocus(false)"
21
24
  ></textarea>
22
25
 
23
- <span v-if="hasRightContent" class="right-content">
26
+ <span v-if="hasRightSlot" class="slot right-slot">
24
27
  <slot name="right"></slot>
25
28
  </span>
26
29
  </div>
27
30
  </template>
28
31
 
29
32
  <script setup lang="ts">
30
- import type { InpuTextC12, IFormFieldC12, IFormData } from '@/types/types.forms';
31
- import { validationConfig } from '@/components/forms/c12/validation-patterns';
32
-
33
- const props = defineProps({
33
+ import propValidators from '../c12/prop-validators';
34
+ const { maxlength, id, name, placeholder, fieldHasError, required, styleClassPassthrough, theme } = defineProps({
35
+ maxlength: {
36
+ type: Number,
37
+ default: 255,
38
+ },
34
39
  id: {
35
40
  type: String,
36
41
  required: true,
@@ -39,132 +44,118 @@ const props = defineProps({
39
44
  type: String,
40
45
  required: true,
41
46
  },
42
- validation: {
47
+ placeholder: {
43
48
  type: String,
44
- default: null,
49
+ default: '',
45
50
  },
46
- required: {
51
+ fieldHasError: {
47
52
  type: Boolean,
48
- value: false,
53
+ default: false,
49
54
  },
50
- c12: {
51
- type: Object as PropType<InpuTextC12>,
52
- required: true,
55
+ required: {
56
+ type: Boolean,
57
+ default: false,
53
58
  },
54
59
  styleClassPassthrough: {
55
- type: String,
56
- default: '',
60
+ type: Array as PropType<string[]>,
61
+ default: () => [],
62
+ },
63
+ theme: {
64
+ type: String as PropType<string>,
65
+ default: 'primary',
66
+ validator(value: string) {
67
+ return propValidators.theme.includes(value);
68
+ },
57
69
  },
58
70
  });
59
71
 
60
72
  const slots = useSlots();
61
- const hasLeftContent = computed(() => slots.left !== undefined);
62
- const hasRightContent = computed(() => slots.right !== undefined);
63
-
64
- const modelValue = defineModel() as Ref<IFormData>;
73
+ const hasLeftSlot = computed(() => slots.left !== undefined);
74
+ const hasRightSlot = computed(() => slots.right !== undefined);
65
75
 
66
- const updateFocus = (name: string, isFocused: boolean) => {
67
- console.log(`textarea | updateFocus: ${name} ${isFocused}`);
68
- modelValue.value.focusedField = isFocused ? name : '';
69
- };
70
-
71
- const isFocused = computed(() => {
72
- return modelValue.value.focusedField == name.value;
76
+ const formTheme = computed(() => {
77
+ return fieldHasError ? 'error' : theme;
73
78
  });
74
79
 
75
- const name = computed(() => {
76
- return props.name !== null ? props.name : props.id;
77
- });
78
-
79
- const validatorLocale = toRef(useRuntimeConfig().public.validatorLocale);
80
-
81
- const componentValidation = validationConfig[validatorLocale.value][props.validation];
82
- const inputField = ref<HTMLInputElement | null>(null);
80
+ const modelValue = defineModel<string | number | readonly string[] | null | undefined>();
81
+ const isDirty = defineModel('isDirty');
82
+ const isActive = defineModel('isActive');
83
83
 
84
- const fieldIsDirty = computed(() => {
85
- return modelValue.value!.formFieldsC12[name.value].isDirty;
86
- });
87
- const fieldHasError = computed(() => {
88
- return modelValue.value!.submitAttempted && !modelValue.value!.formFieldsC12[name.value].isValid;
89
- });
90
-
91
- // const { fieldHasError } = useFormControl(name.value);
92
-
93
- const formFieldC12 = <IFormFieldC12>{
94
- label: props.c12.label,
95
- placeholder: props.c12.placeholder,
96
- errorMessage: props.c12.errorMessage,
97
- useCustomError: false,
98
- customErrors: {},
99
- isValid: false,
100
- isDirty: false,
101
- type: 'string',
102
- previousValue: null,
84
+ const updateFocus = (isFocused: boolean) => {
85
+ isActive.value = isFocused;
103
86
  };
104
- modelValue.value!.formFieldsC12[name.value] = formFieldC12;
105
-
106
- const { initFormFieldsC12 } = useFormControl();
107
- initFormFieldsC12(props.name, formFieldC12);
108
87
 
109
- const fieldValue = computed(() => {
110
- return modelValue.value.data[name.value];
111
- });
112
-
113
- watch(fieldValue, () => {
114
- if (!modelValue.value!.formFieldsC12[name.value].isDirty) {
115
- modelValue.value!.formFieldsC12[name.value].isDirty = modelValue.value.data[name.value] !== '';
116
- }
117
- modelValue.value!.formFieldsC12[name.value].isValid = inputField.value?.validity.valid ?? false;
118
- modelValue.value!.validityState[name.value] = inputField.value?.validity.valid ?? false;
119
-
120
- if (modelValue.value!.formFieldsC12[name.value].useCustomError && modelValue.value.data[props.name] === modelValue.value.formFieldsC12[props.name].previousValue) {
121
- modelValue.value!.validityState[name.value] = false;
122
- modelValue.value!.formFieldsC12[name.value].isValid = false;
123
- modelValue.value.displayErrorMessages = true;
124
- }
125
- });
88
+ const inputField = ref<HTMLInputElement | null>(null);
126
89
 
127
- const isValid = () => {
128
- setTimeout(() => {
129
- modelValue.value!.validityState[name.value] = inputField.value?.validity.valid ?? false;
130
- modelValue.value!.formFieldsC12[name.value].isValid = inputField.value?.validity.valid ?? false;
131
- if (!modelValue.value!.formFieldsC12[name.value].isDirty) {
132
- modelValue.value!.formFieldsC12[name.value].isDirty = modelValue.value.data[name.value] !== '';
133
- }
134
- }, 0);
135
- };
90
+ const { elementClasses, updateElementClasses } = useStyleClassPassthrough(styleClassPassthrough);
136
91
 
137
92
  onMounted(() => {
138
- isValid();
93
+ updateElementClasses(['deep-bristol', 'deep-london', 'deep-bath']);
139
94
  });
140
95
  </script>
141
96
 
142
97
  <style lang="css">
143
98
  .input-textarea-wrapper {
99
+ --_gutter: 12px;
100
+ --_border-width: var(--input-border-width-thin);
101
+ --_outline-width: var(--input-border-width-thin);
102
+
103
+ display: flex;
144
104
  align-items: center;
145
- display: grid;
146
- grid-template-columns: 1fr;
147
105
 
148
- &.has-left-content {
149
- grid-template-columns: auto 1fr;
150
- margin-left: var(--_gutter);
106
+ background-color: var(--theme-form-input-bg);
107
+ border-radius: var(--input-border-width-default);
108
+ border: var(--_border-width) solid var(--theme-form-input-border);
151
109
 
152
- .left-content {
110
+ &:focus-within {
111
+ border: var(--_border-width) solid var(--theme-form-input-border-focus);
112
+ outline: var(--_outline-width) solid hsl(from var(--theme-form-input-outline-focus) h s 50%);
113
+ box-shadow: var(--theme-form-focus-box-shadow);
114
+ }
115
+
116
+ .slot {
117
+ display: inline-block;
118
+ padding-inline: 8px;
119
+
120
+ .icon {
121
+ color: var(--theme-form-input-text);
122
+ }
123
+ }
124
+
125
+ &.has-left-slot {
126
+ .left-slot {
153
127
  display: flex;
154
128
  align-items: center;
155
129
  }
156
130
  }
157
131
 
158
- &.has-right-content {
159
- display: grid;
160
- grid-template-columns: 1fr auto;
161
- /* display: flex; */
162
- margin-right: var(--_gutter);
163
-
164
- .right-content {
132
+ &.has-right-slot {
133
+ .right-slot {
165
134
  display: flex;
166
135
  align-items: center;
167
136
  }
168
137
  }
138
+
139
+ .input-text-core {
140
+ background-color: transparent;
141
+ border: none;
142
+ outline: none;
143
+ box-shadow: none;
144
+ flex-grow: 1;
145
+
146
+ color: var(--theme-form-input-text);
147
+ font-family: var(--font-family);
148
+ font-size: var(--theme-form-button-font-size-normal);
149
+ line-height: var(--line-height);
150
+ padding: 8px 12px;
151
+
152
+ &::placeholder,
153
+ &::-webkit-input-placeholder {
154
+ font-family: var(--font-family);
155
+ font-size: var(--font-size);
156
+ font-style: italic;
157
+ font-weight: 400;
158
+ }
159
+ }
169
160
  }
170
161
  </style>
@@ -0,0 +1,106 @@
1
+ <template>
2
+ <div class="input-textarea-with-label" :data-form-theme="formTheme" :class="[elementClasses, { dirty: isDirty }, { active: isActive }]">
3
+ <label :for="id" class="input-textarea-label body-normal-semibold">{{ label }}</label>
4
+
5
+ <InputTextareaCore v-model="modelValue" v-model:isDirty="isDirty" v-model:isActive="isActive" :maxlength :id :name :placeholder :label :fieldHasError :required :styleClassPassthrough :theme>
6
+ <template v-if="hasLeftSlot" #left>
7
+ <slot name="left"></slot>
8
+ </template>
9
+ <template v-if="hasRightSlot" #right>
10
+ <slot name="right"></slot>
11
+ </template>
12
+ </InputTextareaCore>
13
+ <InputError :errorMessage :fieldHasError :id :isDetached="false" />
14
+ </div>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import propValidators from '../../c12/prop-validators';
19
+ const { maxlength, id, name, placeholder, label, errorMessage, fieldHasError, required, styleClassPassthrough, theme } = defineProps({
20
+ maxlength: {
21
+ type: Number,
22
+ default: 255,
23
+ },
24
+ id: {
25
+ type: String,
26
+ required: true,
27
+ },
28
+ name: {
29
+ type: String,
30
+ required: true,
31
+ },
32
+ placeholder: {
33
+ type: String,
34
+ default: '',
35
+ },
36
+ label: {
37
+ type: String,
38
+ required: true,
39
+ },
40
+ errorMessage: {
41
+ type: [Object, String],
42
+ required: true,
43
+ },
44
+ fieldHasError: {
45
+ type: Boolean,
46
+ default: false,
47
+ },
48
+ required: {
49
+ type: Boolean,
50
+ default: false,
51
+ },
52
+ styleClassPassthrough: {
53
+ type: Array as PropType<string[]>,
54
+ default: () => [],
55
+ },
56
+ theme: {
57
+ type: String as PropType<string>,
58
+ default: 'primary',
59
+ validator(value: string) {
60
+ return propValidators.theme.includes(value);
61
+ },
62
+ },
63
+ });
64
+
65
+ const slots = useSlots();
66
+ const hasLeftSlot = computed(() => slots.left !== undefined);
67
+ const hasRightSlot = computed(() => slots.right !== undefined);
68
+
69
+ const formTheme = computed(() => {
70
+ return fieldHasError ? 'error' : theme;
71
+ });
72
+
73
+ const modelValue = defineModel<string | number | readonly string[] | null | undefined>();
74
+ const isActive = ref<boolean>(false);
75
+ const isDirty = ref<boolean>(false);
76
+
77
+ const { elementClasses, updateElementClasses } = useStyleClassPassthrough(styleClassPassthrough);
78
+
79
+ const testDirty = () => {
80
+ const watchValue = modelValue.value ?? '';
81
+
82
+ if (!isDirty.value && typeof watchValue === 'string' && watchValue.length > 0) {
83
+ isDirty.value = true;
84
+ }
85
+ };
86
+
87
+ onMounted(() => {
88
+ testDirty();
89
+ });
90
+
91
+ watch(
92
+ () => modelValue.value,
93
+ () => {
94
+ testDirty();
95
+ }
96
+ );
97
+ </script>
98
+
99
+ <style lang="css">
100
+ .input-textarea-with-label {
101
+ .input-textarea-label {
102
+ display: block;
103
+ margin-block: 8px;
104
+ }
105
+ }
106
+ </style>
@@ -0,0 +1,62 @@
1
+ <template>
2
+ <footer>
3
+ <ul class="footer-list">
4
+ <li class="footer-item">
5
+ <Icon name="material-symbols-light:mail-rounded" class="icon" />
6
+ <span class="text">email@address.com</span>
7
+ </li>
8
+ <li class="footer-item">
9
+ <Icon name="ph:building-office-light" class="icon" />
10
+ <span class="text">Company number: 12345678</span>
11
+ </li>
12
+ <li class="footer-item">
13
+ <Icon name="material-symbols:call" class="icon" />
14
+ <span class="text">+44 (0) 1223 123 123</span>
15
+ </li>
16
+ <li class="footer-item">
17
+ <Icon name="material-symbols:location-on" class="icon" />
18
+ <span class="text">Some address, City, Postcode</span>
19
+ </li>
20
+ </ul>
21
+ </footer>
22
+ </template>
23
+ <script setup lang="ts"></script>
24
+
25
+ <style lang="css">
26
+ footer {
27
+ display: none;
28
+ padding: 1rem;
29
+ text-align: center;
30
+ background: white;
31
+ margin-bottom: 50px;
32
+
33
+ .footer-list {
34
+ display: inline-grid;
35
+ gap: 10px;
36
+ grid-template-columns: 280px 456px;
37
+ margin-inline: auto;
38
+ padding: 0;
39
+ }
40
+
41
+ .footer-item {
42
+ list-style-type: none;
43
+ gap: 10px;
44
+ display: grid;
45
+ grid-template-columns: 40px auto;
46
+ align-items: center;
47
+
48
+ .icon {
49
+ display: block;
50
+ font-size: 30px;
51
+ text-align: center;
52
+ min-width: 40px;
53
+ }
54
+ .text {
55
+ font-family: 'Poppins', Sans-serif;
56
+ font-family: var(--font-family);
57
+ font-size: var(--step-1);
58
+ text-align: start;
59
+ }
60
+ }
61
+ }
62
+ </style>