@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.
- package/dist/module.d.mts +1 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -0
- package/dist/runtime/components/Button/state.demo.vue +9 -1
- package/dist/runtime/components/Form/index.vue +50 -0
- package/dist/runtime/components/Form/login.demo.vue +68 -0
- package/dist/runtime/components/Input/index.vue +147 -22
- package/dist/runtime/components/Input/label.demo.vue +15 -3
- package/dist/runtime/components/Input/mask.demo.vue +73 -0
- package/dist/runtime/components/Input/variants.demo.vue +16 -0
- package/dist/runtime/components/InputNumber/denominator.demo.vue +38 -0
- package/dist/runtime/components/InputNumber/index.vue +131 -9
- package/dist/runtime/components/InputNumber/minmax.demo.vue +39 -0
- package/dist/runtime/components/InputNumber/nullable.demo.vue +25 -0
- package/dist/runtime/components/InputNumber/step.demo.vue +38 -0
- package/dist/runtime/components/TransitionSize/duration.demo.vue +88 -0
- package/dist/runtime/components/TransitionSize/flex.demo.vue +112 -0
- package/dist/runtime/components/TransitionSize/height.demo.vue +88 -0
- package/dist/runtime/components/TransitionSize/index.vue +69 -27
- package/dist/runtime/components/TransitionSize/width.demo.vue +88 -0
- package/dist/runtime/composables/useBrickflow.d.ts +2 -0
- package/dist/runtime/composables/useFieldRules.d.ts +16 -0
- package/dist/runtime/composables/useFieldRules.js +42 -0
- package/dist/runtime/utils/rules.d.ts +34 -0
- package/dist/runtime/utils/rules.js +68 -0
- package/dist/types.d.mts +2 -0
- package/package.json +2 -1
- 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
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
|
|
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
|
|
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(
|
|
140
|
+
return Boolean(displayValue.value)
|
|
110
141
|
})
|
|
111
142
|
|
|
112
|
-
const
|
|
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(
|
|
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
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
304
|
+
<div
|
|
305
|
+
ref="baseRef"
|
|
306
|
+
:class="[classes, $attrs.class]"
|
|
307
|
+
>
|
|
308
|
+
<TransitionSize
|
|
309
|
+
:flexParent="baseRef"
|
|
310
|
+
flexDirection="end"
|
|
207
311
|
>
|
|
208
|
-
<
|
|
209
|
-
|
|
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="
|
|
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
|
-
<
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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>
|