vueless 1.4.7-beta.1 → 1.4.7-beta.3

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 (78) hide show
  1. package/icons/internal/apps.svg +1 -1
  2. package/icons/internal/calendar_month-fill.svg +1 -1
  3. package/icons/internal/error.svg +1 -1
  4. package/icons/internal/info.svg +1 -1
  5. package/icons/internal/progress_activity.svg +1 -1
  6. package/icons/internal/search.svg +1 -1
  7. package/icons/internal/visibility-fill.svg +1 -1
  8. package/icons/internal/warning.svg +1 -1
  9. package/icons/storybook/account_circle.svg +1 -1
  10. package/icons/storybook/cloud_sync.svg +1 -0
  11. package/icons/storybook/contact_mail.svg +1 -1
  12. package/icons/storybook/date_range.svg +1 -0
  13. package/icons/storybook/delivery_truck_speed.svg +1 -1
  14. package/icons/storybook/directions_bike.svg +1 -1
  15. package/icons/storybook/error.svg +1 -1
  16. package/icons/storybook/event.svg +1 -0
  17. package/icons/storybook/folder.svg +1 -0
  18. package/icons/storybook/format_quote.svg +1 -0
  19. package/icons/storybook/handyman.svg +1 -1
  20. package/icons/storybook/help.svg +1 -1
  21. package/icons/storybook/inbox.svg +1 -1
  22. package/icons/storybook/inbox_customize.svg +1 -1
  23. package/icons/storybook/info.svg +1 -1
  24. package/icons/storybook/key.svg +1 -1
  25. package/icons/storybook/local_shipping.svg +1 -0
  26. package/icons/storybook/location_city.svg +1 -0
  27. package/icons/storybook/lock.svg +1 -1
  28. package/icons/storybook/lock_open.svg +1 -1
  29. package/icons/storybook/mark_email_unread.svg +1 -0
  30. package/icons/storybook/notifications.svg +1 -0
  31. package/icons/storybook/palette.svg +1 -1
  32. package/icons/storybook/person.svg +1 -1
  33. package/icons/storybook/person_search.svg +1 -1
  34. package/icons/storybook/progress_activity.svg +1 -1
  35. package/icons/storybook/rocket_launch.svg +1 -1
  36. package/icons/storybook/sentiment_satisfied.svg +1 -1
  37. package/icons/storybook/shield.svg +1 -0
  38. package/icons/storybook/timer.svg +1 -1
  39. package/icons/storybook/travel_explore.svg +1 -1
  40. package/icons/storybook/workspace_premium.svg +1 -0
  41. package/package.json +32 -31
  42. package/plugin-vite.js +18 -7
  43. package/ui.container-accordion-item/tests/UAccordionItem.test.ts +4 -2
  44. package/ui.container-drawer/tests/UDrawer.test.ts +11 -29
  45. package/ui.container-modal/tests/UModal.test.ts +10 -22
  46. package/ui.data-table/UTableRow.vue +2 -2
  47. package/ui.form-calendar/tests/UCalendar.test.ts +14 -13
  48. package/ui.form-calendar/tests/UCalendarDayView.test.ts +15 -16
  49. package/ui.form-checkbox/UCheckbox.vue +0 -5
  50. package/ui.form-checkbox/storybook/stories.ts +56 -18
  51. package/ui.form-checkbox/tests/UCheckbox.test.ts +0 -17
  52. package/ui.form-checkbox-group/storybook/stories.ts +61 -0
  53. package/ui.form-date-picker/storybook/stories.ts +85 -37
  54. package/ui.form-date-picker-range/storybook/stories.ts +97 -38
  55. package/ui.form-input/storybook/stories.ts +75 -37
  56. package/ui.form-input-file/storybook/stories.ts +44 -5
  57. package/ui.form-input-number/storybook/stories.ts +66 -28
  58. package/ui.form-input-password/storybook/stories.ts +94 -50
  59. package/ui.form-input-search/storybook/stories.ts +40 -4
  60. package/ui.form-label/ULabel.vue +0 -6
  61. package/ui.form-label/storybook/stories.ts +46 -24
  62. package/ui.form-label/tests/ULabel.test.ts +0 -12
  63. package/ui.form-listbox/UListbox.vue +71 -72
  64. package/ui.form-listbox/tests/UListbox.test.ts +3 -1
  65. package/ui.form-radio/URadio.vue +0 -5
  66. package/ui.form-radio/storybook/stories.ts +65 -25
  67. package/ui.form-radio/tests/URadio.test.ts +0 -17
  68. package/ui.form-radio-group/storybook/stories.ts +67 -0
  69. package/ui.form-select/storybook/stories.ts +99 -44
  70. package/ui.form-switch/storybook/stories.ts +35 -11
  71. package/ui.form-textarea/storybook/stories.ts +61 -20
  72. package/ui.loader/tests/ULoader.test.ts +3 -3
  73. package/ui.navigation-progress/tests/UProgress.test.ts +2 -3
  74. package/ui.text-block/tests/UText.test.ts +3 -3
  75. package/ui.text-files/storybook/stories.ts +30 -13
  76. package/ui.text-header/tests/UHeader.test.ts +3 -3
  77. package/ui.text-notify/tests/UNotify.test.ts +14 -18
  78. package/utils/node/helper.js +8 -9
