v-uixy 1.9.1 → 1.9.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.
@@ -1,10 +1,16 @@
1
1
  export type UixyPieChartData = {
2
2
  label: string;
3
3
  value: number;
4
- color: string;
4
+ color?: string;
5
5
  };
6
6
 
7
7
  export interface UixyPieChartProps {
8
8
  data: UixyPieChartData[];
9
+ resolution?: number;
9
10
  thickness?: number;
11
+ total?: string;
12
+ legend?: boolean;
13
+ animate?: boolean;
14
+ interactive?: boolean;
15
+ format?: (value: number) => string;
10
16
  }
@@ -10,37 +10,49 @@
10
10
  @keydown="handleKeydownSelect"
11
11
  v-bind="filteredAttrs"
12
12
  >
13
- <p
14
- v-if="props.placeholder && (!model || model.length === 0)"
15
- class="pointer-events-none select-none text-gray-500 dark:text-gray-700"
16
- >
17
- {{ props.placeholder }}
18
- </p>
19
- <div
20
- v-if="props.multiple"
21
- :class="badgesWrapperStyles({ disabled: props.disabled })"
22
- >
23
- <uixy-badge
24
- v-for="(item, index) in model as string[]"
25
- data-badge
26
- variant="tertiary"
27
- size="xs"
28
- shape="pill"
29
- :key="`${item}--${index}`"
30
- @click.stop="handleClickSelectedOption(item)"
13
+ <div :class="valueWrapperStyles()">
14
+ <p
15
+ v-if="props.placeholder && (!model || model.length === 0)"
16
+ class="pointer-events-none select-none truncate text-gray-500 dark:text-gray-700"
31
17
  >
32
- {{ getLabel(item) }}
33
- </uixy-badge>
18
+ {{ props.placeholder }}
19
+ </p>
20
+ <div
21
+ v-if="props.multiple"
22
+ :class="badgesWrapperStyles({ disabled: props.disabled })"
23
+ >
24
+ <uixy-badge
25
+ v-for="(item, index) in model as string[]"
26
+ data-badge
27
+ variant="tertiary"
28
+ size="xs"
29
+ shape="pill"
30
+ class="max-w-full"
31
+ :key="`${item}--${index}`"
32
+ @click.stop="handleClickSelectedOption(item)"
33
+ >
34
+ <span class="min-w-0 truncate">{{ getLabel(item) }}</span>
35
+ </uixy-badge>
36
+ </div>
37
+ <p v-else class="truncate">{{ getLabel(model as string) }}</p>
38
+ </div>
39
+
40
+ <div :class="trailingStyles()">
41
+ <span
42
+ v-if="hasMax"
43
+ :class="counterStyles({ full: isAtMax, size })"
44
+ aria-live="polite"
45
+ >
46
+ {{ selectedValues.length }}/{{ props.max }}
47
+ </span>
48
+ <uixy-icon
49
+ v-if="!props.multiple && model && minSelected === 0"
50
+ name="close"
51
+ :class="deselectIconStyles({ size })"
52
+ @click.stop="handleDeselect"
53
+ />
54
+ <uixy-icon name="chevron-down" :class="iconStyles({ open, size })" />
34
55
  </div>
35
- <p v-else>{{ getLabel(model as string) }}</p>
36
-
37
- <uixy-icon
38
- v-if="props.deselectable && !props.multiple && model"
39
- name="close"
40
- :class="deselectIconStyles({ size })"
41
- @click.stop="handleDeselect"
42
- />
43
- <uixy-icon name="chevron-down" :class="iconStyles({ open, size })" />
44
56
  </div>
45
57
 
46
58
  <teleport to="body">
@@ -78,6 +90,7 @@
78
90
  :depth="0"
79
91
  :is-selected="isSelected"
80
92
  :is-partially-selected="isPartiallySelected"
93
+ :is-blocked="isBlocked"
81
94
  :expanded-items="effectiveExpandedItems"
82
95
  :leaf-only="props.leafOnly"
83
96
  :multiple="props.multiple"
@@ -105,12 +118,14 @@
105
118
  <ul v-else class="flex flex-col gap-1 py-1">
106
119
  <li
107
120
  v-for="option in filteredOptions"
