srcdev-nuxt-forms 3.0.0 → 4.1.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/assets/styles/ally/_utils.css +20 -0
  2. package/assets/styles/ally/_variables.css +8 -0
  3. package/assets/styles/ally/index.css +2 -0
  4. package/assets/styles/forms/index.css +2 -0
  5. package/assets/styles/forms/themes/_error.css +85 -0
  6. package/assets/styles/forms/themes/_ghost.css +85 -0
  7. package/assets/styles/forms/themes/_input-action-underlined.css +20 -0
  8. package/assets/styles/forms/themes/_input-action.css +20 -0
  9. package/assets/styles/forms/themes/_primary.css +92 -0
  10. package/assets/styles/forms/themes/_secondary.css +85 -0
  11. package/assets/styles/forms/themes/_success.css +85 -0
  12. package/assets/styles/forms/themes/_tertiary.css +85 -0
  13. package/assets/styles/forms/themes/_warning.css +85 -0
  14. package/assets/styles/forms/themes/index.css +9 -0
  15. package/assets/styles/forms/variables/_sizes.css +71 -0
  16. package/assets/styles/forms/variables/_theme.css +11 -0
  17. package/assets/styles/forms/variables/index.css +2 -0
  18. package/assets/styles/main.css +5 -0
  19. package/assets/styles/typography/index.css +2 -0
  20. package/assets/styles/typography/utils/_font-classes.css +190 -0
  21. package/assets/styles/typography/utils/_weights.css +69 -0
  22. package/assets/styles/typography/utils/index.css +2 -0
  23. package/assets/styles/typography/variables/_colors.css +14 -0
  24. package/assets/styles/typography/variables/_reponsive-font-size.css +10 -0
  25. package/assets/styles/typography/variables/index.css +2 -0
  26. package/assets/styles/utils/_margin-helpers.css +334 -0
  27. package/assets/styles/utils/_padding-helpers.css +308 -0
  28. package/assets/styles/utils/_page.css +49 -0
  29. package/assets/styles/utils/index.css +3 -0
  30. package/assets/styles/variables/colors/_blue.css +15 -0
  31. package/assets/styles/variables/colors/_gray.css +16 -0
  32. package/assets/styles/variables/colors/_green.css +15 -0
  33. package/assets/styles/variables/colors/_orange.css +15 -0
  34. package/assets/styles/variables/colors/_red.css +15 -0
  35. package/assets/styles/variables/colors/_yellow.css +15 -0
  36. package/assets/styles/variables/colors/colors.css +6 -0
  37. package/assets/styles/variables/index.css +1 -0
  38. package/components/forms/c12/prop-validators/index.ts +38 -0
  39. package/components/forms/c12/utils.ts +14 -0
  40. package/components/forms/form-errors/InputError.vue +172 -0
  41. package/components/forms/form-errors/tests/InputError.spec.ts +67 -0
  42. package/components/forms/input-button/InputButtonCore.vue +191 -0
  43. package/components/forms/input-button/variants/InputButtonConfirm.vue +66 -0
  44. package/components/forms/input-button/variants/InputButtonSubmit.vue +62 -0
  45. package/components/forms/input-checkbox/MultipleCheckboxes.vue +203 -0
  46. package/components/forms/input-checkbox/SingleCheckbox.vue +169 -0
  47. package/components/forms/input-checkbox/tests/MultipleCheckboxes.spec.ts +98 -0
  48. package/components/forms/input-checkbox/tests/data/tags.json +67 -0
  49. package/components/forms/input-checkbox-radio/InputCheckboxRadioButton.vue +214 -0
  50. package/components/forms/input-checkbox-radio/InputCheckboxRadioCore.vue +191 -0
  51. package/components/forms/input-checkbox-radio/InputCheckboxRadioWithLabel.vue +111 -0
  52. package/components/forms/input-number/InputNumberCore.vue +203 -0
  53. package/components/forms/input-number/variants/InputNumberDefault.vue +154 -0
  54. package/components/forms/input-radio/MultipleRadiobuttons.vue +201 -0
  55. package/components/forms/input-radio/tests/MultipleRadioButtons.spec.ts +89 -0
  56. package/components/forms/input-radio/tests/data/tags.json +67 -0
  57. package/components/forms/input-range/InputRangeCore.vue +274 -0
  58. package/components/forms/input-range/variants/InputRangeDefault.vue +156 -0
  59. package/components/forms/input-range-fancy/InputRangeFancyCore.vue +450 -0
  60. package/components/forms/input-range-fancy/InputRangeFancyWithLabel.vue +124 -0
  61. package/components/forms/input-select/InputSelect.vue +289 -0
  62. package/components/forms/input-text/InputTextCore.vue +331 -0
  63. package/components/forms/input-text/variants/InputPasswordWithLabel.vue +130 -0
  64. package/components/forms/input-text/variants/InputTextAsNumberWithLabel.vue +187 -0
  65. package/components/forms/input-text/variants/InputTextWithLabel.vue +298 -0
  66. package/components/forms/input-textarea/InputTextareaCore.vue +234 -0
  67. package/components/forms/input-textarea/variants/InputTextareaWithLabel.vue +267 -0
  68. package/components/forms/toggle-switch/ToggleSwitchCore.vue +198 -0
  69. package/components/forms/toggle-switch/ToggleSwitchCoreOld.vue +216 -0
  70. package/components/forms/toggle-switch/variants/ToggleSwitchWithLabel.vue +105 -0
  71. package/components/forms/toggle-switch/variants/ToggleSwitchWithLabelInline.vue +102 -0
  72. package/components/forms/ui/FormField.vue +78 -0
  73. package/components/forms/ui/FormWrapper.vue +35 -0
  74. package/components/utils/colour-scheme-select/ColourSchemeSelect.vue +270 -0
  75. package/components/utils/colour-scheme-select/ColourSchemeSelectOld.vue +225 -0
  76. package/components/utils/dark-mode-switcher/DarkModeSwitcher.vue +47 -0
  77. package/composables/useApiRequest.ts +25 -0
  78. package/composables/useColourScheme.ts +25 -0
  79. package/composables/useErrorMessages.ts +59 -0
  80. package/composables/useFormControl.ts +248 -0
  81. package/composables/useSleep.ts +5 -0
  82. package/composables/useStyleClassPassthrough.ts +30 -0
  83. package/composables/useZodValidation.ts +148 -0
  84. package/nuxt.config.ts +0 -3
  85. package/package.json +1 -1
  86. package/types/types.forms.ts +217 -0
  87. package/types/types.zodFormControl.ts +21 -0
