@flitsmeister/design-system 2.2.30 → 2.2.34
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 +10 -4
- package/index.js +1 -0
- package/package.json +2 -1
- package/web/components/fmxButton.vue +18 -22
- package/web/components/fmxDropdown.vue +226 -0
- package/web/components/fmxInput.vue +199 -37
package/README.md
CHANGED
|
@@ -28,13 +28,19 @@ Design tokens are exported as platform-specific files in `tokens/schema_exports/
|
|
|
28
28
|
|
|
29
29
|
### 2. Using Vue Components
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Import the components you need:
|
|
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
|
+
Register and use them as standard Vue 3 single-file components. Overview of each:
|
|
38
|
+
|
|
39
|
+
- **fmxButton** — Button with multiple visual variants. Props: `role` (e.g. primary, secondary, success, warning, danger, info, strong, weak), `buttontype` (filled, text, outline, squared), `size` (sm, md, lg, xl), `icon` (symbol id for your SVG sprite), `iconPosition` (prepend, append), `disabled`, `customClasses`. Use `href` and `target` for link-style buttons. If you use `icon`, the consuming app must provide an SVG sprite that includes the referenced symbol ids.
|
|
40
|
+
|
|
41
|
+
- **fmxInput** — v-model text-style input with optional validation and floating placeholder. Props: `placeholder`, `rules` (array of validator functions), `customClass` (wrapper spacing/layout), `inputClass` (on the input element). To show a visible default border, set on a parent: `--fmx-input-border-default: var(--color-surface-neutral-default);` (focus and error states stay correct). To validate from a parent (e.g. on submit), use a ref and call `validate()`.
|
|
42
|
+
|
|
43
|
+
- **fmxDropdown** — v-model select. 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.
|
|
38
44
|
|
|
39
45
|
### 3. Using Icons
|
|
40
46
|
|
|
@@ -52,7 +58,7 @@ You can also reference icons by their path for use in `<img>`, `<svg>`, or as as
|
|
|
52
58
|
|
|
53
59
|
## About the Design System
|
|
54
60
|
|
|
55
|
-
- Provides Flitsmeister design system colors, typography, buttons, icons, and
|
|
61
|
+
- Provides Flitsmeister design system colors, typography, buttons, icons, input, and dropdown components.
|
|
56
62
|
- Design tokens are managed and exported from Figma, with automated scripts generating platform-specific outputs (CSS, iOS JSON, Android XML, etc.).
|
|
57
63
|
- See the `/tokens/schema_exports/` directory for all available exports.
|
|
58
64
|
|
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.34",
|
|
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/*",
|
|
@@ -120,12 +120,12 @@
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/*
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
SIZES
|
|
124
|
+
sm: 'text-base h-10 px-2 font-medium',
|
|
125
|
+
md: 'text-lg h-12 px-4 font-bold',
|
|
126
|
+
lg: 'text-2xl h-[60px] py-3 px-6 font-bold',
|
|
127
|
+
xl: 'text-3xl h-20 py-md px-8 font-extrabold'
|
|
128
|
+
*/
|
|
129
129
|
&[data-size="sm"] {
|
|
130
130
|
@apply text-base h-10 px-3 font-medium;
|
|
131
131
|
}
|
|
@@ -168,17 +168,17 @@
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
/*
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
171
|
+
ROLES
|
|
172
|
+
primary: 'bg-surface-brand text-content-label',
|
|
173
|
+
secondary: 'bg-surface-brandalt text-content-brand',
|
|
174
|
+
success: 'bg-surface-success text-content-label',
|
|
175
|
+
warning: 'bg-surface-warning text-content-label',
|
|
176
|
+
danger: 'bg-surface-danger text-content-label',
|
|
177
|
+
dangeralt: 'bg-surface-dangeralt text-content-danger',
|
|
178
|
+
info: 'bg-surface-info text-content-label',
|
|
179
|
+
strong: 'bg-black text-white',
|
|
180
|
+
weak: 'bg-white text-content-brand'
|
|
181
|
+
*/
|
|
182
182
|
|
|
183
183
|
/* color schemes per variant */
|
|
184
184
|
&[data-buttontype="filled"],
|
|
@@ -247,11 +247,7 @@
|
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
|
|
251
|
-
TODO: where did active/hover states go?
|
|
252
|
-
*/
|
|
253
|
-
|
|
254
|
-
body &[class][disabled] {
|
|
250
|
+
body &[class][disabled][data-buttontype][data-role] {
|
|
255
251
|
@apply cursor-not-allowed;
|
|
256
252
|
@apply bg-surface-default text-content-weak;
|
|
257
253
|
|
|
@@ -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,49 +1,99 @@
|
|
|
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
|
+
:data-size="size"
|
|
6
7
|
>
|
|
7
8
|
<div class="relative">
|
|
8
9
|
<input
|
|
9
10
|
:type="type"
|
|
10
11
|
:value="modelValue"
|
|
11
12
|
@input="handleInput"
|
|
13
|
+
@focus="handleFocus"
|
|
12
14
|
@blur="handleBlur"
|
|
13
15
|
:disabled="disabled"
|
|
14
16
|
:readonly="readonly"
|
|
15
|
-
|
|
16
|
-
:
|
|
17
|
+
:maxlength="maxLength"
|
|
18
|
+
:autocomplete="autocomplete"
|
|
19
|
+
class="peer fmx-input-border w-full bg-surface-defaulthighest text-content-default border-2 transition duration-300 ease focus:outline-none focus:border-out fmx-input-autofill"
|
|
20
|
+
:class="[
|
|
21
|
+
inputStateClass,
|
|
22
|
+
sizeConfig.input,
|
|
23
|
+
placeholder ? sizeConfig.inputWithPlaceholder : sizeConfig.inputWithoutPlaceholder,
|
|
24
|
+
inputClass,
|
|
25
|
+
]"
|
|
26
|
+
:style="{ letterSpacing: letterSpacing + 'em' }"
|
|
17
27
|
/>
|
|
18
28
|
<label
|
|
29
|
+
v-if="placeholder"
|
|
19
30
|
:class="[
|
|
20
|
-
'absolute px-1 left-2.5
|
|
21
|
-
|
|
31
|
+
'absolute px-1 left-2.5 text-content-defaultalt transition-all transform origin-left pointer-events-none',
|
|
32
|
+
sizeConfig.label,
|
|
33
|
+
sizeConfig.labelText,
|
|
34
|
+
sizeConfig.labelFocused,
|
|
35
|
+
labelFilledClass,
|
|
22
36
|
]"
|
|
23
37
|
>
|
|
24
38
|
{{ placeholder }}
|
|
25
39
|
</label>
|
|
26
40
|
<transition name="icon-scale">
|
|
27
41
|
<span
|
|
28
|
-
|
|
29
|
-
|
|
42
|
+
v-if="icon"
|
|
43
|
+
class="iconwrapper absolute"
|
|
44
|
+
:class="[sizeConfig.icon, iconClass]"
|
|
30
45
|
>
|
|
31
46
|
<Icon :name="icon" />
|
|
32
47
|
</span>
|
|
33
48
|
</transition>
|
|
34
49
|
</div>
|
|
35
50
|
<p
|
|
36
|
-
v-if="errorMessage"
|
|
51
|
+
v-if="errorMessage && !isFocused"
|
|
37
52
|
class="text-content-danger text-sm font-bold mt-1"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
</p>
|
|
53
|
+
v-html="errorMessage"
|
|
54
|
+
/>
|
|
41
55
|
</div>
|
|
42
56
|
</template>
|
|
43
57
|
|
|
44
58
|
<script>
|
|
45
59
|
import Icon from "./Icon.vue";
|
|
46
60
|
|
|
61
|
+
const SIZE_CONFIG = {
|
|
62
|
+
sm: {
|
|
63
|
+
input: "h-10 text-base rounded-2xl px-3",
|
|
64
|
+
inputWithPlaceholder: "pt-5 pb-1",
|
|
65
|
+
inputWithoutPlaceholder: "py-2 flex items-center justify-center",
|
|
66
|
+
label: "top-2.5",
|
|
67
|
+
labelText: "text-base",
|
|
68
|
+
labelFocused:
|
|
69
|
+
"peer-focus:top-[4px] peer-focus:left-2.5 peer-focus:text-xs peer-focus:font-bold",
|
|
70
|
+
labelFilled: "top-[4px] left-2.5 text-xs font-bold",
|
|
71
|
+
icon: "right-2 top-2.5 text-xl leading-none",
|
|
72
|
+
},
|
|
73
|
+
md: {
|
|
74
|
+
input: "h-[60px] text-lg leading-snug rounded-2xl px-3",
|
|
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",
|
|
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",
|
|
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",
|
|
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,179 @@
|
|
|
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
|
-
|
|
95
|
-
return
|
|
96
|
-
"border-outline-danger": this.errorMessage,
|
|
97
|
-
"border-transparent focus:border-outline-brand hover:border-outline-brand":
|
|
98
|
-
!this.errorMessage,
|
|
99
|
-
};
|
|
177
|
+
sizeConfig() {
|
|
178
|
+
return SIZE_CONFIG[this.size] || SIZE_CONFIG.md;
|
|
100
179
|
},
|
|
101
|
-
|
|
180
|
+
inputStateClass() {
|
|
102
181
|
return {
|
|
103
|
-
"
|
|
104
|
-
"top-[6px] left-2.5 text-sm font-bold":
|
|
105
|
-
this.modelValue && this.modelValue.length > 0,
|
|
182
|
+
"border-outline-danger": this.errorMessage && !this.isFocused,
|
|
106
183
|
};
|
|
107
184
|
},
|
|
185
|
+
labelFilledClass() {
|
|
186
|
+
return this.modelValue && this.modelValue.length > 0
|
|
187
|
+
? this.sizeConfig.labelFilled
|
|
188
|
+
: "";
|
|
189
|
+
},
|
|
108
190
|
},
|
|
109
191
|
methods: {
|
|
110
192
|
handleInput(event) {
|
|
111
|
-
|
|
193
|
+
const value = event.target.value;
|
|
194
|
+
this.$emit("update:modelValue", value);
|
|
195
|
+
},
|
|
196
|
+
handleFocus(event) {
|
|
197
|
+
this.isFocused = true;
|
|
198
|
+
this.iconClass = "";
|
|
199
|
+
this.icon = "";
|
|
200
|
+
this.$emit("focus", event);
|
|
112
201
|
},
|
|
113
|
-
handleBlur() {
|
|
202
|
+
handleBlur(event) {
|
|
203
|
+
this.isFocused = false;
|
|
114
204
|
this.runValidation(this.modelValue);
|
|
205
|
+
this.$emit("blur", event);
|
|
115
206
|
},
|
|
116
207
|
runValidation(value) {
|
|
117
|
-
for (
|
|
208
|
+
for (const rule of this.rules) {
|
|
118
209
|
const error = rule(value);
|
|
119
210
|
if (error) {
|
|
120
211
|
this.errorMessage = error;
|
|
121
212
|
this.iconClass = "text-content-danger";
|
|
122
213
|
this.icon = "stroke-warning";
|
|
123
214
|
return;
|
|
124
|
-
} else {
|
|
125
|
-
this.errorMessage = "";
|
|
126
|
-
this.iconClass = "text-content-brand";
|
|
127
|
-
this.icon = "bold-check";
|
|
128
215
|
}
|
|
216
|
+
this.errorMessage = "";
|
|
217
|
+
this.iconClass = "text-content-brand";
|
|
218
|
+
this.icon = "bold-check";
|
|
129
219
|
}
|
|
130
220
|
},
|
|
221
|
+
validate() {
|
|
222
|
+
this.runValidation(this.modelValue);
|
|
223
|
+
},
|
|
131
224
|
},
|
|
132
225
|
};
|
|
133
226
|
</script>
|
|
134
227
|
|
|
135
228
|
<style scoped>
|
|
229
|
+
.inputgroup {
|
|
230
|
+
min-width: 100px;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.inputgroup .fmx-input-border {
|
|
234
|
+
border-color: var(--fmx-input-border-default, transparent);
|
|
235
|
+
}
|
|
236
|
+
.inputgroup .fmx-input-border:focus {
|
|
237
|
+
border-color: var(--color-outline-brand);
|
|
238
|
+
}
|
|
239
|
+
.inputgroup .fmx-input-border:hover:not(:focus) {
|
|
240
|
+
border-color: var(--color-outline-brand);
|
|
241
|
+
}
|
|
242
|
+
.inputgroup .fmx-input-border.border-outline-danger {
|
|
243
|
+
border-color: var(--color-outline-danger);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* Autofill: we only override what applies to the host input (padding, label). Chrome
|
|
247
|
+
* renders the autofill value in Shadow DOM (-internal-input-suggested); we don’t
|
|
248
|
+
* style that, as it’s browser-specific and not reliably styleable. */
|
|
249
|
+
.inputgroup .fmx-input-autofill:-webkit-autofill,
|
|
250
|
+
.inputgroup .fmx-input-autofill:autofill {
|
|
251
|
+
color: var(--color-content-default) !important;
|
|
252
|
+
-webkit-text-fill-color: var(--color-content-default) !important;
|
|
253
|
+
}
|
|
254
|
+
.inputgroup[data-size="sm"] .fmx-input-autofill:-webkit-autofill,
|
|
255
|
+
.inputgroup[data-size="sm"] .fmx-input-autofill:autofill {
|
|
256
|
+
padding-top: 1.25rem;
|
|
257
|
+
padding-bottom: 0.25rem;
|
|
258
|
+
font-size: 16px;
|
|
259
|
+
}
|
|
260
|
+
.inputgroup[data-size="sm"] .fmx-input-autofill:-webkit-autofill ~ label,
|
|
261
|
+
.inputgroup[data-size="sm"] .fmx-input-autofill:autofill ~ label {
|
|
262
|
+
top: 4px;
|
|
263
|
+
left: 0.625rem;
|
|
264
|
+
font-size: 0.75rem;
|
|
265
|
+
font-weight: 700;
|
|
266
|
+
}
|
|
267
|
+
.inputgroup[data-size="md"] .fmx-input-autofill:-webkit-autofill,
|
|
268
|
+
.inputgroup[data-size="md"] .fmx-input-autofill:autofill {
|
|
269
|
+
padding-top: 1.5rem;
|
|
270
|
+
padding-bottom: 0.25rem;
|
|
271
|
+
font-size: 18px;
|
|
272
|
+
}
|
|
273
|
+
.inputgroup[data-size="md"] .fmx-input-autofill:-webkit-autofill ~ label,
|
|
274
|
+
.inputgroup[data-size="md"] .fmx-input-autofill:autofill ~ label {
|
|
275
|
+
top: 6px;
|
|
276
|
+
left: 0.625rem;
|
|
277
|
+
font-size: 0.875rem;
|
|
278
|
+
font-weight: 700;
|
|
279
|
+
}
|
|
280
|
+
.inputgroup[data-size="lg"] .fmx-input-autofill:-webkit-autofill,
|
|
281
|
+
.inputgroup[data-size="lg"] .fmx-input-autofill:autofill {
|
|
282
|
+
padding-top: 1.75rem;
|
|
283
|
+
padding-bottom: 0.25rem;
|
|
284
|
+
font-size: 18px;
|
|
285
|
+
}
|
|
286
|
+
.inputgroup[data-size="lg"] .fmx-input-autofill:-webkit-autofill ~ label,
|
|
287
|
+
.inputgroup[data-size="lg"] .fmx-input-autofill:autofill ~ label {
|
|
288
|
+
top: 8px;
|
|
289
|
+
left: 0.625rem;
|
|
290
|
+
font-size: 0.875rem;
|
|
291
|
+
font-weight: 700;
|
|
292
|
+
}
|
|
293
|
+
|
|
136
294
|
.icon-scale-enter-active,
|
|
137
295
|
.icon-scale-leave-active {
|
|
138
296
|
transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
|
139
297
|
}
|
|
140
|
-
|
|
141
|
-
|
|
298
|
+
|
|
299
|
+
.icon-scale-enter-from,
|
|
300
|
+
.icon-scale-leave-to {
|
|
301
|
+
transform: scale(0) translateX(100%);
|
|
142
302
|
}
|
|
303
|
+
|
|
143
304
|
.icon-scale-enter-to,
|
|
144
305
|
.icon-scale-leave-from {
|
|
145
306
|
transform: scale(1);
|
|
@@ -147,11 +308,12 @@
|
|
|
147
308
|
|
|
148
309
|
span.iconwrapper {
|
|
149
310
|
display: block;
|
|
150
|
-
height:
|
|
151
|
-
width:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
311
|
+
height: 1.5rem;
|
|
312
|
+
width: 1.5rem;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
span.iconwrapper svg {
|
|
316
|
+
width: 100%;
|
|
317
|
+
height: 100%;
|
|
156
318
|
}
|
|
157
319
|
</style>
|