@@ -114,29 +114,51 @@ Sizes.args = { enum: "size" };
114
114
  export const LabelAlign = EnumTemplate.bind({});
115
115
  LabelAlign.args = { enum: "align" };
116
116
 
117
- export const LabelSlot = DefaultTemplate.bind({});
118
- LabelSlot.args = {
119
- label: "Email Address",
120
- slotTemplate: `
121
- <template #label="{ label }">
122
- <URow align="center" gap="xs">
123
- <UIcon name="mail" size="sm" class="text-primary" />
124
- <UChip icon="asterisk" color="error" size="2xs">
125
- <UText :label="label" class="mr-2" />
126
- </UChip>
127
- </URow>
128
- </template>
129
- `,
130
- };
117
+ export const Slots: StoryFn<ULabelArgs> = () => ({
118
+ components: { ULabel, UText, URow, ULink, UIcon, UCol, UChip },
119
+ template: `
120
+ <UCol gap="3xl">
121
+ <ULabel label="Work email">
122
+ <UText label="johndoe@example.com" />
123
+ <template #label="{ label }">
124
+ <URow align="center" gap="xs">
125
+ <UIcon name="mail" size="sm" class="text-primary" />
126
+ <UChip icon="asterisk" color="error" size="xs">
127
+ <UText :label="label" class="mr-2" />
128
+ </UChip>
129
+ </URow>
130
+ </template>
131
+ </ULabel>
131
132
 
132
- export const BottomSlot = DefaultTemplate.bind({});
133
- BottomSlot.args = {
134
- slotTemplate: `
135
- <template #bottom>
136
- <URow align="center" gap="2xs" class="mt-1">
137
- <UIcon name="error" size="xs" />
138
- <UText>Invalid email address. <ULink label="Need help?" /></UText>
139
- </URow>
140
- </template>
133
+ <ULabel label="Work email">
134
+ <UText label="johndoe@example.com" />
135
+ <template #description>
136
+ <URow align="center" gap="2xs" class="text-neutral">
137
+ <UIcon name="mark_email_unread" size="xs" color="primary" />
138
+ <UText size="sm">
139
+ We will send a
140
+ <ULink label="confirmation link" underlined size="sm" />
141
+ to this address.
142
+ </UText>
143
+ </URow>
144
+ </template>
145
+ </ULabel>
146
+
147
+ <ULabel label="Email" :error="true">
148
+ <UText label="not-an-email" />
149
+ <template #error>
150
+ <URow align="center" gap="2xs">
151
+ <UIcon name="error" size="xs" color="error" />
152
+ <UText size="sm" color="error">
153
+ <ul>
154
+ <li>Email address format is invalid</li>
155
+ <li>Use a name@domain.com style address</li>
156
+ <li>Remove spaces and special characters from the local part</li>
157
+ </ul>
158
+ </UText>
159
+ </URow>
160
+ </template>
161
+ </ULabel>
162
+ </UCol>
141
163
  `,
142
- };
164
+ });
@@ -369,18 +369,6 @@ describe("ULabel.vue", () => {
369
369
 
370
370
  expect(component.find("[vl-key='label']").text()).toBe(slotContent);
371
371
  });
372
-
373
- it("Bottom – renders content from bottom slot", () => {
374
- const testClass = "custom-bottom";
375
-
376
- const component = mount(ULabel, {
377
- slots: {
378
- bottom: `<div class="${testClass}">Bottom Slot Content</div>`,
379
- },
380
- });
381
-
382
- expect(component.find(`.${testClass}`).exists()).toBe(true);
383
- });
384
372
  });
385
373
 
386
374
  describe("Events", () => {
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { watch, computed, useId, ref, useTemplateRef, nextTick } from "vue";
2
+ import { watch, computed, useId, ref, useTemplateRef } from "vue";
3
3
  import { isEqual } from "lodash-es";
4
4
 
5
5
  import { useUI } from "../composables/useUI";
@@ -144,78 +144,77 @@ const filteredOptions = computed(() => {
144
144
  watch(
145
145
  () => [props.options, props.size, props.visibleOptions, props.searchable, searchModel.value],
146
146
  () => {
147
- nextTick(() => {
148
- const options = [
149
- ...(optionsRef.value || []),
150
- ...(addOptionRef.value ? [addOptionRef.value] : []),
151
- ...(emptyOptionRef.value ? [emptyOptionRef.value] : []),
152
- ];
153
-
154
- if (props.visibleOptions) {
155
- options.slice(0, props.visibleOptions);
156
- }
157
-
158
- const maxHeight = options
159
- .slice(0, props.visibleOptions)
160
- .map((el) => {
161
- const styles = window.getComputedStyle(el);
162
- const height = parseFloat(styles.height || "0");
163
- const marginTop = parseFloat(styles.marginTop || "0");
164
- const marginBottom = parseFloat(styles.marginBottom || "0");
165
-
166
- return height + marginTop + marginBottom;
167
- })
168
- .reduce((acc, cur) => acc + cur, 0);
169
-
170
- const wrapperStyle = getComputedStyle(wrapperRef.value as Element);
171
- const wrapperPaddingTop = parseFloat(wrapperStyle.paddingTop || "0");
172
- const wrapperPaddingBottom = parseFloat(wrapperStyle.paddingBottom || "0");
173
- const wrapperBorderTop = parseFloat(wrapperStyle.borderTopWidth || "0");
174
- const wrapperBorderBottom = parseFloat(wrapperStyle.borderBottomWidth || "0");
175
- const wrapperGap = parseFloat(wrapperStyle.gap || "0");
176
-
177
- const addOptionHeight = addOptionRef.value?.getBoundingClientRect().height || 0;
178
-
179
- const inputEl = listboxInputRef.value?.input as HTMLInputElement | undefined;
180
- let listboxInputHeight = 0;
181
-
182
- let listboxInputWrapperPaddingTop = 0;
183
- let listboxInputBorderTop = 0;
184
- let listboxInputBorderBottom = 0;
185
-
186
- if (inputEl) {
187
- const listboxInputStyle = getComputedStyle(inputEl);
188
- const listboxInputLabelStyle = inputEl.parentElement
189
- ? getComputedStyle(inputEl.parentElement)
190
- : undefined;
191
- const listboxInputWrapperStyle = getComputedStyle(
192
- inputEl.parentElement?.parentElement?.parentElement as Element,
193
- );
194
-
195
- listboxInputHeight = parseFloat(listboxInputStyle.height || "0");
196
-
197
- listboxInputWrapperPaddingTop = parseFloat(listboxInputWrapperStyle.paddingTop || "0");
198
-
199
- listboxInputBorderTop = parseFloat(listboxInputLabelStyle?.borderTop || "0");
200
- listboxInputBorderBottom = parseFloat(listboxInputLabelStyle?.borderBottom || "0");
201
- }
202
-
203
- wrapperMaxHeight.value = `${
204
- maxHeight +
205
- addOptionHeight +
206
- (props.searchable ? wrapperGap : 0) +
207
- wrapperPaddingTop +
208
- wrapperPaddingBottom +
209
- wrapperBorderTop +
210
- wrapperBorderBottom +
211
- listboxInputHeight +
212
- listboxInputBorderTop +
213
- listboxInputBorderBottom +
214
- listboxInputWrapperPaddingTop
215
- }px`;
216
- });
147
+ const options = [
148
+ ...(optionsRef.value || []),
149
+ ...(addOptionRef.value ? [addOptionRef.value] : []),
150
+ ...(emptyOptionRef.value ? [emptyOptionRef.value] : []),
151
+ ];
152
+
153
+ if (props.visibleOptions) {
154
+ options.slice(0, props.visibleOptions);
155
+ }
156
+
157
+ const maxHeight = options
158
+ .slice(0, props.visibleOptions)
159
+ .map((el) => {
160
+ const styles = window.getComputedStyle(el);
161
+ const height = parseFloat(styles.height || "0");
162
+ const marginTop = parseFloat(styles.marginTop || "0");
163
+ const marginBottom = parseFloat(styles.marginBottom || "0");
164
+
165
+ return height + marginTop + marginBottom;
166
+ })
167
+ .reduce((acc, cur) => acc + cur, 0);
168
+
169
+ const wrapperEl = wrapperRef.value;
170
+ const wrapperStyle = wrapperEl ? getComputedStyle(wrapperEl) : null;
171
+ const wrapperPaddingTop = parseFloat(wrapperStyle?.paddingTop || "0");
172
+ const wrapperPaddingBottom = parseFloat(wrapperStyle?.paddingBottom || "0");
173
+ const wrapperBorderTop = parseFloat(wrapperStyle?.borderTopWidth || "0");
174
+ const wrapperBorderBottom = parseFloat(wrapperStyle?.borderBottomWidth || "0");
175
+ const wrapperGap = parseFloat(wrapperStyle?.gap || "0");
176
+
177
+ const addOptionHeight = addOptionRef.value?.getBoundingClientRect().height || 0;
178
+
179
+ const inputEl = listboxInputRef.value?.input as HTMLInputElement | undefined;
180
+ let listboxInputHeight = 0;
181
+
182
+ let listboxInputWrapperPaddingTop = 0;
183
+ let listboxInputBorderTop = 0;
184
+ let listboxInputBorderBottom = 0;
185
+
186
+ if (inputEl) {
187
+ const listboxInputStyle = getComputedStyle(inputEl);
188
+ const listboxInputLabelStyle = inputEl.parentElement
189
+ ? getComputedStyle(inputEl.parentElement)
190
+ : undefined;
191
+ const listboxInputWrapperStyle = getComputedStyle(
192
+ inputEl.parentElement?.parentElement?.parentElement as Element,
193
+ );
194
+
195
+ listboxInputHeight = parseFloat(listboxInputStyle.height || "0");
196
+
197
+ listboxInputWrapperPaddingTop = parseFloat(listboxInputWrapperStyle.paddingTop || "0");
198
+
199
+ listboxInputBorderTop = parseFloat(listboxInputLabelStyle?.borderTop || "0");
200
+ listboxInputBorderBottom = parseFloat(listboxInputLabelStyle?.borderBottom || "0");
201
+ }
202
+
203
+ wrapperMaxHeight.value = `${
204
+ maxHeight +
205
+ addOptionHeight +
206
+ (props.searchable ? wrapperGap : 0) +
207
+ wrapperPaddingTop +
208
+ wrapperPaddingBottom +
209
+ wrapperBorderTop +
210
+ wrapperBorderBottom +
211
+ listboxInputHeight +
212
+ listboxInputBorderTop +
213
+ listboxInputBorderBottom +
214
+ listboxInputWrapperPaddingTop
215
+ }px`;
217
216
  },
218
- { immediate: true },
217
+ { immediate: true, flush: "post" },
219
218
  );
220
219
 
221
220
  watch(
@@ -411,8 +411,10 @@ describe("UListbox.vue", () => {
411
411
  await flushPromises();
412
412
 
413
413
  const wrapper = component.find('[vl-key="wrapper"]');
414
+ const styleAttr = wrapper.attributes("style");
415
+ const inlineMaxHeight = (wrapper.element as HTMLElement).style.maxHeight;
414
416
 
415
- expect(wrapper.attributes("style")).toContain(expectedStyle);
417
+ expect(styleAttr ?? inlineMaxHeight).toContain(expectedStyle);
416
418
  });
417
419
  });
418
420
 
@@ -151,10 +151,5 @@ const { getDataTest, radioLabelAttrs, radioAttrs } = useUI<Config>(defaultConfig
151
151
  :data-test="getDataTest()"
152
152
  @change="onChange"
153
153
  />
154
-
155
- <template #bottom>
156
- <!-- @slot Use it to add something below the radio. -->
157
- <slot name="bottom" />
158
- </template>
159
154
  </ULabel>
160
155
  </template>
@@ -15,6 +15,8 @@ import UIcon from "../../ui.image-icon/UIcon.vue";
15
15
 
16
16
  import type { Meta, StoryFn } from "@storybook/vue3-vite";
17
17
  import type { Props } from "../types";
18
+ import { ref } from "vue";
19
+ import UCol from "../../ui.container-col/UCol.vue";
18
20
 
19
21
  interface URadioArgs extends Props {
20
22
  slotTemplate?: string;
@@ -107,30 +109,68 @@ Sizes.args = { name: "Sizes", enum: "size", label: "{enumValue}", modelValue: 1
107
109
  export const Colors = EnumTemplate.bind({});
108
110
  Colors.args = { name: "Colors", enum: "color", label: "{enumValue}", modelValue: 1 };
109
111
 
110
- export const LabelSlot = DefaultTemplate.bind({});
111
- LabelSlot.args = {
112
- name: "LabelSlot",
113
- value: "courier",
114
- color: "primary",
115
- slotTemplate: `
116
- <template #label>
117
- <URow gap="2xs" align="center">
118
- <UText>I agree to the <ULink label="Privacy Policy" /></UText>
119
- <UIcon name="contract" size="xs" />
120
- </URow>
121
- </template>
122
- `,
123
- };
112
+ export const Slots: StoryFn<URadioArgs> = (args) => ({
113
+ components: { URadio, UCol, UText, URow, ULink, UIcon },
114
+ setup: () => ({
115
+ args,
116
+ labelModel: ref(""),
117
+ descriptionModel: ref(""),
118
+ errorModel: ref(""),
119
+ }),
120
+ template: `
121
+ <UCol gap="3xl">
122
+ <URadio
123
+ v-bind="args"
124
+ v-model="labelModel"
125
+ name="LabelSlot"
126
+ value="courier"
127
+ >
128
+ <template #label>
129
+ <URow align="center" gap="2xs">
130
+ <UText>Courier delivery</UText>
131
+ <UIcon name="local_shipping" size="xs" color="neutral" />
132
+ </URow>
133
+ </template>
134
+ </URadio>
124
135
 
125
- export const BottomSlot = DefaultTemplate.bind({});
126
- BottomSlot.args = {
127
- name: "terms",
128
- label: "I agree to the Terms and Conditions",
129
- value: "terms",
130
- color: "primary",
131
- slotTemplate: `
132
- <template #bottom>
133
- <UBadge label="Required to proceed" color="warning" variant="outlined" size="sm" class="mt-2" />
134
- </template>
136
+ <URadio
137
+ v-bind="args"
138
+ v-model="descriptionModel"
139
+ name="DescriptionSlot"
140
+ value="pro"
141
+ >
142
+ <template #description>
143
+ <URow align="center" gap="2xs" class="text-neutral">
144
+ <UIcon name="workspace_premium" size="xs" class="mt-0.5" color="primary" />
145
+ <UText size="sm">
146
+ Includes all core features.
147
+ <ULink label="Compare plans" underlined size="sm" />.
148
+ </UText>
149
+ </URow>
150
+ </template>
151
+ </URadio>
152
+
153
+ <URadio
154
+ v-bind="args"
155
+ v-model="errorModel"
156
+ name="ErrorSlot"
157
+ label="Select"
158
+ value="1"
159
+ :error="true"
160
+ >
161
+ <template #error>
162
+ <URow align="center" gap="2xs">
163
+ <UIcon name="error" size="xs" color="error" />
164
+ <UText size="sm" color="error">
165
+ <ul>
166
+ <li>You must select one of the available options</li>
167
+ <li>This field is required to proceed</li>
168
+ <li>Choose an option above to clear this error</li>
169
+ </ul>
170
+ </UText>
171
+ </URow>
172
+ </template>
173
+ </URadio>
174
+ </UCol>
135
175
  `,
136
- };
176
+ });
@@ -241,22 +241,5 @@ describe("URadio.vue", () => {
241
241
 
242
242
  expect(errorElement.text()).toBe(customError);
243
243
  });
