@brickflow/ui 0.0.56 → 0.0.58
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 +4 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +35 -7
- package/dist/runtime/brickflow-ui.d.ts +1 -0
- package/dist/runtime/components/Button/index.vue +5 -3
- package/dist/runtime/components/Dialog/DemoContent.vue +33 -0
- package/dist/runtime/components/Dialog/DemoForm.vue +54 -0
- package/dist/runtime/components/Dialog/DemoSimple.vue +23 -0
- package/dist/runtime/components/Dialog/DemoStack.vue +41 -0
- package/dist/runtime/components/Dialog/Host.vue +654 -0
- package/dist/runtime/components/Dialog/basic.demo.vue +27 -0
- package/dist/runtime/components/Dialog/form.demo.vue +34 -0
- package/dist/runtime/components/Dialog/index.vue +53 -0
- package/dist/runtime/components/Dialog/scroll.demo.vue +27 -0
- package/dist/runtime/components/Dialog/stack.demo.vue +19 -0
- package/dist/runtime/components/Form/index.vue +46 -1
- package/dist/runtime/components/Form/login.demo.vue +12 -1
- package/dist/runtime/components/Form/rules.demo.vue +108 -0
- package/dist/runtime/components/Icon/gallery.demo.vue +1 -1
- package/dist/runtime/components/Icon/index.vue +3 -1
- package/dist/runtime/components/Input/index.vue +151 -27
- 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/controls.demo.vue +145 -0
- package/dist/runtime/components/InputNumber/denominator.demo.vue +1 -1
- package/dist/runtime/components/InputNumber/index.vue +29 -8
- 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/useDialog.d.ts +38 -0
- package/dist/runtime/composables/useDialog.js +40 -0
- package/dist/runtime/composables/useFieldRules.d.ts +16 -0
- package/dist/runtime/composables/useFieldRules.js +42 -0
- package/dist/runtime/composables/useScreen.d.ts +8 -0
- package/dist/runtime/plugins/dialog.d.ts +3 -0
- package/dist/runtime/plugins/dialog.js +22 -0
- package/dist/runtime/tailwind.d.ts +1 -1
- package/dist/runtime/tailwind.js +2 -2
- package/dist/runtime/utils/rules.d.ts +34 -0
- package/dist/runtime/utils/rules.js +68 -0
- package/dist/types.d.mts +4 -0
- package/package.json +7 -6
- package/dist/runtime/icon-font.d.ts +0 -4
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export const uiDemo = {
|
|
3
|
+
description: 'Keep form state inside the dialog, require an explicit choice, and return the saved value.',
|
|
4
|
+
title: 'Form and explicit dismissal',
|
|
5
|
+
}
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
import { defineAsyncComponent, ref } from 'vue'
|
|
10
|
+
|
|
11
|
+
import { useDialog } from '../../composables/useDialog'
|
|
12
|
+
import Button from '../Button/index.vue'
|
|
13
|
+
|
|
14
|
+
const dialog = useDialog()
|
|
15
|
+
const savedName = ref('')
|
|
16
|
+
const LazyDemoForm = defineAsyncComponent(() => import('./DemoForm.vue'))
|
|
17
|
+
|
|
18
|
+
function openForm(): void {
|
|
19
|
+
dialog.open(LazyDemoForm, { onSave: (name: string) => (savedName.value = name) }, { preventClose: true })
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
25
|
+
<Button @click="openForm">Create project</Button>
|
|
26
|
+
<p
|
|
27
|
+
v-if="savedName"
|
|
28
|
+
class="text-sm text-zinc-300"
|
|
29
|
+
role="status"
|
|
30
|
+
>
|
|
31
|
+
Saved: {{ savedName }}
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { IconName } from '#brickflow-ui'
|
|
3
|
+
|
|
4
|
+
import type { DialogPlacement } from '../../composables/useDialog'
|
|
5
|
+
import type { ScreenKey } from '../../composables/useScreen'
|
|
6
|
+
|
|
7
|
+
export const dialogConfig = defineUiConfig<{
|
|
8
|
+
elasticHeight?: boolean
|
|
9
|
+
loadingDelay: number
|
|
10
|
+
loadingIconName: IconName
|
|
11
|
+
loadingIconTransition: string
|
|
12
|
+
placement: Record<ScreenKey, DialogPlacement>
|
|
13
|
+
}>()
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
defineProps<{
|
|
18
|
+
dragY?: number
|
|
19
|
+
elasticHeight: boolean
|
|
20
|
+
placement: 'bottom' | 'center' | 'fullscreen'
|
|
21
|
+
}>()
|
|
22
|
+
|
|
23
|
+
const placementClasses = {
|
|
24
|
+
bottom: UI_STYLE.panelPlacementClasses.bottom,
|
|
25
|
+
center: UI_STYLE.panelPlacementClasses.center,
|
|
26
|
+
fullscreen: UI_STYLE.panelPlacementClasses.fullscreen,
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div
|
|
32
|
+
:class="[UI_STYLE.panel, placementClasses[placement]]"
|
|
33
|
+
data-dialog-panel
|
|
34
|
+
role="dialog"
|
|
35
|
+
aria-modal="true"
|
|
36
|
+
tabindex="-1"
|
|
37
|
+
:style="{
|
|
38
|
+
'--dialog-elastic-height':
|
|
39
|
+
placement === 'bottom' && elasticHeight ? `${Math.max(0, -(dragY ?? 0))}px` : undefined,
|
|
40
|
+
transform: dragY && (!elasticHeight || dragY > 0) ? `translateY(${dragY}px)` : undefined,
|
|
41
|
+
}"
|
|
42
|
+
>
|
|
43
|
+
<div
|
|
44
|
+
v-if="placement === 'bottom'"
|
|
45
|
+
:class="UI_STYLE.dragHandle"
|
|
46
|
+
data-dialog-handle
|
|
47
|
+
aria-hidden="true"
|
|
48
|
+
>
|
|
49
|
+
<span :class="UI_STYLE.dragHandleIndicator" />
|
|
50
|
+
</div>
|
|
51
|
+
<slot />
|
|
52
|
+
</div>
|
|
53
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export const uiDemo = {
|
|
3
|
+
description: 'The content component owns overflow. Scroll to an edge before dragging the sheet.',
|
|
4
|
+
title: 'Long content and scrolling',
|
|
5
|
+
}
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
import { defineAsyncComponent } from 'vue'
|
|
10
|
+
|
|
11
|
+
import { useDialog } from '../../composables/useDialog'
|
|
12
|
+
import Button from '../Button/index.vue'
|
|
13
|
+
|
|
14
|
+
const dialog = useDialog()
|
|
15
|
+
const LazyDemoContent = defineAsyncComponent(() => import('./DemoContent.vue'))
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div class="flex flex-wrap gap-2">
|
|
20
|
+
<Button @click="dialog.open(LazyDemoContent, { text: 'Scrollable bottom sheet' }, { placement: 'bottom' })">
|
|
21
|
+
Scrollable sheet
|
|
22
|
+
</Button>
|
|
23
|
+
<Button @click="dialog.open(LazyDemoContent, { text: 'Scrollable centered dialog' }, { placement: 'center' })">
|
|
24
|
+
Scrollable center
|
|
25
|
+
</Button>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export const uiDemo = {
|
|
3
|
+
title: 'Stacked dialogs',
|
|
4
|
+
}
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { defineAsyncComponent } from 'vue'
|
|
9
|
+
|
|
10
|
+
import { useDialog } from '../../composables/useDialog'
|
|
11
|
+
import Button from '../Button/index.vue'
|
|
12
|
+
|
|
13
|
+
const dialog = useDialog()
|
|
14
|
+
const LazyDemoStack = defineAsyncComponent(() => import('./DemoStack.vue'))
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<Button @click="dialog.open(LazyDemoStack, { level: 1 })">Open stack</Button>
|
|
19
|
+
</template>
|
|
@@ -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
|
|
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@
|
|
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
|
/>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export const uiDemo = {
|
|
3
|
+
description: 'Each field has one rule. Submit it to see that rule’s validation message.',
|
|
4
|
+
title: 'Basic rules',
|
|
5
|
+
}
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
import { reactive } from 'vue'
|
|
10
|
+
|
|
11
|
+
import { rule, type Rule } from '../../utils/rules'
|
|
12
|
+
import Button from '../Button/index.vue'
|
|
13
|
+
import Input from '../Input/index.vue'
|
|
14
|
+
import Form from './index.vue'
|
|
15
|
+
|
|
16
|
+
const demos: { description: string; id: string; label: string; placeholder: string; rules: Rule<string>[] }[] = [
|
|
17
|
+
{
|
|
18
|
+
description: 'Leave empty, or enter only spaces.',
|
|
19
|
+
id: 'required',
|
|
20
|
+
label: 'Required',
|
|
21
|
+
placeholder: 'Enter any text',
|
|
22
|
+
rules: [rule.required()],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
description: 'Try “hello”, then “hello@example.com”.',
|
|
26
|
+
id: 'email',
|
|
27
|
+
label: 'Email',
|
|
28
|
+
placeholder: 'hello@example.com',
|
|
29
|
+
rules: [rule.email()],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
description: 'Enter exactly 5 characters.',
|
|
33
|
+
id: 'length',
|
|
34
|
+
label: 'Length (5)',
|
|
35
|
+
placeholder: 'abcde',
|
|
36
|
+
rules: [rule.length(5)],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
description: 'Enter fewer than 5 characters, then 5 or more.',
|
|
40
|
+
id: 'minLength',
|
|
41
|
+
label: 'Minimum length (5)',
|
|
42
|
+
placeholder: 'At least 5 characters',
|
|
43
|
+
rules: [rule.minLength(5)],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
description: 'Enter more than 5 characters.',
|
|
47
|
+
id: 'maxLength',
|
|
48
|
+
label: 'Maximum length (5)',
|
|
49
|
+
placeholder: 'Up to 5 characters',
|
|
50
|
+
rules: [rule.maxLength(5)],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
description: 'Enter letters or numbers, then add a space or symbol.',
|
|
54
|
+
id: 'regex',
|
|
55
|
+
label: 'Regex (letters and numbers)',
|
|
56
|
+
placeholder: 'abc123',
|
|
57
|
+
rules: [rule.regex(/^[a-z0-9]+$/i, 'Use letters and numbers only')],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
description: 'Try “example.com”, then “https://example.com”.',
|
|
61
|
+
id: 'url',
|
|
62
|
+
label: 'URL',
|
|
63
|
+
placeholder: 'https://example.com',
|
|
64
|
+
rules: [rule.url()],
|
|
65
|
+
},
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
const values = reactive<Record<string, string>>({})
|
|
69
|
+
const submitted = reactive<Record<string, boolean>>({})
|
|
70
|
+
|
|
71
|
+
function updateValue(id: string, value: string): void {
|
|
72
|
+
values[id] = value
|
|
73
|
+
submitted[id] = false
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<template>
|
|
78
|
+
<div class="grid gap-6">
|
|
79
|
+
<Form
|
|
80
|
+
v-for="demo in demos"
|
|
81
|
+
:key="demo.id"
|
|
82
|
+
class="rounded-lg"
|
|
83
|
+
@submit="submitted[demo.id] = true"
|
|
84
|
+
>
|
|
85
|
+
<Input
|
|
86
|
+
:value="values[demo.id] ?? ''"
|
|
87
|
+
:label="demo.label"
|
|
88
|
+
:description="demo.description"
|
|
89
|
+
:placeholder="demo.placeholder"
|
|
90
|
+
:rules="demo.rules"
|
|
91
|
+
@input="updateValue(demo.id, $event)"
|
|
92
|
+
/>
|
|
93
|
+
<Button
|
|
94
|
+
class="self-start"
|
|
95
|
+
type="submit"
|
|
96
|
+
>
|
|
97
|
+
Test rule
|
|
98
|
+
</Button>
|
|
99
|
+
<p
|
|
100
|
+
v-if="submitted[demo.id]"
|
|
101
|
+
class="text-sm text-green-400"
|
|
102
|
+
role="status"
|
|
103
|
+
>
|
|
104
|
+
Passed
|
|
105
|
+
</p>
|
|
106
|
+
</Form>
|
|
107
|
+
</div>
|
|
108
|
+
</template>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
const filteredIconNames = computed(() => {
|
|
21
21
|
const query = search.value.trim().toLocaleLowerCase()
|
|
22
22
|
|
|
23
|
-
return query ? iconNames.filter((name) => name.toLocaleLowerCase().includes(query)) : iconNames
|
|
23
|
+
return query ? iconNames.filter((name: string) => name.toLocaleLowerCase().includes(query)) : iconNames
|
|
24
24
|
})
|
|
25
25
|
|
|
26
26
|
const copyIconName = async (name: string): Promise<void> => {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { IconName } from '#brickflow-ui'
|
|
3
|
+
|
|
2
4
|
const UI_CONFIG = defineUiConfig<{
|
|
3
|
-
hidePasswordIconName:
|
|
4
|
-
searchClearIconName:
|
|
5
|
-
showPasswordIconName:
|
|
5
|
+
hidePasswordIconName: IconName
|
|
6
|
+
searchClearIconName: IconName
|
|
7
|
+
showPasswordIconName: IconName
|
|
6
8
|
sizeClasses: Record<
|
|
7
9
|
string,
|
|
8
10
|
{
|
|
@@ -31,10 +33,16 @@
|
|
|
31
33
|
</script>
|
|
32
34
|
|
|
33
35
|
<script setup lang="ts">
|
|
36
|
+
import type { MaskaDetail, MaskInput, MaskInputOptions, MaskType } from 'maska'
|
|
37
|
+
|
|
34
38
|
import { twMerge } from 'tailwind-merge'
|
|
35
|
-
import { computed, ref, shallowRef, useAttrs, useId, useSlots, watch } from 'vue'
|
|
39
|
+
import { computed, nextTick, ref, shallowRef, useAttrs, useId, useSlots, watch } from 'vue'
|
|
40
|
+
|
|
41
|
+
import type { Rule } from '../../utils/rules'
|
|
36
42
|
|
|
43
|
+
import { useFieldRules } from '../../composables/useFieldRules'
|
|
37
44
|
import Icon from '../Icon/index.vue'
|
|
45
|
+
import TransitionSize from '../TransitionSize/index.vue'
|
|
38
46
|
|
|
39
47
|
defineOptions({ inheritAttrs: false })
|
|
40
48
|
|
|
@@ -45,18 +53,21 @@
|
|
|
45
53
|
autoWidth?: boolean
|
|
46
54
|
description?: string
|
|
47
55
|
disabled?: boolean
|
|
48
|
-
icon?:
|
|
56
|
+
icon?: IconName
|
|
49
57
|
label?: string
|
|
58
|
+
mask?: MaskInputOptions | MaskType
|
|
50
59
|
onBlur?: (value: string, target: HTMLInputElement) => void
|
|
51
60
|
onChange?: (value: string, target: HTMLInputElement) => void
|
|
52
61
|
onFocus?: (value: string, target: HTMLInputElement | null) => void
|
|
53
62
|
onIconClick?: () => void
|
|
54
63
|
onInput?: (value: string, target: HTMLInputElement | null) => void
|
|
64
|
+
onMask?: (detail: MaskaDetail, target: HTMLInputElement) => void
|
|
55
65
|
onTrailingIconClick?: () => void
|
|
56
66
|
placeholder?: string
|
|
57
67
|
readonly?: boolean
|
|
68
|
+
rules?: Rule<string>[]
|
|
58
69
|
size?: InputSize
|
|
59
|
-
trailingIcon?:
|
|
70
|
+
trailingIcon?: IconName
|
|
60
71
|
type?: 'decimal' | 'email' | 'numeric' | 'password' | 'search' | 'tel' | 'text' | 'url'
|
|
61
72
|
value?: null | string
|
|
62
73
|
variant?: InputVariant
|
|
@@ -69,14 +80,17 @@
|
|
|
69
80
|
disabled: false,
|
|
70
81
|
icon: undefined,
|
|
71
82
|
label: undefined,
|
|
83
|
+
mask: undefined,
|
|
72
84
|
onBlur: undefined,
|
|
73
85
|
onChange: undefined,
|
|
74
86
|
onFocus: undefined,
|
|
75
87
|
onIconClick: undefined,
|
|
76
88
|
onInput: undefined,
|
|
89
|
+
onMask: undefined,
|
|
77
90
|
onTrailingIconClick: undefined,
|
|
78
91
|
placeholder: '',
|
|
79
92
|
readonly: false,
|
|
93
|
+
rules: undefined,
|
|
80
94
|
size: 'md',
|
|
81
95
|
trailingIcon: undefined,
|
|
82
96
|
type: 'text',
|
|
@@ -86,10 +100,13 @@
|
|
|
86
100
|
)
|
|
87
101
|
|
|
88
102
|
const model = shallowRef('')
|
|
103
|
+
const maskedValue = shallowRef<null | string>(null)
|
|
104
|
+
let maskInput: MaskInput | undefined
|
|
89
105
|
|
|
90
106
|
const attrs = useAttrs()
|
|
91
107
|
const slots = useSlots()
|
|
92
108
|
const generatedId = useId()
|
|
109
|
+
const baseRef = ref<HTMLInputElement | null>(null)
|
|
93
110
|
const inputRef = ref<HTMLInputElement | null>(null)
|
|
94
111
|
const widthMeasureRef = ref<HTMLSpanElement | null>(null)
|
|
95
112
|
const measuredWidth = ref(0)
|
|
@@ -106,12 +123,27 @@
|
|
|
106
123
|
const fieldId = computed(() => String(attrs.id ?? generatedId))
|
|
107
124
|
const descriptionId = computed(() => `${fieldId.value}-description`)
|
|
108
125
|
const currentValue = computed(() => (props.value === undefined ? model.value : (props.value ?? '')))
|
|
109
|
-
const
|
|
126
|
+
const {
|
|
127
|
+
error,
|
|
128
|
+
input: validateInput,
|
|
129
|
+
validate,
|
|
130
|
+
} = useFieldRules(
|
|
131
|
+
currentValue,
|
|
132
|
+
computed(() => props.rules),
|
|
133
|
+
focus,
|
|
134
|
+
)
|
|
135
|
+
const displayValue = computed(() =>
|
|
136
|
+
props.mask === null || props.mask === undefined
|
|
137
|
+
? currentValue.value
|
|
138
|
+
: (maskedValue.value ?? currentValue.value),
|
|
139
|
+
)
|
|
140
|
+
const widthMeasureText = computed(() => displayValue.value || props.placeholder || ' ')
|
|
110
141
|
const isEmptyField = computed(() => {
|
|
111
|
-
return Boolean(
|
|
142
|
+
return Boolean(displayValue.value)
|
|
112
143
|
})
|
|
113
144
|
|
|
114
|
-
const
|
|
145
|
+
const hasError = computed(() => Boolean(error.value))
|
|
146
|
+
const hasDescription = computed(() => Boolean(props.description || slots.description || hasError.value))
|
|
115
147
|
const inputType = computed(() => {
|
|
116
148
|
return props.type === 'password' && !showPassword.value ? 'password' : 'text'
|
|
117
149
|
})
|
|
@@ -132,7 +164,12 @@
|
|
|
132
164
|
])
|
|
133
165
|
const labelClasses = computed(() => twMerge(UI_STYLE.label, sizeClass.value.label, variantClass.value.label))
|
|
134
166
|
const descriptionClasses = computed(() =>
|
|
135
|
-
twMerge(
|
|
167
|
+
twMerge(
|
|
168
|
+
UI_STYLE.description,
|
|
169
|
+
sizeClass.value.description,
|
|
170
|
+
variantClass.value.description,
|
|
171
|
+
hasError.value && UI_STYLE.state.errorDescription,
|
|
172
|
+
),
|
|
136
173
|
)
|
|
137
174
|
const leadingIconClasses = computed(() =>
|
|
138
175
|
twMerge(
|
|
@@ -164,6 +201,64 @@
|
|
|
164
201
|
{ flush: 'post', immediate: true },
|
|
165
202
|
)
|
|
166
203
|
|
|
204
|
+
watch(
|
|
205
|
+
[() => props.mask, inputRef],
|
|
206
|
+
async ([mask, input], _previous, onCleanup) => {
|
|
207
|
+
maskInput?.destroy()
|
|
208
|
+
maskInput = undefined
|
|
209
|
+
maskedValue.value = null
|
|
210
|
+
|
|
211
|
+
if (mask === null || mask === undefined || !input) {
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let canceled = false
|
|
216
|
+
onCleanup(() => {
|
|
217
|
+
canceled = true
|
|
218
|
+
maskInput?.destroy()
|
|
219
|
+
maskInput = undefined
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const { MaskInput } = await import('maska')
|
|
223
|
+
if (canceled) {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const options: MaskInputOptions = typeof mask === 'object' && !Array.isArray(mask) ? mask : { mask }
|
|
228
|
+
const onMaska = options.onMaska
|
|
229
|
+
|
|
230
|
+
maskInput = new MaskInput(input, {
|
|
231
|
+
...options,
|
|
232
|
+
onMaska: (detail) => {
|
|
233
|
+
maskedValue.value = detail.masked
|
|
234
|
+
model.value = detail.masked
|
|
235
|
+
if (Array.isArray(onMaska)) {
|
|
236
|
+
onMaska.forEach((callback) => callback(detail))
|
|
237
|
+
} else {
|
|
238
|
+
onMaska?.(detail)
|
|
239
|
+
}
|
|
240
|
+
props.onMask?.(detail, input)
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
},
|
|
244
|
+
{ flush: 'post', immediate: true },
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
watch(
|
|
248
|
+
() => props.value,
|
|
249
|
+
async () => {
|
|
250
|
+
if (!maskInput || !inputRef.value) {
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
maskedValue.value = null
|
|
254
|
+
await nextTick()
|
|
255
|
+
if (maskInput && inputRef.value) {
|
|
256
|
+
maskInput.updateValue(inputRef.value)
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
{ flush: 'post' },
|
|
260
|
+
)
|
|
261
|
+
|
|
167
262
|
function clear(): void {
|
|
168
263
|
setValue('')
|
|
169
264
|
focus()
|
|
@@ -189,30 +284,46 @@
|
|
|
189
284
|
}
|
|
190
285
|
|
|
191
286
|
function handleInput(event: Event): void {
|
|
287
|
+
if (props.mask !== null && props.mask !== undefined && event instanceof CustomEvent) {
|
|
288
|
+
return
|
|
289
|
+
}
|
|
192
290
|
setValue((event.target as HTMLInputElement).value)
|
|
193
291
|
}
|
|
194
292
|
|
|
195
293
|
function setValue(value: string): void {
|
|
196
294
|
model.value = value
|
|
295
|
+
validateInput(value)
|
|
296
|
+
if (props.mask !== null && props.mask !== undefined) {
|
|
297
|
+
maskedValue.value = value
|
|
298
|
+
}
|
|
197
299
|
props.onInput?.(value, inputRef.value)
|
|
198
300
|
}
|
|
199
301
|
|
|
200
|
-
defineExpose({ clear, fieldId, focus, hasFocus, inputRef })
|
|
302
|
+
defineExpose({ clear, fieldId, focus, hasFocus, inputRef, validate })
|
|
201
303
|
</script>
|
|
202
304
|
|
|
203
305
|
<template>
|
|
204
|
-
<div
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
306
|
+
<div
|
|
307
|
+
ref="baseRef"
|
|
308
|
+
:class="[classes, $attrs.class]"
|
|
309
|
+
>
|
|
310
|
+
<TransitionSize
|
|
311
|
+
:flexParent="baseRef"
|
|
312
|
+
flexDirection="end"
|
|
209
313
|
>
|
|
210
|
-
<
|
|
211
|
-
|
|
314
|
+
<label
|
|
315
|
+
v-if="props.label || $slots.label"
|
|
316
|
+
:for="fieldId"
|
|
317
|
+
:class="labelClasses"
|
|
318
|
+
>
|
|
319
|
+
<slot name="label">{{ props.label }}</slot>
|
|
320
|
+
</label>
|
|
321
|
+
</TransitionSize>
|
|
212
322
|
|
|
213
323
|
<label
|
|
214
324
|
:for="fieldId"
|
|
215
325
|
:class="fieldWrapClasses"
|
|
326
|
+
:aria-invalid="hasError ? 'true' : undefined"
|
|
216
327
|
:style="props.autoWidth ? { maxWidth: '100%', width: 'fit-content' } : undefined"
|
|
217
328
|
>
|
|
218
329
|
<span
|
|
@@ -244,7 +355,7 @@
|
|
|
244
355
|
v-bind="{ ...$attrs, class: undefined, id: undefined }"
|
|
245
356
|
:id="fieldId"
|
|
246
357
|
ref="inputRef"
|
|
247
|
-
:value="
|
|
358
|
+
:value="displayValue"
|
|
248
359
|
:type="inputType"
|
|
249
360
|
:inputmode="props.type === 'password' ? undefined : props.type"
|
|
250
361
|
:autocomplete="props.autocomplete"
|
|
@@ -252,6 +363,7 @@
|
|
|
252
363
|
:readonly="props.readonly"
|
|
253
364
|
:placeholder="props.placeholder"
|
|
254
365
|
:aria-describedby="hasDescription ? descriptionId : undefined"
|
|
366
|
+
:aria-invalid="hasError ? 'true' : undefined"
|
|
255
367
|
:class="fieldClasses"
|
|
256
368
|
:style="fieldStyle"
|
|
257
369
|
@input="handleInput"
|
|
@@ -307,13 +419,25 @@
|
|
|
307
419
|
</slot>
|
|
308
420
|
</label>
|
|
309
421
|
|
|
310
|
-
<
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
422
|
+
<TransitionSize :flexParent="baseRef">
|
|
423
|
+
<div
|
|
424
|
+
v-if="hasDescription"
|
|
425
|
+
:id="descriptionId"
|
|
426
|
+
:class="descriptionClasses"
|
|
427
|
+
>
|
|
428
|
+
<slot
|
|
429
|
+
v-if="hasError"
|
|
430
|
+
name="error"
|
|
431
|
+
>
|
|
432
|
+
{{ error }}
|
|
433
|
+
</slot>
|
|
434
|
+
<slot
|
|
435
|
+
v-else
|
|
436
|
+
name="description"
|
|
437
|
+
>
|
|
438
|
+
{{ props.description }}
|
|
439
|
+
</slot>
|
|
440
|
+
</div>
|
|
441
|
+
</TransitionSize>
|
|
318
442
|
</div>
|
|
319
443
|
</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>
|