@@ -0,0 +1,234 @@
1
+ <template>
2
+ <div
3
+ class="input-textarea-wrapper"
4
+ :data-form-theme="formTheme"
5
+ :data-size="size"
6
+ :class="[inputVariant, { dirty: isDirty }, { active: isActive }, { error: fieldHasError }, { 'has-left-slot': hasLeftSlot }, { 'has-right-slot': hasRightSlot }]"
7
+ >
8
+ <span v-if="hasLeftSlot" class="slot left-slot">
9
+ <slot name="left"></slot>
10
+ </span>
11
+
12
+ <textarea
13
+ :maxlength
14
+ :placeholder
15
+ :id
16
+ :name
17
+ :required
18
+ :class="['input-textarea-core', elementClasses, { dirty: isDirty }, { active: isActive }]"
19
+ v-model="modelValue"
20
+ ref="inputField"
21
+ :aria-invalid="fieldHasError"
22
+ :aria-describedby="`${id}-error-message`"
23
+ @focusin="updateFocus(true)"
24
+ @focusout="updateFocus(false)"
25
+ ></textarea>
26
+
27
+ <span v-if="hasRightSlot" class="slot right-slot">
28
+ <slot name="right"></slot>
29
+ </span>
30
+ </div>
31
+ </template>
32
+
33
+ <script setup lang="ts">
34
+ import propValidators from '../c12/prop-validators';
35
+ const props = defineProps({
36
+ maxlength: {
37
+ type: Number,
38
+ default: 255,
39
+ },
40
+ id: {
41
+ type: String,
42
+ required: true,
43
+ },
44
+ name: {
45
+ type: String,
46
+ required: true,
47
+ },
48
+ placeholder: {
49
+ type: String,
50
+ default: '',
51
+ },
52
+ fieldHasError: {
53
+ type: Boolean,
54
+ default: false,
55
+ },
56
+ required: {
57
+ type: Boolean,
58
+ default: false,
59
+ },
60
+ styleClassPassthrough: {
61
+ type: Array as PropType<string[]>,
62
+ default: () => [],
63
+ },
64
+ theme: {
65
+ type: String as PropType<string>,
66
+ default: 'primary',
67
+ validator(value: string) {
68
+ return propValidators.theme.includes(value);
69
+ },
70
+ },
71
+ size: {
72
+ type: String as PropType<string>,
73
+ default: 'normal',
74
+ validator(value: string) {
75
+ return propValidators.size.includes(value);
76
+ },
77
+ },
78
+ inputVariant: {
79
+ type: String as PropType<string>,
80
+ default: 'normal',
81
+ validator(value: string) {
82
+ return propValidators.inputVariant.includes(value);
83
+ },
84
+ },
85
+ });
86
+
87
+ const slots = useSlots();
88
+ const hasLeftSlot = computed(() => slots.left !== undefined);
89
+ const hasRightSlot = computed(() => slots.right !== undefined);
90
+
91
+ const formTheme = computed(() => {
92
+ return props.fieldHasError ? 'error' : props.theme;
93
+ });
94
+
95
+ const modelValue = defineModel<string | number | readonly string[] | null | undefined>();
96
+ const isDirty = defineModel('isDirty');
97
+ const isActive = defineModel('isActive');
98
+
99
+ const updateFocus = (isFocused: boolean) => {
100
+ isActive.value = isFocused;
101
+ };
102
+
103
+ const inputField = ref<HTMLInputElement | null>(null);
104
+
105
+ const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
106
+ </script>
107
+
108
+ <style lang="css">
109
+ .input-textarea-wrapper {
110
+ --_focus-box-shadow: var(--box-shadow-off);
111
+ --_input-textarea-core-color: var(--theme-form-input-text-color-normal);
112
+
113
+ --_input-textarea-wrapper-background-color: var(--theme-form-input-bg-normal);
114
+
115
+ --_input-textarea-wrapper-border: var(--form-element-border-width) solid var(--theme-form-input-border);
116
+ --_input-textarea-wrapper-border-radius: var(--form-input-border-radius);
117
+
118
+ --_input-textarea-wrapper-outline: var(--form-element-outline-width) solid var(--theme-form-input-outline);
119
+ --_input-textarea-wrapper-opacity: 1;
120
+ --_input-textarea-wrapper-box-shadow: var(--_focus-box-shadow);
121
+ --_input-textarea-wrapper-margin-inline: 0;
122
+ --_input-textarea-wrapper-padding-block: 0;
123
+
124
+ &.underlined {
125
+ --_input-textarea-core-color: var(--theme-form-input-text-color-underlined);
126
+ --_input-textarea-wrapper-background-color: transparent;
127
+ --_input-textarea-wrapper-padding-block: 0.5rem;
128
+ }
129
+
130
+ &.outlined {
131
+ --_input-textarea-wrapper-margin-inline: 1px;
132
+ --_input-textarea-wrapper-background-color: var(--theme-form-input-bg-outlined);
133
+ }
134
+
135
+ &.normal {
136
+ &:focus-within {
137
+ --_input-textarea-wrapper-box-shadow: var(--box-shadow-on);
138
+ --_input-textarea-wrapper-outline: var(--form-element-outline-width) solid hsl(from var(--theme-form-input-outline-focus) h s 90%);
139
+ }
140
+ }
141
+
142
+ &:not(.normal) {
143
+ --_input-textarea-wrapper-border: none;
144
+ --_input-textarea-wrapper-box-shadow: none;
145
+ --_input-textarea-wrapper-outline: none;
146
+ --_input-textarea-wrapper-opacity: 0;
147
+
148
+ &:focus {
149
+ --_input-textarea-wrapper-border: none;
150
+ --_input-textarea-wrapper-box-shadow: none;
151
+ --_input-textarea-wrapper-outline: none;
152
+ --_input-textarea-wrapper-background-color: transparent;
153
+ }
154
+
155
+ &:focus-within {
156
+ --_input-textarea-wrapper-border: none;
157
+ --_input-textarea-wrapper-box-shadow: none;
158
+ --_input-textarea-wrapper-outline: none;
159
+ --_input-textarea-wrapper-background-color: transparent;
160
+ }
161
+
162
+ &.active,
163
+ &.dirty {
164
+ --_input-textarea-wrapper-border: none;
165
+ --_input-textarea-wrapper-box-shadow: none;
166
+ --_input-textarea-wrapper-outline: none;
167
+ --_input-textarea-wrapper-background-color: transparent;
168
+
169
+ --_input-textarea-wrapper-opacity: 1;
170
+ }
171
+ }
172
+
173
+ display: flex;
174
+ align-items: center;
175
+
176
+ background-color: var(--_input-textarea-wrapper-background-color);
177
+ border-radius: var(--_input-textarea-wrapper-border-radius);
178
+ border: var(--_input-textarea-wrapper-border);
179
+ outline: var(--_input-textarea-wrapper-outline);
180
+ box-shadow: var(--_input-textarea-wrapper-box-shadow);
181
+ opacity: var(--_input-textarea-wrapper-opacity);
182
+
183
+ margin-inline: var(--_input-textarea-wrapper-margin-inline);
184
+ padding-block: var(--_input-textarea-wrapper-padding-block);
185
+
186
+ &:not(.normal) {
187
+ transition: opacity 0.2s ease-in-out;
188
+ }
189
+
190
+ &.has-left-slot {
191
+ .left-slot {
192
+ display: flex;
193
+ align-items: center;
194
+ margin-inline-start: 1rem;
195
+ }
196
+ }
197
+
198
+ &.has-right-slot {
199
+ .right-slot {
200
+ display: flex;
201
+ align-items: center;
202
+ margin-inline-end: 1rem;
203
+ }
204
+ }
205
+
206
+ .input-textarea-core {
207
+ background-color: transparent;
208
+ border: none;
209
+ outline: none;
210
+ box-shadow: none;
211
+ flex-grow: 1;
212
+
213
+ min-height: 4lh;
214
+ field-sizing: content;
215
+
216
+ color: var(--_input-textarea-core-color);
217
+ font-family: var(--font-family);
218
+ font-size: var(--form-element-font-size);
219
+ line-height: var(--form-element-line-height);
220
+
221
+ padding-inline: var(--form-text-padding-inline);
222
+ padding-block-start: var(--form-element-padding-block-start);
223
+ padding-block-end: var(--form-element-padding-block-end);
224
+
225
+ &::placeholder,
226
+ &::-webkit-input-placeholder {
227
+ font-family: var(--font-family);
228
+ font-size: var(--form-element-font-size);
229
+ font-style: italic;
230
+ font-weight: 400;
231
+ }
232
+ }
233
+ }
234
+ </style>
@@ -0,0 +1,267 @@
1
+ <template>
2
+ <div class="input-textarea-with-label" :data-form-theme="formTheme" :class="[elementClasses, inputVariant, { dirty: isDirty }, { active: isActive }]">
3
+ <label :for="id" class="input-textarea-label body-normal-semibold">{{ label }}</label>
4
+
5
+ <InputTextareaCore
6
+ v-model="modelValue"
7
+ v-model:isDirty="isDirty"
8
+ v-model:isActive="isActive"
9
+ :maxlength
10
+ :id
11
+ :name
12
+ :placeholder
13
+ :label
14
+ :fieldHasError
15
+ :required
16
+ :styleClassPassthrough
17
+ :theme
18
+ :size
19
+ :inputVariant
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
+ </InputTextareaCore>
28
+ <InputError :errorMessage :showError="fieldHasError" :id :isDetached="false" />
29
+ </div>
30
+ </template>
31
+
32
+ <script setup lang="ts">
33
+ import propValidators from '../../c12/prop-validators';
34
+ const props = defineProps({
35
+ maxlength: {
36
+ type: Number,
37
+ default: 255,
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
+ size: {
75
+ type: String as PropType<string>,
76
+ default: 'normal',
77
+ validator(value: string) {
78
+ return propValidators.size.includes(value);
79
+ },
80
+ },
81
+ inputVariant: {
82
+ type: String as PropType<string>,
83
+ default: 'normal',
84
+ validator(value: string) {
85
+ return propValidators.inputVariant.includes(value);
86
+ },
87
+ },
88
+ });
89
+
90
+ const slots = useSlots();
91
+ const hasLeftSlot = computed(() => slots.left !== undefined);
92
+ const hasRightSlot = computed(() => slots.right !== undefined);
93
+
94
+ const id = useId();
95
+ const formTheme = computed(() => {
96
+ return props.fieldHasError ? 'error' : props.theme;
97
+ });
98
+
99
+ const modelValue = defineModel<string | number | readonly string[] | null | undefined>();
100
+ const isActive = ref<boolean>(false);
101
+ const isDirty = ref<boolean>(false);
102
+
103
+ const { elementClasses, updateElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
104
+
105
+ const testDirty = () => {
106
+ const watchValue = modelValue.value ?? '';
107
+
108
+ if (!isDirty.value && typeof watchValue === 'string' && watchValue.length > 0) {
109
+ isDirty.value = true;
110
+ }
111
+ };
112
+
113
+ onMounted(() => {
114
+ testDirty();
115
+ });
116
+
117
+ watch(
118
+ () => modelValue.value,
119
+ () => {
120
+ testDirty();
121
+ }
122
+ );
123
+ </script>
124
+
125
+ <style lang="css">
126
+ .input-textarea-with-label {
127
+ --_input-textarea-with-label-margin-block-start: 0;
128
+ --_input-textarea-with-label-background-color: transparent;
129
+
130
+ --_input-textarea-wrapper-border: var(--form-element-border-width) solid var(--theme-form-input-border);
131
+ --_input-textarea-wrapper-border-radius: var(--form-input-border-radius);
132
+ --_input-textarea-wrapper-border-underlined: var(--form-element-border-width-underlined) solid var(--theme-form-input-border);
133
+ --_input-textarea-wrapper-outline: var(--form-element-outline-width) solid var(--theme-form-input-outline);
134
+ --_input-textarea-wrapper-box-shadow: var(--_focus-box-shadow);
135
+ --_input-textarea-wrapper-padding-block: 0;
136
+
137
+ --_focus-box-shadow: var(--box-shadow-off);
138
+
139
+ /* Label vars */
140
+ --_label-textarea-color: var(--theme-form-input-text-label-color-normal);
141
+ --_label-textarea-margin-block: 0.8rem;
142
+ --_label-textarea-size: var(--step-2);
143
+ --_label-textarea-weight: normal;
144
+ --_label-textarea-height: auto;
145
+ --_label-textarea-line-height: 1.5;
146
+ --_label-textarea-background-color: var(--_input-textarea-with-label-background-color);
147
+
148
+ &.underlined {
149
+ --_label-textarea-color: var(--theme-form-input-text-label-color-underlined);
150
+ --_label-offset: 1rem 0;
151
+ --_input-textarea-with-label-background-color: color-mix(in srgb, currentColor 5%, transparent);
152
+
153
+ --_input-textarea-wrapper-underlined-border-radius-top-left: 0;
154
+ --_input-textarea-wrapper-underlined-border-radius-top-right: 0;
155
+ --_input-textarea-wrapper-underlined-border-radius-bottom-left: 4px;
156
+ --_input-textarea-wrapper-underlined-border-radius-bottom-right: 4px;
157
+
158
+ --_label-textarea-background-color: transparent;
159
+ --_label-textarea-height: fit-content;
160
+
161
+ &:has(.input-textarea-wrapper.active),
162
+ &:has(.input-textarea-wrapper.dirty) {
163
+ --_label-offset: 0 -3.2rem;
164
+ --_label-textarea-weight: bolder;
165
+ --_label-textarea-size: var(--step-1);
166
+ /* line-height: 1.5; */
167
+ /* padding: 0.2rem 1.2rem; */
168
+ }
169
+ }
170
+
171
+ &.outlined {
172
+ --_label-textarea-color: var(--theme-form-input-text-label-color-outlined);
173
+
174
+ --_label-offset: 1rem 0;
175
+ --_input-textarea-with-label-background-color: var(--theme-form-input-bg-normal);
176
+
177
+ --_input-textarea-wrapper-padding-block: 0.4em 0;
178
+ --_label-textarea-height: fit-content;
179
+
180
+ &:has(.input-textarea-wrapper.active),
181
+ &:has(.input-textarea-wrapper.dirty) {
182
+ --_label-offset: 1rem -2.8rem;
183
+ --_label-textarea-weight: normal;
184
+ /* --_label-textarea-size: var(--step-1); */
185
+ /* line-height: 1.5; */
186
+ /* padding: 0.2rem 1.2rem; */
187
+ }
188
+ }
189
+
190
+ &:not(.normal) {
191
+ --_input-textarea-with-label-margin-block-start: 3em;
192
+
193
+ &:has(.input-textarea-wrapper.active),
194
+ &:has(.input-textarea-wrapper.dirty) {
195
+ /* --_label-offset: 1rem -2.8rem; */
196
+ /* font-size: var(--step-1); */
197
+ /* line-height: 1.5; */
198
+ /* padding: 0.2rem 1.2rem; */
199
+ }
200
+
201
+ &:focus-within {
202
+ --_input-textarea-wrapper-box-shadow: var(--box-shadow-on);
203
+ --_input-textarea-wrapper-outline: var(--form-element-outline-width) solid hsl(from var(--theme-form-input-outline-focus) h s 90%);
204
+ }
205
+ }
206
+
207
+ /*
208
+ * Apply generic styles
209
+ **/
210
+
211
+ background-color: var(--_input-textarea-with-label-background-color);
212
+ border-radius: var(--_input-textarea-wrapper-border-radius);
213
+ /* overflow: clip; */
214
+
215
+ &.underlined {
216
+ border-bottom: var(--_input-textarea-wrapper-border-underlined);
217
+ border-top-left-radius: var(--_input-textarea-wrapper-underlined-border-radius-top-left);
218
+ border-top-right-radius: var(--_input-textarea-wrapper-underlined-border-radius-top-right);
219
+ border-bottom-left-radius: var(--_input-textarea-wrapper-underlined-border-radius-bottom-left);
220
+ border-bottom-right-radius: var(--_input-textarea-wrapper-underlined-border-radius-bottom-right);
221
+ }
222
+
223
+ &.outlined {
224
+ border: var(--_input-textarea-wrapper-border);
225
+ outline: var(--_input-textarea-wrapper-outline);
226
+ box-shadow: var(--_input-textarea-wrapper-box-shadow);
227
+ padding-block: var(--_input-textarea-wrapper-padding-block);
228
+ }
229
+
230
+ &:not(.normal) {
231
+ display: grid;
232
+ grid-template-columns: 1fr;
233
+ grid-template-rows: 1fr;
234
+ grid-template-areas: 'underlined-textarea-stack';
235
+
236
+ margin-block-start: var(--_input-textarea-with-label-margin-block-start);
237
+
238
+ .input-textarea-label {
239
+ grid-area: underlined-textarea-stack;
240
+ z-index: 2;
241
+ }
242
+ .input-textarea-wrapper {
243
+ grid-area: underlined-textarea-stack;
244
+ z-index: 1;
245
+ }
246
+ }
247
+
248
+ .input-textarea-label {
249
+ display: inline-block;
250
+ color: var(--_label-textarea-color);
251
+ background-color: var(--_label-textarea-background-color);
252
+ margin-block: var(--_label-textarea-margin-block);
253
+ font-size: var(--_label-textarea-size);
254
+ font-weight: var(--_label-textarea-weight);
255
+ line-height: var(--_label-textarea-line-height);
256
+ translate: var(--_label-offset);
257
+ width: fit-content;
258
+ height: var(--_label-textarea-height);
259
+ transition: font-size 0.2s ease-in-out, translate 0.2s ease-in-out;
260
+
261
+ &:not(.normal) {
262
+ display: flex;
263
+ align-items: center;
264
+ }
265
+ }
266
+ }
267
+ </style>
@@ -0,0 +1,198 @@
1
+ <template>
2
+ <div class="toggle-switch-core" :class="elementClasses" :data-size="size" :data-form-theme="formTheme">
3
+ <div @click="toggleSwitchValue" class="toggle-switch-wrapper" :class="[{ round }, { 'use-default-icons': useDefaultIcons }]" :for="inputId">
4
+ <input type="checkbox" v-model="modelValue" :true-value :false-value :aria-invalid="fieldHasError" :id="inputId" :aria-describedby="`${id}-description`" :name :required :checked="isChecked" />
5
+ <div class="symbol-wrapper" :class="[{ round }]">
6
+ <div class="symbol" :class="[{ round }]">
7
+ <div class="symbol-icon icon-on">
8
+ <slot name="iconOn">
9
+ <Icon name="material-symbols:circle-outline" class="icon" />
10
+ </slot>
11
+ </div>
12
+
13
+ <div class="symbol-icon icon-off">
14
+ <slot name="iconOff">
15
+ <Icon name="material-symbols:circle-outline" class="icon" />
16
+ </slot>
17
+ </div>
18
+ </div>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ import propValidators from '../c12/prop-validators';
26
+
27
+ const props = defineProps({
28
+ id: {
29
+ type: String,
30
+ required: true,
31
+ },
32
+ name: {
33
+ type: String,
34
+ required: true,
35
+ },
36
+ required: {
37
+ type: Boolean,
38
+ default: false,
39
+ },
40
+ fieldHasError: {
41
+ type: Boolean,
42
+ default: false,
43
+ },
44
+ trueValue: {
45
+ type: [String, Number, Boolean],
46
+ default: true,
47
+ },
48
+ falseValue: {
49
+ type: [String, Number, 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
+ round: {
64
+ type: Boolean,
65
+ default: true,
66
+ },
67
+ size: {
68
+ type: String as PropType<string>,
69
+ default: 'normal',
70
+ validator(value: string) {
71
+ return propValidators.size.includes(value);
72
+ },
73
+ },
74
+ ariaDescribedby: {
75
+ type: String,
76
+ default: null,
77
+ },
78
+ });
79
+
80
+ const slots = useSlots();
81
+ const hasIconOnSlot = computed(() => slots.iconOn !== undefined);
82
+ const hasIconOffSlot = computed(() => slots.iconOff !== undefined);
83
+ const useDefaultIcons = computed(() => !hasIconOnSlot.value && !hasIconOffSlot.value);
84
+
85
+ const formTheme = computed(() => {
86
+ return props.fieldHasError ? 'error' : props.theme;
87
+ });
88
+
89
+ const modelValue = defineModel();
90
+ const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
91
+
92
+ const inputId = computed(() => `toggle-sitch-${props.id}`);
93
+
94
+ const isChecked = computed(() => {
95
+ return modelValue.value === props.trueValue;
96
+ });
97
+
98
+ const toggleSwitchValue = () => {
99
+ modelValue.value = modelValue.value === props.trueValue ? props.falseValue : props.trueValue;
100
+ console.log(`toggleSwitchValue(${modelValue.value})`);
101
+ };
102
+ </script>
103
+
104
+ <style lang="css">
105
+ .toggle-switch-core {
106
+ .toggle-switch-label {
107
+ display: block;
108
+ }
109
+
110
+ .toggle-switch-wrapper {
111
+ --theme-form-toggle-border-color: var(--blue-12);
112
+ --theme-form-toggle-border-width: 0.1rem;
113
+ --theme-form-toggle-outline-color: var(--gray-2);
114
+ --theme-form-toggle-outline-width: 0.1rem;
115
+
116
+ --_transition-duration: 0.4s;
117
+ --_switch-padding: 0.2rem;
118
+ --_icon-color: inherit;
119
+ --_icon-on-opacity: 0;
120
+ --_icon-off-opacity: 1;
121
+ --_icon-font-size: 2.4rem;
122
+ --_symbol-size: 3.4rem;
123
+ --_symbol-background-color: var(--blue-12);
124
+ --_symbol-outline-color: transparent;
125
+ --_symbol-outline-width: 1px;
126
+ --_symbol-margin-inline-start: 0;
127
+ --_symbol-checked-offset: calc(var(--_symbol-size) * 0.75);
128
+
129
+ &.use-default-icons {
130
+ --_icon-color: transparent;
131
+ }
132
+
133
+ display: flex;
134
+ flex-direction: column;
135
+
136
+ input {
137
+ height: 0;
138
+ width: 0;
139
+ visibility: hidden;
140
+ }
141
+
142
+ &:has(input:checked) {
143
+ --_icon-on-opacity: 1;
144
+ --_icon-off-opacity: 0;
145
+ --_symbol-margin-inline-start: var(--_symbol-checked-offset);
146
+ }
147
+
148
+ .symbol-wrapper {
149
+ /* background: blue; */
150
+ border: var(--theme-form-toggle-border-width) solid var(--theme-form-toggle-border-color);
151
+ outline: var(--theme-form-toggle-outline-width) solid var(--theme-form-toggle-outline-color);
152
+ border-radius: calc(var(--_symbol-size) + calc(var(--theme-form-toggle-border-width) * 2) + calc(var(--_switch-padding) * 2));
153
+ display: inline-flex;
154
+ align-items: center;
155
+ justify-content: start;
156
+ width: calc(var(--_symbol-size) + var(--_symbol-checked-offset) + calc(var(--theme-form-toggle-border-width) * 2) + calc(var(--_switch-padding) * 2));
157
+ padding: var(--_switch-padding);
158
+
159
+ .symbol {
160
+ display: inline-grid;
161
+ grid-template-areas: 'icon';
162
+ place-content: center;
163
+
164
+ aspect-ratio: 1/1;
165
+ /* width: var(--_symbol-size); */
166
+ padding: calc(calc(var(--_symbol-size) - var(--_icon-font-size)) / 2);
167
+
168
+ outline: var(--_symbol-outline-width) solid var(--_symbol-outline-color);
169
+ border-radius: 50%;
170
+ margin-inline-start: var(--_symbol-margin-inline-start);
171
+
172
+ background-color: var(--_symbol-background-color);
173
+
174
+ overflow: clip;
175
+
176
+ transition: margin var(--_transition-duration);
177
+
178
+ .symbol-icon {
179
+ display: grid;
180
+ grid-area: icon;
181
+ place-content: center;
182
+
183
+ color: var(--_icon-color);
184
+ font-size: var(--_icon-font-size);
185
+ transition: opacity var(--_transition-duration);
186
+
187
+ &.icon-on {
188
+ opacity: var(--_icon-on-opacity);
189
+ }
190
+ &.icon-off {
191
+ opacity: var(--_icon-off-opacity);
192
+ }
193
+ }
194
+ }
195
+ }
196
+ }
197
+ }
198
+ </style>