@brickflow/ui 0.0.55 → 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.
Files changed (28) hide show
  1. package/dist/module.d.mts +1 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +2 -0
  4. package/dist/runtime/components/Button/state.demo.vue +9 -1
  5. package/dist/runtime/components/Form/index.vue +50 -0
  6. package/dist/runtime/components/Form/login.demo.vue +68 -0
  7. package/dist/runtime/components/Input/index.vue +147 -22
  8. package/dist/runtime/components/Input/label.demo.vue +15 -3
  9. package/dist/runtime/components/Input/mask.demo.vue +73 -0
  10. package/dist/runtime/components/Input/variants.demo.vue +16 -0
  11. package/dist/runtime/components/InputNumber/denominator.demo.vue +38 -0
  12. package/dist/runtime/components/InputNumber/index.vue +131 -9
  13. package/dist/runtime/components/InputNumber/minmax.demo.vue +39 -0
  14. package/dist/runtime/components/InputNumber/nullable.demo.vue +25 -0
  15. package/dist/runtime/components/InputNumber/step.demo.vue +38 -0
  16. package/dist/runtime/components/TransitionSize/duration.demo.vue +88 -0
  17. package/dist/runtime/components/TransitionSize/flex.demo.vue +112 -0
  18. package/dist/runtime/components/TransitionSize/height.demo.vue +88 -0
  19. package/dist/runtime/components/TransitionSize/index.vue +69 -27
  20. package/dist/runtime/components/TransitionSize/width.demo.vue +88 -0
  21. package/dist/runtime/composables/useBrickflow.d.ts +2 -0
  22. package/dist/runtime/composables/useFieldRules.d.ts +16 -0
  23. package/dist/runtime/composables/useFieldRules.js +42 -0
  24. package/dist/runtime/utils/rules.d.ts +34 -0
  25. package/dist/runtime/utils/rules.js +68 -0
  26. package/dist/types.d.mts +2 -0
  27. package/package.json +2 -1
  28. package/dist/runtime/components/InputNumber/basic.demo.vue +0 -33
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.55",
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,
@@ -21,11 +21,19 @@
21
21
  <div class="flex flex-wrap items-center gap-3">
22
22
  <button
23
23
  type="button"
24
- class="cursor-pointer rounded-xl bg-blue-500 px-3 py-2"
24
+ class="w-30 cursor-pointer rounded-xl px-3 py-2"
25
+ :class="stateActive ? 'bg-blue-500' : 'bg-blue-800'"
25
26
  @click="stateActive = !stateActive"
26
27
  >
27
28
  {{ stateActive ? 'On' : 'Off' }}
28
29
  </button>
30
+ <Button
31
+ :loading="stateActive"
32
+ :icon="firstIconName"
33
+ >
34
+ Loading
35
+ </Button>
36
+ <Button :loading="stateActive">Loading</Button>
29
37
  <div class="basis-full">
30
38
  <div class="relative flex w-fit flex-col items-start gap-3 pl-13">
31
39
  <div class="absolute top-0 left-0 z-0 size-full object-cover pt-7 pl-13">
