@flux-ui/components 3.1.2 → 3.1.4

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 (72) hide show
  1. package/dist/component/FluxAvatarGroup.vue.d.ts +17 -0
  2. package/dist/component/FluxButton.vue.d.ts +2 -0
  3. package/dist/component/FluxContextMenu.vue.d.ts +26 -0
  4. package/dist/component/FluxDataTable.vue.d.ts +20 -10
  5. package/dist/component/FluxDescriptionItem.vue.d.ts +19 -0
  6. package/dist/component/FluxDescriptionList.vue.d.ts +17 -0
  7. package/dist/component/FluxFlyout.vue.d.ts +9 -2
  8. package/dist/component/FluxFormCombobox.vue.d.ts +20 -0
  9. package/dist/component/FluxFormRating.vue.d.ts +21 -0
  10. package/dist/component/FluxFormTagsInput.vue.d.ts +27 -0
  11. package/dist/component/FluxFormTextArea.vue.d.ts +6 -1
  12. package/dist/component/FluxInlineEdit.vue.d.ts +41 -0
  13. package/dist/component/FluxMenu.vue.d.ts +1 -0
  14. package/dist/component/FluxMenuFlyout.vue.d.ts +22 -0
  15. package/dist/component/FluxTableCell.vue.d.ts +1 -0
  16. package/dist/component/FluxTour.vue.d.ts +35 -0
  17. package/dist/component/FluxTourItem.vue.d.ts +18 -0
  18. package/dist/component/FluxVirtualScroller.vue.d.ts +27 -0
  19. package/dist/component/index.d.ts +12 -0
  20. package/dist/component/primitive/AnchorPopup.vue.d.ts +7 -1
  21. package/dist/component/primitive/SelectBase.vue.d.ts +3 -0
  22. package/dist/composable/private/index.d.ts +1 -0
  23. package/dist/composable/private/useMenuFlyout.d.ts +42 -0
  24. package/dist/data/di.d.ts +35 -0
  25. package/dist/data/i18n.d.ts +7 -0
  26. package/dist/index.css +449 -5
  27. package/dist/index.js +2156 -408
  28. package/dist/index.js.map +1 -1
  29. package/package.json +7 -7
  30. package/src/component/FluxAvatarGroup.vue +52 -0
  31. package/src/component/FluxButton.vue +3 -0
  32. package/src/component/FluxContextMenu.vue +134 -0
  33. package/src/component/FluxDataTable.vue +113 -32
  34. package/src/component/FluxDescriptionItem.vue +43 -0
  35. package/src/component/FluxDescriptionList.vue +37 -0
  36. package/src/component/FluxDestructiveButton.vue +2 -1
  37. package/src/component/FluxFlyout.vue +16 -3
  38. package/src/component/FluxFormCombobox.vue +98 -0
  39. package/src/component/FluxFormRating.vue +172 -0
  40. package/src/component/FluxFormTagsInput.vue +249 -0
  41. package/src/component/FluxFormTextArea.vue +16 -1
  42. package/src/component/FluxInlineEdit.vue +176 -0
  43. package/src/component/FluxMenu.vue +13 -3
  44. package/src/component/FluxMenuFlyout.vue +118 -0
  45. package/src/component/FluxPrimaryButton.vue +2 -1
  46. package/src/component/FluxPrimaryLinkButton.vue +2 -1
  47. package/src/component/FluxPublishButton.vue +2 -1
  48. package/src/component/FluxSecondaryButton.vue +2 -1
  49. package/src/component/FluxSecondaryLinkButton.vue +2 -1
  50. package/src/component/FluxTableCell.vue +2 -0
  51. package/src/component/FluxTour.vue +332 -0
  52. package/src/component/FluxTourItem.vue +27 -0
  53. package/src/component/FluxVirtualScroller.vue +96 -0
  54. package/src/component/index.ts +12 -0
  55. package/src/component/primitive/AnchorPopup.vue +27 -0
  56. package/src/component/primitive/SelectBase.vue +37 -2
  57. package/src/composable/private/index.ts +1 -0
  58. package/src/composable/private/useMenuFlyout.ts +417 -0
  59. package/src/css/component/AvatarGroup.module.scss +22 -0
  60. package/src/css/component/ContextMenu.module.scss +17 -0
  61. package/src/css/component/DescriptionList.module.scss +98 -0
  62. package/src/css/component/Form.module.scss +51 -0
  63. package/src/css/component/FormRating.module.scss +47 -0
  64. package/src/css/component/InlineEdit.module.scss +45 -0
  65. package/src/css/component/Menu.module.scss +4 -1
  66. package/src/css/component/MenuFlyout.module.scss +38 -0
  67. package/src/css/component/Table.module.scss +16 -0
  68. package/src/css/component/Tour.module.scss +108 -0
  69. package/src/css/component/VirtualScroller.module.scss +17 -0
  70. package/src/css/mixin/button-active.scss +3 -1
  71. package/src/data/di.ts +40 -0
  72. package/src/data/i18n.ts +7 -0
