@brickflow/ui 0.0.56 → 0.0.57

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/dist/module.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
2
  export { BrickflowConfig } from '../dist/runtime/composables/useBrickflow.js';
3
3
  export { BrickflowI18n, BrickflowRouteLocationParam } from '../dist/runtime/composables/useTranslate.js';
4
+ export { Rule, RuleMessages, rule } from '../dist/runtime/utils/rules.js';
4
5
 
5
6
  interface ModuleOptions {
6
7
  componentPrefix?: string;
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  },
5
5
  "configKey": "brickflowUi",
6
6
  "name": "@brickflow/ui",
7
- "version": "0.0.56",
7
+ "version": "0.0.57",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -5,6 +5,7 @@ import { mkdir, access, readFile, writeFile, readdir } from 'node:fs/promises';
5
5
  import { join, parse, basename, extname, dirname, resolve, relative } from 'node:path';
6
6
  import { isBrickflowUiStyleReference, getBrickflowUiStyleReferencePath, defineBrickflowUi } from '../dist/runtime/tailwind.js';
7
7
  import { generateFonts, FontAssetType, OtherAssetType } from 'fantasticon';
8
+ export { rule } from '../dist/runtime/utils/rules.js';
8
9
 
9
10
  const getIconFontCssPath$1 = (outputDir, name) => join(outputDir, `${name}.minify.css`);
