sprintify-ui 0.0.103 → 0.0.104

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 (39) hide show
  1. package/dist/sprintify-ui.es.js +8252 -8040
  2. package/dist/style.css +1 -1
  3. package/dist/types/src/components/BaseAutocomplete.vue.d.ts +23 -3
  4. package/dist/types/src/components/BaseAutocompleteFetch.vue.d.ts +21 -3
  5. package/dist/types/src/components/BaseBelongsTo.vue.d.ts +21 -3
  6. package/dist/types/src/components/BaseDropdown.vue.d.ts +9 -0
  7. package/dist/types/src/components/BaseDropdownAutocomplete.vue.d.ts +123 -0
  8. package/dist/types/src/components/BaseTable.vue.d.ts +2 -2
  9. package/dist/types/src/components/index.d.ts +2 -1
  10. package/dist/types/src/types/index.d.ts +1 -1
  11. package/package.json +1 -1
  12. package/src/components/BaseAutocomplete.stories.js +6 -0
  13. package/src/components/BaseAutocomplete.vue +65 -15
  14. package/src/components/BaseAutocompleteDropdown.vue +2 -2
  15. package/src/components/BaseAutocompleteFetch.stories.js +6 -0
  16. package/src/components/BaseAutocompleteFetch.vue +29 -3
  17. package/src/components/BaseBelongsTo.stories.js +6 -0
  18. package/src/components/BaseBelongsTo.vue +12 -2
  19. package/src/components/BaseButtonGroup.vue +1 -1
  20. package/src/components/BaseColor.vue +3 -3
  21. package/src/components/BaseDropdown.stories.js +1 -1
  22. package/src/components/BaseDropdown.vue +15 -9
  23. package/src/components/BaseDropdownAutocomplete.stories.js +178 -0
  24. package/src/components/BaseDropdownAutocomplete.vue +225 -0
  25. package/src/components/BaseRadioGroup.vue +4 -1
  26. package/src/components/BaseTable.vue +1 -1
  27. package/src/components/BaseTagAutocomplete.vue +4 -4
  28. package/src/components/index.ts +2 -0
  29. package/src/types/index.ts +1 -1
  30. package/dist/types/src/components/BaseFormField.d.ts +0 -81
  31. package/dist/types/src/components/BaseLocaleForm.vue.d.ts +0 -439
  32. package/dist/types/src/components/BaseNumberForm.vue.d.ts +0 -401
  33. package/dist/types/src/components/BasePasswordForm.vue.d.ts +0 -384
  34. package/dist/types/src/components/BaseTextareaForm.vue.d.ts +0 -413
  35. package/src/components/BaseFormField.ts +0 -117
  36. package/src/components/BaseLocaleForm.vue +0 -142
  37. package/src/components/BaseNumberForm.vue +0 -67
  38. package/src/components/BasePasswordForm.vue +0 -59
  39. package/src/components/BaseTextareaForm.vue +0 -101
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div ref="autocomplete">
3
- <div class="relative">
3
+ <div class="relative z-[1]">
4
4
  <div class="relative">
5
5
  <input
6
6
  ref="inputElement"
@@ -9,7 +9,7 @@
9
9
  :placeholder="
10
10
  placeholder ? placeholder : $t('sui.autocomplete_placeholder')
11
11
  "
12
- class="w-full rounded disabled:cursor-not-allowed disabled:text-slate-300"
12
+ class="w-full rounded pr-9 disabled:cursor-not-allowed disabled:text-slate-300"
13
13
  :class="[
14
14
  hasErrorInternal ? 'border-red-600' : 'border-slate-300',
15
15
  inputClass,
@@ -41,7 +41,7 @@
41
41
  </div>
42
42
 
43
43
  <div
44
- v-if="normalizedModelValue && !disabled && modelValueShow"
44
+ v-if="normalizedModelValue && !disabled && showModelValue"
45
45
  class="absolute top-0 right-0 flex h-full items-center p-1"
46
46
  >
47
47
  <button
@@ -64,7 +64,7 @@
64
64
  class="w-full overflow-hidden"
65
65
  :class="[
66
66
  inline
67
- ? 'relative mt-1'
67
+ ? 'relative'
68
68
  : 'absolute top-1 z-menu min-h-[110px] w-full overflow-hidden rounded border border-slate-300 bg-white shadow-md',
69
69
  ]"
70
70
  >
@@ -75,7 +75,7 @@
75
75
  :size="size"
76
76
  :loading="loading"
77
77
  :loading-bottom="loadingBottom"
78
- :dropdown-class="inline ? '' : 'p-1'"
78
+ :dropdown-class="inline ? 'pt-1' : 'p-1'"
79
79
  :keywords="keywords"