@@ -0,0 +1,98 @@
1
+ <template>
2
+ <SelectBase
3
+ v-model:searchQuery="modelSearch"
4
+ :aria-invalid="error ? true : undefined"
5
+ :class="clsx(
6
+ isCondensed && $formStyle.isCondensed,
7
+ isSecondary && $formStyle.isSecondary,
8
+ error && $formStyle.isInvalid
9
+ )"
10
+ :disabled="disabled"
11
+ is-searchable
12
+ :is-creatable="isCreatable"
13
+ :is-loading="isLoading"
14
+ :is-multiple="isMultiple"
15
+ :options="groups"
16
+ :placeholder="placeholder"
17
+ :selected="selected"
18
+ @create="onCreate"
19
+ @deselect="onDeselect"
20
+ @select="onSelect"/>
21
+ </template>
22
+
23
+ <script
24
+ lang="ts"
25
+ setup>
26
+ import type { FluxFormInputBaseProps, FluxFormSelectEntry, FluxFormSelectOption, FluxFormSelectValue } from '@flux-ui/types';
27
+ import { clsx } from 'clsx';
28
+ import { computed, ref, toRef, unref } from 'vue';
29
+ import { SelectBase } from '~flux/components/component/primitive';
30
+ import { useDisabled } from '~flux/components/composable';
31
+ import { useFormSelect } from '~flux/components/composable/private';
32
+ import { isFluxFormSelectGroup, isFluxFormSelectOption } from '~flux/components/data';
33
+ import $formStyle from '~flux/components/css/component/Form.module.scss';
34
+
35
+ const modelSearch = defineModel<string>('searchQuery', {
36
+ default: ''
37
+ });
38
+
39
+ const modelValue = defineModel<FluxFormSelectValue>({
40
+ required: true
41
+ });
42
+
43
+ const {
44
+ disabled: componentDisabled,
45
+ isMultiple,
46
+ options
47
+ } = defineProps<Pick<FluxFormInputBaseProps, 'autoFocus' | 'disabled' | 'error' | 'isCondensed' | 'isLoading' | 'isReadonly' | 'isSecondary' | 'name' | 'placeholder'> & {
48
+ readonly isCreatable?: boolean;
49
+ readonly isMultiple?: boolean;
50
+ readonly options: FluxFormSelectEntry[];
51
+ }>();
52
+
53
+ const disabled = useDisabled(toRef(() => componentDisabled));
54
+ const createdOptions = ref<FluxFormSelectOption[]>([]);
55
+
56
+ const allOptions = computed<FluxFormSelectEntry[]>(() => {
57
+ const seen = new Set<string | number | null>();
58
+
59
+ return [...options, ...createdOptions.value].filter(o => {
60
+ if (isFluxFormSelectGroup(o)) {
61
+ return true;
62
+ }
63
+
64
+ if (seen.has(o.value)) {
65
+ return false;
66
+ }
67
+
68
+ seen.add(o.value);
69
+ return true;
70
+ });
71
+ });
72
+
73
+ const {groups, selected, values} = useFormSelect(modelValue, isMultiple, allOptions, modelSearch);
74
+
75
+ function onDeselect(id: string | number | null): void {
76
+ if (isMultiple) {
77
+ modelValue.value = unref(values).filter(v => v !== id);
78
+ }
79
+ }
80
+
81
+ function onSelect(id: string | number | null): void {
82
+ if (isMultiple) {
83
+ modelValue.value = [...unref(values), id];
84
+ } else {
85
+ modelValue.value = id;
86
+ }
87
+ }
88
+
89
+ function onCreate(query: string): void {
90
+ const exists = unref(allOptions).some(o => isFluxFormSelectOption(o) && o.value === query);
91
+
92
+ if (!exists) {
93
+ createdOptions.value = [...createdOptions.value, {label: query, value: query}];
94
+ }
95
+
96
+ onSelect(query);
97
+ }
98
+ </script>
@@ -0,0 +1,172 @@
1
+ <template>
2
+ <div
3
+ :class="clsx(
4
+ $style.formRating,
5
+ disabled && $style.isDisabled,
6
+ error && $style.isInvalid
7
+ )"
8
+ :id="id"
9
+ :style="{
10
+ fontSize: size && `${size}px`
11
+ }"
12
+ role="slider"
13
+ :aria-disabled="disabled ? true : undefined"
14
+ :aria-invalid="error ? true : undefined"
15
+ :aria-readonly="isReadonly ? true : undefined"
16
+ :aria-valuemin="0"
17
+ :aria-valuemax="count"
18
+ :aria-valuenow="modelValue ?? 0"
19
+ :aria-valuetext="`${modelValue ?? 0} / ${count}`"
20
+ :tabindex="isInteractive ? 0 : undefined"
21
+ @keydown="onKeyDown"
22
+ @mouseleave="hoverValue = null">
23
+ <input
24
+ v-if="name"
25
+ type="hidden"
26
+ :name="name"
27
+ :value="modelValue ?? ''">
28
+
29
+ <button
30
+ v-for="star of count"
31
+ :key="star"
32
+ :class="$style.formRatingStar"
33
+ type="button"
34
+ tabindex="-1"
35
+ aria-hidden="true"
36
+ :disabled="!isInteractive"
37
+ :style="{
38
+ '--fill': fillFor(star)
39
+ }"
40
+ @click="onClick(star, $event)"
41
+ @mousemove="onMouseMove(star, $event)">
42
+ <FluxIcon
43
+ :class="$style.formRatingStarEmpty"
44
+ :name="icon"/>
45
+
46
+ <FluxIcon
47
+ :class="$style.formRatingStarFull"
48
+ :name="icon"/>
49
+ </button>
50
+ </div>
51
+ </template>
52
+
53
+ <script
54
+ lang="ts"
55
+ setup>
56
+ import type { FluxFormInputBaseProps, FluxIconName } from '@flux-ui/types';
57
+ import { clsx } from 'clsx';
58
+ import { computed, ref, toRef } from 'vue';
59
+ import { useDisabled, useFormFieldInjection } from '~flux/components/composable';
60
+ import FluxIcon from './FluxIcon.vue';
61
+ import $style from '~flux/components/css/component/FormRating.module.scss';
62
+
63
+ const modelValue = defineModel<number | null>({
64
+ default: null
65
+ });
66
+
67
+ const {
68
+ allowHalf = false,
69
+ clearable = false,
70
+ count = 5,
71
+ disabled: componentDisabled,
72
+ icon = 'star',
73
+ isReadonly
74
+ } = defineProps<Pick<FluxFormInputBaseProps, 'disabled' | 'error' | 'isReadonly' | 'name'> & {
75
+ readonly allowHalf?: boolean;
76
+ readonly clearable?: boolean;
77
+ readonly count?: number;
78
+ readonly icon?: FluxIconName;
79
+ readonly size?: number;
80
+ }>();
81
+
82
+ const emit = defineEmits<{
83
+ change: [number | null];
84
+ }>();
85
+
86
+ const disabled = useDisabled(toRef(() => componentDisabled));
87
+ const {id} = useFormFieldInjection();
88
+
89
+ const hoverValue = ref<number | null>(null);
90
+
91
+ const isInteractive = computed(() => !disabled.value && !isReadonly);
92
+ const displayValue = computed(() => hoverValue.value ?? modelValue.value ?? 0);
93
+
94
+ function fillFor(star: number): number {
95
+ return Math.min(1, Math.max(0, displayValue.value - (star - 1)));
96
+ }
97
+
98
+ function resolveStarValue(star: number, evt: MouseEvent): number {
99
+ if (!allowHalf) {
100
+ return star;
101
+ }
102
+
103
+ const {left, width} = (evt.currentTarget as HTMLElement).getBoundingClientRect();
104
+ return evt.clientX - left < width / 2 ? star - 0.5 : star;
105
+ }
106
+
107
+ function commit(value: number | null): void {
108
+ modelValue.value = value;
109
+ emit('change', value);
110
+ }
111
+
112
+ function onMouseMove(star: number, evt: MouseEvent): void {
113
+ if (!isInteractive.value) {
114
+ return;
115
+ }
116
+
117
+ hoverValue.value = resolveStarValue(star, evt);
118
+ }
119
+
120
+ function onClick(star: number, evt: MouseEvent): void {
121
+ if (!isInteractive.value) {
122
+ return;
123
+ }
124
+
125
+ const value = resolveStarValue(star, evt);
126
+ commit(clearable && modelValue.value === value ? null : value);
127
+ }
128
+
129
+ function onKeyDown(evt: KeyboardEvent): void {
130
+ if (!isInteractive.value) {
131
+ return;
132
+ }
133
+
134
+ const step = allowHalf ? 0.5 : 1;
135
+ const current = modelValue.value ?? 0;
136
+
137
+ switch (evt.key) {
138
+ case 'ArrowRight':
139
+ case 'ArrowUp':
140
+ evt.preventDefault();
141
+ commit(Math.min(count, current + step));
142
+ break;
143
+
144
+ case 'ArrowLeft':
145
+ case 'ArrowDown':
146
+ evt.preventDefault();
147
+ commit(Math.max(0, current - step));
148
+ break;
149
+
150
+ case 'Home':
151
+ evt.preventDefault();
152
+ commit(0);
153
+ break;
154
+
155
+ case 'End':
156
+ evt.preventDefault();
157
+ commit(count);
158
+ break;
159
+
160
+ default:
161
+ if (/^[0-9]$/.test(evt.key)) {
162
+ const digit = Number(evt.key);
163
+
164
+ if (digit <= count) {
165
+ evt.preventDefault();
166
+ commit(digit);
167
+ }
168
+ }
169
+ break;
170
+ }
171
+ }
172
+ </script>
@@ -0,0 +1,249 @@
1
+ <template>
2
+ <Anchor
3
+ ref="anchor"
4
+ :class="clsx(
5
+ disabled ? $style.formTagsInputDisabled : $style.formTagsInputEnabled,
6
+ isCondensed && $style.isCondensed,
7
+ isSecondary && $style.isSecondary,
8
+ error && $style.isInvalid
9
+ )"
10
+ :id="id"
11
+ role="group"
12
+ tag-name="div"
13
+ @click="focusInput">
14
+ <FluxTag
15
+ v-for="(tag, index) of modelValue"
16
+ :key="`${tag}-${index}`"
17
+ :color="tagColor"
18
+ :label="tag"
19
+ is-deletable
20
+ @delete="removeAt(index)"/>
21
+
22
+ <input
23
+ ref="input"
24
+ v-model="query"
25
+ :class="$style.formTagsInputField"
26
+ :name="name"
27
+ autocomplete="off"
28
+ :disabled="disabled"
29
+ :placeholder="modelValue.length === 0 ? placeholder : undefined"
30
+ :readonly="isReadonly"
31
+ type="text"
32
+ @input="onInput"
33
+ @keydown="onKeyDown"
34
+ @paste="onPaste">
35
+ </Anchor>
36
+
37
+ <Teleport to="body">
38
+ <FluxFadeTransition>
39
+ <AnchorPopup
40
+ v-if="isOpen && filteredSuggestions.length > 0"
41
+ ref="popup"
42
+ :class="$style.formTagsInputPopup"
43
+ :anchor="anchorRef"
44
+ direction="vertical"
45
+ use-anchor-width>
46
+ <FluxMenu>
47
+ <FluxMenuItem
48
+ v-for="(suggestion, index) of filteredSuggestions"
49
+ :key="suggestion.value ?? index"
50
+ :icon-leading="suggestion.icon"
51
+ :is-highlighted="highlightedIndex === index"
52
+ :label="suggestion.label"
53
+ type="button"
54
+ @click="addSuggestion(suggestion)"/>
55
+ </FluxMenu>
56
+ </AnchorPopup>
57
+ </FluxFadeTransition>
58
+ </Teleport>
59
+ </template>
60
+
61
+ <script
62
+ lang="ts"
63
+ setup>
64
+ import { useClickOutside } from '@basmilius/common';
65
+ import type { FluxColor, FluxFormInputBaseProps, FluxFormSelectOption } from '@flux-ui/types';
66
+ import { clsx } from 'clsx';
67
+ import { type ComponentPublicInstance, computed, nextTick, ref, toRef, useTemplateRef } from 'vue';
68
+ import { useDisabled, useFormFieldInjection } from '~flux/components/composable';
69
+ import { FluxFadeTransition } from '~flux/components/transition';
70
+ import { Anchor, AnchorPopup } from '~flux/components/component/primitive';
71
+ import FluxMenu from './FluxMenu.vue';
72
+ import FluxMenuItem from './FluxMenuItem.vue';
73
+ import FluxTag from './FluxTag.vue';
74
+ import $style from '~flux/components/css/component/Form.module.scss';
75
+
76
+ const modelValue = defineModel<string[]>({
77
+ default: () => []
78
+ });
79
+
80
+ const query = defineModel<string>('searchQuery', {
81
+ default: ''
82
+ });
83
+
84
+ const {
85
+ allowDuplicates,
86
+ delimiters = ['Enter', ','],
87
+ disabled: componentDisabled,
88
+ max,
89
+ suggestions,
90
+ tagColor,
91
+ validate
92
+ } = defineProps<Pick<FluxFormInputBaseProps, 'disabled' | 'error' | 'isCondensed' | 'isReadonly' | 'isSecondary' | 'name' | 'placeholder'> & {
93
+ readonly allowDuplicates?: boolean;
94
+ readonly delimiters?: string[];
95
+ readonly max?: number;
96
+ readonly suggestions?: FluxFormSelectOption[];
97
+ readonly tagColor?: FluxColor;
98
+ readonly validate?: (value: string) => boolean;
99
+ }>();
100
+
101
+ const emit = defineEmits<{
102
+ add: [string];
103
+ remove: [string];
104
+ }>();
105
+
106
+ const disabled = useDisabled(toRef(() => componentDisabled));
107
+ const {id} = useFormFieldInjection();
108
+
109
+ const anchorRef = useTemplateRef<ComponentPublicInstance>('anchor');
110
+ const popupRef = useTemplateRef<ComponentPublicInstance>('popup');
111
+ const inputElementRef = useTemplateRef<HTMLInputElement>('input');
112
+
113
+ const isOpen = ref(false);
114
+ const highlightedIndex = ref(-1);
115
+
116
+ const isMaxReached = computed(() => max !== undefined && modelValue.value.length >= max);
117
+ const filteredSuggestions = computed(() => {
118
+ if (!suggestions) {
119
+ return [];
120
+ }
121
+
122
+ const search = query.value.trim().toLowerCase();
123
+
124
+ return suggestions.filter(suggestion =>
125
+ !modelValue.value.includes(suggestion.label) &&
126
+ (search === '' || suggestion.label.toLowerCase().includes(search)));
127
+ });
128
+
129
+ function focusInput(): void {
130
+ inputElementRef.value?.focus();
131
+ }
132
+
133
+ function addTag(rawValue: string): void {
134
+ const value = rawValue.trim();
135
+
136
+ if (value === '' || isMaxReached.value) {
137
+ return;
138
+ }
139
+
140
+ if (!allowDuplicates && modelValue.value.includes(value)) {
141
+ query.value = '';
142
+ return;
143
+ }
144
+
145
+ if (validate && !validate(value)) {
146
+ return;
147
+ }
148
+
149
+ modelValue.value = [...modelValue.value, value];
150
+ emit('add', value);
151
+
152
+ query.value = '';
153
+ highlightedIndex.value = -1;
154
+ isOpen.value = false;
155
+ }
156
+
157
+ function removeAt(index: number): void {
158
+ const removed = modelValue.value[index];
159
+ modelValue.value = modelValue.value.filter((_, i) => i !== index);
160
+ emit('remove', removed);
161
+ }
162
+
163
+ function addSuggestion(suggestion: FluxFormSelectOption): void {
164
+ addTag(suggestion.label);
165
+ nextTick(() => focusInput());
166
+ }
167
+
168
+ function onInput(): void {
169
+ isOpen.value = query.value.trim() !== '' && filteredSuggestions.value.length > 0;
170
+ highlightedIndex.value = -1;
171
+ }
172
+
173
+ function onKeyDown(evt: KeyboardEvent): void {
174
+ if (evt.key === 'Escape' && isOpen.value) {
175
+ evt.preventDefault();
176
+ isOpen.value = false;
177
+ return;
178
+ }
179
+
180
+ if (isOpen.value && (evt.key === 'ArrowDown' || evt.key === 'ArrowUp')) {
181
+ evt.preventDefault();
182
+
183
+ const count = filteredSuggestions.value.length;
184
+
185
+ if (count === 0) {
186
+ return;
187
+ }
188
+
189
+ highlightedIndex.value = evt.key === 'ArrowDown'
190
+ ? (highlightedIndex.value + 1) % count
191
+ : (highlightedIndex.value - 1 + count) % count;
192
+
193
+ return;
194
+ }
195
+
196
+ if (evt.key === 'Enter') {
197
+ if (isOpen.value && highlightedIndex.value >= 0) {
198
+ evt.preventDefault();
199
+ addSuggestion(filteredSuggestions.value[highlightedIndex.value]);
200
+ return;
201
+ }
202
+
203
+ if (delimiters.includes('Enter')) {
204
+ evt.preventDefault();
205
+ addTag(query.value);
206
+ }
207
+
208
+ return;
209
+ }
210
+
211
+ if (evt.key.length === 1 && delimiters.includes(evt.key)) {
212
+ evt.preventDefault();
213
+ addTag(query.value);
214
+ return;
215
+ }
216
+
217
+ if (evt.key === 'Backspace' && query.value === '' && modelValue.value.length > 0) {
218
+ removeAt(modelValue.value.length - 1);
219
+ }
220
+ }
221
+
222
+ function onPaste(evt: ClipboardEvent): void {
223
+ if (!evt.clipboardData) {
224
+ return;
225
+ }
226
+
227
+ const separators = [...delimiters.filter(delimiter => delimiter !== 'Enter'), '\n'];
228
+ const text = evt.clipboardData.getData('text');
229
+
230
+ let parts = [text];
231
+
232
+ for (const separator of separators) {
233
+ parts = parts.flatMap(part => part.split(separator));
234
+ }
235
+
236
+ parts = parts.map(part => part.trim()).filter(part => part !== '');
237
+
238
+ if (parts.length <= 1) {
239
+ return;
240
+ }
241
+
242
+ evt.preventDefault();
243
+ parts.forEach(addTag);
244
+ }
245
+
246
+ if (typeof window !== 'undefined') {
247
+ useClickOutside([anchorRef, popupRef], isOpen, () => isOpen.value = false);
248
+ }
249
+ </script>
@@ -29,9 +29,10 @@
29
29
  <script
30
30
  lang="ts"
31
31
  setup>
32
+ import { unrefTemplateElement } from '@flux-ui/internals';
32
33
  import type { FluxAutoCompleteType, FluxFormInputBaseProps } from '@flux-ui/types';
33
34
  import { clsx } from 'clsx';
34
- import { toRef } from 'vue';
35
+ import { toRef, useTemplateRef } from 'vue';
35
36
  import { useDisabled, useFormFieldInjection } from '~flux/components/composable';
36
37
  import $style from '~flux/components/css/component/Form.module.scss';
37
38
 
@@ -55,5 +56,19 @@
55
56
  }>();
56
57
 
57
58
  const disabled = useDisabled(toRef(() => componentDisabled));
59
+ const inputRef = useTemplateRef<HTMLTextAreaElement>('input');
58
60
  const {id} = useFormFieldInjection();
61
+
62
+ function blur(): void {
63
+ unrefTemplateElement(inputRef)?.blur();
64
+ }
65
+
66
+ function focus(): void {
67
+ unrefTemplateElement(inputRef)?.focus();
68
+ }
69
+
70
+ defineExpose({
71
+ blur,
72
+ focus
73
+ });
59
74
  </script>