v-uixy 1.7.2 → 1.8.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.
Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/templates/components/Checkbox/Checkbox.styles.ts +1 -1
  3. package/templates/components/DatePicker/DatePicker.component.vue +6 -3
  4. package/templates/components/DatePicker/DatePicker.styles.ts +30 -11
  5. package/templates/components/DatePicker/DatePicker.types.ts +2 -0
  6. package/templates/components/DateRangePicker/DateRangePicker.component.vue +6 -3
  7. package/templates/components/DateRangePicker/DateRangePicker.types.ts +2 -0
  8. package/templates/components/Input/Input.component.vue +4 -1
  9. package/templates/components/Input/Input.styles.ts +24 -2
  10. package/templates/components/Input/Input.types.ts +2 -0
  11. package/templates/components/Modal/Modal.component.vue +3 -1
  12. package/templates/components/Modal/Modal.styles.ts +1 -1
  13. package/templates/components/Modal/Modal.types.ts +1 -0
  14. package/templates/components/Popover/Popover.component.vue +1 -0
  15. package/templates/components/Popover/Popover.types.ts +1 -0
  16. package/templates/components/SegmentedControl/SegmentedControl.component.vue +1 -0
  17. package/templates/components/SegmentedControl/SegmentedControl.types.ts +1 -0
  18. package/templates/components/Select/Select.component.vue +57 -8
  19. package/templates/components/Select/Select.styles.ts +56 -3
  20. package/templates/components/Select/Select.types.ts +3 -0
  21. package/templates/components/Select/SelectTreeItem.vue +4 -0
  22. package/templates/components/Textarea/Textarea.component.vue +3 -1
  23. package/templates/components/Textarea/Textarea.styles.ts +6 -1
  24. package/templates/components/Textarea/Textarea.types.ts +3 -0
  25. package/templates/composables/usePosition.ts +31 -4
  26. package/templates/types/field.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "v-uixy",
3
- "version": "1.7.2",
3
+ "version": "1.8.1",
4
4
  "private": false,
5
5
  "author": {
6
6
  "name": "Alan Haber",
@@ -23,7 +23,7 @@ export const iconStyles = styles("h-3 w-3", {
23
23
  },
24
24
  });
25
25
 