244
-
245
- it("Bottom – renders custom content from bottom slot", () => {
246
- const customBottomContent = "Custom Bottom Content";
247
-
248
- const component = mount(URadio, {
249
- props: {
250
- label: "Test Label",
251
- },
252
- slots: {
253
- bottom: customBottomContent,
254
- },
255
- });
256
-
257
- const labelComponent = component.getComponent(ULabel);
258
-
259
- expect(labelComponent.text()).toContain(customBottomContent);
260
- });
261
244
  });
262
245
  });
@@ -13,9 +13,12 @@ import UCol from "../../ui.container-col/UCol.vue";
13
13
  import URow from "../../ui.container-row/URow.vue";
14
14
  import UBadge from "../../ui.text-badge/UBadge.vue";
15
15
  import UText from "../../ui.text-block/UText.vue";
16
+ import ULink from "../../ui.button-link/ULink.vue";
17
+ import UIcon from "../../ui.image-icon/UIcon.vue";
16
18
 
17
19
  import type { Meta, StoryFn } from "@storybook/vue3-vite";
18
20
  import type { Props } from "../types";
21
+ import { ref } from "vue";
19
22
 
20
23
  interface URadioGroupArgs extends Props {
21
24
  slotTemplate?: string;
@@ -170,3 +173,67 @@ CustomKeys.parameters = {
170
173
  },
171
174
  },
