@flitsmeister/design-system 2.2.22 → 2.2.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/index.js +1 -0
- package/package.json +2 -1
- package/web/components/fmxButton.vue +16 -12
- package/web/components/fmxDropdown.vue +226 -0
- package/web/components/fmxInput.vue +139 -35
package/README.md
CHANGED
|
@@ -31,11 +31,13 @@ Design tokens are exported as platform-specific files in `tokens/schema_exports/
|
|
|
31
31
|
You can import individual Vue components directly from the package:
|
|
32
32
|
|
|
33
33
|
```js
|
|
34
|
-
import { fmxButton, fmxInput } from "@flitsmeister/design-system";
|
|
34
|
+
import { fmxButton, fmxInput, fmxDropdown } from "@flitsmeister/design-system";
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
These are standard Vue 3 single-file components. Register and use them in your app as you would any other Vue component.
|
|
38
38
|
|
|
39
|
+
**fmxDropdown** is a v-model select component. Pass `options` as an array of `{ value, label }`, and bind `v-model` to the selected value. Use the `option` slot to customize option rendering. Set `fullScreen` to true for a mobile-friendly modal overlay.
|
|
40
|
+
|
|
39
41
|
### 3. Using Icons
|
|
40
42
|
|
|
41
43
|
SVG icon files are available via the `icons/` export path. You can import any icon directly:
|
|
@@ -52,7 +54,7 @@ You can also reference icons by their path for use in `<img>`, `<svg>`, or as as
|
|
|
52
54
|
|
|
53
55
|
## About the Design System
|
|
54
56
|
|
|
55
|
-
- Provides Flitsmeister design system colors, typography, buttons, icons, and
|
|
57
|
+
- Provides Flitsmeister design system colors, typography, buttons, icons, input, and dropdown components.
|
|
56
58
|
- Design tokens are managed and exported from Figma, with automated scripts generating platform-specific outputs (CSS, iOS JSON, Android XML, etc.).
|
|
57
59
|
- See the `/tokens/schema_exports/` directory for all available exports.
|
|
58
60
|
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flitsmeister/design-system",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.33",
|
|
4
4
|
"description": "Flitsmeister design system and demo site",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./index.js",
|
|
8
8
|
"./fmxInput": "./web/components/fmxInput.vue",
|
|
9
9
|
"./fmxButton": "./web/components/fmxButton.vue",
|
|
10
|
+
"./fmxDropdown": "./web/components/fmxDropdown.vue",
|
|
10
11
|
"./main.css": "./dist-lib/main.css",
|
|
11
12
|
"./themes/*": "./tokens/schema_exports/*",
|
|
12
13
|
"./components/*": "./web/components/*",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<button
|
|
3
|
+
:type="type"
|
|
3
4
|
:class="buttonClasses"
|
|
4
5
|
:title="title"
|
|
5
6
|
:href="href"
|
|
@@ -9,23 +10,26 @@
|
|
|
9
10
|
:data-buttontype="buttontype"
|
|
10
11
|
:data-size="size"
|
|
11
12
|
>
|
|
12
|
-
<
|
|
13
|
+
<Icon
|
|
13
14
|
v-if="icon && iconPosition === 'prepend'"
|
|
14
|
-
class="
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
icon-class="mr-2"
|
|
16
|
+
:name="icon"
|
|
17
|
+
/>
|
|
17
18
|
<slot></slot>
|
|
18
|
-
<
|
|
19
|
+
<Icon
|
|
19
20
|
v-if="icon && iconPosition === 'append'"
|
|
20
|
-
class="
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
icon-class="ml-2"
|
|
22
|
+
:name="icon"
|
|
23
|
+
/>
|
|
23
24
|
</button>
|
|
24
25
|
</template>
|
|
25
26
|
|
|
26
27
|
<script>
|
|
28
|
+
import Icon from "./Icon.vue";
|
|
29
|
+
|
|
27
30
|
export default {
|
|
28
31
|
name: "fmxButton",
|
|
32
|
+
components: { Icon },
|
|
29
33
|
props: {
|
|
30
34
|
type: {
|
|
31
35
|
type: String,
|
|
@@ -71,6 +75,10 @@
|
|
|
71
75
|
validator: (value) =>
|
|
72
76
|
["filled", "text", "outline", "squared"].includes(value),
|
|
73
77
|
},
|
|
78
|
+
/**
|
|
79
|
+
* Optional icon: symbol id for the SVG sprite.
|
|
80
|
+
* The consuming app must inject an SVG sprite containing symbols with these ids (e.g. via Vite/Webpack sprite plugin).
|
|
81
|
+
*/
|
|
74
82
|
icon: {
|
|
75
83
|
type: String,
|
|
76
84
|
default: "",
|
|
@@ -259,10 +267,6 @@
|
|
|
259
267
|
}
|
|
260
268
|
}
|
|
261
269
|
|
|
262
|
-
.icon {
|
|
263
|
-
margin: 0 0.5rem;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
270
|
@keyframes horizontal-shaking {
|
|
267
271
|
0% {
|
|
268
272
|
transform: translateX(0);
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="fmx-dropdown relative" ref="dropdownRoot">
|
|
3
|
+
<label
|
|
4
|
+
v-if="label"
|
|
5
|
+
class="block px-1 left-2.5 text-content-defaultalt text-lg mb-1"
|
|
6
|
+
>
|
|
7
|
+
{{ label }}
|
|
8
|
+
</label>
|
|
9
|
+
<button
|
|
10
|
+
type="button"
|
|
11
|
+
:disabled="disabled"
|
|
12
|
+
:aria-expanded="isOpen"
|
|
13
|
+
aria-haspopup="listbox"
|
|
14
|
+
class="fmx-dropdown-trigger w-full bg-surface-defaulthighest text-content-default text-lg leading-snug border-2 rounded-2xl px-3 py-3 h-[60px] focus:outline-none focus:border-out hover:border-outline-brand inline-flex items-center justify-between disabled:opacity-50 disabled:cursor-not-allowed"
|
|
15
|
+
:class="[triggerClass, { 'fmx-dropdown-trigger--just-changed': justChanged }]"
|
|
16
|
+
@click="toggle"
|
|
17
|
+
>
|
|
18
|
+
<span class="truncate text-left flex-1">
|
|
19
|
+
{{ selectedLabel }}
|
|
20
|
+
</span>
|
|
21
|
+
<svg
|
|
22
|
+
class="fmx-dropdown-chevron h-5 w-5 flex-shrink-0 ml-2 transition-transform"
|
|
23
|
+
:class="{ 'rotate-180': isOpen }"
|
|
24
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
25
|
+
viewBox="0 0 20 20"
|
|
26
|
+
fill="currentColor"
|
|
27
|
+
aria-hidden="true"
|
|
28
|
+
>
|
|
29
|
+
<path
|
|
30
|
+
fill-rule="evenodd"
|
|
31
|
+
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
|
32
|
+
clip-rule="evenodd"
|
|
33
|
+
/>
|
|
34
|
+
</svg>
|
|
35
|
+
</button>
|
|
36
|
+
|
|
37
|
+
<transition name="fmx-dropdown">
|
|
38
|
+
<div
|
|
39
|
+
v-if="isOpen && !fullScreen"
|
|
40
|
+
class="fmx-dropdown-panel absolute left-0 right-0 mt-2 rounded-2xl shadow-lg bg-surface-defaulthighest border-2 border-surface-neutral-default overflow-hidden z-10"
|
|
41
|
+
role="listbox"
|
|
42
|
+
>
|
|
43
|
+
<div class="py-1 max-h-[250px] overflow-y-auto">
|
|
44
|
+
<button
|
|
45
|
+
v-for="opt in options"
|
|
46
|
+
:key="opt.value"
|
|
47
|
+
type="button"
|
|
48
|
+
role="option"
|
|
49
|
+
:aria-selected="opt.value === modelValue"
|
|
50
|
+
class="fmx-dropdown-option block w-full text-left px-4 py-3 text-lg text-content-default hover:bg-surface-defaulthigher focus:bg-surface-defaulthigher focus:outline-none"
|
|
51
|
+
:class="{ 'bg-surface-defaulthigher': opt.value === modelValue }"
|
|
52
|
+
@click="select(opt)"
|
|
53
|
+
>
|
|
54
|
+
<slot name="option" :option="opt" :selected="opt.value === modelValue">
|
|
55
|
+
{{ opt.label }}
|
|
56
|
+
</slot>
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</transition>
|
|
61
|
+
|
|
62
|
+
<Teleport to="body">
|
|
63
|
+
<transition name="fmx-dropdown-modal">
|
|
64
|
+
<div
|
|
65
|
+
v-if="isOpen && fullScreen"
|
|
66
|
+
class="fmx-dropdown-overlay fixed inset-0 z-50 flex items-center justify-center backdrop-blur-sm bg-white/30"
|
|
67
|
+
@click.self="close"
|
|
68
|
+
>
|
|
69
|
+
<div
|
|
70
|
+
class="fmx-dropdown-modal-panel bg-surface-defaulthighest rounded-2xl shadow-lg border-2 border-surface-neutral-default overflow-hidden w-[min(300px,90vw)] max-h-[80vh] flex flex-col"
|
|
71
|
+
role="listbox"
|
|
72
|
+
>
|
|
73
|
+
<div class="py-1 overflow-y-auto max-h-[250px]">
|
|
74
|
+
<button
|
|
75
|
+
v-for="opt in options"
|
|
76
|
+
:key="opt.value"
|
|
77
|
+
type="button"
|
|
78
|
+
role="option"
|
|
79
|
+
:aria-selected="opt.value === modelValue"
|
|
80
|
+
class="fmx-dropdown-option block w-full text-left px-4 py-3 text-lg text-content-default hover:bg-surface-defaulthigher focus:bg-surface-defaulthigher focus:outline-none"
|
|
81
|
+
:class="{ 'bg-surface-defaulthigher': opt.value === modelValue }"
|
|
82
|
+
@click="select(opt)"
|
|
83
|
+
>
|
|
84
|
+
<slot name="option" :option="opt" :selected="opt.value === modelValue">
|
|
85
|
+
{{ opt.label }}
|
|
86
|
+
</slot>
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</transition>
|
|
92
|
+
</Teleport>
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
<script>
|
|
97
|
+
export default {
|
|
98
|
+
name: 'fmxDropdown',
|
|
99
|
+
props: {
|
|
100
|
+
modelValue: {
|
|
101
|
+
type: [String, Number],
|
|
102
|
+
default: null,
|
|
103
|
+
},
|
|
104
|
+
options: {
|
|
105
|
+
type: Array,
|
|
106
|
+
default: () => [],
|
|
107
|
+
validator(opts) {
|
|
108
|
+
return opts.every((o) => o && typeof o.value !== 'undefined' && typeof o.label === 'string')
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
label: {
|
|
112
|
+
type: String,
|
|
113
|
+
default: '',
|
|
114
|
+
},
|
|
115
|
+
placeholder: {
|
|
116
|
+
type: String,
|
|
117
|
+
default: '',
|
|
118
|
+
},
|
|
119
|
+
disabled: {
|
|
120
|
+
type: Boolean,
|
|
121
|
+
default: false,
|
|
122
|
+
},
|
|
123
|
+
triggerClass: {
|
|
124
|
+
type: [String, Array, Object],
|
|
125
|
+
default: '',
|
|
126
|
+
},
|
|
127
|
+
fullScreen: {
|
|
128
|
+
type: Boolean,
|
|
129
|
+
default: false,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
emits: ['update:modelValue'],
|
|
133
|
+
data() {
|
|
134
|
+
return {
|
|
135
|
+
isOpen: false,
|
|
136
|
+
justChanged: false,
|
|
137
|
+
changeTimeout: null,
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
computed: {
|
|
141
|
+
selectedLabel() {
|
|
142
|
+
const opt = this.options.find((o) => o.value === this.modelValue)
|
|
143
|
+
return opt ? opt.label : this.placeholder
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
methods: {
|
|
147
|
+
toggle() {
|
|
148
|
+
if (this.disabled) return
|
|
149
|
+
this.isOpen = !this.isOpen
|
|
150
|
+
},
|
|
151
|
+
close() {
|
|
152
|
+
this.isOpen = false
|
|
153
|
+
},
|
|
154
|
+
select(opt) {
|
|
155
|
+
const changed = opt.value !== this.modelValue
|
|
156
|
+
this.$emit('update:modelValue', opt.value)
|
|
157
|
+
this.isOpen = false
|
|
158
|
+
if (!changed) return
|
|
159
|
+
if (this.changeTimeout) clearTimeout(this.changeTimeout)
|
|
160
|
+
this.justChanged = true
|
|
161
|
+
this.changeTimeout = setTimeout(() => {
|
|
162
|
+
this.justChanged = false
|
|
163
|
+
this.changeTimeout = null
|
|
164
|
+
}, 200)
|
|
165
|
+
},
|
|
166
|
+
handleClickOutside(e) {
|
|
167
|
+
if (this.$refs.dropdownRoot && !this.$refs.dropdownRoot.contains(e.target)) {
|
|
168
|
+
this.isOpen = false
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
mounted() {
|
|
173
|
+
document.addEventListener('click', this.handleClickOutside)
|
|
174
|
+
},
|
|
175
|
+
beforeUnmount() {
|
|
176
|
+
document.removeEventListener('click', this.handleClickOutside)
|
|
177
|
+
if (this.changeTimeout) clearTimeout(this.changeTimeout)
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
</script>
|
|
181
|
+
|
|
182
|
+
<style lang="scss" scoped>
|
|
183
|
+
.fmx-dropdown-trigger {
|
|
184
|
+
border-color: var(--color-surface-neutral-default);
|
|
185
|
+
transition: background-color 0.2s ease-out, border-color 0.2s ease-out;
|
|
186
|
+
}
|
|
187
|
+
.fmx-dropdown-trigger--just-changed {
|
|
188
|
+
background-color: var(--color-surface-defaulthigher);
|
|
189
|
+
}
|
|
190
|
+
.fmx-dropdown-panel,
|
|
191
|
+
.fmx-dropdown-modal-panel {
|
|
192
|
+
border-color: var(--color-surface-neutral-default);
|
|
193
|
+
}
|
|
194
|
+
@media (prefers-reduced-motion: reduce) {
|
|
195
|
+
.fmx-dropdown-trigger {
|
|
196
|
+
transition-duration: 0.01ms;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
</style>
|
|
200
|
+
|
|
201
|
+
<style lang="scss">
|
|
202
|
+
.fmx-dropdown-enter-active,
|
|
203
|
+
.fmx-dropdown-leave-active {
|
|
204
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
205
|
+
}
|
|
206
|
+
.fmx-dropdown-enter-from,
|
|
207
|
+
.fmx-dropdown-leave-to {
|
|
208
|
+
opacity: 0;
|
|
209
|
+
transform: translateY(-4px);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.fmx-dropdown-modal-enter-active,
|
|
213
|
+
.fmx-dropdown-modal-leave-active {
|
|
214
|
+
&,
|
|
215
|
+
.fmx-dropdown-modal-panel {
|
|
216
|
+
transition: all 0.2s ease-out;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
.fmx-dropdown-modal-enter-from,
|
|
220
|
+
.fmx-dropdown-modal-leave-to {
|
|
221
|
+
opacity: 0;
|
|
222
|
+
.fmx-dropdown-modal-panel {
|
|
223
|
+
transform: scale(1.05) translateY(-12px);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
</style>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
class="inputgroup w-full
|
|
3
|
+
class="inputgroup w-full mb-6"
|
|
4
4
|
:class="[customClass]"
|
|
5
5
|
:data-inputgroup-type="type"
|
|
6
6
|
>
|
|
@@ -9,41 +9,91 @@
|
|
|
9
9
|
:type="type"
|
|
10
10
|
:value="modelValue"
|
|
11
11
|
@input="handleInput"
|
|
12
|
+
@focus="handleFocus"
|
|
12
13
|
@blur="handleBlur"
|
|
13
14
|
:disabled="disabled"
|
|
14
15
|
:readonly="readonly"
|
|
15
|
-
|
|
16
|
-
:
|
|
16
|
+
:maxlength="maxLength"
|
|
17
|
+
:autocomplete="autocomplete"
|
|
18
|
+
class="peer w-full bg-surface-defaulthighest text-content-default border-2 transition duration-300 ease focus:outline-none focus:border-out autofill:bg-surface-defaulthighest autofill:text-content-default"
|
|
19
|
+
:class="[
|
|
20
|
+
inputStateClass,
|
|
21
|
+
sizeConfig.input,
|
|
22
|
+
placeholder ? sizeConfig.inputWithPlaceholder : sizeConfig.inputWithoutPlaceholder,
|
|
23
|
+
inputClass,
|
|
24
|
+
]"
|
|
25
|
+
:style="{ letterSpacing: letterSpacing + 'em' }"
|
|
17
26
|
/>
|
|
18
27
|
<label
|
|
28
|
+
v-if="placeholder"
|
|
19
29
|
:class="[
|
|
20
|
-
'absolute px-1 left-2.5
|
|
21
|
-
|
|
30
|
+
'absolute px-1 left-2.5 text-content-defaultalt transition-all transform origin-left pointer-events-none',
|
|
31
|
+
sizeConfig.label,
|
|
32
|
+
sizeConfig.labelText,
|
|
33
|
+
sizeConfig.labelFocused,
|
|
34
|
+
labelFilledClass,
|
|
22
35
|
]"
|
|
23
36
|
>
|
|
24
37
|
{{ placeholder }}
|
|
25
38
|
</label>
|
|
26
39
|
<transition name="icon-scale">
|
|
27
40
|
<span
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
v-if="icon"
|
|
42
|
+
class="iconwrapper absolute"
|
|
43
|
+
:class="[sizeConfig.icon, iconClass]"
|
|
30
44
|
>
|
|
31
45
|
<Icon :name="icon" />
|
|
32
46
|
</span>
|
|
33
47
|
</transition>
|
|
34
48
|
</div>
|
|
35
49
|
<p
|
|
36
|
-
v-if="errorMessage"
|
|
50
|
+
v-if="errorMessage && !isFocused"
|
|
37
51
|
class="text-content-danger text-sm font-bold mt-1"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
</p>
|
|
52
|
+
v-html="errorMessage"
|
|
53
|
+
/>
|
|
41
54
|
</div>
|
|
42
55
|
</template>
|
|
43
56
|
|
|
44
57
|
<script>
|
|
45
58
|
import Icon from "./Icon.vue";
|
|
46
59
|
|
|
60
|
+
const SIZE_CONFIG = {
|
|
61
|
+
sm: {
|
|
62
|
+
input: "h-10 text-base rounded-2xl px-3 autofill:pt-5 autofill:pb-1",
|
|
63
|
+
inputWithPlaceholder: "pt-5 pb-1",
|
|
64
|
+
inputWithoutPlaceholder: "py-2 flex items-center justify-center",
|
|
65
|
+
label: "top-2.5",
|
|
66
|
+
labelText: "text-base",
|
|
67
|
+
labelFocused:
|
|
68
|
+
"peer-focus:top-[4px] peer-focus:left-2.5 peer-focus:text-xs peer-focus:font-bold peer-autofill:top-[4px] peer-autofill:left-2.5 peer-autofill:text-xs peer-autofill:font-bold",
|
|
69
|
+
labelFilled: "top-[4px] left-2.5 text-xs font-bold",
|
|
70
|
+
icon: "right-2 top-2.5 text-xl leading-none",
|
|
71
|
+
},
|
|
72
|
+
md: {
|
|
73
|
+
input:
|
|
74
|
+
"h-[60px] text-lg leading-snug rounded-2xl px-3 autofill:pt-6 autofill:pb-1",
|
|
75
|
+
inputWithPlaceholder: "pt-6 pb-1",
|
|
76
|
+
inputWithoutPlaceholder: "py-3 flex items-center justify-center",
|
|
77
|
+
label: "top-3.5",
|
|
78
|
+
labelText: "text-lg",
|
|
79
|
+
labelFocused:
|
|
80
|
+
"peer-focus:top-[6px] peer-focus:left-2.5 peer-focus:text-sm peer-focus:font-bold peer-autofill:top-[6px] peer-autofill:left-2.5 peer-autofill:text-sm peer-autofill:font-bold",
|
|
81
|
+
labelFilled: "top-[6px] left-2.5 text-sm font-bold",
|
|
82
|
+
icon: "right-2 top-4 text-2xl leading-none",
|
|
83
|
+
},
|
|
84
|
+
lg: {
|
|
85
|
+
input: "h-14 text-lg rounded-2xl px-4 autofill:pt-7 autofill:pb-1",
|
|
86
|
+
inputWithPlaceholder: "pt-7 pb-1",
|
|
87
|
+
inputWithoutPlaceholder: "py-4 flex items-center justify-center",
|
|
88
|
+
label: "top-4",
|
|
89
|
+
labelText: "text-lg",
|
|
90
|
+
labelFocused:
|
|
91
|
+
"peer-focus:top-[8px] peer-focus:left-2.5 peer-focus:text-sm peer-focus:font-bold peer-autofill:top-[8px] peer-autofill:left-2.5 peer-autofill:text-sm peer-autofill:font-bold",
|
|
92
|
+
labelFilled: "top-[8px] left-2.5 text-sm font-bold",
|
|
93
|
+
icon: "right-3 top-5 text-2xl leading-none",
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
47
97
|
export default {
|
|
48
98
|
name: "fmxInput",
|
|
49
99
|
components: {
|
|
@@ -60,7 +110,7 @@
|
|
|
60
110
|
},
|
|
61
111
|
placeholder: {
|
|
62
112
|
type: String,
|
|
63
|
-
default: "
|
|
113
|
+
default: "",
|
|
64
114
|
},
|
|
65
115
|
modelValue: {
|
|
66
116
|
type: [String, Number],
|
|
@@ -78,68 +128,121 @@
|
|
|
78
128
|
type: Boolean,
|
|
79
129
|
default: false,
|
|
80
130
|
},
|
|
131
|
+
maxLength: {
|
|
132
|
+
type: Number,
|
|
133
|
+
default: null,
|
|
134
|
+
},
|
|
135
|
+
letterSpacing: {
|
|
136
|
+
type: Number,
|
|
137
|
+
default: 0,
|
|
138
|
+
},
|
|
139
|
+
autocomplete: {
|
|
140
|
+
type: String,
|
|
141
|
+
default: "",
|
|
142
|
+
},
|
|
81
143
|
customClass: {
|
|
82
144
|
type: String,
|
|
83
145
|
default: "",
|
|
84
146
|
},
|
|
147
|
+
inputClass: {
|
|
148
|
+
type: [String, Array, Object],
|
|
149
|
+
default: "",
|
|
150
|
+
},
|
|
151
|
+
size: {
|
|
152
|
+
type: String,
|
|
153
|
+
default: "md",
|
|
154
|
+
validator: (value) => ["sm", "md", "lg"].includes(value),
|
|
155
|
+
},
|
|
85
156
|
},
|
|
157
|
+
emits: ["update:modelValue", "focus", "blur"],
|
|
86
158
|
data() {
|
|
87
159
|
return {
|
|
88
160
|
errorMessage: "",
|
|
89
161
|
icon: "",
|
|
90
162
|
iconClass: "text-current",
|
|
163
|
+
isFocused: false,
|
|
91
164
|
};
|
|
92
165
|
},
|
|
166
|
+
watch: {
|
|
167
|
+
modelValue: {
|
|
168
|
+
immediate: true,
|
|
169
|
+
handler(newValue) {
|
|
170
|
+
if (!this.isFocused && newValue?.length > 0) {
|
|
171
|
+
this.runValidation(newValue);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
93
176
|
computed: {
|
|
94
|
-
|
|
177
|
+
sizeConfig() {
|
|
178
|
+
return SIZE_CONFIG[this.size] || SIZE_CONFIG.md;
|
|
179
|
+
},
|
|
180
|
+
inputStateClass() {
|
|
95
181
|
return {
|
|
96
|
-
"border-outline-danger": this.errorMessage,
|
|
182
|
+
"border-outline-danger": this.errorMessage && !this.isFocused,
|
|
183
|
+
"border-outline-brand": this.isFocused,
|
|
97
184
|
"border-transparent focus:border-outline-brand hover:border-outline-brand":
|
|
98
185
|
!this.errorMessage,
|
|
99
186
|
};
|
|
100
187
|
},
|
|
101
|
-
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
this.modelValue && this.modelValue.length > 0,
|
|
106
|
-
};
|
|
188
|
+
labelFilledClass() {
|
|
189
|
+
return this.modelValue && this.modelValue.length > 0
|
|
190
|
+
? this.sizeConfig.labelFilled
|
|
191
|
+
: "";
|
|
107
192
|
},
|
|
108
193
|
},
|
|
109
194
|
methods: {
|
|
110
195
|
handleInput(event) {
|
|
111
|
-
|
|
196
|
+
const value = event.target.value;
|
|
197
|
+
this.$emit("update:modelValue", value);
|
|
198
|
+
},
|
|
199
|
+
handleFocus(event) {
|
|
200
|
+
this.isFocused = true;
|
|
201
|
+
this.iconClass = "";
|
|
202
|
+
this.icon = "";
|
|
203
|
+
this.$emit("focus", event);
|
|
112
204
|
},
|
|
113
|
-
handleBlur() {
|
|
205
|
+
handleBlur(event) {
|
|
206
|
+
this.isFocused = false;
|
|
114
207
|
this.runValidation(this.modelValue);
|
|
208
|
+
this.$emit("blur", event);
|
|
115
209
|
},
|
|
116
210
|
runValidation(value) {
|
|
117
|
-
for (
|
|
211
|
+
for (const rule of this.rules) {
|
|
118
212
|
const error = rule(value);
|
|
119
213
|
if (error) {
|
|
120
214
|
this.errorMessage = error;
|
|
121
215
|
this.iconClass = "text-content-danger";
|
|
122
216
|
this.icon = "stroke-warning";
|
|
123
217
|
return;
|
|
124
|
-
} else {
|
|
125
|
-
this.errorMessage = "";
|
|
126
|
-
this.iconClass = "text-content-brand";
|
|
127
|
-
this.icon = "bold-check";
|
|
128
218
|
}
|
|
219
|
+
this.errorMessage = "";
|
|
220
|
+
this.iconClass = "text-content-brand";
|
|
221
|
+
this.icon = "bold-check";
|
|
129
222
|
}
|
|
130
223
|
},
|
|
224
|
+
validate() {
|
|
225
|
+
this.runValidation(this.modelValue);
|
|
226
|
+
},
|
|
131
227
|
},
|
|
132
228
|
};
|
|
133
229
|
</script>
|
|
134
230
|
|
|
135
231
|
<style scoped>
|
|
232
|
+
.inputgroup {
|
|
233
|
+
min-width: 100px;
|
|
234
|
+
}
|
|
235
|
+
|
|
136
236
|
.icon-scale-enter-active,
|
|
137
237
|
.icon-scale-leave-active {
|
|
138
238
|
transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
|
139
239
|
}
|
|
140
|
-
|
|
141
|
-
|
|
240
|
+
|
|
241
|
+
.icon-scale-enter-from,
|
|
242
|
+
.icon-scale-leave-to {
|
|
243
|
+
transform: scale(0) translateX(100%);
|
|
142
244
|
}
|
|
245
|
+
|
|
143
246
|
.icon-scale-enter-to,
|
|
144
247
|
.icon-scale-leave-from {
|
|
145
248
|
transform: scale(1);
|
|
@@ -147,11 +250,12 @@
|
|
|
147
250
|
|
|
148
251
|
span.iconwrapper {
|
|
149
252
|
display: block;
|
|
150
|
-
height:
|
|
151
|
-
width:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
253
|
+
height: 1.5rem;
|
|
254
|
+
width: 1.5rem;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
span.iconwrapper svg {
|
|
258
|
+
width: 100%;
|
|
259
|
+
height: 100%;
|
|
156
260
|
}
|
|
157
261
|
</style>
|