@@ -0,0 +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
+
42
+ <template>
43
+ <form
44
+ class="flex flex-col gap-4"
45
+ novalidate
46
+ @submit="handleSubmit"
47
+ >
48
+ <slot />
49
+ </form>
50
+ </template>
@@ -0,0 +1,68 @@
1
+ <script lang="ts">
2
+ export const uiDemo = {
3
+ title: 'Email & Password',
4
+ }
5
+ </script>
6
+
7
+ <script lang="ts" setup>
8
+ import { ref } from 'vue'
9
+
10
+ import { rule } from '../../utils/rules'
11
+ import Button from '../Button/index.vue'
12
+ import Input from '../Input/index.vue'
13
+ import Form from './index.vue'
14
+
15
+ const email = ref('')
16
+ const password = ref('')
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)]
24
+
25
+ function handleSubmit(): void {
26
+ submittedEmail.value = email.value
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <Form
32
+ class="max-w-sm"
33
+ @submit.prevent="handleSubmit"
34
+ >
35
+ <Input
36
+ :value="email"
37
+ label="Email"
38
+ placeholder="example@oyes.com"
39
+ type="email"
40
+ :rules="emailRules"
41
+ description="Only .com"
42
+ autocomplete="email"
43
+ @input="email = $event"
44
+ />
45
+ <Input
46
+ :value="password"
47
+ placeholder="**********"
48
+ label="Password"
49
+ type="password"
50
+ :rules="passwordRules"
51
+ autocomplete="current-password"
52
+ @input="password = $event"
53
+ />
54
+ <Button
55
+ type="submit"
56
+ class="self-start"
57
+ >
58
+ Sign in
59
+ </Button>
60
+ <p
61
+ v-if="submittedEmail"
62
+ role="status"
63
+ class="text-sm"
64
+ >
65
+ Submitted for {{ submittedEmail }}
66
+ </p>
67
+ </Form>
68
+ </template>
@@ -31,29 +31,39 @@
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
 
41
47
  const props = withDefaults(
42
48
  defineProps<{
43
49
  align?: 'center' | 'left' | 'right'
50
+ autocomplete?: string
44
51
  autoWidth?: boolean
45
52
  description?: string
46
53
  disabled?: boolean
47
54
  icon?: string
48
55
  label?: string
56
+ mask?: MaskInputOptions | MaskType
49
57
  onBlur?: (value: string, target: HTMLInputElement) => void
50
58
  onChange?: (value: string, target: HTMLInputElement) => void
51
59
  onFocus?: (value: string, target: HTMLInputElement | null) => void
52
60
  onIconClick?: () => void
53
61
  onInput?: (value: string, target: HTMLInputElement | null) => void
62
+ onMask?: (detail: MaskaDetail, target: HTMLInputElement) => void
54
63
  onTrailingIconClick?: () => void
55
64
  placeholder?: string
56
65
  readonly?: boolean
66
+ rules?: Rule<string>[]
57
67
  size?: InputSize
58
68
  trailingIcon?: string
59
69
  type?: 'decimal' | 'email' | 'numeric' | 'password' | 'search' | 'tel' | 'text' | 'url'
@@ -62,19 +72,23 @@
62
72
  }>(),
63
73
  {
64
74
  align: 'left',
75
+ autocomplete: undefined,
65
76
  autoWidth: false,
66
77
  description: undefined,
67
78
  disabled: false,
68
79
  icon: undefined,
69
80
  label: undefined,
81
+ mask: undefined,
70
82
  onBlur: undefined,
71
83
  onChange: undefined,
72
84
  onFocus: undefined,
73
85
  onIconClick: undefined,
74
86
  onInput: undefined,
87
+ onMask: undefined,
75
88
  onTrailingIconClick: undefined,
76
89
  placeholder: '',
77
90
  readonly: false,
91
+ rules: undefined,
78
92
  size: 'md',
79
93
  trailingIcon: undefined,
80
94
  type: 'text',
@@ -84,10 +98,13 @@
84
98
  )
85
99
 
86
100
  const model = shallowRef('')
101
+ const maskedValue = shallowRef<null | string>(null)
102
+ let maskInput: MaskInput | undefined
87
103
 
88
104
  const attrs = useAttrs()
89
105
  const slots = useSlots()
90
106
  const generatedId = useId()
107
+ const baseRef = ref<HTMLInputElement | null>(null)
91
108
  const inputRef = ref<HTMLInputElement | null>(null)
92
109
  const widthMeasureRef = ref<HTMLSpanElement | null>(null)
93
110
  const measuredWidth = ref(0)
@@ -104,12 +121,27 @@
104
121
  const fieldId = computed(() => String(attrs.id ?? generatedId))
105
122
  const descriptionId = computed(() => `${fieldId.value}-description`)
106
123
  const currentValue = computed(() => (props.value === undefined ? model.value : (props.value ?? '')))
107
- 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 || ' ')
108
139
  const isEmptyField = computed(() => {
109
- return Boolean(currentValue.value)
140
+ return Boolean(displayValue.value)
110
141
  })
111
142
 
112
- 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))
113
145
  const inputType = computed(() => {
114
146
  return props.type === 'password' && !showPassword.value ? 'password' : 'text'
115
147
  })
@@ -130,7 +162,12 @@
130
162
  ])
131
163
  const labelClasses = computed(() => twMerge(UI_STYLE.label, sizeClass.value.label, variantClass.value.label))
132
164
  const descriptionClasses = computed(() =>
133
- 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
+ ),
134
171
  )