172
175
  };
176
+
177
+ export const Slots: StoryFn<URadioGroupArgs> = (args) => ({
178
+ components: { URadioGroup, UCol, UText, URow, ULink, UIcon },
179
+ setup: () => ({
180
+ args,
181
+ modelValueLabel: ref(null),
182
+ modelValueDescription: ref(null),
183
+ modelValueError: ref(null),
184
+ }),
185
+ template: `
186
+ <UCol gap="3xl">
187
+ <URadioGroup
188
+ v-bind="args"
189
+ v-model="modelValueLabel"
190
+ name="LabelSlot"
191
+ >
192
+ <template #label>
193
+ <URow align="center" gap="2xs">
194
+ <UText>Choose a delivery option</UText>
195
+ <UIcon name="local_shipping" size="xs" color="neutral" />
196
+ </URow>
197
+ </template>
198
+ </URadioGroup>
199
+
200
+ <URadioGroup
201
+ v-bind="args"
202
+ v-model="modelValueDescription"
203
+ name="SlotsDescription"
204
+ label="Delivery"
205
+ >
206
+ <template #description>
207
+ <URow align="center" gap="2xs" class="text-neutral">
208
+ <UIcon name="local_shipping" size="xs" color="primary" />
209
+ <UText size="sm">
210
+ Shipping times are estimates.
211
+ <ULink label="Full policy" underlined size="sm" />.
212
+ </UText>
213
+ </URow>
214
+ </template>
215
+ </URadioGroup>
216
+
217
+ <URadioGroup
218
+ v-bind="args"
219
+ v-model="modelValueError"
220
+ name="SlotsError"
221
+ label="Delivery"
222
+ :error="true"
223
+ >
224
+ <template #error>
225
+ <URow align="center" gap="2xs">
226
+ <UIcon name="error" size="xs" color="error" />
227
+ <UText size="sm" color="error">
228
+ <ul>
229
+ <li>Please choose one option from the group</li>
230
+ <li>At least one selection is required</li>
231
+ <li>Review the labels and pick a valid answer</li>
232
+ </ul>
233
+ </UText>
234
+ </URow>
235
+ </template>
236
+ </URadioGroup>
237
+ </UCol>
238
+ `,
239
+ });
@@ -334,54 +334,109 @@ export const IconProps: StoryFn<USelectArgs> = (args) => ({
334
334
  });
