@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.
Files changed (47) hide show
  1. package/dist/module.d.mts +4 -2
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +35 -7
  4. package/dist/runtime/brickflow-ui.d.ts +1 -0
  5. package/dist/runtime/components/Button/index.vue +5 -3
  6. package/dist/runtime/components/Dialog/DemoContent.vue +33 -0
  7. package/dist/runtime/components/Dialog/DemoForm.vue +54 -0
  8. package/dist/runtime/components/Dialog/DemoSimple.vue +23 -0
  9. package/dist/runtime/components/Dialog/DemoStack.vue +41 -0
  10. package/dist/runtime/components/Dialog/Host.vue +654 -0
  11. package/dist/runtime/components/Dialog/basic.demo.vue +27 -0
  12. package/dist/runtime/components/Dialog/form.demo.vue +34 -0
  13. package/dist/runtime/components/Dialog/index.vue +53 -0
  14. package/dist/runtime/components/Dialog/scroll.demo.vue +27 -0
  15. package/dist/runtime/components/Dialog/stack.demo.vue +19 -0
  16. package/dist/runtime/components/Form/index.vue +46 -1
  17. package/dist/runtime/components/Form/login.demo.vue +12 -1
  18. package/dist/runtime/components/Form/rules.demo.vue +108 -0
  19. package/dist/runtime/components/Icon/gallery.demo.vue +1 -1
  20. package/dist/runtime/components/Icon/index.vue +3 -1
  21. package/dist/runtime/components/Input/index.vue +151 -27
  22. package/dist/runtime/components/Input/label.demo.vue +15 -3
  23. package/dist/runtime/components/Input/mask.demo.vue +73 -0
  24. package/dist/runtime/components/Input/variants.demo.vue +16 -0
  25. package/dist/runtime/components/InputNumber/controls.demo.vue +145 -0
  26. package/dist/runtime/components/InputNumber/denominator.demo.vue +1 -1
  27. package/dist/runtime/components/InputNumber/index.vue +29 -8
  28. package/dist/runtime/components/TransitionSize/duration.demo.vue +88 -0
  29. package/dist/runtime/components/TransitionSize/flex.demo.vue +112 -0
  30. package/dist/runtime/components/TransitionSize/height.demo.vue +88 -0
  31. package/dist/runtime/components/TransitionSize/index.vue +69 -27
  32. package/dist/runtime/components/TransitionSize/width.demo.vue +88 -0
  33. package/dist/runtime/composables/useBrickflow.d.ts +2 -0
  34. package/dist/runtime/composables/useDialog.d.ts +38 -0
  35. package/dist/runtime/composables/useDialog.js +40 -0
  36. package/dist/runtime/composables/useFieldRules.d.ts +16 -0
  37. package/dist/runtime/composables/useFieldRules.js +42 -0
  38. package/dist/runtime/composables/useScreen.d.ts +8 -0
  39. package/dist/runtime/plugins/dialog.d.ts +3 -0
  40. package/dist/runtime/plugins/dialog.js +22 -0
  41. package/dist/runtime/tailwind.d.ts +1 -1
  42. package/dist/runtime/tailwind.js +2 -2
  43. package/dist/runtime/utils/rules.d.ts +34 -0
  44. package/dist/runtime/utils/rules.js +68 -0
  45. package/dist/types.d.mts +4 -0
  46. package/package.json +7 -6
  47. 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 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
  />
@@ -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,9 +1,11 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from 'vue'
3
3
 
4
+ import type { IconName } from '#brickflow-ui'
5
+
4
6
  const props = withDefaults(
5
7
  defineProps<{
6
- name: string
8
+ name: IconName
7
9
  }>(),
8
10
  {},
9
11
  )
@@ -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: string
4
- searchClearIconName: string
5
- showPasswordIconName: string
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?: string
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?: string
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 widthMeasureText = computed(() => currentValue.value || props.placeholder || ' ')
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(currentValue.value)
142
+ return Boolean(displayValue.value)
112
143
  })
113
144
 
114
- const hasDescription = computed(() => Boolean(props.description || slots.description || slots.error))
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(UI_STYLE.description, sizeClass.value.description, variantClass.value.description),
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 :class="[classes, $attrs.class]">
205
- <label
206
- v-if="props.label || $slots.label"
207
- :for="fieldId"
208
- :class="labelClasses"
306
+ <div
307
+ ref="baseRef"
308
+ :class="[classes, $attrs.class]"
309
+ >
310
+ <TransitionSize
311
+ :flexParent="baseRef"
312
+ flexDirection="end"
209
313
  >
210
- <slot name="label">{{ props.label }}</slot>
211
- </label>
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="currentValue"
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
- <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>
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>