135
172
  const leadingIconClasses = computed(() =>
136
173
  twMerge(
@@ -162,6 +199,64 @@
162
199
  { flush: 'post', immediate: true },
163
200
  )
164
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
+
165
260
  function clear(): void {
166
261
  setValue('')
167
262
  focus()
@@ -187,30 +282,46 @@
187
282
  }
188
283
 
189
284
  function handleInput(event: Event): void {
285
+ if (props.mask !== null && props.mask !== undefined && event instanceof CustomEvent) {
286
+ return
287
+ }
190
288
  setValue((event.target as HTMLInputElement).value)
191
289
  }
192
290
 
193
291
  function setValue(value: string): void {
194
292
  model.value = value
293
+ validateInput(value)
294
+ if (props.mask !== null && props.mask !== undefined) {
295
+ maskedValue.value = value
296
+ }
195
297
  props.onInput?.(value, inputRef.value)
196
298
  }
197
299
 
198
- defineExpose({ clear, fieldId, focus, hasFocus, inputRef })
300
+ defineExpose({ clear, fieldId, focus, hasFocus, inputRef, validate })
199
301
  </script>
200
302
 
201
303
  <template>
202
- <div :class="[classes, $attrs.class]">
203
- <label
204
- v-if="props.label || $slots.label"
205
- :for="fieldId"
206
- :class="labelClasses"
304
+ <div
305
+ ref="baseRef"
306
+ :class="[classes, $attrs.class]"
307
+ >
308
+ <TransitionSize
309
+ :flexParent="baseRef"
310
+ flexDirection="end"
207
311
  >
208
- <slot name="label">{{ props.label }}</slot>
209
- </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>
210
320
 
211
321
  <label
212
322
  :for="fieldId"
213
323
  :class="fieldWrapClasses"
324
+ :aria-invalid="hasError ? 'true' : undefined"
214
325
  :style="props.autoWidth ? { maxWidth: '100%', width: 'fit-content' } : undefined"
215
326
  >
216
327
  <span
@@ -242,13 +353,15 @@
242
353
  v-bind="{ ...$attrs, class: undefined, id: undefined }"
243
354
  :id="fieldId"
244
355
  ref="inputRef"
245
- :value="currentValue"
356
+ :value="displayValue"
246
357
  :type="inputType"
247
358
  :inputmode="props.type === 'password' ? undefined : props.type"
359
+ :autocomplete="props.autocomplete"
248
360
  :disabled="props.disabled"
249
361
  :readonly="props.readonly"
250
362
  :placeholder="props.placeholder"
251
363
  :aria-describedby="hasDescription ? descriptionId : undefined"
364
+ :aria-invalid="hasError ? 'true' : undefined"
252
365
  :class="fieldClasses"
253
366
  :style="fieldStyle"
254
367
  @input="handleInput"
@@ -304,13 +417,25 @@
304
417
  </slot>
305
418
  </label>
306
419
 
307
- <div
308
- v-if="hasDescription"
309
- :id="descriptionId"
310
- :class="descriptionClasses"
311
- >
312
- <slot name="error" />
313
- <slot name="description">{{ props.description }}</slot>
314
- </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>
315
440
  </div>
316
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}`"
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ export const uiDemo = {
3
+ description: 'Denominator limits decimal places: 0 for integers, 2 for up to two decimal digits.',
4
+ title: 'Denominator',
5
+ }
6
+ </script>
7
+
8
+ <script lang="ts" setup>
9
+ import { ref } from 'vue'
10
+
11
+ import InputNumber from './index.vue'
12
+
13
+ const price = ref<number>(8)
14
+ const price2 = ref<number>(12.55)
15
+ </script>
16
+
17
+ <template>
18
+ <div class="flex max-w-sm flex-col gap-4">
19
+ <InputNumber
20
+ :value="price"
21
+ :denominator="0"
22
+ label="Denominator: 0"
23
+ :description="`Value: ${price}`"
24
+ :min="-99"
25
+ :max="200"
26
+ @input="price = $event"
27
+ ></InputNumber>
28
+ <InputNumber
29
+ :value="price2"
30
+ :denominator="2"
31
+ label="Denominator: 2"
32
+ :description="`Value: ${price2}`"
33
+ :min="-99"
34
+ :max="200"
35
+ @input="price2 = $event"
36
+ ></InputNumber>
37
+ </div>
38
+ </template>