@polymarbot/nuxt-layer-shadcn-ui 0.8.7 → 0.9.0
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/app/components/ui/Checkbox/index.stories.ts +9 -0
- package/app/components/ui/Checkbox/index.vue +4 -1
- package/app/components/ui/Checkbox/types.ts +3 -1
- package/app/components/ui/DatePicker/index.stories.ts +9 -0
- package/app/components/ui/DatePicker/index.vue +3 -0
- package/app/components/ui/DatePicker/types.ts +2 -0
- package/app/components/ui/DateRangePicker/index.stories.ts +9 -0
- package/app/components/ui/DateRangePicker/index.vue +3 -0
- package/app/components/ui/DateRangePicker/types.ts +2 -0
- package/app/components/ui/Drawer/index.stories.ts +78 -1
- package/app/components/ui/Drawer/index.vue +31 -9
- package/app/components/ui/Drawer/types.ts +5 -0
- package/app/components/ui/FormItem/index.stories.ts +137 -10
- package/app/components/ui/FormItem/index.vue +10 -3
- package/app/components/ui/Input/index.vue +3 -1
- package/app/components/ui/InputCurrency/index.stories.ts +9 -0
- package/app/components/ui/InputCurrency/types.ts +3 -1
- package/app/components/ui/InputNumber/index.vue +10 -7
- package/app/components/ui/InputOtp/index.stories.ts +10 -0
- package/app/components/ui/InputOtp/index.vue +5 -1
- package/app/components/ui/InputOtp/types.ts +1 -0
- package/app/components/ui/InputPercent/index.stories.ts +11 -0
- package/app/components/ui/InputPercent/index.vue +6 -0
- package/app/components/ui/InputPercent/types.ts +3 -0
- package/app/components/ui/InputRange/index.stories.ts +11 -0
- package/app/components/ui/Modal/index.stories.ts +78 -0
- package/app/components/ui/Modal/index.vue +31 -9
- package/app/components/ui/Modal/types.ts +5 -0
- package/app/components/ui/RadioCardGroup/index.stories.ts +9 -0
- package/app/components/ui/RadioCardGroup/index.vue +4 -0
- package/app/components/ui/RadioCardGroup/types.ts +1 -0
- package/app/components/ui/RadioGroup/index.stories.ts +9 -0
- package/app/components/ui/RadioGroup/index.vue +4 -0
- package/app/components/ui/RadioGroup/types.ts +1 -0
- package/app/components/ui/SearchSelect/index.stories.ts +9 -0
- package/app/components/ui/SearchSelect/index.vue +2 -0
- package/app/components/ui/SearchSelect/types.ts +1 -0
- package/app/components/ui/Select/index.stories.ts +9 -0
- package/app/components/ui/Select/index.vue +6 -0
- package/app/components/ui/Select/types.ts +1 -0
- package/app/components/ui/Textarea/index.vue +3 -1
- package/app/composables/useFormItemInvalid.ts +16 -0
- package/package.json +2 -2
|
@@ -10,6 +10,7 @@ const meta = {
|
|
|
10
10
|
defaultValue: { control: 'select', options: [ true, false, 'indeterminate' ]},
|
|
11
11
|
disabled: { control: 'boolean' },
|
|
12
12
|
required: { control: 'boolean' },
|
|
13
|
+
invalid: { control: 'boolean' },
|
|
13
14
|
name: { control: 'text' },
|
|
14
15
|
value: { control: 'text' },
|
|
15
16
|
},
|
|
@@ -18,6 +19,7 @@ const meta = {
|
|
|
18
19
|
defaultValue: false,
|
|
19
20
|
disabled: false,
|
|
20
21
|
required: false,
|
|
22
|
+
invalid: false,
|
|
21
23
|
name: '',
|
|
22
24
|
value: '',
|
|
23
25
|
},
|
|
@@ -104,6 +106,13 @@ export const Indeterminate: Story = {
|
|
|
104
106
|
}),
|
|
105
107
|
}
|
|
106
108
|
|
|
109
|
+
export const Invalid: Story = {
|
|
110
|
+
parameters: noControls,
|
|
111
|
+
args: {
|
|
112
|
+
invalid: true,
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
|
|
107
116
|
export const Disabled: Story = {
|
|
108
117
|
parameters: {
|
|
109
118
|
...noControls,
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
import { Checkbox as ShadcnCheckbox } from '../../shadcn/checkbox'
|
|
3
3
|
import type { CheckboxProps } from './types'
|
|
4
4
|
|
|
5
|
-
defineProps<CheckboxProps>()
|
|
5
|
+
const props = defineProps<CheckboxProps>()
|
|
6
|
+
|
|
7
|
+
const isInvalid = useFormItemInvalid(() => props.invalid)
|
|
6
8
|
</script>
|
|
7
9
|
|
|
8
10
|
<template>
|
|
9
11
|
<ShadcnCheckbox
|
|
10
12
|
v-slot="{ state }"
|
|
13
|
+
:aria-invalid="isInvalid || undefined"
|
|
11
14
|
class="
|
|
12
15
|
data-[state=indeterminate]:border-primary
|
|
13
16
|
data-[state=indeterminate]:bg-primary
|
|
@@ -16,6 +16,7 @@ const meta = {
|
|
|
16
16
|
showTime: { control: 'boolean' },
|
|
17
17
|
disabled: { control: 'boolean' },
|
|
18
18
|
readonly: { control: 'boolean' },
|
|
19
|
+
invalid: { control: 'boolean' },
|
|
19
20
|
placeholder: { control: 'text' },
|
|
20
21
|
minDate: { control: 'date' },
|
|
21
22
|
maxDate: { control: 'date' },
|
|
@@ -29,6 +30,7 @@ const meta = {
|
|
|
29
30
|
showTime: false,
|
|
30
31
|
disabled: false,
|
|
31
32
|
readonly: false,
|
|
33
|
+
invalid: false,
|
|
32
34
|
placeholder: '',
|
|
33
35
|
minDate: undefined,
|
|
34
36
|
maxDate: undefined,
|
|
@@ -224,6 +226,13 @@ export const Readonly: Story = {
|
|
|
224
226
|
}),
|
|
225
227
|
}
|
|
226
228
|
|
|
229
|
+
export const Invalid: Story = {
|
|
230
|
+
parameters: noControls,
|
|
231
|
+
args: {
|
|
232
|
+
invalid: true,
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
|
|
227
236
|
export const InModal: Story = {
|
|
228
237
|
parameters: {
|
|
229
238
|
...noControls,
|
|
@@ -20,6 +20,7 @@ const props = withDefaults(defineProps<DatePickerProps>(), {
|
|
|
20
20
|
showTime: false,
|
|
21
21
|
disabled: false,
|
|
22
22
|
readonly: false,
|
|
23
|
+
invalid: false,
|
|
23
24
|
placeholder: undefined,
|
|
24
25
|
minDate: undefined,
|
|
25
26
|
maxDate: undefined,
|
|
@@ -28,6 +29,8 @@ const props = withDefaults(defineProps<DatePickerProps>(), {
|
|
|
28
29
|
class: undefined,
|
|
29
30
|
})
|
|
30
31
|
|
|
32
|
+
useFormItemInvalid(() => props.invalid)
|
|
33
|
+
|
|
31
34
|
const emit = defineEmits<{
|
|
32
35
|
'update:modelValue': [value: Date | string | number | null]
|
|
33
36
|
}>()
|
|
@@ -27,6 +27,8 @@ export interface DatePickerProps {
|
|
|
27
27
|
disabled?: boolean
|
|
28
28
|
/** Readonly mode */
|
|
29
29
|
readonly?: boolean
|
|
30
|
+
/** Mark the field as invalid (renders the inner Input with destructive styling) */
|
|
31
|
+
invalid?: boolean
|
|
30
32
|
/** Placeholder text */
|
|
31
33
|
placeholder?: string
|
|
32
34
|
/** Minimum selectable date */
|
|
@@ -13,6 +13,7 @@ const meta = {
|
|
|
13
13
|
showTime: { control: 'boolean' },
|
|
14
14
|
disabled: { control: 'boolean' },
|
|
15
15
|
readonly: { control: 'boolean' },
|
|
16
|
+
invalid: { control: 'boolean' },
|
|
16
17
|
startPlaceholder: { control: 'text' },
|
|
17
18
|
endPlaceholder: { control: 'text' },
|
|
18
19
|
maxSpanDays: { control: 'number' },
|
|
@@ -28,6 +29,7 @@ const meta = {
|
|
|
28
29
|
showTime: false,
|
|
29
30
|
disabled: false,
|
|
30
31
|
readonly: false,
|
|
32
|
+
invalid: false,
|
|
31
33
|
startPlaceholder: '',
|
|
32
34
|
endPlaceholder: '',
|
|
33
35
|
maxSpanDays: undefined,
|
|
@@ -196,3 +198,10 @@ export const Readonly: Story = {
|
|
|
196
198
|
`,
|
|
197
199
|
}),
|
|
198
200
|
}
|
|
201
|
+
|
|
202
|
+
export const Invalid: Story = {
|
|
203
|
+
parameters: noControls,
|
|
204
|
+
args: {
|
|
205
|
+
invalid: true,
|
|
206
|
+
},
|
|
207
|
+
}
|
|
@@ -10,6 +10,7 @@ const props = withDefaults(defineProps<DateRangePickerProps>(), {
|
|
|
10
10
|
showTime: false,
|
|
11
11
|
disabled: false,
|
|
12
12
|
readonly: false,
|
|
13
|
+
invalid: false,
|
|
13
14
|
startPlaceholder: undefined,
|
|
14
15
|
endPlaceholder: undefined,
|
|
15
16
|
minDate: undefined,
|
|
@@ -20,6 +21,8 @@ const props = withDefaults(defineProps<DateRangePickerProps>(), {
|
|
|
20
21
|
class: undefined,
|
|
21
22
|
})
|
|
22
23
|
|
|
24
|
+
useFormItemInvalid(() => props.invalid)
|
|
25
|
+
|
|
23
26
|
const emit = defineEmits<{
|
|
24
27
|
'update:start': [value: Date | string | number | null]
|
|
25
28
|
'update:end': [value: Date | string | number | null]
|
|
@@ -21,6 +21,8 @@ export interface DateRangePickerProps {
|
|
|
21
21
|
disabled?: boolean
|
|
22
22
|
/** Readonly mode */
|
|
23
23
|
readonly?: boolean
|
|
24
|
+
/** Mark the field as invalid (renders both inputs with destructive styling) */
|
|
25
|
+
invalid?: boolean
|
|
24
26
|
/** Placeholder for start date input */
|
|
25
27
|
startPlaceholder?: string
|
|
26
28
|
/** Placeholder for end date input */
|
|
@@ -4,7 +4,7 @@ import { useArgsModel } from '#storybook/argsModel'
|
|
|
4
4
|
import Button from '../Button/index.vue'
|
|
5
5
|
import Input from '../Input/index.vue'
|
|
6
6
|
import type { ButtonVariant } from '../Button/types'
|
|
7
|
-
import type { DrawerSide } from './types'
|
|
7
|
+
import type { DrawerAction, DrawerSide } from './types'
|
|
8
8
|
import Drawer from './index.vue'
|
|
9
9
|
|
|
10
10
|
const sides: DrawerSide[] = [ 'top', 'right', 'bottom', 'left' ]
|
|
@@ -257,6 +257,83 @@ export const WithTrigger: Story = {
|
|
|
257
257
|
}),
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
export const PreventClose: Story = {
|
|
261
|
+
parameters: {
|
|
262
|
+
...noControls,
|
|
263
|
+
docs: {
|
|
264
|
+
source: {
|
|
265
|
+
code: `
|
|
266
|
+
<template>
|
|
267
|
+
<Drawer
|
|
268
|
+
v-model:visible="visible"
|
|
269
|
+
title="Type to Continue"
|
|
270
|
+
description="beforeClose intercepts the confirm action; cancel/X/ESC close normally."
|
|
271
|
+
showCancel
|
|
272
|
+
confirmText="Submit"
|
|
273
|
+
:beforeClose="onBeforeClose"
|
|
274
|
+
>
|
|
275
|
+
<Input v-model="value" placeholder="Type 'confirm' to close" />
|
|
276
|
+
<p v-if="error" class="mt-2 text-sm text-destructive">{{ error }}</p>
|
|
277
|
+
</Drawer>
|
|
278
|
+
</template>
|
|
279
|
+
|
|
280
|
+
<script setup lang="ts">
|
|
281
|
+
import type { DrawerAction } from '#components'
|
|
282
|
+
|
|
283
|
+
const visible = ref(false)
|
|
284
|
+
const value = ref('')
|
|
285
|
+
const error = ref('')
|
|
286
|
+
|
|
287
|
+
function onBeforeClose (action: DrawerAction) {
|
|
288
|
+
if (action === 'cancel') return
|
|
289
|
+
if (value.value !== 'confirm') {
|
|
290
|
+
error.value = "Value must be 'confirm' to close."
|
|
291
|
+
return false
|
|
292
|
+
}
|
|
293
|
+
error.value = ''
|
|
294
|
+
return new Promise(resolve => setTimeout(resolve, 1000))
|
|
295
|
+
}
|
|
296
|
+
</script>
|
|
297
|
+
`.trim(),
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
render: () => ({
|
|
302
|
+
components: { Drawer, Button, Input },
|
|
303
|
+
setup () {
|
|
304
|
+
const visible = ref(false)
|
|
305
|
+
const value = ref('')
|
|
306
|
+
const error = ref('')
|
|
307
|
+
function onBeforeClose (action: DrawerAction) {
|
|
308
|
+
if (action === 'cancel') return
|
|
309
|
+
if (value.value !== 'confirm') {
|
|
310
|
+
error.value = 'Value must be "confirm" to close.'
|
|
311
|
+
return false
|
|
312
|
+
}
|
|
313
|
+
error.value = ''
|
|
314
|
+
return new Promise<void>(resolve => setTimeout(resolve, 1000))
|
|
315
|
+
}
|
|
316
|
+
return { visible, value, error, onBeforeClose }
|
|
317
|
+
},
|
|
318
|
+
template: `
|
|
319
|
+
<div>
|
|
320
|
+
<Button @click="visible = true">Open Drawer</Button>
|
|
321
|
+
<Drawer
|
|
322
|
+
v-model:visible="visible"
|
|
323
|
+
title="Type to Continue"
|
|
324
|
+
description="beforeClose intercepts the confirm action; cancel/X/ESC close normally."
|
|
325
|
+
showCancel
|
|
326
|
+
confirmText="Submit"
|
|
327
|
+
:beforeClose="onBeforeClose"
|
|
328
|
+
>
|
|
329
|
+
<Input v-model="value" placeholder="Type 'confirm' to close" />
|
|
330
|
+
<p v-if="error" class="mt-2 text-sm text-destructive">{{ error }}</p>
|
|
331
|
+
</Drawer>
|
|
332
|
+
</div>
|
|
333
|
+
`,
|
|
334
|
+
}),
|
|
335
|
+
}
|
|
336
|
+
|
|
260
337
|
export const EventHandling: Story = {
|
|
261
338
|
parameters: noControls,
|
|
262
339
|
render: () => ({
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
SheetTitle,
|
|
10
10
|
SheetTrigger,
|
|
11
11
|
} from '../../shadcn/sheet'
|
|
12
|
-
import type { DrawerProps } from './types'
|
|
12
|
+
import type { DrawerAction, DrawerProps } from './types'
|
|
13
13
|
|
|
14
14
|
defineOptions({ inheritAttrs: false })
|
|
15
15
|
|
|
@@ -25,6 +25,7 @@ const props = withDefaults(defineProps<DrawerProps>(), {
|
|
|
25
25
|
cancelText: undefined,
|
|
26
26
|
confirmVariant: 'default',
|
|
27
27
|
cancelVariant: 'outline',
|
|
28
|
+
beforeClose: undefined,
|
|
28
29
|
class: undefined,
|
|
29
30
|
})
|
|
30
31
|
|
|
@@ -47,6 +48,8 @@ const resolvedCancelText = computed(
|
|
|
47
48
|
)
|
|
48
49
|
|
|
49
50
|
const sheetOpen = ref(props.visible ?? false)
|
|
51
|
+
const internalLoading = ref(false)
|
|
52
|
+
const isLoading = computed(() => internalLoading.value || props.loading)
|
|
50
53
|
|
|
51
54
|
watch(() => props.visible, value => {
|
|
52
55
|
if (value !== undefined) sheetOpen.value = value
|
|
@@ -59,18 +62,37 @@ watch(sheetOpen, value => {
|
|
|
59
62
|
})
|
|
60
63
|
|
|
61
64
|
function onOpenUpdate (value: boolean) {
|
|
62
|
-
if (!value &&
|
|
65
|
+
if (!value && isLoading.value) return
|
|
63
66
|
if (value) sheetOpen.value = true
|
|
64
|
-
else
|
|
67
|
+
else handleClose('cancel')
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
function onConfirm () {
|
|
68
71
|
emit('confirm')
|
|
69
|
-
|
|
72
|
+
handleClose('confirm')
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
function onCancel () {
|
|
73
76
|
emit('cancel')
|
|
77
|
+
handleClose('cancel')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function handleClose (action: DrawerAction) {
|
|
81
|
+
if (!props.beforeClose) {
|
|
82
|
+
sheetOpen.value = false
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
const result = props.beforeClose(action)
|
|
86
|
+
if (result === false) return
|
|
87
|
+
if (result instanceof Promise) {
|
|
88
|
+
internalLoading.value = true
|
|
89
|
+
result.then(() => {
|
|
90
|
+
sheetOpen.value = false
|
|
91
|
+
}).finally(() => {
|
|
92
|
+
internalLoading.value = false
|
|
93
|
+
})
|
|
94
|
+
return
|
|
95
|
+
}
|
|
74
96
|
sheetOpen.value = false
|
|
75
97
|
}
|
|
76
98
|
|
|
@@ -136,8 +158,8 @@ const contentClass = computed(() =>
|
|
|
136
158
|
class="min-h-0 flex-1"
|
|
137
159
|
>
|
|
138
160
|
<div
|
|
139
|
-
:inert="
|
|
140
|
-
:class="[
|
|
161
|
+
:inert="isLoading || disabled || undefined"
|
|
162
|
+
:class="[ isLoading || disabled ? 'opacity-50' : undefined ]"
|
|
141
163
|
class="p-4"
|
|
142
164
|
>
|
|
143
165
|
<slot />
|
|
@@ -159,7 +181,7 @@ const contentClass = computed(() =>
|
|
|
159
181
|
v-if="showCancel"
|
|
160
182
|
class="min-w-24"
|
|
161
183
|
:variant="cancelVariant"
|
|
162
|
-
:disabled="
|
|
184
|
+
:disabled="isLoading"
|
|
163
185
|
@click="onCancel"
|
|
164
186
|
>
|
|
165
187
|
{{ resolvedCancelText }}
|
|
@@ -167,7 +189,7 @@ const contentClass = computed(() =>
|
|
|
167
189
|
<Button
|
|
168
190
|
:class="showCancel ? 'min-w-24' : 'min-w-32'"
|
|
169
191
|
:variant="confirmVariant"
|
|
170
|
-
:loading="
|
|
192
|
+
:loading="isLoading"
|
|
171
193
|
:disabled="disabled || confirmDisabled"
|
|
172
194
|
@click="onConfirm"
|
|
173
195
|
>
|
|
@@ -179,7 +201,7 @@ const contentClass = computed(() =>
|
|
|
179
201
|
|
|
180
202
|
<SheetClose
|
|
181
203
|
v-if="showClose"
|
|
182
|
-
:disabled="
|
|
204
|
+
:disabled="isLoading"
|
|
183
205
|
class="
|
|
184
206
|
top-3 right-3 size-8 text-muted-foreground ring-offset-background
|
|
185
207
|
hover:bg-accent/50 hover:text-foreground
|
|
@@ -2,6 +2,10 @@ import type { ButtonVariants } from '../../shadcn/button'
|
|
|
2
2
|
|
|
3
3
|
export type DrawerSide = 'top' | 'right' | 'bottom' | 'left'
|
|
4
4
|
|
|
5
|
+
export type DrawerAction = 'confirm' | 'cancel'
|
|
6
|
+
|
|
7
|
+
export type DrawerBeforeClose = (action: DrawerAction) => boolean | undefined | Promise<unknown>
|
|
8
|
+
|
|
5
9
|
export interface DrawerProps {
|
|
6
10
|
visible?: boolean
|
|
7
11
|
loading?: boolean
|
|
@@ -20,5 +24,6 @@ export interface DrawerProps {
|
|
|
20
24
|
cancelText?: string
|
|
21
25
|
confirmVariant?: ButtonVariants['variant']
|
|
22
26
|
cancelVariant?: ButtonVariants['variant']
|
|
27
|
+
beforeClose?: DrawerBeforeClose
|
|
23
28
|
class?: ClassValue
|
|
24
29
|
}
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import type { RadioCardGroupOption } from '../RadioCardGroup/types'
|
|
3
|
+
import type { RadioGroupItem } from '../RadioGroup/types'
|
|
4
|
+
import type { SelectOption } from '../Select/types'
|
|
5
|
+
import type {
|
|
6
|
+
SearchSelectLoadMethodParams,
|
|
7
|
+
SearchSelectLoadMethodResult,
|
|
8
|
+
} from '../SearchSelect/types'
|
|
9
|
+
import Checkbox from '../Checkbox/index.vue'
|
|
10
|
+
import DatePicker from '../DatePicker/index.vue'
|
|
11
|
+
import DateRangePicker from '../DateRangePicker/index.vue'
|
|
2
12
|
import Input from '../Input/index.vue'
|
|
13
|
+
import InputCurrency from '../InputCurrency/index.vue'
|
|
14
|
+
import InputNumber from '../InputNumber/index.vue'
|
|
15
|
+
import InputOtp from '../InputOtp/index.vue'
|
|
16
|
+
import InputPercent from '../InputPercent/index.vue'
|
|
17
|
+
import InputRange from '../InputRange/index.vue'
|
|
18
|
+
import RadioCardGroup from '../RadioCardGroup/index.vue'
|
|
19
|
+
import RadioGroup from '../RadioGroup/index.vue'
|
|
20
|
+
import SearchSelect from '../SearchSelect/index.vue'
|
|
21
|
+
import Select from '../Select/index.vue'
|
|
22
|
+
import Textarea from '../Textarea/index.vue'
|
|
3
23
|
import FormItem from './index.vue'
|
|
4
24
|
|
|
5
25
|
const orientations = [ 'vertical', 'horizontal', 'responsive' ] as const
|
|
@@ -51,15 +71,6 @@ export const Required: Story = {
|
|
|
51
71
|
},
|
|
52
72
|
}
|
|
53
73
|
|
|
54
|
-
export const WithError: Story = {
|
|
55
|
-
parameters: noControls,
|
|
56
|
-
args: {
|
|
57
|
-
label: 'Username',
|
|
58
|
-
required: true,
|
|
59
|
-
error: 'Username is already taken',
|
|
60
|
-
},
|
|
61
|
-
}
|
|
62
|
-
|
|
63
74
|
export const WithDescription: Story = {
|
|
64
75
|
parameters: noControls,
|
|
65
76
|
args: {
|
|
@@ -76,6 +87,122 @@ export const Horizontal: Story = {
|
|
|
76
87
|
},
|
|
77
88
|
}
|
|
78
89
|
|
|
90
|
+
const selectOptions: SelectOption[] = [
|
|
91
|
+
{ label: 'React', value: 'react' },
|
|
92
|
+
{ label: 'Vue', value: 'vue' },
|
|
93
|
+
{ label: 'Angular', value: 'angular' },
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
const radioOptions: RadioGroupItem[] = [
|
|
97
|
+
{ value: 'a', label: 'Option A' },
|
|
98
|
+
{ value: 'b', label: 'Option B' },
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
const radioCardOptions: RadioCardGroupOption[] = [
|
|
102
|
+
{ value: 'card-a', title: 'Card A', description: 'First option' },
|
|
103
|
+
{ value: 'card-b', title: 'Card B', description: 'Second option' },
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
function mockLoadMethod (params: SearchSelectLoadMethodParams): Promise<SearchSelectLoadMethodResult<string>> {
|
|
107
|
+
const items = selectOptions.slice(params.offset, params.offset + params.limit)
|
|
108
|
+
return Promise.resolve({ items, total: selectOptions.length })
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const InvalidControls: Story = {
|
|
112
|
+
parameters: {
|
|
113
|
+
...noControls,
|
|
114
|
+
docs: {
|
|
115
|
+
description: {
|
|
116
|
+
story: 'Every input-like control automatically inherits the surrounding `FormItem`\'s error state via provide/inject — no need to wire `invalid` on each control. The same destructive styling appears whether the error comes from `<FormItem :error>` or the control\'s own `invalid` prop.',
|
|
117
|
+
},
|
|
118
|
+
source: {
|
|
119
|
+
code: `
|
|
120
|
+
<template>
|
|
121
|
+
<FormItem required label="Username" error="Required">
|
|
122
|
+
<Input placeholder="Type your username" />
|
|
123
|
+
</FormItem>
|
|
124
|
+
</template>
|
|
125
|
+
`.trim(),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
render: () => ({
|
|
130
|
+
components: {
|
|
131
|
+
FormItem,
|
|
132
|
+
Input,
|
|
133
|
+
Textarea,
|
|
134
|
+
InputNumber,
|
|
135
|
+
InputCurrency,
|
|
136
|
+
InputPercent,
|
|
137
|
+
InputRange,
|
|
138
|
+
InputOtp,
|
|
139
|
+
Select,
|
|
140
|
+
SearchSelect,
|
|
141
|
+
DatePicker,
|
|
142
|
+
DateRangePicker,
|
|
143
|
+
Checkbox,
|
|
144
|
+
RadioGroup,
|
|
145
|
+
RadioCardGroup,
|
|
146
|
+
},
|
|
147
|
+
setup: () => ({
|
|
148
|
+
error: 'This field is required',
|
|
149
|
+
selectOptions,
|
|
150
|
+
radioOptions,
|
|
151
|
+
radioCardOptions,
|
|
152
|
+
mockLoadMethod,
|
|
153
|
+
}),
|
|
154
|
+
template: `
|
|
155
|
+
<div class="max-w-md space-y-4">
|
|
156
|
+
<FormItem required label="Input" :error="error">
|
|
157
|
+
<Input placeholder="Type something" />
|
|
158
|
+
</FormItem>
|
|
159
|
+
<FormItem required label="Textarea" :error="error">
|
|
160
|
+
<Textarea placeholder="Long text" />
|
|
161
|
+
</FormItem>
|
|
162
|
+
<FormItem required label="InputNumber" :error="error">
|
|
163
|
+
<InputNumber />
|
|
164
|
+
</FormItem>
|
|
165
|
+
<FormItem required label="InputCurrency" :error="error">
|
|
166
|
+
<InputCurrency :modelValue="1000" currency="JPY" />
|
|
167
|
+
</FormItem>
|
|
168
|
+
<FormItem required label="InputPercent" :error="error">
|
|
169
|
+
<InputPercent :modelValue="0.5" />
|
|
170
|
+
</FormItem>
|
|
171
|
+
<FormItem required label="InputRange" :error="error">
|
|
172
|
+
<InputRange />
|
|
173
|
+
</FormItem>
|
|
174
|
+
<FormItem required label="InputOtp" :error="error">
|
|
175
|
+
<InputOtp />
|
|
176
|
+
</FormItem>
|
|
177
|
+
<FormItem required label="Select" :error="error">
|
|
178
|
+
<Select :options="selectOptions" placeholder="Pick a framework" />
|
|
179
|
+
</FormItem>
|
|
180
|
+
<FormItem required label="SearchSelect" :error="error">
|
|
181
|
+
<SearchSelect :loadMethod="mockLoadMethod" autoLoad />
|
|
182
|
+
</FormItem>
|
|
183
|
+
<FormItem required label="DatePicker" :error="error">
|
|
184
|
+
<DatePicker />
|
|
185
|
+
</FormItem>
|
|
186
|
+
<FormItem required label="DateRangePicker" :error="error">
|
|
187
|
+
<DateRangePicker />
|
|
188
|
+
</FormItem>
|
|
189
|
+
<FormItem required label="Checkbox" :error="error">
|
|
190
|
+
<label class="flex items-center gap-2 cursor-pointer">
|
|
191
|
+
<Checkbox />
|
|
192
|
+
<span class="text-sm">Accept terms</span>
|
|
193
|
+
</label>
|
|
194
|
+
</FormItem>
|
|
195
|
+
<FormItem required label="RadioGroup" :error="error">
|
|
196
|
+
<RadioGroup :items="radioOptions" />
|
|
197
|
+
</FormItem>
|
|
198
|
+
<FormItem required label="RadioCardGroup" :error="error">
|
|
199
|
+
<RadioCardGroup :options="radioCardOptions" />
|
|
200
|
+
</FormItem>
|
|
201
|
+
</div>
|
|
202
|
+
`,
|
|
203
|
+
}),
|
|
204
|
+
}
|
|
205
|
+
|
|
79
206
|
export const Responsive: Story = {
|
|
80
207
|
parameters: {
|
|
81
208
|
...noControls,
|
|
@@ -98,7 +225,7 @@ export const Responsive: Story = {
|
|
|
98
225
|
template: `
|
|
99
226
|
<div class="max-w-md">
|
|
100
227
|
<div class="@container/field-group resize-x overflow-auto rounded border border-dashed border-border bg-card p-4" style="min-width: 200px;">
|
|
101
|
-
<FormItem label="Address" orientation="responsive" description="Your mailing address">
|
|
228
|
+
<FormItem required label="Address" orientation="responsive" description="Your mailing address">
|
|
102
229
|
<Input placeholder="Enter address" />
|
|
103
230
|
</FormItem>
|
|
104
231
|
</div>
|
|
@@ -10,15 +10,22 @@ import type { FormItemProps } from './types'
|
|
|
10
10
|
|
|
11
11
|
const props = defineProps<FormItemProps>()
|
|
12
12
|
|
|
13
|
+
useFormItemInvalid(() => !!props.error)
|
|
14
|
+
|
|
13
15
|
const errorArray = computed(() => {
|
|
14
16
|
if (!props.error) return undefined
|
|
15
17
|
return [{ message: props.error }]
|
|
16
18
|
})
|
|
17
19
|
|
|
18
20
|
const labelClass = computed(() => {
|
|
19
|
-
|
|
20
|
-
if (props.orientation === '
|
|
21
|
-
|
|
21
|
+
const base = 'group-data-[invalid=true]/field:text-foreground'
|
|
22
|
+
if (props.orientation === 'horizontal') return cn(base, `
|
|
23
|
+
mt-2 justify-end text-right
|
|
24
|
+
`)
|
|
25
|
+
if (props.orientation === 'responsive') return cn(base, `
|
|
26
|
+
@md/field-group:justify-end @md/field-group:text-right @md/field-group:mt-2
|
|
27
|
+
`)
|
|
28
|
+
return base
|
|
22
29
|
})
|
|
23
30
|
</script>
|
|
24
31
|
|
|
@@ -16,6 +16,8 @@ const emit = defineEmits<{
|
|
|
16
16
|
'change': [value: string | undefined]
|
|
17
17
|
}>()
|
|
18
18
|
|
|
19
|
+
const isInvalid = useFormItemInvalid(() => props.invalid)
|
|
20
|
+
|
|
19
21
|
const $slots = defineSlots<{
|
|
20
22
|
prefix?: () => unknown
|
|
21
23
|
suffix?: () => unknown
|
|
@@ -65,7 +67,7 @@ function clearInput () {
|
|
|
65
67
|
v-bind="$attrs"
|
|
66
68
|
:readonly="readonly"
|
|
67
69
|
:disabled="disabled"
|
|
68
|
-
:aria-invalid="
|
|
70
|
+
:aria-invalid="isInvalid || undefined"
|
|
69
71
|
:data-1p-ignore="autocomplete === 'off' || !autocomplete ? true : undefined"
|
|
70
72
|
:autocomplete="autocomplete || 'off'"
|
|
71
73
|
@input="handleInput"
|
|
@@ -11,11 +11,13 @@ const meta = {
|
|
|
11
11
|
modelValue: { control: 'number' },
|
|
12
12
|
currency: { control: 'text' },
|
|
13
13
|
currencyDisplay: { control: 'select', options: currencyDisplays },
|
|
14
|
+
invalid: { control: 'boolean' },
|
|
14
15
|
},
|
|
15
16
|
args: {
|
|
16
17
|
modelValue: 1000,
|
|
17
18
|
currency: 'JPY',
|
|
18
19
|
currencyDisplay: 'symbol',
|
|
20
|
+
invalid: false,
|
|
19
21
|
},
|
|
20
22
|
render: args => {
|
|
21
23
|
const onUpdate = useArgsModel()
|
|
@@ -78,3 +80,10 @@ export const JpyNameDisplay: Story = {
|
|
|
78
80
|
currencyDisplay: 'name',
|
|
79
81
|
},
|
|
80
82
|
}
|
|
83
|
+
|
|
84
|
+
export const Invalid: Story = {
|
|
85
|
+
parameters: noControls,
|
|
86
|
+
args: {
|
|
87
|
+
invalid: true,
|
|
88
|
+
},
|
|
89
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import type { InputNumberProps } from '../InputNumber/types'
|
|
2
|
+
|
|
3
|
+
export interface InputCurrencyProps extends /* @vue-ignore */ Pick<InputNumberProps, 'invalid'> {
|
|
2
4
|
currency?: string
|
|
3
5
|
currencyDisplay?: 'symbol' | 'narrowSymbol' | 'code' | 'name'
|
|
4
6
|
}
|