335
335
 
336
336
  export const Slots: StoryFn<USelectArgs> = (args) => ({
337
- components: { USelect, URow, UIcon, UText },
338
- setup: () => ({ args, leftSlotModel: ref("paypal"), rightSlotModel: ref("bank") }),
337
+ components: { USelect, URow, UCol, UIcon, UText, ULink },
338
+ setup: () => ({
339
+ args,
340
+ leftSlotModel: ref("paypal"),
341
+ rightSlotModel: ref("bank"),
342
+ descriptionSlotModel: ref(null),
343
+ errorSlotModel: ref(null),
344
+ }),
339
345
  template: `
340
- <URow>
341
- <USelect
342
- v-model="leftSlotModel"
343
- label="Select Payment Method"
344
- :options="[
345
- { label: 'Visa', value: 'visa', icon: 'credit_card', details: '•••• 4242' },
346
- { label: 'PayPal', value: 'paypal', icon: 'payments', details: 'user@example.com' },
347
- { label: 'Bank Transfer', value: 'bank', icon: 'account_balance', details: 'Acct **** 1234' },
348
- { label: 'Apple Pay', value: 'apple', icon: 'phone_iphone', details: 'iPhone 15' },
349
- ]"
350
- >
351
- <template #left="{ options }">
352
- <UIcon
353
- v-if="leftSlotModel"
354
- :name="options?.icon"
355
- color="primary"
356
- size="sm"
357
- />
358
- </template>
359
- </USelect>
346
+ <UCol gap="3xl">
347
+ <URow block>
348
+ <USelect
349
+ v-model="leftSlotModel"
350
+ label="Select Payment Method"
351
+ :options="[
352
+ { label: 'Visa', value: 'visa', icon: 'credit_card', details: '•••• 4242' },
353
+ { label: 'PayPal', value: 'paypal', icon: 'payments', details: 'user@example.com' },
354
+ { label: 'Bank Transfer', value: 'bank', icon: 'account_balance', details: 'Acct **** 1234' },
355
+ { label: 'Apple Pay', value: 'apple', icon: 'phone_iphone', details: 'iPhone 15' },
356
+ ]"
357
+ >
358
+ <template #left="{ options }">
359
+ <UIcon
360
+ v-if="leftSlotModel"
361
+ :name="options?.icon"
362
+ color="primary"
363
+ size="sm"
364
+ />
365
+ </template>
366
+ </USelect>
367
+
368
+ <USelect
369
+ v-model="rightSlotModel"
370
+ label="Select Payment Method"
371
+ :options="[
372
+ { label: 'Visa', value: 'visa', icon: 'credit_card', details: '•••• 4242' },
373
+ { label: 'PayPal', value: 'paypal', icon: 'payments', details: 'user@example.com' },
374
+ { label: 'Bank Transfer', value: 'bank', icon: 'account_balance', details: 'Acct **** 1234' },
375
+ { label: 'Apple Pay', value: 'apple', icon: 'phone_iphone', details: 'iPhone 15' },
376
+ ]"
377
+ >
378
+ <template #right="{ options }">
379
+ <UText
380
+ v-if="rightSlotModel"
381
+ size="sm"
382
+ variant="lifted"
383
+ class="text-nowrap"
384
+ >
385
+ {{ options?.details }}
386
+ </UText>
387
+ </template>
388
+ </USelect>
389
+ </URow>
360
390
 
361
- <USelect
362
- v-model="rightSlotModel"
363
- label="Select Payment Method"
364
- :options="[
365
- { label: 'Visa', value: 'visa', icon: 'credit_card', details: '•••• 4242' },
366
- { label: 'PayPal', value: 'paypal', icon: 'payments', details: 'user@example.com' },
367
- { label: 'Bank Transfer', value: 'bank', icon: 'account_balance', details: 'Acct **** 1234' },
368
- { label: 'Apple Pay', value: 'apple', icon: 'phone_iphone', details: 'iPhone 15' },
369
- ]"
370
- >
371
- <template #right="{ options }">
372
- <UText
373
- v-if="rightSlotModel"
374
- size="sm"
375
- variant="lifted"
376
- class="text-nowrap"
377
- >
378
- {{ options?.details }}
379
- </UText>
380
- </template>
381
- </USelect>
382
- </URow>
391
+ <URow block>
392
+ <USelect
393
+ v-bind="args"
394
+ v-model="descriptionSlotModel"
395
+ label="City"
396
+ :options="args.options"
397
+ >
398
+ <template #description>
399
+ <URow align="center" gap="2xs" class="text-neutral">
400
+ <UIcon name="location_city" size="xs" color="primary" />
401
+ <UText size="sm">
402
+ Only cities we ship to.
403
+ <ULink label="Shipping zones" underlined size="sm" />.
404
+ </UText>
405
+ </URow>
406
+ </template>
407
+ </USelect>
408
+
409
+ <USelect
410
+ v-bind="args"
411
+ v-model="errorSlotModel"
412
+ label="City"
413
+ :options="args.options"
414
+ :error="true"
415
+ >
416
+ <template #error>
417
+ <URow align="center" gap="2xs">
418
+ <UIcon name="error" size="xs" color="error" />
419
+ <UText size="sm" color="error">
420
+ <ul>
421
+ <li>Please select an option from the list</li>
422
+ <li>The current value is not available anymore</li>
423
+ <li>Pick a valid choice to continue</li>
424
+ </ul>
425
+ </UText>
426
+ </URow>
427
+ </template>
428
+ </USelect>
429
+ </URow>
430
+ </UCol>
383
431
  `,
384
432
  });
433
+ Slots.parameters = {
434
+ docs: {
435
+ story: {
436
+ height: "400px",
437
+ },
438
+ },
439
+ };
385
440
 
386
441
  export const ToggleSlots: StoryFn<USelectArgs> = (args) => ({
387
442
  components: { USelect, URow, UIcon },