80
80
  @select="onSelect"
81
81
  @scroll-bottom="emit('scrollBottom')"
@@ -166,7 +166,7 @@ const props = defineProps({
166
166
  default: 'focus',
167
167
  type: String as PropType<'focus' | 'always'>,
168
168
  },
169
- modelValueShow: {
169
+ showModelValue: {
170
170
  default: true,
171
171
  type: Boolean,
172
172
  },
@@ -174,6 +174,18 @@ const props = defineProps({
174
174
  default: true,
175
175
  type: Boolean,
176
176
  },
177
+ /** Show an 'empty option', should use with showModelValue = true for better UX */
178
+ showEmptyOption: {
179
+ default: false,
180
+ type: Boolean,
181
+ },
182
+ emptyOptionLabel: {
183
+ default() {
184
+ const i18n = useI18n();
185
+ return i18n.t('sui.none');
186
+ },
187
+ type: String,
188
+ },
177
189
  });
178
190
 
179
191
  const emit = defineEmits([
@@ -216,10 +228,26 @@ const normalizedModelValue =
216
228
  hasOptions.normalizedModelValue as ComputedRef<NormalizedOption | null>;
217
229
 
218
230
  const filteredNormalizedOptions = computed((): NormalizedOption[] => {
231
+ let options = normalizedOptions.value;
232
+
233
+ if (props.showEmptyOption) {
234
+ const emptyOption = {
235
+ [props.valueKey]: null,
236
+ [props.labelKey]: props.emptyOptionLabel,
237
+ option: null,
238
+ };
239
+
240
+ options = [
241
+ { value: null, label: props.emptyOptionLabel, option: emptyOption },
242
+ ...options,
243
+ ];
244
+ }
245
+
219
246
  if (shouldFilter.value === false) {
220
- return normalizedOptions.value;
247
+ return options;
221
248
  }
222
- return normalizedOptions.value.filter((option) => {
249
+
250
+ return options.filter((option) => {
223
251
  if (props.filter !== undefined) {
224
252
  return props.filter(option);
225
253
  }
@@ -235,12 +263,16 @@ watch(
235
263
  () => normalizedModelValue.value,
236
264
  () => {
237
265
  if (normalizedModelValue.value) {
238
- if (props.modelValueShow) {
266
+ if (props.showModelValue) {
239
267
  setKeywords(normalizedModelValue.value?.label);
240
268
  }
241
269
  } else {
242
- if (props.modelValueShow) {
243
- setKeywords('');
270
+ if (props.showModelValue) {
271
+ if (props.showEmptyOption) {
272
+ setKeywords(props.emptyOptionLabel);
273
+ } else {
274
+ setKeywords('');
275
+ }
244
276
  }
245
277
  }
246
278
  },
@@ -268,8 +300,16 @@ function close() {
268
300
  blur();
269
301
  timerId = setTimeout(() => {
270
302
  // If no valid modelValue is set on close, set the keywords to the original value
271
- if (props.modelValueShow && normalizedModelValue.value) {
272
- setKeywords(normalizedModelValue.value.label);
303
+ if (props.showModelValue) {
304
+ if (normalizedModelValue.value) {
305
+ setKeywords(normalizedModelValue.value.label);
306
+ } else {
307
+ if (props.showEmptyOption) {
308
+ setKeywords(props.emptyOptionLabel);
309
+ } else {
310
+ setKeywords('');
311
+ }
312
+ }
273
313
  }
274
314
  }, 10);
275
315
  emit('close');
@@ -282,7 +322,7 @@ const onTextInput = (event: Event) => {
282
322
  emit('typing', keywords.value);
283
323
 
284
324
  // If keywords is empty, emit null
285
- if (keywords.value == '') {
325
+ if (keywords.value == '' && !props.showEmptyOption) {
286
326
  update(null);
287
327
  }
288
328
  };
@@ -321,8 +361,18 @@ function onSelect(normalizedModelValue: Option | null | undefined) {
321
361
  function update(normalizedSelection: Option | null | undefined) {
322
362
  // Re-activate filter
323
363
  shouldFilter.value = false;
364
+
324
365
  // Emit update
325
- const selection = normalizedSelection ? normalizedSelection.option : null;
366
+ let selection = normalizedSelection ? normalizedSelection.option : null;
367
+
368
+ if (
369
+ props.showEmptyOption &&
370
+ normalizedSelection &&
371
+ normalizedSelection.value == null
372
+ ) {
373
+ selection = null;
374
+ }
375
+
326
376
  emitUpdate(selection);
327
377
  }
328
378
 
@@ -18,7 +18,7 @@
18
18
  <ul v-else :class="dropdownClass">
19
19
  <li
20
20
  v-for="(option, index) in options"
21
- :key="option.value"
21
+ :key="option.value ? option.value : 'null'"
22
22
  :data-index="index"
23
23
  class="block"
24
24
  >
@@ -222,7 +222,7 @@ onMounted(() => {
222
222
  () => {
223
223
  emit('scrollBottom');
224
224
  },
225
- { distance: 60 }
225
+ { distance: 100 }
226
226
  );
227
227
  });
228
228
 
@@ -58,6 +58,12 @@ Disabled.args = {
58
58
  disabled: true,
59
59
  };
60
60
 
61
+ export const ShowEmptyOption = Template.bind({});
62
+ ShowEmptyOption.args = {
63
+ showEmptyOption: true,
64
+ emptyOptionLabel: 'No Jedi',
65
+ };
66
+
61
67
  export const Inline = Template.bind({});
62
68
  Inline.args = {
63
69
  inline: true,
@@ -15,9 +15,11 @@
15
15
  :size="size"
16
16
  :inline="inline"
17
17
  :dropdown-show="dropdownShow"
18
- :model-value-show="modelValueShow"
18
+ :show-model-value="showModelValue"
19
19
  :visible-focus="visibleFocus"
20
- :filter="() => true"
20
+ :show-empty-option="showEmptyOption"
21
+ :empty-option-label="emptyOptionLabel"
22
+ :filter="filterOptions"
21
23
  @clear="onClear"
22
24
  @open="onOpen"
23
25
  @typing="onTyping"
@@ -114,7 +116,7 @@ const props = defineProps({
114
116
  default: 'focus',
115
117
  type: String as PropType<'focus' | 'always'>,
116
118
  },
117
- modelValueShow: {
119
+ showModelValue: {
118
120
  default: true,
119
121
  type: Boolean,
120
122
  },
@@ -122,6 +124,14 @@ const props = defineProps({
122
124
  default: true,
123
125
  type: Boolean,
124
126
  },
127
+ showEmptyOption: {
128
+ default: false,
129
+ type: Boolean,
130
+ },
131
+ emptyOptionLabel: {
132
+ default: undefined,
133
+ type: String,
134
+ },
125
135
  });
126
136
 
127
137
  const emit = defineEmits([
@@ -217,6 +227,22 @@ const debouncedSearch = debounce(() => {
217
227
  search();
218
228
  }, 300);
219
229
 
230
+ function filterOptions(option: Option): boolean {
231
+ // Do nothing if showEmptyOption is false
232
+ if (!props.showEmptyOption) {
233
+ return true;
234
+ }
235
+
236
+ // Hide the empty value on search
237
+ if (
238
+ option.value == null &&
239
+ !option.label.toLowerCase().includes(keywords.value.toLowerCase())
240
+ ) {
241
+ return false;
242
+ }
243
+ return true;
244
+ }
245
+
220
246
  defineExpose({
221
247
  focus: () => autocomplete.value?.focus(),
222
248
  blur: () => autocomplete.value?.blur(),
@@ -54,6 +54,12 @@ Inline.args = {
54
54
  inline: true,
55
55
  };
56
56
 
57
+ export const ShowEmptyOption = Template.bind({});
58
+ ShowEmptyOption.args = {
59
+ showEmptyOption: true,
60
+ emptyOptionLabel: 'No Jedi',
61
+ };
62
+
57
63
  export const Sizes = (args) => ({
58
64
  components: { BaseBelongsTo },
59
65
  setup() {
@@ -13,7 +13,9 @@
13
13
  :inline="inline"
14
14
  :size="size"
15
15
  :dropdown-show="dropdownShow"
16
- :model-value-show="modelValueShow"
16
+ :show-model-value="showModelValue"
17
+ :show-empty-option="showEmptyOption"
18
+ :empty-option-label="emptyOptionLabel"
17
19
  :visible-focus="visibleFocus"
18
20
  @update:model-value="onUpdate"
19
21
  >
@@ -95,7 +97,7 @@ const props = defineProps({
95
97
  default: 'focus',
96
98
  type: String as PropType<'focus' | 'always'>,
97
99
  },
98
- modelValueShow: {
100
+ showModelValue: {
99
101
  default: true,
100
102
  type: Boolean,
101
103
  },
@@ -103,6 +105,14 @@ const props = defineProps({
103
105
  default: true,
104
106
  type: Boolean,
105
107
  },
108
+ showEmptyOption: {
109
+ default: false,
110
+ type: Boolean,
111
+ },
112
+ emptyOptionLabel: {
113
+ default: undefined,
114
+ type: String,
115
+ },
106
116
  });
107
117
 
108
118
  const http = config.http;
@@ -2,7 +2,7 @@
2
2
  <div class="flex flex-wrap" :style="{ margin: '-' + spacing }">
3
3
  <div
4
4
  v-for="option in normalizedOptions"
5
- :key="option.value"
5
+ :key="option.value ? option.value : 'null'"
6
6
  :style="{ padding: spacing }"
7
7
  >
8
8
  <button
@@ -16,14 +16,14 @@
16
16
  >
17
17
  <template #option="option">
18
18
  <div
19
- :style="{ background: option.option.value }"
20
- class="p-3 border-none rounded-md"
19
+ :style="{ backgroundColor: option.option.value + '' }"
20
+ class="rounded-md border-none p-3"
21
21
  :class="[
22
22
  option.selected.value ? 'text-white' : 'text-transparent',
23
23
  disabled ? ' cursor-not-allowed opacity-50' : '',
24
24
  ]"
25
25
  >
26
- <BaseIcon icon="heroicons-solid:check-circle" class="w-5 h-5" />
26
+ <BaseIcon icon="heroicons-solid:check-circle" class="h-5 w-5" />
27
27
  </div>
28
28
  </template>
29
29
  </BaseButtonGroup>
@@ -110,7 +110,7 @@ export const WithAutocomplete = (args) => ({
110
110
  :inline="true"
111
111
  :visibleFocus="false"
112
112
  dropdownShow="always"
113
- :modelValueShow="false"
113
+ :showModelValue="false"
114
114
  @update:modelValue="onUpdate($event, close)"
115
115
  ></BaseAutocomplete>
116
116
  </div>
@@ -15,15 +15,17 @@
15
15
  leave-from-class="transform scale-100 opacity-100"
16
16
  leave-to-class="transform scale-90 opacity-0"
17
17
  >
18
- <div v-show="showDropdown" class="inline-block">
19
- <slot
20
- name="dropdown"
21
- :show-dropdown="showDropdown"
22
- :close="close"
23
- :open="open"
24
- :toggle="toggle"
25
- ></slot>
26
- </div>
18
+ <template v-if="showDropdown || keepAlive">
19
+ <div v-show="showDropdown" class="inline-block">
20
+ <slot
21
+ name="dropdown"
22
+ :show-dropdown="showDropdown"
23
+ :close="close"
24
+ :open="open"
25
+ :toggle="toggle"
26
+ ></slot>
27
+ </div>
28
+ </template>
27
29
  </Transition>
28
30
  </div>
29
31
  </Teleport>
@@ -58,6 +60,10 @@ const props = defineProps({
58
60
  default: false,
59
61
  type: Boolean,
60
62
  },
63
+ keepAlive: {
64
+ default: true,
65
+ type: Boolean,
66
+ },
61
67
  });
62
68
 
63
69
  const buttonX = ref(0);
@@ -0,0 +1,178 @@
1
+ import BaseBadge from './BaseBadge.vue';
2
+ import BaseDropdownAutocomplete from './BaseDropdownAutocomplete.vue';
3
+ import ShowValue from '@/../.storybook/components/ShowValue.vue';
4
+ import { options } from '../../.storybook/utils';
5
+
6
+ export default {
7
+ title: 'Components/BaseDropdownAutocomplete',
8
+ component: BaseDropdownAutocomplete,
9
+ argTypes: {
10
+ placement: {
11
+ control: {
12
+ type: 'select',
13
+ options: ['top-start', 'top-end', 'bottom-start', 'bottom-end'],
14
+ },
15
+ },
16
+ },
17
+ args: {
18
+ placement: 'bottom-start',
19
+ padding: 8,
20
+ emptyOptionLabel: 'Nothing',
21
+ },
22
+ };
23
+
24
+ export const Autocomplete = (args) => ({
25
+ components: { BaseDropdownAutocomplete, BaseBadge, ShowValue },
26
+ setup() {
27
+ const value = ref(null);
28
+ return { args, options, value };
29
+ },
30
+ template: `
31
+ <BaseDropdownAutocomplete
32
+ v-bind="args"
33
+ v-model="value"
34
+ >
35
+ <template #button="{ newValue }">
36
+ <BaseBadge v-if="newValue">
37
+ {{ newValue.label }}
38
+ </BaseBadge>
39
+ <div v-else>
40
+ <BaseBadge contrast="low">
41
+ Nothing
42
+ </BaseBadge>
43
+ </div>
44
+ </template>
45
+ <template #option="{ option }">
46
+ <BaseBadge :contrast="option?.value ? 'high' : 'low'">
47
+ {{ option.label }}
48
+ </BaseBadge>
49
+ </template>
50
+ </BaseDropdownAutocomplete>
51
+ <ShowValue :value="value" />
52
+ `,
53
+ });
54
+
55
+ Autocomplete.args = {
56
+ options: options,
57
+ multiple: false,
58
+ labelKey: 'label',
59
+ valueKey: 'value',
60
+ };
61
+
62
+ export const AutocompleteFetch = (args) => ({
63
+ components: { BaseDropdownAutocomplete, BaseBadge, ShowValue },
64
+ setup() {
65
+ const value = ref(null);
66
+ return { args, options, value };
67
+ },
68
+ template: `
69
+ <BaseDropdownAutocomplete
70
+ v-bind="args"
71
+ v-model="value"
72
+ >
73
+ <template #button="{ newValue }">
74
+ <BaseBadge v-if="newValue" contrast="high">
75
+ {{ newValue.name }}
76
+ </BaseBadge>
77
+ <div v-else>
78
+ <BaseBadge contrast="low">
79
+ Nothing
80
+ </BaseBadge>
81
+ </div>
82
+ </template>
83
+ <template #option="{ option }">
84
+ <BaseBadge :contrast="option?.id ? 'high' : 'low'">
85
+ {{ option.name }}
86
+ </BaseBadge>
87
+ </template>
88
+ </BaseDropdownAutocomplete>
89
+ <ShowValue :value="value" />
90
+ `,
91
+ });
92
+
93
+ AutocompleteFetch.args = {
94
+ valueKey: 'id',
95
+ labelKey: 'name',
96
+ url: 'https://effettandem.com/api/content/tags',
97
+ multiple: false,
98
+ };
99
+
100
+ export const TagAutocomplete = (args) => ({
101
+ components: { BaseDropdownAutocomplete, BaseBadge, ShowValue },
102
+ setup() {
103
+ const value = ref([]);
104
+ return { args, options, value };
105
+ },
106
+ template: `
107
+ <BaseDropdownAutocomplete
108
+ v-bind="args"
109
+ v-model="value"
110
+ >
111
+ <template #button="{ newValue }">
112
+ <div v-if="newValue && newValue.length" class="flex flex-wrap gap-0.5">
113
+ <BaseBadge v-for="item in newValue" :key="item.value" contrast="high">
114
+ {{ item.label }}
115
+ </BaseBadge>
116
+ </div>
117
+ <div v-else>
118
+ <BaseBadge contrast="low">
119
+ Nothing
120
+ </BaseBadge>
121
+ </div>
122
+ </template>
123
+ <template #option="{ option }">
124
+ <BaseBadge contrast="high">
125
+ {{ option.label }}
126
+ </BaseBadge>
127
+ </template>
128
+ </BaseDropdownAutocomplete>
129
+ <ShowValue :value="value" />
130
+ `,
131
+ });
132
+
133
+ TagAutocomplete.args = {
134
+ options: options,
135
+ multiple: true,
136
+ labelKey: 'label',
137
+ valueKey: 'value',
138
+ };
139
+
140
+ export const TagAutocompleteFetch = (args) => ({
141
+ components: { BaseDropdownAutocomplete, BaseBadge, ShowValue },
142
+ setup() {
143
+ const value = ref([]);
144
+ return { args, options, value };
145
+ },
146
+ template: `
147
+ <BaseDropdownAutocomplete
148
+ v-bind="args"
149
+ v-model="value"
150
+ >
151
+ <template #button="{ newValue }">
152
+ <div v-if="newValue && newValue.length" class="flex flex-wrap gap-0.5">
153
+ <BaseBadge v-for="item in newValue" :key="item.value" contrast="high">
154
+ {{ item.name }}
155
+ </BaseBadge>
156
+ </div>
157
+ <div v-else>
158
+ <BaseBadge contrast="low">
159
+ Nothing
160
+ </BaseBadge>
161
+ </div>
162
+ </template>
163
+ <template #option="{ option }">
164
+ <BaseBadge contrast="high">
165
+ {{ option.name }}
166
+ </BaseBadge>
167
+ </template>
168
+ </BaseDropdownAutocomplete>
169
+ <ShowValue :value="value" />
170
+ `,
171
+ });
172
+
173
+ TagAutocompleteFetch.args = {
174
+ valueKey: 'id',
175
+ labelKey: 'name',
176
+ url: 'https://effettandem.com/api/content/tags',
177
+ multiple: true,
178
+ };