108
- :tabIndex="option.disabled ? -1 : 0"
121
+ :tabIndex="option.disabled || isBlocked(option) ? -1 : 0"
122
+ :aria-disabled="isBlocked(option) || undefined"
109
123
  :key="option.value ?? option.label"
110
124
  :class="
111
125
  itemStyles({
112
126
  selected: isSelected(option),
113
127
  disabled: !!option.disabled,
128
+ blocked: isBlocked(option),
114
129
  size,
115
130
  })
116
131
  "
@@ -197,6 +212,9 @@
197
212
  dropdownPanelStyles,
198
213
  listItemPaddingStyles,
199
214
  checkIconStyles,
215
+ valueWrapperStyles,
216
+ trailingStyles,
217
+ counterStyles,
200
218
  } from "./Select.styles";
201
219
  import type { UixySelectEmits, UixySelectProps } from "./Select.types";
202
220
  import { UixyBadge } from "../Badge";
@@ -207,7 +225,10 @@
207
225
  import { useOverlayLayer } from "~/composables";
208
226
  import SelectTreeItem from "./SelectTreeItem.vue";
209
227
 
210
- const props = defineProps<UixySelectProps>();
228
+ const props = withDefaults(defineProps<UixySelectProps>(), {
229
+ deselectable: true,
230
+ maxBehavior: "block",
231
+ });
211
232
 
212
233
  const emits = defineEmits<UixySelectEmits>();
213
234
 
@@ -249,8 +270,13 @@
249
270
  filteredOptions,
250
271
  effectiveExpandedItems,
251
272
  customAddVisible,
273
+ selectedValues,
274
+ hasMax,
275
+ isAtMax,
276
+ minSelected,
252
277
  isSelected,
253
278
  isPartiallySelected,
279
+ isBlocked,
254
280
  getLabel,
255
281
  toggleExpand,
256
282
  handleOpen,
@@ -340,7 +366,7 @@
340
366
 
341
367
  if (!isOpen) return;
342
368
 
343
- acquireLayer();
369
+ acquireLayer(refTrigger.value);
344
370
 
345
371
  void updateDropdownPosition();
346
372
 
@@ -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 placeholder:text-gray-500 dark:placeholder:text-gray-700 dark:outline-gray-800 outline-offset-1 outline-gray-300",
4
+ "relative flex items-center gap-2 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:
@@ -14,35 +14,56 @@ export const selectStyles = styles(
14
14
  "bg-white dark:bg-gray-1000 focus-visible:border-gray-500 dark:focus-visible:border-gray-600 cursor-pointer",
15
15
  },
16
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]",
17
+ sm: "pl-2.5 pr-1.5 py-1.5 text-xs min-h-[32px]",
18
+ md: "pl-3 pr-2 py-2 text-sm min-h-[38px]",
19
+ xl: "pl-3.5 pr-2.5 py-2.5 text-base min-h-[42px]",
20
+ },
21
+ },
22
+ );
23
+
24
+ export const valueWrapperStyles = styles("min-w-0 flex-1");
25
+
26
+ export const trailingStyles = styles(
27
+ "ml-auto flex shrink-0 items-center gap-1.5",
28
+ );
29
+
30
+ export const counterStyles = styles(
31
+ "pointer-events-none shrink-0 select-none tabular-nums transition-colors duration-150 ease-in-out",
32
+ {
33
+ full: {
34
+ true: "text-gray-900 dark:text-gray-300",
35
+ false: "text-gray-500 dark:text-gray-600",
36
+ },
37
+ size: {
38
+ sm: "text-[10px]",
39
+ md: "text-xs",
40
+ xl: "text-sm",
20
41
  },
21
42
  },
22
43
  );
23
44
 
24
45
  export const iconStyles = styles(
25
- "pointer-events-none absolute top-1/2 -translate-y-1/2 select-none transition-transform duration-200 ease-in-out",
46
+ "pointer-events-none shrink-0 select-none transition-transform duration-200 ease-in-out",
26
47
  {
27
48
  open: {
28
49
  true: "rotate-180",
29
50
  false: "",
30
51
  },
31
52
  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",
53
+ sm: "h-3.5 w-3.5",
54
+ md: "h-4 w-4",
55
+ xl: "h-4 w-4",
35
56
  },
36
57
  },
37
58
  );
38
59
 
39
60
  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",
