@polymarbot/nuxt-layer-shadcn-ui 0.8.8 → 0.9.1
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/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 +13 -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/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 +9 -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 */
|
|
@@ -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
|
}
|
|
@@ -28,26 +28,32 @@ const model = computed({
|
|
|
28
28
|
set: value => emit('update:modelValue', value),
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
+
const isInvalid = useFormItemInvalid(() => props.invalid)
|
|
32
|
+
|
|
31
33
|
const contentClass = computed(() =>
|
|
32
34
|
cn(
|
|
33
35
|
`
|
|
34
|
-
|
|
35
|
-
transition-[color,box-shadow]
|
|
36
|
+
h-9 rounded-md border-input shadow-xs
|
|
36
37
|
dark:bg-input/30
|
|
38
|
+
flex items-center border transition-[color,box-shadow]
|
|
37
39
|
`,
|
|
38
40
|
`
|
|
39
41
|
has-[[data-slot=input]:focus-visible]:border-ring
|
|
40
|
-
has-[[data-slot=input]:focus-visible]:ring-[3px]
|
|
41
42
|
has-[[data-slot=input]:focus-visible]:ring-ring/50
|
|
43
|
+
has-[[data-slot=input]:focus-visible]:ring-[3px]
|
|
42
44
|
`,
|
|
43
|
-
|
|
45
|
+
isInvalid.value && `
|
|
44
46
|
border-destructive ring-destructive/20
|
|
45
47
|
dark:ring-destructive/40
|
|
48
|
+
has-[[data-slot=input]:focus-visible]:border-destructive
|
|
49
|
+
has-[[data-slot=input]:focus-visible]:ring-destructive/20
|
|
50
|
+
dark:has-[[data-slot=input]:focus-visible]:ring-destructive/40
|
|
46
51
|
`,
|
|
47
52
|
),
|
|
48
53
|
)
|
|
49
54
|
|
|
50
|
-
const
|
|
55
|
+
const buttonClass = 'static translate-y-0 shrink-0 cursor-pointer'
|
|
56
|
+
const inputClass = 'flex-1 min-w-0 border-0 shadow-none focus-visible:ring-0 rounded-none'
|
|
51
57
|
</script>
|
|
52
58
|
|
|
53
59
|
<template>
|
|
@@ -61,7 +67,7 @@ const inputClass = 'flex-1 border-0 shadow-none focus-visible:ring-0 rounded-non
|
|
|
61
67
|
<NumberFieldContent :class="contentClass">
|
|
62
68
|
<NumberFieldDecrement
|
|
63
69
|
v-if="showButtons"
|
|
64
|
-
class="
|
|
70
|
+
:class="buttonClass"
|
|
65
71
|
/>
|
|
66
72
|
<NumberFieldInput
|
|
67
73
|
:placeholder="placeholder"
|
|
@@ -69,7 +75,7 @@ const inputClass = 'flex-1 border-0 shadow-none focus-visible:ring-0 rounded-non
|
|
|
69
75
|
/>
|
|
70
76
|
<NumberFieldIncrement
|
|
71
77
|
v-if="showButtons"
|
|
72
|
-
class="
|
|
78
|
+
:class="buttonClass"
|
|
73
79
|
/>
|
|
74
80
|
</NumberFieldContent>
|
|
75
81
|
</NumberField>
|
|
@@ -10,11 +10,13 @@ const meta = {
|
|
|
10
10
|
modelValue: { control: 'text' },
|
|
11
11
|
length: { control: 'number' },
|
|
12
12
|
disabled: { control: 'boolean' },
|
|
13
|
+
invalid: { control: 'boolean' },
|
|
13
14
|
},
|
|
14
15
|
args: {
|
|
15
16
|
modelValue: '',
|
|
16
17
|
length: 6,
|
|
17
18
|
disabled: false,
|
|
19
|
+
invalid: false,
|
|
18
20
|
},
|
|
19
21
|
render: args => {
|
|
20
22
|
const onUpdate = useArgsModel()
|
|
@@ -53,6 +55,14 @@ export const Disabled: Story = {
|
|
|
53
55
|
},
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
export const Invalid: Story = {
|
|
59
|
+
parameters: noControls,
|
|
60
|
+
args: {
|
|
61
|
+
invalid: true,
|
|
62
|
+
modelValue: '123456',
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
56
66
|
export const EventHandling: Story = {
|
|
57
67
|
parameters: noControls,
|
|
58
68
|
render: () => ({
|
|
@@ -6,10 +6,11 @@ import {
|
|
|
6
6
|
} from '../../shadcn/pin-input'
|
|
7
7
|
import type { InputOtpProps } from './types'
|
|
8
8
|
|
|
9
|
-
withDefaults(defineProps<InputOtpProps>(), {
|
|
9
|
+
const props = withDefaults(defineProps<InputOtpProps>(), {
|
|
10
10
|
modelValue: '',
|
|
11
11
|
length: 6,
|
|
12
12
|
disabled: false,
|
|
13
|
+
invalid: false,
|
|
13
14
|
})
|
|
14
15
|
|
|
15
16
|
const emit = defineEmits<{
|
|
@@ -17,6 +18,8 @@ const emit = defineEmits<{
|
|
|
17
18
|
'complete': [value: string]
|
|
18
19
|
}>()
|
|
19
20
|
|
|
21
|
+
const isInvalid = useFormItemInvalid(() => props.invalid)
|
|
22
|
+
|
|
20
23
|
function handleComplete (values: string[]) {
|
|
21
24
|
emit('complete', values.join(''))
|
|
22
25
|
}
|
|
@@ -39,6 +42,7 @@ function handleUpdate (value: string[]) {
|
|
|
39
42
|
v-for="(_, index) in length"
|
|
40
43
|
:key="index"
|
|
41
44
|
:index="index"
|
|
45
|
+
:aria-invalid="isInvalid || undefined"
|
|
42
46
|
/>
|
|
43
47
|
</PinInputGroup>
|
|
44
48
|
</PinInput>
|
|
@@ -7,9 +7,11 @@ const meta = {
|
|
|
7
7
|
component: InputPercent as any,
|
|
8
8
|
argTypes: {
|
|
9
9
|
modelValue: { control: 'number' },
|
|
10
|
+
invalid: { control: 'boolean' },
|
|
10
11
|
},
|
|
11
12
|
args: {
|
|
12
13
|
modelValue: 0.5,
|
|
14
|
+
invalid: false,
|
|
13
15
|
},
|
|
14
16
|
render: args => {
|
|
15
17
|
const onUpdate = useArgsModel()
|
|
@@ -29,4 +31,13 @@ const meta = {
|
|
|
29
31
|
export default meta
|
|
30
32
|
type Story = StoryObj<typeof meta>
|
|
31
33
|
|
|
34
|
+
const noControls = { controls: { disable: true }} satisfies Story['parameters']
|
|
35
|
+
|
|
32
36
|
export const Default: Story = {}
|
|
37
|
+
|
|
38
|
+
export const Invalid: Story = {
|
|
39
|
+
parameters: noControls,
|
|
40
|
+
args: {
|
|
41
|
+
invalid: true,
|
|
42
|
+
},
|
|
43
|
+
}
|
|
@@ -15,6 +15,7 @@ const meta = {
|
|
|
15
15
|
startPlaceholder: { control: 'text' },
|
|
16
16
|
endPlaceholder: { control: 'text' },
|
|
17
17
|
disabled: { control: 'boolean' },
|
|
18
|
+
invalid: { control: 'boolean' },
|
|
18
19
|
as: { control: false },
|
|
19
20
|
},
|
|
20
21
|
args: {
|
|
@@ -25,6 +26,7 @@ const meta = {
|
|
|
25
26
|
startPlaceholder: '',
|
|
26
27
|
endPlaceholder: '',
|
|
27
28
|
disabled: false,
|
|
29
|
+
invalid: false,
|
|
28
30
|
as: undefined,
|
|
29
31
|
},
|
|
30
32
|
render: args => {
|
|
@@ -71,6 +73,15 @@ export const Disabled: Story = {
|
|
|
71
73
|
},
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
export const Invalid: Story = {
|
|
77
|
+
parameters: noControls,
|
|
78
|
+
args: {
|
|
79
|
+
invalid: true,
|
|
80
|
+
start: 20,
|
|
81
|
+
end: 80,
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
74
85
|
export const CustomInput: Story = {
|
|
75
86
|
parameters: {
|
|
76
87
|
...noControls,
|
|
@@ -43,11 +43,13 @@ const meta = {
|
|
|
43
43
|
modelValue: { control: 'text' },
|
|
44
44
|
options: { control: 'object' },
|
|
45
45
|
disabled: { control: 'boolean' },
|
|
46
|
+
invalid: { control: 'boolean' },
|
|
46
47
|
},
|
|
47
48
|
args: {
|
|
48
49
|
modelValue: 'current',
|
|
49
50
|
options,
|
|
50
51
|
disabled: false,
|
|
52
|
+
invalid: false,
|
|
51
53
|
},
|
|
52
54
|
render: args => {
|
|
53
55
|
const onUpdate = useArgsModel()
|
|
@@ -106,6 +108,13 @@ export const Disabled: Story = {
|
|
|
106
108
|
},
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
export const Invalid: Story = {
|
|
112
|
+
parameters: noControls,
|
|
113
|
+
args: {
|
|
114
|
+
invalid: true,
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
|
|
109
118
|
export const EventHandling: Story = {
|
|
110
119
|
parameters: {
|
|
111
120
|
...noControls,
|
|
@@ -8,9 +8,12 @@ import type { RadioCardGroupProps } from './types'
|
|
|
8
8
|
const props = withDefaults(defineProps<RadioCardGroupProps>(), {
|
|
9
9
|
modelValue: undefined,
|
|
10
10
|
disabled: false,
|
|
11
|
+
invalid: false,
|
|
11
12
|
class: undefined,
|
|
12
13
|
})
|
|
13
14
|
|
|
15
|
+
const isInvalid = useFormItemInvalid(() => props.invalid)
|
|
16
|
+
|
|
14
17
|
const emit = defineEmits<{
|
|
15
18
|
'update:modelValue': [value: string]
|
|
16
19
|
}>()
|
|
@@ -47,6 +50,7 @@ const mergedClass = computed(() => cn('gap-3', props.class))
|
|
|
47
50
|
<ShadcnRadioGroupItem
|
|
48
51
|
:value="option.value"
|
|
49
52
|
:disabled="option.disabled"
|
|
53
|
+
:aria-invalid="isInvalid || undefined"
|
|
50
54
|
/>
|
|
51
55
|
<div class="gap-0.5 grid flex-1">
|
|
52
56
|
<span class="text-sm font-medium">
|
|
@@ -23,12 +23,14 @@ const meta = {
|
|
|
23
23
|
items: { control: 'object' },
|
|
24
24
|
modelValue: { control: 'text' },
|
|
25
25
|
disabled: { control: 'boolean' },
|
|
26
|
+
invalid: { control: 'boolean' },
|
|
26
27
|
orientation: { control: 'inline-radio', options: [ 'vertical', 'horizontal' ]},
|
|
27
28
|
},
|
|
28
29
|
args: {
|
|
29
30
|
items: options,
|
|
30
31
|
modelValue: 'option1',
|
|
31
32
|
disabled: false,
|
|
33
|
+
invalid: false,
|
|
32
34
|
orientation: 'vertical',
|
|
33
35
|
},
|
|
34
36
|
render: args => {
|
|
@@ -70,6 +72,13 @@ export const Disabled: Story = {
|
|
|
70
72
|
},
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
export const Invalid: Story = {
|
|
76
|
+
parameters: noControls,
|
|
77
|
+
args: {
|
|
78
|
+
invalid: true,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
export const CustomSlots: Story = {
|
|
74
83
|
parameters: {
|
|
75
84
|
...noControls,
|
|
@@ -9,10 +9,13 @@ const props = withDefaults(defineProps<RadioGroupProps>(), {
|
|
|
9
9
|
items: () => [],
|
|
10
10
|
modelValue: undefined,
|
|
11
11
|
disabled: false,
|
|
12
|
+
invalid: false,
|
|
12
13
|
orientation: 'vertical',
|
|
13
14
|
class: undefined,
|
|
14
15
|
})
|
|
15
16
|
|
|
17
|
+
const isInvalid = useFormItemInvalid(() => props.invalid)
|
|
18
|
+
|
|
16
19
|
const emit = defineEmits<{
|
|
17
20
|
'update:modelValue': [value: string]
|
|
18
21
|
}>()
|
|
@@ -58,6 +61,7 @@ const mergedClass = computed(() => cn(
|
|
|
58
61
|
<ShadcnRadioGroupItem
|
|
59
62
|
:value="item.value"
|
|
60
63
|
:disabled="item.disabled"
|
|
64
|
+
:aria-invalid="isInvalid || undefined"
|
|
61
65
|
/>
|
|
62
66
|
<slot
|
|
63
67
|
name="label"
|
|
@@ -52,6 +52,7 @@ const meta = {
|
|
|
52
52
|
loadLimit: { control: 'number' },
|
|
53
53
|
autoLoad: { control: 'boolean' },
|
|
54
54
|
disabled: { control: 'boolean' },
|
|
55
|
+
invalid: { control: 'boolean' },
|
|
55
56
|
},
|
|
56
57
|
args: {
|
|
57
58
|
modelValue: undefined,
|
|
@@ -64,6 +65,7 @@ const meta = {
|
|
|
64
65
|
loadLimit: 20,
|
|
65
66
|
autoLoad: false,
|
|
66
67
|
disabled: false,
|
|
68
|
+
invalid: false,
|
|
67
69
|
},
|
|
68
70
|
render: args => {
|
|
69
71
|
const onUpdate = useArgsModel()
|
|
@@ -182,6 +184,13 @@ export const Disabled: Story = {
|
|
|
182
184
|
},
|
|
183
185
|
}
|
|
184
186
|
|
|
187
|
+
export const Invalid: Story = {
|
|
188
|
+
parameters: noControls,
|
|
189
|
+
args: {
|
|
190
|
+
invalid: true,
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
|
|
185
194
|
export const EventHandling: Story = {
|
|
186
195
|
parameters: {
|
|
187
196
|
...noControls,
|
|
@@ -16,6 +16,7 @@ const props = withDefaults(defineProps<SearchSelectProps<TValue, TMeta>>(), {
|
|
|
16
16
|
loadLimit: 20,
|
|
17
17
|
placeholder: undefined,
|
|
18
18
|
disabled: false,
|
|
19
|
+
invalid: false,
|
|
19
20
|
searchPlaceholder: undefined,
|
|
20
21
|
emptyText: undefined,
|
|
21
22
|
searchEmptyText: undefined,
|
|
@@ -191,6 +192,7 @@ defineExpose({ refresh: resetAndLoad })
|
|
|
191
192
|
:searchPlaceholder="searchPlaceholder"
|
|
192
193
|
:emptyText="computedEmptyText"
|
|
193
194
|
:disabled="disabled"
|
|
195
|
+
:invalid="invalid"
|
|
194
196
|
:loading="isLoading"
|
|
195
197
|
@search="handleSearch"
|
|
196
198
|
@open="handleOpen"
|
|
@@ -27,6 +27,7 @@ export interface SearchSelectProps<V extends string | number = string, M = unkno
|
|
|
27
27
|
loadLimit?: number
|
|
28
28
|
placeholder?: string
|
|
29
29
|
disabled?: boolean
|
|
30
|
+
invalid?: boolean
|
|
30
31
|
searchPlaceholder?: string
|
|
31
32
|
/** Message when no options available (no search keyword) */
|
|
32
33
|
emptyText?: string
|
|
@@ -38,6 +38,7 @@ const meta = {
|
|
|
38
38
|
modelValue: { control: 'text' },
|
|
39
39
|
placeholder: { control: 'text' },
|
|
40
40
|
disabled: { control: 'boolean' },
|
|
41
|
+
invalid: { control: 'boolean' },
|
|
41
42
|
loading: { control: 'boolean' },
|
|
42
43
|
filter: { control: 'boolean' },
|
|
43
44
|
multiple: { control: 'boolean' },
|
|
@@ -48,6 +49,7 @@ const meta = {
|
|
|
48
49
|
modelValue: undefined,
|
|
49
50
|
placeholder: 'Select an option',
|
|
50
51
|
disabled: false,
|
|
52
|
+
invalid: false,
|
|
51
53
|
loading: false,
|
|
52
54
|
filter: false,
|
|
53
55
|
multiple: false,
|
|
@@ -290,6 +292,13 @@ export const Loading: Story = {
|
|
|
290
292
|
},
|
|
291
293
|
}
|
|
292
294
|
|
|
295
|
+
export const Invalid: Story = {
|
|
296
|
+
parameters: noControls,
|
|
297
|
+
args: {
|
|
298
|
+
invalid: true,
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
|
|
293
302
|
export const EventHandling: Story = {
|
|
294
303
|
parameters: noControls,
|
|
295
304
|
render: () => ({
|
|
@@ -29,6 +29,7 @@ const props = withDefaults(defineProps<SelectProps<TValue, TMeta>>(), {
|
|
|
29
29
|
modelValue: undefined,
|
|
30
30
|
placeholder: undefined,
|
|
31
31
|
disabled: false,
|
|
32
|
+
invalid: false,
|
|
32
33
|
loading: false,
|
|
33
34
|
filter: false,
|
|
34
35
|
searchPlaceholder: undefined,
|
|
@@ -36,6 +37,8 @@ const props = withDefaults(defineProps<SelectProps<TValue, TMeta>>(), {
|
|
|
36
37
|
multiple: false,
|
|
37
38
|
})
|
|
38
39
|
|
|
40
|
+
const isInvalid = useFormItemInvalid(() => props.invalid)
|
|
41
|
+
|
|
39
42
|
const emit = defineEmits<{
|
|
40
43
|
'update:modelValue': [value: TValue | TValue[]]
|
|
41
44
|
'search': [value: string]
|
|
@@ -154,10 +157,16 @@ function handleClear (event: MouseEvent) {
|
|
|
154
157
|
role="combobox"
|
|
155
158
|
tabindex="0"
|
|
156
159
|
:aria-expanded="open"
|
|
160
|
+
:aria-invalid="isInvalid || undefined"
|
|
157
161
|
:data-disabled="disabled || undefined"
|
|
158
162
|
:data-state="open ? 'open' : 'closed'"
|
|
159
163
|
class="
|
|
160
164
|
data-[state=open]:border-ring data-[state=open]:ring-ring/50
|
|
165
|
+
aria-invalid:ring-destructive/20 aria-invalid:border-destructive
|
|
166
|
+
dark:aria-invalid:ring-destructive/40
|
|
167
|
+
aria-invalid:data-[state=open]:border-destructive
|
|
168
|
+
aria-invalid:data-[state=open]:ring-destructive/20
|
|
169
|
+
dark:aria-invalid:data-[state=open]:ring-destructive/40
|
|
161
170
|
cursor-pointer
|
|
162
171
|
data-[state=open]:ring-[3px]
|
|
163
172
|
"
|
|
@@ -13,6 +13,7 @@ export type SelectBaseProps<V extends string | number = string, M = unknown> = {
|
|
|
13
13
|
options?: SelectOption<V, M>[]
|
|
14
14
|
placeholder?: string
|
|
15
15
|
disabled?: boolean
|
|
16
|
+
invalid?: boolean
|
|
16
17
|
/** Show a spinner in place of the chevron */
|
|
17
18
|
loading?: boolean
|
|
18
19
|
/** true: enable client-side label filter; function: custom filter (disables internal filter) */
|
|
@@ -17,6 +17,8 @@ const emit = defineEmits<{
|
|
|
17
17
|
'change': [value: string]
|
|
18
18
|
}>()
|
|
19
19
|
|
|
20
|
+
const isInvalid = useFormItemInvalid(() => props.invalid)
|
|
21
|
+
|
|
20
22
|
function handleInput (event: Event) {
|
|
21
23
|
const target = event.target as HTMLTextAreaElement
|
|
22
24
|
emit('update:modelValue', target.value)
|
|
@@ -40,7 +42,7 @@ const mergedClass = computed(() =>
|
|
|
40
42
|
:modelValue="modelValue"
|
|
41
43
|
:rows="rows"
|
|
42
44
|
:class="mergedClass"
|
|
43
|
-
:aria-invalid="
|
|
45
|
+
:aria-invalid="isInvalid || undefined"
|
|
44
46
|
:data-1p-ignore="autocomplete === 'off' || !autocomplete ? true : undefined"
|
|
45
47
|
:autocomplete="autocomplete || 'off'"
|
|
46
48
|
v-bind="$attrs"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { InjectionKey, MaybeRefOrGetter, Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
const formItemInvalidKey: InjectionKey<Ref<boolean>> = Symbol('formItemInvalid')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the effective invalid state for an input-like component, combining
|
|
7
|
+
* its own `invalid` prop with any ancestor FormItem's error state. Re-provides
|
|
8
|
+
* the combined state so nested input-likes (e.g. Input inside DatePicker)
|
|
9
|
+
* inherit it without manual prop forwarding.
|
|
10
|
+
*/
|
|
11
|
+
export function useFormItemInvalid (localInvalid?: MaybeRefOrGetter<boolean | undefined>) {
|
|
12
|
+
const ancestor = inject(formItemInvalidKey, ref(false))
|
|
13
|
+
const effective = computed(() => !!toValue(localInvalid) || ancestor.value)
|
|
14
|
+
provide(formItemInvalidKey, effective)
|
|
15
|
+
return effective
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polymarbot/nuxt-layer-shadcn-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Nuxt layer providing shadcn-vue based UI components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./nuxt.config.ts",
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"vue-i18n": "^11",
|
|
43
43
|
"vue-router": "^4 || ^5"
|
|
44
44
|
},
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "84fc05150b4f347b2927ab7bd444101fa8696b76"
|
|
46
46
|
}
|