10
11
  async function generateIcons({ inputDir, name, outputDir }) {
@@ -977,6 +978,7 @@ const module$1 = defineNuxtModule({
977
978
  viteConfig.plugins.push(tailwindcss());
978
979
  });
979
980
  addImports([
981
+ { from: resolver.resolve("./runtime/utils/rules"), name: "rule" },
980
982
  ...["provideBrickflow", "useBrickflow", "BrickflowConfig"].map((name) => ({
981
983
  from: resolver.resolve("./runtime/composables/useBrickflow"),
982
984
  name,
@@ -1,5 +1,50 @@
1
+ <script setup lang="ts">
2
+ import { provide } from 'vue'
3
+
4
+ import { formRulesKey } from '../../composables/useFieldRules'
5
+
6
+ const props = defineProps<{
7
+ onInvalid?: (event: SubmitEvent) => void
8
+ onSubmit?: (event: SubmitEvent) => void
9
+ }>()
10
+
11
+ const fields = new Set<{ focus: () => void; validate: () => boolean }>()
12
+ provide(formRulesKey, {
13
+ register(field: { focus: () => void; validate: () => boolean }) {
14
+ fields.add(field)
15
+ return () => fields.delete(field)
16
+ },
17
+ })
18
+
19
+ function handleSubmit(event: SubmitEvent): void {
20
+ event.preventDefault()
21
+ if (validate()) {
22
+ props.onSubmit?.(event)
23
+ } else {
24
+ props.onInvalid?.(event)
25
+ }
26
+ }
27
+
28
+ function validate(): boolean {
29
+ let firstInvalid: undefined | { focus: () => void }
30
+ for (const field of fields) {
31
+ if (!field.validate() && !firstInvalid) {
32
+ firstInvalid = field
33
+ }
34
+ }
35
+ firstInvalid?.focus()
36
+ return !firstInvalid
37
+ }
38
+
39
+ defineExpose({ validate })
40
+ </script>
41
+
1
42
  <template>
2
- <form class="flex flex-col gap-4">
43
+ <form
44
+ class="flex flex-col gap-4"
45
+ novalidate
46
+ @submit="handleSubmit"
47
+ >
3
48
  <slot />
4
49
  </form>
5
50
  </template>
@@ -7,6 +7,7 @@
7
7
  <script lang="ts" setup>
8
8
  import { ref } from 'vue'
9
9
 
10
+ import { rule } from '../../utils/rules'
10
11
  import Button from '../Button/index.vue'
11
12
  import Input from '../Input/index.vue'
12
13
  import Form from './index.vue'
@@ -14,6 +15,12 @@
14
15
  const email = ref('')
15
16
  const password = ref('')
16
17
  const submittedEmail = ref('')
18
+ const emailRules = [
19
+ rule.required(),
20
+ rule.email(),
21
+ rule.regex(/\.com$/i, () => 'CUSTOM RULE: Use a .com email address'),
22
+ ]
23
+ const passwordRules = [rule.required(), rule.minLength(8)]
17
24
 
18
25
  function handleSubmit(): void {
19
26
  submittedEmail.value = email.value
@@ -28,15 +35,19 @@
28
35
  <Input
29
36
  :value="email"
30
37
  label="Email"
31
- placeholder="example@brickflow.com"
38
+ placeholder="example@oyes.com"
32
39
  type="email"
40
+ :rules="emailRules"
41
+ description="Only .com"
33
42
  autocomplete="email"
34
43
  @input="email = $event"
35
44
  />
36
45
  <Input
37
46
  :value="password"
47
+ placeholder="**********"
38
48
  label="Password"
39
49
  type="password"
50
+ :rules="passwordRules"
40
51
  autocomplete="current-password"
41
52
  @input="password = $event"
42
53
  />
@@ -31,10 +31,16 @@
31
31
  </script>
32
32
 
33
33
  <script setup lang="ts">
34
+ import type { MaskaDetail, MaskInput, MaskInputOptions, MaskType } from 'maska'
35
+
34
36
  import { twMerge } from 'tailwind-merge'
35
- import { computed, ref, shallowRef, useAttrs, useId, useSlots, watch } from 'vue'
37
+ import { computed, nextTick, ref, shallowRef, useAttrs, useId, useSlots, watch } from 'vue'
38
+
39
+ import type { Rule } from '../../utils/rules'
36
40
 
41
+ import { useFieldRules } from '../../composables/useFieldRules'
37
42
  import Icon from '../Icon/index.vue'
43
+ import TransitionSize from '../TransitionSize/index.vue'
38
44
 
39
45
  defineOptions({ inheritAttrs: false })
40
46
 
@@ -47,14 +53,17 @@
47
53
  disabled?: boolean
48
54
  icon?: string
49
55
  label?: string
56
+ mask?: MaskInputOptions | MaskType
50
57
  onBlur?: (value: string, target: HTMLInputElement) => void
51
58
  onChange?: (value: string, target: HTMLInputElement) => void
52
59
  onFocus?: (value: string, target: HTMLInputElement | null) => void
53
60
  onIconClick?: () => void
54
61
  onInput?: (value: string, target: HTMLInputElement | null) => void
62
+ onMask?: (detail: MaskaDetail, target: HTMLInputElement) => void
55
63
  onTrailingIconClick?: () => void
56
64
  placeholder?: string
57
65
  readonly?: boolean
66
+ rules?: Rule<string>[]
58
67
  size?: InputSize
59
68
  trailingIcon?: string
60
69
  type?: 'decimal' | 'email' | 'numeric' | 'password' | 'search' | 'tel' | 'text' | 'url'
@@ -69,14 +78,17 @@
69
78
  disabled: false,
70
79
  icon: undefined,
71
80
  label: undefined,
81
+ mask: undefined,
72
82
  onBlur: undefined,
73
83
  onChange: undefined,
74
84
  onFocus: undefined,
75
85
  onIconClick: undefined,
76
86
  onInput: undefined,
87
+ onMask: undefined,
77
88
  onTrailingIconClick: undefined,
78
89
  placeholder: '',
79
90
  readonly: false,
91
+ rules: undefined,
80
92
  size: 'md',
81
93
  trailingIcon: undefined,
82
94
  type: 'text',
@@ -86,10 +98,13 @@
86
98
  )
87
99
 
88
100
  const model = shallowRef('')
101
+ const maskedValue = shallowRef<null | string>(null)
102
+ let maskInput: MaskInput | undefined
89
103
 
90
104
  const attrs = useAttrs()
91
105
  const slots = useSlots()
92
106
  const generatedId = useId()
107
+ const baseRef = ref<HTMLInputElement | null>(null)
93
108
  const inputRef = ref<HTMLInputElement | null>(null)
94
109
  const widthMeasureRef = ref<HTMLSpanElement | null>(null)
95
110
  const measuredWidth = ref(0)
@@ -106,12 +121,27 @@
106
121
  const fieldId = computed(() => String(attrs.id ?? generatedId))
107
122
  const descriptionId = computed(() => `${fieldId.value}-description`)
108
123
  const currentValue = computed(() => (props.value === undefined ? model.value : (props.value ?? '')))
109
- const widthMeasureText = computed(() => currentValue.value || props.placeholder || ' ')
124
+ const {
125
+ error,
126
+ input: validateInput,
127
+ validate,
128
+ } = useFieldRules(
129
+ currentValue,
130
+ computed(() => props.rules),
131
+ focus,
132
+ )
133
+ const displayValue = computed(() =>
134
+ props.mask === null || props.mask === undefined
135
+ ? currentValue.value
136
+ : (maskedValue.value ?? currentValue.value),
137
+ )
138
+ const widthMeasureText = computed(() => displayValue.value || props.placeholder || ' ')
110
139
  const isEmptyField = computed(() => {
111
- return Boolean(currentValue.value)
140
+ return Boolean(displayValue.value)
112
141
  })
113
142
 
114
- const hasDescription = computed(() => Boolean(props.description || slots.description || slots.error))
143
+ const hasError = computed(() => Boolean(error.value))
144
+ const hasDescription = computed(() => Boolean(props.description || slots.description || hasError.value))
115
145
  const inputType = computed(() => {
116
146
  return props.type === 'password' && !showPassword.value ? 'password' : 'text'
117
147
  })
@@ -132,7 +162,12 @@
132
162
  ])
133
163
  const labelClasses = computed(() => twMerge(UI_STYLE.label, sizeClass.value.label, variantClass.value.label))
134
164
  const descriptionClasses = computed(() =>
135
- twMerge(UI_STYLE.description, sizeClass.value.description, variantClass.value.description),
165
+ twMerge(
166
+ UI_STYLE.description,
167
+ sizeClass.value.description,
168
+ variantClass.value.description,
169
+ hasError.value && UI_STYLE.state.errorDescription,
170
+ ),
136
171
  )
137
172
  const leadingIconClasses = computed(() =>
138
173
  twMerge(
@@ -164,6 +199,64 @@
164
199
  { flush: 'post', immediate: true },
165
200
  )
166
201
 
202
+ watch(
203
+ [() => props.mask, inputRef],
204
+ async ([mask, input], _previous, onCleanup) => {
205
+ maskInput?.destroy()
206
+ maskInput = undefined
207
+ maskedValue.value = null
208
+
209
+ if (mask === null || mask === undefined || !input) {
210
+ return
211
+ }
212
+
213
+ let canceled = false
214
+ onCleanup(() => {
215
+ canceled = true
216
+ maskInput?.destroy()
217
+ maskInput = undefined
218
+ })
219
+
220
+ const { MaskInput } = await import('maska')
221
+ if (canceled) {
222
+ return
223
+ }
224
+
225
+ const options: MaskInputOptions = typeof mask === 'object' && !Array.isArray(mask) ? mask : { mask }
226
+ const onMaska = options.onMaska
227
+
228
+ maskInput = new MaskInput(input, {
229
+ ...options,
230
+ onMaska: (detail) => {
231
+ maskedValue.value = detail.masked
232
+ model.value = detail.masked
233
+ if (Array.isArray(onMaska)) {
234
+ onMaska.forEach((callback) => callback(detail))
235
+ } else {
236
+ onMaska?.(detail)
237
+ }
238
+ props.onMask?.(detail, input)
239
+ },
240
+ })
241
+ },
242
+ { flush: 'post', immediate: true },
243
+ )
244
+
245
+ watch(
246
+ () => props.value,
247
+ async () => {
248
+ if (!maskInput || !inputRef.value) {
249
+ return
250
+ }
251
+ maskedValue.value = null
252
+ await nextTick()
253
+ if (maskInput && inputRef.value) {
254
+ maskInput.updateValue(inputRef.value)
255
+ }
256
+ },
257
+ { flush: 'post' },
258
+ )
259
+
167
260
  function clear(): void {
168
261
  setValue('')
169
262
  focus()
@@ -189,30 +282,46 @@
189
282
  }
190
283
 
191
284
  function handleInput(event: Event): void {
285
+ if (props.mask !== null && props.mask !== undefined && event instanceof CustomEvent) {
286
+ return
287
+ }
192
288
  setValue((event.target as HTMLInputElement).value)
193
289
  }
194
290
 
195
291
  function setValue(value: string): void {
196
292
  model.value = value
293
+ validateInput(value)
294
+ if (props.mask !== null && props.mask !== undefined) {
295
+ maskedValue.value = value
296
+ }
197
297
  props.onInput?.(value, inputRef.value)
198
298
  }
199
299
 
200
- defineExpose({ clear, fieldId, focus, hasFocus, inputRef })
300
+ defineExpose({ clear, fieldId, focus, hasFocus, inputRef, validate })
201
301
  </script>
202
302
 
203
303
  <template>
204
- <div :class="[classes, $attrs.class]">
205
- <label
206
- v-if="props.label || $slots.label"
207
- :for="fieldId"
208
- :class="labelClasses"
304
+ <div
305
+ ref="baseRef"
306
+ :class="[classes, $attrs.class]"
307
+ >
308
+ <TransitionSize
309
+ :flexParent="baseRef"
310
+ flexDirection="end"
209
311
  >
210
- <slot name="label">{{ props.label }}</slot>
211
- </label>
312
+ <label
313
+ v-if="props.label || $slots.label"
314
+ :for="fieldId"
315
+ :class="labelClasses"
316
+ >
317
+ <slot name="label">{{ props.label }}</slot>
318
+ </label>
319
+ </TransitionSize>
212
320
 
213
321
  <label
214
322
  :for="fieldId"
215
323
  :class="fieldWrapClasses"
324
+ :aria-invalid="hasError ? 'true' : undefined"
216
325
  :style="props.autoWidth ? { maxWidth: '100%', width: 'fit-content' } : undefined"
217
326
  >
218
327
  <span
@@ -244,7 +353,7 @@
244
353
  v-bind="{ ...$attrs, class: undefined, id: undefined }"
245
354
  :id="fieldId"
246
355
  ref="inputRef"
247
- :value="currentValue"
356
+ :value="displayValue"
248
357
  :type="inputType"
249
358
  :inputmode="props.type === 'password' ? undefined : props.type"
250
359
  :autocomplete="props.autocomplete"
@@ -252,6 +361,7 @@
252
361
  :readonly="props.readonly"
253
362
  :placeholder="props.placeholder"
254
363
  :aria-describedby="hasDescription ? descriptionId : undefined"
364
+ :aria-invalid="hasError ? 'true' : undefined"
255
365
  :class="fieldClasses"
256
366
  :style="fieldStyle"
257
367
  @input="handleInput"
@@ -307,13 +417,25 @@
307
417
  </slot>
308
418
  </label>
309
419
 
310
- <div
311
- v-if="hasDescription"
312
- :id="descriptionId"
313
- :class="descriptionClasses"
314
- >
315
- <slot name="error" />
316
- <slot name="description">{{ props.description }}</slot>
317
- </div>
420
+ <TransitionSize :flexParent="baseRef">
421
+ <div
422
+ v-if="hasDescription"
423
+ :id="descriptionId"
424
+ :class="descriptionClasses"
425
+ >
426
+ <slot
427
+ v-if="hasError"
428
+ name="error"
429
+ >
430
+ {{ error }}
431
+ </slot>
432
+ <slot
433
+ v-else
434
+ name="description"
435
+ >
436
+ {{ props.description }}
437
+ </slot>
438
+ </div>
439
+ </TransitionSize>
318
440
  </div>
319
441
  </template>
@@ -5,16 +5,28 @@
5
5
  </script>
6
6
 
7
7
  <script lang="ts" setup>
8
+ import { shallowRef } from 'vue'
9
+
8
10
  import Input from './index.vue'
11
+
12
+ const active = shallowRef(true)
9
13
  </script>
10
14
 
11
15
  <template>
12
16
  <div class="max-w-sm">
13
17
  <Input
14
- label="Email address"
15
- description="We will use this address to contact you."
16
- placeholder="name@example.com"
18
+ :label="active ? 'Email address' : undefined"
19
+ :description="active ? 'We will use this address to contact you.' : undefined"
20
+ :placeholder="active ? 'name@example.com' : undefined"
17
21
  type="email"
18
22
  />
23
+ <button
24
+ type="button"
25
+ class="mt-4 w-30 cursor-pointer rounded-xl px-3 py-2"
26
+ :class="active ? 'bg-blue-500' : 'bg-blue-800'"
27
+ @click="active = !active"
28
+ >
29
+ {{ active ? 'Hide' : 'Show' }}
30
+ </button>
19
31
  </div>
20
32
  </template>
@@ -0,0 +1,73 @@
1
+ <script lang="ts">
2
+ export const uiDemo = {
3
+ description:
4
+ 'Use Maska tokens in the mask prop. Pass an options object for custom tokens or an array for alternative masks.',
5
+ title: 'Mask',
6
+ }
7
+ </script>
8
+
9
+ <script lang="ts" setup>
10
+ import { ref } from 'vue'
11
+
12
+ import Input from './index.vue'
13
+
14
+ const phone = ref('')
15
+ const digits = ref('')
16
+ const alternativeMasks = ['###-###', '###-###-###']
17
+ const customMask = {
18
+ mask: 'AA-####',
19
+ tokens: {
20
+ A: { pattern: /[a-z]/i, transform: (character: string) => character.toUpperCase() },
21
+ },
22
+ }
23
+ </script>
24
+
25
+ <template>
26
+ <div class="flex max-w-sm flex-col gap-5">
27
+ <p class="text-sm text-zinc-400">
28
+ Default tokens:
29
+ <code>#</code>
30
+ is a digit,
31
+ <code>@</code>
32
+ is a letter, and
33
+ <code>*</code>
34
+ is a letter or digit. Other characters are literals; prefix a token with
35
+ <code>!</code>
36
+ to display it literally. See
37
+ <a
38
+ class="text-blue-400 underline"
39
+ href="https://beholdr.github.io/maska/v3/#/tokens"
40
+ target="_blank"
41
+ rel="noopener noreferrer"
42
+ >
43
+ Maska token documentation
44
+ </a>
45
+ .
46
+ </p>
47
+ <Input
48
+ :value="phone"
49
+ mask="+## (###) ###-##-##"
50
+ label="Phone: +## (###) ###-##-##"
51
+ placeholder="+48 (123) 456-78-90"
52
+ type="tel"
53
+ @input="(value) => (phone = value)"
54
+ @mask="(detail) => (digits = detail.unmasked)"
55
+ />
56
+ <div class="text-sm text-zinc-400">Masked: {{ phone }} · Raw: {{ digits }}</div>
57
+ <Input
58
+ mask="@@-####"
59
+ label="Letters and digits: @@-####"
60
+ placeholder="AB-1234"
61
+ />
62
+ <Input
63
+ :mask="alternativeMasks"
64
+ label="Alternative lengths: ###-### or ###-###-###"
65
+ placeholder="123-456-789"
66
+ />
67
+ <Input
68
+ :mask="customMask"
69
+ label="Custom uppercase token: AA-####"
70
+ placeholder="AB-1234"
71
+ />
72
+ </div>
73
+ </template>
@@ -5,11 +5,19 @@
5
5
  </script>
6
6
 
7
7
  <script lang="ts" setup>
8
+ import { onMounted, ref } from 'vue'
9
+
8
10
  import { uiConfig } from '#brickflow-ui'
9
11
 
12
+ import { rule } from '../../utils/rules'
10
13
  import Input from './index.vue'
11
14
 
12
15
  const inputConfig = uiConfig.input
16
+ const errorInputs = ref<InstanceType<typeof Input>[]>([])
17
+
18
+ onMounted(() => {
19
+ errorInputs.value.forEach((input) => input.validate())
20
+ })
13
21
  </script>
14
22
 
15
23
  <template>
@@ -24,6 +32,14 @@
24
32
  :label="`Variant ${variant}`"
25
33
  placeholder="Type here"
26
34
  />
35
+ <Input
36
+ ref="errorInputs"
37
+ :variant="variant"
38
+ :label="`Error ${variant}`"
39
+ :rules="[rule.required(), rule.minLength(4)]"
40
+ description="This description is hidden while the field has an error"
41
+ placeholder="Required field"
42
+ />
27
43
  <Input
28
44
  :variant="variant"
29
45
  :label="`Disabled ${variant}`"
@@ -10,7 +10,7 @@
10
10
 
11
11
  import InputNumber from './index.vue'
12
12
 
13
- const price = ref<number>(8.55)
13
+ const price = ref<number>(8)
14
14
  const price2 = ref<number>(12.55)
15
15
  </script>
16
16
 
@@ -1,5 +1,7 @@
1
1
  <script setup lang="ts" generic="T extends boolean = false">
2
- import { shallowRef, watch } from 'vue'
2
+ import { computed, ref, shallowRef, watch } from 'vue'
3
+
4
+ import type { Rule } from '../../utils/rules'
3
5
 
4
6
  import { isEmpty } from '../../utils/checkup.js'
5
7
  import Input from '../Input/index.vue'
@@ -28,6 +30,7 @@
28
30
  onTrailingIconClick?: () => void
29
31
  placeholder?: string
30
32
  readonly?: boolean
33
+ rules?: Rule<null | number>[]
31
34
  size?: InputProps['size']
32
35
  step?: number
33
36
  trailingIcon?: string
@@ -55,6 +58,7 @@
55
58
  onTrailingIconClick: undefined,
56
59
  placeholder: undefined,
57
60
  readonly: false,
61
+ rules: undefined,
58
62
  size: undefined,
59
63
  step: 1,
60
64
  trailingIcon: undefined,
@@ -64,7 +68,17 @@
64
68
 
65
69
  type InputProps = InstanceType<typeof Input>['$props']
66
70
 
71
+ const inputRef = ref<InstanceType<typeof Input> | null>(null)
67
72
  const inputValue = shallowRef(prepareValue(props.value))
73
+ const inputRules = computed(() =>
74
+ props.rules?.map(
75
+ (rule): Rule<string> =>
76
+ (value) =>
77
+ rule(value === '' ? null : Number(value)),
78
+ ),
79
+ )
80
+
81
+ defineExpose({ validate: () => inputRef.value?.validate() ?? true })
68
82
 
69
83
  watch(
70
84
  () => props.value,
@@ -99,6 +113,10 @@
99
113
  const allowedRegexp = /^-?\d*(?:\.\d*)?$/
100
114
  const validNumberRegexp = /^-?(?:\d+(?:\.\d+)?|\.\d+)$/
101
115
 
116
+ function emitInput(value: typeof props.value): void {
117
+ props.onInput?.(value)
118
+ }
119
+
102
120
  function handleInput(rawValue: string, target: HTMLInputElement | null): void {
103
121
  if (!target) {
104
122
  return
@@ -109,7 +127,7 @@
109
127
  if (isEmpty(nextValue)) {
110
128
  if (props.nullable) {
111
129
  inputValue.value = ''
112
- props.onInput?.(null as typeof props.value)
130
+ emitInput(null as typeof props.value)
113
131
  }
114
132
  return
115
133
  }
@@ -128,7 +146,7 @@
128
146
  if (isEmpty(nextValue) || !validNumberRegexp.test(nextValue)) {
129
147
  if (isEmpty(nextValue) && props.nullable) {
130
148
  inputValue.value = ''
131
- props.onInput?.(null as typeof props.value)
149
+ emitInput(null as typeof props.value)
132
150
  }
133
151
  return
134
152
  }
@@ -143,7 +161,7 @@
143
161
  nextValueNumber = props.max
144
162
  }
145
163
 
146
- props.onInput?.(nextValueNumber)
164
+ emitInput(nextValueNumber)
147
165
  }
148
166
 
149
167
  function handleKeydown(event: KeyboardEvent): void {
@@ -174,7 +192,7 @@
174
192
  }
175
193
 
176
194
  target.value = prepareValue(nextValue)
177
- props.onInput?.(nextValue)
195
+ emitInput(nextValue)
178
196
  }
179
197
 
180
198
  function prepareValue(value: typeof props.value): string {
@@ -184,6 +202,7 @@
184
202
 
185
203
  <template>
186
204
  <Input
205
+ ref="inputRef"
187
206
  v-bind="{
188
207
  ...$attrs,
189
208
  }"
@@ -197,6 +216,7 @@
197
216
  :icon="props.icon"
198
217
  :placeholder="props.placeholder"
199
218
  :readonly="props.readonly"
219
+ :rules="inputRules"
200
220
  :size="props.size"
201
221
  :trailingIcon="props.trailingIcon"
202
222
  :type="props.denominator === 0 ? 'numeric' : 'decimal'"