61
+ "shrink-0 cursor-pointer text-gray-500 dark:text-gray-600 hover:text-black dark:hover:text-gray-400",
41
62
  {
42
63
  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",
64
+ sm: "h-3.5 w-3.5",
65
+ md: "h-4 w-4",
66
+ xl: "h-4 w-4",
46
67
  },
47
68
  },
48
69
  );
@@ -71,6 +92,10 @@ export const itemStyles = styles(
71
92
  true: "pointer-events-none text-gray-500 dark:text-gray-700",
72
93
  false: "",
73
94
  },
95
+ blocked: {
96
+ true: "cursor-not-allowed text-gray-400 dark:text-gray-700 hover:bg-transparent dark:hover:bg-transparent focus-visible:bg-transparent dark:focus-visible:bg-transparent",
97
+ false: "",
98
+ },
74
99
  size: {
75
100
  sm: "py-1.5 px-2.5",
76
101
  md: "py-2 px-3",
@@ -20,6 +20,8 @@ export interface UixySelectEmits {
20
20
  (event: "openChange", isOpen: boolean): void;
21
21
  }
22
22
 
23
+ export type UixySelectMaxBehavior = "block" | "replace";
24
+
23
25
  export type UixySelectProps = {
24
26
  options: UixySelectOptionType[];
25
27
  status: "valid" | "error" | "default";
@@ -31,7 +33,9 @@ export type UixySelectProps = {
31
33
  helperText?: string;
32
34
  errorText?: string;
33
35
  autoFocus?: boolean;
36
+ min?: number;
34
37
  max?: number;
38
+ maxBehavior?: UixySelectMaxBehavior;
35
39
  tree?: boolean;
36
40
  leafOnly?: boolean;
37
41
  autoOpen?: boolean;
@@ -1,17 +1,19 @@
1
1
  <template>
2
2
  <li class="flex flex-col">
3
3
  <div
4
- :tabIndex="option.disabled ? -1 : 0"
4
+ :tabIndex="option.disabled || isOptionBlocked ? -1 : 0"
5
+ :aria-disabled="isOptionBlocked || undefined"
5
6
  :class="
6
7
  itemStyles({
7
8
  selected: isSelected(option),
8
9
  disabled: !!option.disabled,
10
+ blocked: isOptionBlocked,
9
11
  size: props.size ?? 'md',
10
12
  })
11
13
  "
12
14
  :style="{
13
15
  paddingLeft: `${depth * 28 + 12}px`,
14
- cursor: option.disabled ? 'not-allowed' : 'pointer',
16
+ cursor: option.disabled || isOptionBlocked ? 'not-allowed' : 'pointer',
15
17
  }"
16
18
  @click.stop="handleItemClick"
17
19
  >
@@ -65,6 +67,7 @@
65
67
  :depth="depth + 1"
66
68
  :is-selected="isSelected"
67
69
  :is-partially-selected="isPartiallySelected"
70
+ :is-blocked="isBlocked"
68
71
  :expanded-items="expandedItems"
69
72
  :leaf-only="leafOnly"
70
73
  :multiple="multiple"
@@ -91,6 +94,7 @@
91
94
  depth: number;
92
95
  isSelected: (option: UixySelectOptionType) => boolean;
93
96
  isPartiallySelected?: (option: UixySelectOptionType) => boolean;
97
+ isBlocked?: (option: UixySelectOptionType) => boolean;
94
98
  expandedItems: string[];
95
99
  leafOnly?: boolean;
96
100
  multiple?: boolean;
@@ -139,6 +143,10 @@
139
143
  props.expandedItems.includes(getValue(props.option)),
140
144
  );
141
145
 
146
+ const isOptionBlocked = computed(
147
+ () => !!props.isBlocked?.(props.option),
148
+ );
149
+
142
150
  const isNotSelectable = computed(
143
151
  () =>
144
152
  props.option.disabled ||
@@ -146,7 +154,7 @@
146
154
  );
147
155
 
148
156
  const handleItemClick = () => {
149
- if (props.option.disabled) return;
157
+ if (props.option.disabled || isOptionBlocked.value) return;
150
158
 
151
159
  if (props.leafOnly && hasChildrenInOriginal.value && !props.multiple) {
152
160
  emit("toggle-expand", getValue(props.option));