26
- export const labelStyles = styles("text-sm", {
26
+ export const labelStyles = styles("text-xs", {
27
27
  disabled: {
28
28
  true: "text-gray-500 dark:text-gray-600",
29
29
  false: "",
@@ -9,14 +9,14 @@
9
9
  role="button"
10
10
  :tabindex="props.disabled ? -1 : 0"
11
11
  :aria-expanded="active"
12
- :class="triggerStyles({ status, open: active })"
12
+ :class="triggerStyles({ status, open: active, size })"
13
13
  @click="onTriggerClick"
14
14
  @keydown.enter.prevent="onTriggerClick"
15
15
  @keydown.space.prevent="onTriggerClick"
16
16
  >
17
17
  <uixy-icon
18
18
  :name="props.icon ?? 'calendar'"
19
- :class="triggerIconStyles({ disabled: props.disabled })"
19
+ :class="triggerIconStyles({ disabled: props.disabled, size })"
20
20
  />
21
21
  <span
22
22
  :class="
@@ -28,7 +28,7 @@
28
28
  <uixy-icon
29
29
  v-if="model && !props.disabled"
30
30
  name="close"
31
- class="h-4 w-4 shrink-0 cursor-pointer text-gray-400 hover:text-black dark:text-gray-600 dark:hover:text-white"
31
+ :class="closeIconStyles({ size })"
32
32
  @click.stop="clear"
33
33
  />
34
34
  </div>
@@ -122,6 +122,7 @@
122
122
  triggerStyles,
123
123
  triggerValueStyles,
124
124
  triggerIconStyles,
125
+ closeIconStyles,
125
126
  labelStyles,
126
127
  helperStyles,
127
128
  panelStyles,
@@ -156,6 +157,8 @@
156
157
  return props.status ?? "default";
157
158
  });
158
159
 
160
+ const size = computed(() => props.size ?? "md");
161
+
159
162
  const helperText = computed(() => {
160
163
  if (status.value === "error") return props.errorText ?? "";
161
164
 
@@ -1,7 +1,7 @@
1
1
  import styles from "~/utils/styles";
2
2
 
3
3
  export const triggerStyles = styles(
4
- "relative flex w-full items-center gap-2 cursor-pointer transition-colors ease-in-out duration-150 border border-gray-300 dark:border-gray-900 rounded-1 pl-3 pr-3 py-2 text-sm min-h-[38px] outline-offset-1 outline-gray-300 dark:outline-gray-800 select-none",
4
+ "relative flex w-full items-center gap-2 cursor-pointer transition-colors ease-in-out duration-150 border border-gray-300 dark:border-gray-900 rounded-1 outline-offset-1 outline-gray-300 dark:outline-gray-800 select-none",
5
5
  {
6
6
  status: {
7
7
  default:
@@ -15,7 +15,12 @@ export const triggerStyles = styles(
15
15
  true: "border-gray-500 dark:border-gray-600",
16
16
  false: "",
17
17
  },
18
- }
18
+ size: {
19
+ sm: "pl-2.5 pr-2.5 py-1.5 text-xs min-h-[32px]",
20
+ md: "pl-3 pr-3 py-2 text-sm min-h-[38px]",
21
+ xl: "pl-3.5 pr-3.5 py-2.5 text-base min-h-[42px]",
22
+ },
23
+ },
19
24
  );
20
25
 
21
26
  export const triggerValueStyles = styles("flex-1 truncate text-left", {
@@ -30,13 +35,29 @@ export const triggerValueStyles = styles("flex-1 truncate text-left", {
30
35
  });
31
36
 
32
37
  export const triggerIconStyles = styles(
33
- "h-4 w-4 shrink-0 text-gray-400 dark:text-gray-600",
38
+ "shrink-0 text-gray-400 dark:text-gray-600",
34
39
  {
35
40
  disabled: {
36
41
  true: "dark:text-gray-700",
37
42
  false: "",
38
43
  },
39
- }
44
+ size: {
45
+ sm: "h-3.5 w-3.5",
46
+ md: "h-4 w-4",
47
+ xl: "h-5 w-5",
48
+ },
49
+ },
50
+ );
51
+
52
+ export const closeIconStyles = styles(
53
+ "shrink-0 cursor-pointer text-gray-400 hover:text-black dark:text-gray-600 dark:hover:text-white",
54
+ {
55
+ size: {
56
+ sm: "h-3.5 w-3.5",
57
+ md: "h-4 w-4",
58
+ xl: "h-5 w-5",
59
+ },
60
+ },
40
61
  );
41
62
 
42
63
  export const labelStyles = styles(
@@ -47,7 +68,7 @@ export const labelStyles = styles(
47
68
  error: "text-error-600",
48
69
  disabled: "text-gray-500 dark:text-gray-700",
49
70
  },
50
- }
71
+ },
51
72
  );
52
73
 
53
74
  export const helperStyles = styles(
@@ -58,17 +79,15 @@ export const helperStyles = styles(
58
79
  error: "text-error-600",
59
80
  disabled: "text-gray-400 dark:text-gray-800",
60
81
  },
61
- }
82
+ },
62
83
  );
63
84
 
64
85
  export const panelStyles = styles(
65
- "flex flex-col gap-3 rounded-2 border border-gray-300 bg-gray-100 p-3 shadow-lg dark:border-gray-900 dark:bg-black"
86
+ "flex flex-col gap-3 rounded-2 border border-gray-300 bg-gray-100 p-3 shadow-lg dark:border-gray-900 dark:bg-black",
66
87
  );
67
88
 
68
89
  export const sectionLabelStyles = styles(
69
- "text-xs font-500 text-gray-500 dark:text-gray-600"
90
+ "text-xs font-500 text-gray-500 dark:text-gray-600",
70
91
  );
71
92
 
72
- export const footerStyles = styles(
73
- "flex items-center justify-between gap-2"
74
- );
93
+ export const footerStyles = styles("flex items-center justify-between gap-2");
@@ -1,4 +1,5 @@
1
1
  import type { IconName } from "~/types/icons";
2
+ import type { UixyFieldSize } from "~/types/field";
2
3
 
3
4
  export interface UixyDatePickerProps {
4
5
  placeholder?: string;
@@ -14,4 +15,5 @@ export interface UixyDatePickerProps {
14
15
  isDateDisabled?: (date: Date) => boolean;
15
16
  direction?: "top" | "bottom";
16
17
  closeOnSelect?: boolean;
18
+ size?: UixyFieldSize;
17
19
  }
@@ -9,14 +9,14 @@
9
9
  role="button"
10
10
  :tabindex="props.disabled ? -1 : 0"
11
11
  :aria-expanded="active"
12
- :class="triggerStyles({ status, open: active })"
12
+ :class="triggerStyles({ status, open: active, size })"
13
13
  @click="onTriggerClick"
14
14
  @keydown.enter.prevent="onTriggerClick"
15
15
  @keydown.space.prevent="onTriggerClick"
16
16
  >
17
17
  <uixy-icon
18
18
  :name="props.icon ?? 'calendar'"
19
- :class="triggerIconStyles({ disabled: props.disabled })"
19
+ :class="triggerIconStyles({ disabled: props.disabled, size })"
20
20
  />
21
21
  <span
22
22
  :class="
@@ -31,7 +31,7 @@
31
31
  <uixy-icon
32
32
  v-if="hasValue && !props.disabled"
33
33
  name="close"
34
- class="h-4 w-4 shrink-0 cursor-pointer text-gray-400 hover:text-black dark:text-gray-600 dark:hover:text-white"
34
+ :class="closeIconStyles({ size })"
35
35
  @click.stop="clear"
36
36
  />
37
37
  </div>
@@ -145,6 +145,7 @@
145
145
  triggerStyles,
146
146
  triggerValueStyles,
147
147
  triggerIconStyles,
148
+ closeIconStyles,
148
149
  labelStyles,
149
150
  helperStyles,
150
151
  panelStyles,
@@ -187,6 +188,8 @@
187
188
  return props.status ?? "default";
188
189
  });
189
190
 
191
+ const size = computed(() => props.size ?? "md");
192
+
190
193
  const helperText = computed(() => {
191
194
  if (status.value === "error") return props.errorText ?? "";
192
195
 
@@ -1,4 +1,5 @@
1
1
  import type { IconName } from "~/types/icons";
2
+ import type { UixyFieldSize } from "~/types/field";
2
3
 
3
4
  export interface UixyDateRange {
4
5
  start: Date | null;
@@ -19,4 +20,5 @@ export interface UixyDateRangePickerProps {
19
20
  isDateDisabled?: (date: Date) => boolean;
20
21
  direction?: "top" | "bottom";
21
22
  closeOnSelect?: boolean;
23
+ size?: UixyFieldSize;
22
24
  }
@@ -8,7 +8,7 @@
8
8
  :id
9
9
  :disabled="props.disabled"
10
10
  :placeholder="props.placeholder"
11
- :class="inputStyles({ status, icon }, $attrs.class as string)"
11
+ :class="inputStyles({ status, icon, size }, $attrs.class as string)"
12
12
  v-bind="filteredAttrs"
13
13
  />
14
14
  <uixy-icon
@@ -19,6 +19,7 @@
19
19
  iconStyles({
20
20
  status,
21
21
  icon,
22
+ size,
22
23
  targetable: !!hasIconClickEmit,
23
24
  })
24
25
  "
@@ -94,6 +95,8 @@
94
95
  props.disabled ? "disabled" : (props.status ?? "default"),
95
96
  );
96
97
 
98
+ const size = computed(() => props.size ?? "md");
99
+
97
100
  const text = computed(
98
101
  () =>
99
102
  ({
@@ -1,7 +1,7 @@
1
1
  import styles from "~/utils/styles";
2
2
 
3
3
  export const inputStyles = styles(
4
- "peer transition-colors ease-in-out duration-150 border border-gray-300 dark:border-gray-900 rounded-1 px-3 py-2 text-sm placeholder:text-gray-500 dark:placeholder:text-gray-700 dark:outline-gray-800 outline-offset-1 outline-gray-300",
4
+ "peer transition-colors ease-in-out duration-150 border border-gray-300 dark:border-gray-900 rounded-1 placeholder:text-gray-500 dark:placeholder:text-gray-700 dark:outline-gray-800 outline-offset-1 outline-gray-300",
5
5
  {
6
6
  status: {
7
7
  default:
@@ -12,16 +12,27 @@ export const inputStyles = styles(
12
12
  valid:
13
13
  "bg-white dark:bg-gray-1000 focus-visible:border-gray-500 dark:focus-visible:border-gray-600",
14
14
  },
15
+ size: {
16
+ sm: "px-2.5 py-1.5 text-xs",
17
+ md: "px-3 py-2 text-sm",
18
+ xl: "px-3.5 py-2.5 text-base",
19
+ },
15
20
  icon: {
16
21
  left: "pl-8",
17
22
  right: "pr-8",
18
23
  none: "",
19
24
  },
20
25
  },
26
+ {
27
+ "size.sm+icon.left": "pl-7",
28
+ "size.sm+icon.right": "pr-7",
29
+ "size.xl+icon.left": "pl-9",
30
+ "size.xl+icon.right": "pr-9",
31
+ },
21
32
  );
22
33
 
23
34
  export const iconStyles = styles(
24
- "select-none pointer-events-none h-5 w-5 absolute bottom-2 transition-colors duration-150 ease-in-out",
35
+ "select-none pointer-events-none absolute transition-colors duration-150 ease-in-out",
25
36
  {
26
37
  status: {
27
38
  default:
@@ -32,6 +43,11 @@ export const iconStyles = styles(
32
43
  valid:
33
44
  "text-gray-400 peer-focus:text-black hover:text-black dark:text-gray-700 dark:peer-focus:text-gray-500 dark:hover:text-gray-500",
34
45
  },
46
+ size: {
47
+ sm: "h-4 w-4 bottom-1.5",
48
+ md: "h-5 w-5 bottom-2",
49
+ xl: "h-5 w-5 bottom-2.5",
50
+ },
35
51
  icon: {
36
52
  left: "left-2",
37
53
  right: "right-2",
@@ -42,6 +58,12 @@ export const iconStyles = styles(
42
58
  false: "",
43
59
  },
44
60
  },
61
+ {
62
+ "size.sm+icon.left": "left-1.5",
63
+ "size.sm+icon.right": "right-1.5",
64
+ "size.xl+icon.left": "left-2.5",
65
+ "size.xl+icon.right": "right-2.5",
66
+ },
45
67
  );
46
68
 
47
69
  export const labelStyles = styles(
@@ -1,4 +1,5 @@
1
1
  import type { IconName } from "~/types/icons";
2
+ import type { UixyFieldSize } from "~/types/field";
2
3
 
3
4
  export interface UixyInputProps {
4
5
  status?: "valid" | "error" | "default";
@@ -11,6 +12,7 @@ export interface UixyInputProps {
11
12
  helperText?: string;
12
13
  errorText?: string;
13
14
  autoFocus?: boolean;
15
+ size?: UixyFieldSize;
14
16
  }
15
17
 
16
18
  export interface UixyInputEmits {
@@ -13,10 +13,12 @@
13
13
  @click.self="handleClick"
14
14
  >
15
15
  <motion.div
16
+ class="flex w-full justify-center"
16
17
  :initial="{ scale: 0.8 }"
17
18
  :animate="{ scale: 1 }"
18
19
  :exit="{ scale: 0.8 }"
19
20
  :transition="{ duration: 0.15, ease: 'easeInOut' }"
21
+ @click.self="handleClick"
20
22
  >
21
23
  <slot />
22
24
  </motion.div>
@@ -40,7 +42,7 @@
40
42
  const open = defineModel<boolean>();
41
43
 
42
44
  const handleClick = () => {
43
- if (props.persistent) return;
45
+ if (props.persistent || props.loading) return;
44
46
 
45
47
  open.value = false;
46
48
  };
@@ -1,7 +1,7 @@
1
1
  import styles from "~/utils/styles";
2
2
 
3
3
  const modalStyles = styles(
4
- "fixed left-0 top-0 z-20 flex size-full items-center justify-center bg-white/5 backdrop-blur-[2px] dark:bg-gray-900/5"
4
+ "fixed left-0 top-0 z-[60] flex size-full items-center justify-center bg-white/5 backdrop-blur-[2px] dark:bg-gray-900/5"
5
5
  );
6
6
 
7
7
  export { modalStyles };
@@ -1,3 +1,4 @@
1
1
  export interface UixyModalProps {
2
2
  persistent?: boolean;
3
+ loading?: boolean;
3
4
  }
@@ -39,6 +39,7 @@
39
39
 
40
40
  const { active, handleOpen, handleLeave, styles, refElement } = usePosition({
41
41
  direction: props.direction ?? "bottom",
42
+ align: props.align ?? "center",
42
43
  });
43
44
 
44
45
  const handleClickOutside = (e: MouseEvent) => {
@@ -1,5 +1,6 @@
1
1
  export interface UixyPopoverProps {
2
2
  direction?: "top" | "bottom";
3
+ align?: "center" | "start";
3
4
  closeOnClick?: boolean;
4
5
  group?: string;
5
6
  }
@@ -14,6 +14,7 @@
14
14
  type="button"
15
15
  role="tab"
16
16
  :aria-selected="model === option.value"
17
+ :aria-label="option.ariaLabel"
17
18
  :disabled="props.disabled || option.disabled"
18
19
  :class="
19
20
  segmentStyles({
@@ -5,6 +5,7 @@ export interface UixySegmentedOption {
5
5
  value: string | number;
6
6
  icon?: IconName;
7
7
  disabled?: boolean;
8
+ ariaLabel?: string;
8
9
  }
9
10
 
10
11
  export interface UixySegmentedControlProps {
@@ -2,7 +2,7 @@
2
2
  <div class="flex flex-col gap-1">
3
3
  <div class="relative flex flex-col-reverse gap-1" ref="refOptions">
4
4
  <div
5
- :class="selectStyles({ status }, $attrs.class as string)"
5
+ :class="selectStyles({ status, size }, $attrs.class as string)"
6
6
  :tabIndex="props.disabled ? -1 : 0"
7
7
  :auto-focus="props.autoFocus"
8
8
  ref="refTrigger"
@@ -37,17 +37,17 @@
37
37
  <uixy-icon
38
38
  v-if="props.deselectable && !props.multiple && model"
39
39
  name="close"
40
- class="absolute right-7 top-1/2 h-4 w-4 -translate-y-1/2 cursor-pointer z-10 text-gray-500 dark:text-gray-600 hover:text-black dark:hover:text-gray-400"
40
+ :class="deselectIconStyles({ size })"
41
41
  @click.stop="handleDeselect"
42
42
  />
43
- <uixy-icon name="chevron-down" :class="iconStyles({ open })" />
43
+ <uixy-icon name="chevron-down" :class="iconStyles({ open, size })" />
44
44
  </div>
45
45
 
46
46
  <teleport to="body">
47
47
  <animate-presence>
48
48
  <motion.div
49
49
  v-if="open && !props.disabled"
50
- class="scrollbar z-50 max-h-60 overflow-y-auto rounded-1 border border-gray-300 bg-white p-1 text-sm shadow-sm dark:border-gray-900 dark:bg-gray-1000"
50
+ :class="dropdownPanelStyles({ size })"
51
51
  :style="dropdownStyles"
52
52
  :initial="{ opacity: 0, y: -4, scale: 0.98 }"
53
53
  :animate="{ opacity: 1, y: 0, scale: 1 }"
@@ -63,6 +63,7 @@
63
63
  icon="search"
64
64
  iconPositon="right"
65
65
  placeholder="Search..."
66
+ :size="size"
66
67
  />
67
68
  </div>
68
69
 
@@ -81,6 +82,7 @@
81
82
  :multiple="props.multiple"
82
83
  :search-term="search"
83
84
  :original-options="props.options"
85
+ :size="size"
84
86
  @toggle-expand="toggleExpand"
85
87
  @select="handleClick"
86
88
  />
@@ -88,7 +90,12 @@
88
90
 
89
91
  <li
90
92
  v-if="filteredOptions.length === 0"
91
- class="px-3 text-center dark:text-gray-600"
93
+ :class="
94
+ listItemPaddingStyles(
95
+ { size },
96
+ 'text-center dark:text-gray-600',
97
+ )
98
+ "
92
99
  >
93
100
  No results found
94
101
  </li>
@@ -103,6 +110,7 @@
103
110
  itemStyles({
104
111
  selected: isSelected(option),
105
112
  disabled: !!option.disabled,
113
+ size,
106
114
  })
107
115
  "
108
116
  @keydown="handleKeydownOption($event, option)"
@@ -112,13 +120,18 @@
112
120
  <uixy-icon
113
121
  v-if="isSelected(option)"
114
122
  name="check"
115
- class="absolute right-2 top-2 size-4"
123
+ :class="checkIconStyles({ size })"
116
124
  />
117
125
  </li>
118
126
 
119
127
  <li
120
128
  v-if="customAddVisible"
121
- class="relative py-2 px-3 rounded-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-900 text-gray-500 dark:text-gray-600"
129
+ :class="
130
+ listItemPaddingStyles(
131
+ { size },
132
+ 'relative rounded-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-900 text-gray-500 dark:text-gray-600',
133
+ )
134
+ "
122
135
  @click="handleCustomAdd"
123
136
  >
124
137
  Add: "{{ search }}"
@@ -126,7 +139,12 @@
126
139
 
127
140
  <li
128
141
  v-if="filteredOptions.length === 0 && !customAddVisible"
129
- class="px-3 text-center dark:text-gray-600"
142
+ :class="
143
+ listItemPaddingStyles(
144
+ { size },
145
+ 'text-center dark:text-gray-600',
146
+ )
147
+ "
130
148
  >
131
149
  No results found
132
150
  </li>
@@ -174,6 +192,10 @@
174
192
  itemStyles,
175
193
  helperStyles,
176
194
  badgesWrapperStyles,
195
+ deselectIconStyles,
196
+ dropdownPanelStyles,
197
+ listItemPaddingStyles,
198
+ checkIconStyles,
177
199
  } from "./Select.styles";
178
200
  import type { UixySelectEmits, UixySelectProps } from "./Select.types";
179
201
  import { UixyBadge } from "../Badge";
@@ -232,9 +254,13 @@
232
254
  handleCustomAdd,
233
255
  } = useSelect<T>(props, model as Ref<T>, emits, refDropdownEl);
234
256
 
257
+ let positionRequestId = 0;
258
+
235
259
  const updateDropdownPosition = async () => {
236
260
  if (!open.value || props.disabled) return;
237
261
 
262
+ const requestId = ++positionRequestId;
263
+
238
264
  await nextTick();
239
265
  await new Promise<void>((r) => requestAnimationFrame(() => r()));
240
266
 
@@ -274,6 +300,8 @@
274
300
  const maxLeft = scrollX + window.innerWidth - width - pad;
275
301
  left = Math.min(Math.max(minLeft, left), Math.max(minLeft, maxLeft));
276
302
 
303
+ if (requestId !== positionRequestId) return;
304
+
277
305
  dropdownStyles.value = {
278
306
  position: "absolute",
279
307
  top: `${Math.round(top)}px`,
@@ -290,6 +318,13 @@
290
318
  void updateDropdownPosition();
291
319
  };
292
320
 
321
+ const dropdownResizeObserver =
322
+ typeof ResizeObserver !== "undefined"
323
+ ? new ResizeObserver(() => {
324
+ void updateDropdownPosition();
325
+ })
326
+ : null;
327
+
293
328
  watch(
294
329
  () => open.value,
295
330
  (isOpen) => {
@@ -308,13 +343,25 @@
308
343
  window.addEventListener("scroll", onAnyScroll, true);
309
344
  }
310
345
 
346
+ if (dropdownResizeObserver) {
347
+ nextTick(() => {
348
+ if (refDropdownEl.value)
349
+ dropdownResizeObserver.observe(refDropdownEl.value);
350
+ });
351
+ }
352
+
311
353
  onWatcherCleanup(() => {
312
354
  window.removeEventListener("resize", onResize);
313
355
  window.removeEventListener("scroll", onAnyScroll, true);
356
+ dropdownResizeObserver?.disconnect();
314
357
  });
315
358
  },
316
359
  );
317
360
 
361
+ onUnmounted(() => {
362
+ dropdownResizeObserver?.disconnect();
363
+ });
364
+
318
365
  onMounted(() => {
319
366
  if (props.autoOpen && !props.disabled) {
320
367
  const delayMs = (props.delay ?? 0) * 1000;
@@ -352,6 +399,8 @@
352
399
  props.disabled ? "disabled" : (props.status ?? "default"),
353
400
  );
354
401
 
402
+ const size = computed(() => props.size ?? "md");
403
+
355
404
  const text = computed(
356
405
  () =>
357
406
  ({
@@ -1,7 +1,7 @@
1
1
  import styles from "~/utils/styles";
2
2
 
3
3
  export const selectStyles = styles(
4
- "relative select-none peer transition-colors ease-in-out duration-150 border border-gray-300 dark:border-gray-900 rounded-1 pl-3 pr-6 py-2 text-sm placeholder:text-gray-500 dark:placeholder:text-gray-700 dark:outline-gray-800 min-h-[38px] outline-offset-1 outline-gray-300",
4
+ "relative select-none peer transition-colors ease-in-out duration-150 border border-gray-300 dark:border-gray-900 rounded-1 placeholder:text-gray-500 dark:placeholder:text-gray-700 dark:outline-gray-800 outline-offset-1 outline-gray-300",
5
5
  {
6
6
  status: {
7
7
  default:
@@ -13,16 +13,37 @@ export const selectStyles = styles(
13
13
  valid:
14
14
  "bg-white dark:bg-gray-1000 focus-visible:border-gray-500 dark:focus-visible:border-gray-600 cursor-pointer",
15
15
  },
16
+ size: {
17
+ sm: "pl-2.5 pr-5 py-1.5 text-xs min-h-[32px]",
18
+ md: "pl-3 pr-6 py-2 text-sm min-h-[38px]",
19
+ xl: "pl-3.5 pr-7 py-2.5 text-base min-h-[42px]",
20
+ },
16
21
  },
17
22
  );
18
23
 
19
24
  export const iconStyles = styles(
20
- "pointer-events-none absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 select-none transition-transform duration-200 ease-in-out",
25
+ "pointer-events-none absolute top-1/2 -translate-y-1/2 select-none transition-transform duration-200 ease-in-out",
21
26
  {
22
27
  open: {
23
28
  true: "rotate-180",
24
29
  false: "",
25
30
  },
31
+ size: {
32
+ sm: "h-3.5 w-3.5 right-1.5",
33
+ md: "h-4 w-4 right-2",
34
+ xl: "h-4 w-4 right-2.5",
35
+ },
36
+ },
37
+ );
38
+
39
+ export const deselectIconStyles = styles(
40
+ "absolute top-1/2 -translate-y-1/2 cursor-pointer z-10 text-gray-500 dark:text-gray-600 hover:text-black dark:hover:text-gray-400",
41
+ {
42
+ size: {
43
+ sm: "h-3.5 w-3.5 right-6",
44
+ md: "h-4 w-4 right-7",
45
+ xl: "h-4 w-4 right-8",
46
+ },
26
47
  },
27
48
  );
28
49
 
@@ -40,7 +61,7 @@ export const labelStyles = styles(
40
61
  );
41
62
 
42
63
  export const itemStyles = styles(
43
- "relative py-2 px-3 rounded-1 cursor-pointer focus-visible:bg-gray-200 focus-visible:border-gray-100 hover:bg-gray-200 dark:focus-visible:bg-gray-900 dark:hover:bg-gray-900 dark:focus-visible:border-gray-600",
64
+ "relative rounded-1 cursor-pointer focus-visible:bg-gray-200 focus-visible:border-gray-100 hover:bg-gray-200 dark:focus-visible:bg-gray-900 dark:hover:bg-gray-900 dark:focus-visible:border-gray-600",
44
65
  {
45
66
  selected: {
46
67
  true: "bg-gray-200 dark:bg-gray-900",
@@ -50,9 +71,41 @@ export const itemStyles = styles(
50
71
  true: "pointer-events-none text-gray-500 dark:text-gray-700",
51
72
  false: "",
52
73
  },
74
+ size: {
75
+ sm: "py-1.5 px-2.5",
76
+ md: "py-2 px-3",
77
+ xl: "py-2.5 px-3.5",
78
+ },
53
79
  },
54
80
  );
55
81
 
82
+ export const listItemPaddingStyles = styles("", {
83
+ size: {
84
+ sm: "py-1.5 px-2.5",
85
+ md: "py-2 px-3",
86
+ xl: "py-2.5 px-3.5",
87
+ },
88
+ });
89
+
90
+ export const dropdownPanelStyles = styles(
91
+ "scrollbar z-50 max-h-60 overflow-y-auto rounded-1 border border-gray-300 bg-white p-1 shadow-sm dark:border-gray-900 dark:bg-gray-1000",
92
+ {
93
+ size: {
94
+ sm: "text-xs",
95
+ md: "text-sm",
96
+ xl: "text-base",
97
+ },
98
+ },
99
+ );
100
+
101
+ export const checkIconStyles = styles("absolute", {
102
+ size: {
103
+ sm: "size-3.5 top-1.5 right-1.5",
104
+ md: "size-4 top-2 right-2",
105
+ xl: "size-5 top-2.5 right-2.5",
106
+ },
107
+ });
108
+
56
109
  export const helperStyles = styles(
57
110
  "transition-colors ease-in-out duration-150 text-xs",
58
111
  {
@@ -1,3 +1,5 @@
1
+ import type { UixyFieldSize } from "~/types/field";
2
+
1
3
  export interface UixySelectOptionType {
2
4
  label: string;
3
5
  value?: string;
@@ -38,4 +40,5 @@ export type UixySelectProps = {
38
40
  allowCustom?: boolean;
39
41
  clearSearchOnSelect?: boolean;
40
42
  followOnScroll?: boolean;
43
+ size?: UixyFieldSize;
41
44
  } & (UixyMultipleSelectProps | UixySingleSelectProps);
@@ -6,6 +6,7 @@
6
6
  itemStyles({
7
7
  selected: isSelected(option),
8
8
  disabled: !!option.disabled,
9
+ size: props.size ?? 'md',
9
10
  })
10
11
  "
11
12
  :style="{
@@ -69,6 +70,7 @@
69
70
  :multiple="multiple"
70
71
  :search-term="searchTerm"
71
72
  :original-options="originalOptions"
73
+ :size="size"
72
74
  @toggle-expand="$emit('toggle-expand', $event)"
73
75
  @select="$emit('select', $event)"
74
76
  />
@@ -81,6 +83,7 @@
81
83
  import { motion, AnimatePresence } from "motion-v";
82
84
  import { itemStyles } from "./Select.styles";
83
85
  import type { UixySelectOptionType } from "./Select.types";
86
+ import type { UixyFieldSize } from "~/types/field";
84
87
  import { UixyIcon } from "../Icon";
85
88
 
86
89
  const props = defineProps<{
@@ -93,6 +96,7 @@
93
96
  multiple?: boolean;
94
97
  searchTerm?: string;
95
98
  originalOptions?: UixySelectOptionType[];
99
+ size?: UixyFieldSize;
96
100
  }>();
97
101
 
98
102
  const emit = defineEmits<{
@@ -11,7 +11,7 @@
11
11
  :max-length="props.maxLength"
12
12
  :class="
13
13
  textareaStyles(
14
- { status, noResize: !!props.noResize },
14
+ { status, noResize: !!props.noResize, size },
15
15
  $attrs.class as string,
16
16
  )
17
17
  "
@@ -74,6 +74,8 @@
74
74
  props.disabled ? "disabled" : (props.status ?? "default"),
75
75
  );
76
76
 
77
+ const size = computed(() => props.size ?? "md");
78
+
77
79
  const text = computed(
78
80
  () =>
79
81
  ({
@@ -1,7 +1,7 @@
1
1
  import styles from "~/utils/styles";
2
2
 
3
3
  export const textareaStyles = styles(
4
- "peer transition-colors ease-in-out duration-150 border border-gray-300 dark:border-gray-900 rounded-1 px-3 py-2 text-sm placeholder:text-gray-500 dark:placeholder:text-gray-700 dark:outline-gray-800 outline-offset-1 outline-gray-300 scrollbar",
4
+ "peer transition-colors ease-in-out duration-150 border border-gray-300 dark:border-gray-900 rounded-1 placeholder:text-gray-500 dark:placeholder:text-gray-700 dark:outline-gray-800 outline-offset-1 outline-gray-300 scrollbar",
5
5
  {
6
6
  status: {
7
7
  default:
@@ -13,6 +13,11 @@ export const textareaStyles = styles(
13
13
  valid:
14
14
  "bg-white dark:bg-gray-1000 focus-visible:border-gray-500 dark:focus-visible:border-gray-600",
15
15
  },
16
+ size: {
17
+ sm: "px-2.5 py-1.5 text-xs",
18
+ md: "px-3 py-2 text-sm",
19
+ xl: "px-3.5 py-2.5 text-base",
20
+ },
16
21
  noResize: {
17
22
  true: "resize-none",
18
23
  false: "",
@@ -1,3 +1,5 @@
1
+ import type { UixyFieldSize } from "~/types/field";
2
+
1
3
  export interface UixyTextareaProps {
2
4
  status?: "valid" | "error" | "default";
3
5
  label?: string;
@@ -10,4 +12,5 @@ export interface UixyTextareaProps {
10
12
  noResize?: boolean;
11
13
  rows?: number;
12
14
  maxLength?: number;
15
+ size?: UixyFieldSize;
13
16
  }
@@ -1,9 +1,11 @@
1
1
  import { ref, computed, watch, onBeforeUnmount, nextTick, type Ref } from "vue";
2
2
 
3
3
  type Direction = "top" | "bottom";
4
+ type Align = "center" | "start";
4
5
 
5
6
  interface UsePositionOptions {
6
7
  direction?: Direction;
8
+ align?: Align;
7
9
  }
8
10
 
9
11
  const GAP = 6;
@@ -17,7 +19,10 @@ function hasFixedParent(el: HTMLElement | null): boolean {
17
19
  return false;
18
20
  }
19
21
 
20
- export function usePosition({ direction = "bottom" }: UsePositionOptions) {
22
+ export function usePosition({
23
+ direction = "bottom",
24
+ align = "center",
25
+ }: UsePositionOptions) {
21
26
  const refElement = ref<HTMLElement>();
22
27
 
23
28
  const active = ref(false);
@@ -26,7 +31,7 @@ export function usePosition({ direction = "bottom" }: UsePositionOptions) {
26
31
  const baseStyles = computed(() => ({
27
32
  position: hasFixedParent(target.value) ? "fixed" : "absolute",
28
33
  width: "max-content",
29
- zIndex: 20,
34
+ zIndex: 70,
30
35
  }));
31
36
 
32
37
  const styles = ref<Record<string, string | number>>({
@@ -65,7 +70,10 @@ export function usePosition({ direction = "bottom" }: UsePositionOptions) {
65
70
  ? targetRect.top - tooltipRect.height - GAP + scrollY
66
71
  : targetRect.bottom + GAP + scrollY;
67
72
 
68
- let left = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2;
73
+ let left =
74
+ align === "start"
75
+ ? targetRect.left
76
+ : targetRect.left + targetRect.width / 2 - tooltipRect.width / 2;
69
77
  const newStyles: Record<string, string | number> = {
70
78
  ...baseStyles.value,
71
79
  top,
@@ -87,13 +95,31 @@ export function usePosition({ direction = "bottom" }: UsePositionOptions) {
87
95
  };
88
96
  };
89
97
 
98
+ let resizeObserver: ResizeObserver | null = null;
99
+
100
+ const observeResize = () => {
101
+ if (typeof ResizeObserver === "undefined") return;
102
+ resizeObserver = new ResizeObserver(() => {
103
+ void updatePosition();
104
+ });
105
+ if (target.value) resizeObserver.observe(target.value);
106
+ if (refElement.value) resizeObserver.observe(refElement.value);
107
+ };
108
+
109
+ const unobserveResize = () => {
110
+ resizeObserver?.disconnect();
111
+ resizeObserver = null;
112
+ };
113
+
90
114
  watch(
91
- [active, () => direction],
115
+ [active, () => direction, () => align],
92
116
  ([isActive]) => {
93
117
  if (isActive) {
94
118
  updatePosition();
119
+ observeResize();
95
120
  window.addEventListener("resize", updatePosition);
96
121
  } else {
122
+ unobserveResize();
97
123
  window.removeEventListener("resize", updatePosition);
98
124
  }
99
125
  },
@@ -101,6 +127,7 @@ export function usePosition({ direction = "bottom" }: UsePositionOptions) {
101
127
  );
102
128
 
103
129
  onBeforeUnmount(() => {
130
+ unobserveResize();
104
131
  window.removeEventListener("resize", updatePosition);
105
132
  });
106
133
 
@@ -0,0 +1 @@
1
+ export type UixyFieldSize = "sm" | "md" | "xl";