@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.
Files changed (43) hide show
  1. package/app/components/ui/Checkbox/index.stories.ts +9 -0
  2. package/app/components/ui/Checkbox/index.vue +4 -1
  3. package/app/components/ui/Checkbox/types.ts +3 -1
  4. package/app/components/ui/DatePicker/index.stories.ts +9 -0
  5. package/app/components/ui/DatePicker/index.vue +3 -0
  6. package/app/components/ui/DatePicker/types.ts +2 -0
  7. package/app/components/ui/DateRangePicker/index.stories.ts +9 -0
  8. package/app/components/ui/DateRangePicker/index.vue +3 -0
  9. package/app/components/ui/DateRangePicker/types.ts +2 -0
  10. package/app/components/ui/Drawer/index.stories.ts +78 -1
  11. package/app/components/ui/Drawer/index.vue +31 -9
  12. package/app/components/ui/Drawer/types.ts +5 -0
  13. package/app/components/ui/FormItem/index.stories.ts +137 -10
  14. package/app/components/ui/FormItem/index.vue +10 -3
  15. package/app/components/ui/Input/index.vue +3 -1
  16. package/app/components/ui/InputCurrency/index.stories.ts +9 -0
  17. package/app/components/ui/InputCurrency/types.ts +3 -1
  18. package/app/components/ui/InputNumber/index.vue +10 -7
  19. package/app/components/ui/InputOtp/index.stories.ts +10 -0
  20. package/app/components/ui/InputOtp/index.vue +5 -1
  21. package/app/components/ui/InputOtp/types.ts +1 -0
  22. package/app/components/ui/InputPercent/index.stories.ts +11 -0
  23. package/app/components/ui/InputPercent/index.vue +6 -0
  24. package/app/components/ui/InputPercent/types.ts +3 -0
  25. package/app/components/ui/InputRange/index.stories.ts +11 -0
  26. package/app/components/ui/Modal/index.stories.ts +78 -0
  27. package/app/components/ui/Modal/index.vue +31 -9
  28. package/app/components/ui/Modal/types.ts +5 -0
  29. package/app/components/ui/RadioCardGroup/index.stories.ts +9 -0
  30. package/app/components/ui/RadioCardGroup/index.vue +4 -0
  31. package/app/components/ui/RadioCardGroup/types.ts +1 -0
  32. package/app/components/ui/RadioGroup/index.stories.ts +9 -0
  33. package/app/components/ui/RadioGroup/index.vue +4 -0
  34. package/app/components/ui/RadioGroup/types.ts +1 -0
  35. package/app/components/ui/SearchSelect/index.stories.ts +9 -0
  36. package/app/components/ui/SearchSelect/index.vue +2 -0
  37. package/app/components/ui/SearchSelect/types.ts +1 -0
  38. package/app/components/ui/Select/index.stories.ts +9 -0
  39. package/app/components/ui/Select/index.vue +6 -0
  40. package/app/components/ui/Select/types.ts +1 -0
  41. package/app/components/ui/Textarea/index.vue +3 -1
  42. package/app/composables/useFormItemInvalid.ts +16 -0
  43. 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
@@ -1,3 +1,5 @@
1
1
  import type { CheckboxRootProps } from 'reka-ui'
2
2
 
3
- export interface CheckboxProps extends /* @vue-ignore */ CheckboxRootProps {}
3
+ export interface CheckboxProps extends /* @vue-ignore */ CheckboxRootProps {
4
+ invalid?: boolean
5
+ }
@@ -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 && props.loading) return
65
+ if (!value && isLoading.value) return
63
66
  if (value) sheetOpen.value = true
64
- else onCancel()
67
+ else handleClose('cancel')
65
68
  }
66
69
 
67
70
  function onConfirm () {
68
71
  emit('confirm')
69
- sheetOpen.value = false
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="loading || disabled || undefined"
140
- :class="[ loading || disabled ? 'opacity-50' : undefined ]"
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="loading"
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="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="loading"
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
- if (props.orientation === 'horizontal') return 'justify-end text-right mt-2'
20
- if (props.orientation === 'responsive') return '@md/field-group:justify-end @md/field-group:text-right @md/field-group:mt-2'
21
- return undefined
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="invalid || undefined"
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
- export interface InputCurrencyProps {
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
  }