vueless 0.0.159 → 0.0.161

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.
@@ -58,8 +58,8 @@ export default function useUI(defaultConfig = {}, propsConfigGetter = null, topL
58
58
 
59
59
  const commonAttrs = {
60
60
  ...attrs,
61
- component: isDev && (attrs.component || componentName || null),
62
- "config-key": isDev && (attrs["config-key"] || configKey || null),
61
+ component: isDev ? attrs.component || componentName || null : null,
62
+ "config-key": isDev ? attrs["config-key"] || configKey || null : null,
63
63
  "child-component": isDev && attrs.component ? componentName : null,
64
64
  "child-config-key": isDev && attrs.component ? configKey : null,
65
65
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vueless",
3
- "version": "0.0.159",
3
+ "version": "0.0.161",
4
4
  "license": "MIT",
5
5
  "description": "Vue Styleless Component Framework.",
6
6
  "homepage": "https://vueless.com",
@@ -39,20 +39,22 @@ export function useAttrs(
39
39
 
40
40
  const wrapperClasses = computed(() => cvaWrapper({ compact: props.compact }));
41
41
 
42
- const customHeaderItemClasses = computed(() => cvaCustomHeaderItem({ compact: props.compact }));
42
+ const stickyHeaderColumnClasses = computed(() => cvaCustomHeaderItem({ compact: props.compact }));
43
43
 
44
44
  const headerCellClasses = computed(() => cvaHeaderCell({ compact: props.compact }));
45
45
 
46
46
  const wrapperAttrs = getAttrs("wrapper", { classes: wrapperClasses });
47
- const customHeaderItemAttrsRaw = getAttrs("stickyHeaderColumn", {
48
- classes: customHeaderItemClasses,
47
+ const stickyHeaderAttrsRaw = getAttrs("stickyHeader");
48
+ const stickyHeaderColumnAttrsRaw = getAttrs("stickyHeaderColumn", {
49
+ classes: stickyHeaderColumnClasses,
49
50
  });
50
51
  const selectedRowsCountAttrs = getAttrs("selectedRowsCount");
51
52
  const selectAllCheckboxAttrs = getAttrs("selectAllCheckbox", { isComponent: true });
52
53
  const stickyHeaderLoaderAttrs = getAttrs("stickyHeaderLoader", { isComponent: true });
54
+ const tableWrapperAttrsRaw = getAttrs("tableWrapper");
53
55
  const tableAttrs = getAttrs("table");
54
56
  const headerAttrs = getAttrs("header");
55
- const headerRowAttrs = getAttrs("headerRow");
57
+ const headerRowAttrsRaw = getAttrs("headerRow");
56
58
  const headerCellAttrsRaw = getAttrs("headerCell", { classes: headerCellClasses });
57
59
  const headerSelectAllCheckboxAttrs = getAttrs("headerSelectAllCheckbox", { isComponent: true });
58
60
  const tableLoaderAttrs = getAttrs("tableLoader", { isComponent: true });
@@ -66,6 +68,7 @@ export function useAttrs(
66
68
  const toggleItemButtonAttrs = getAttrs("toggleItemButton");
67
69
  const secondaryRowAttrs = getAttrs("secondaryRow");
68
70
  const secondaryRowEmptyAttrs = getAttrs("secondaryRowEmpty");
71
+ const footerAttrsRaw = getAttrs("footer");
69
72
  const footerRowAttrs = getAttrs("footerRow");
70
73
  const footerCellAttrs = getAttrs("footerCell", { isComponent: true });
71
74
  const footerStickyRowAttrs = getAttrs("footerStickyRow");
@@ -74,6 +77,7 @@ export function useAttrs(
74
77
  const beforeFirstRowCellAttrs = getAttrs("beforeFirstRowCell", { isComponent: true });
75
78
  const afterLastRowAttrsRaw = getAttrs("afterLastRow");
76
79
  const afterLastRowCellAttrs = getAttrs("afterLastRowCell", { isComponent: true });
80
+ const toggleButtonWrapperAttrsRaw = getAttrs("toggleButtonWrapper");
77
81
 
78
82
  const bodyRowAttrsRaw = getAttrs("bodyRow");
79
83
 
@@ -101,35 +105,35 @@ export function useAttrs(
101
105
  }));
102
106
 
103
107
  const tableWrapperAttrs = computed(() => ({
108
+ ...tableWrapperAttrsRaw.value,
104
109
  class: cx([
105
- config.value.innerWrapper,
106
- selectedRows.value.length && config.value.innerWrapperSelected,
110
+ tableWrapperAttrsRaw.value.class,
111
+ selectedRows.value.length && config.value.tableWrapperSelected,
107
112
  ]),
108
113
  }));
109
114
 
110
- const headerRowCellAttrs = computed(() => ({
115
+ const headerRowAttrs = computed(() => ({
116
+ ...headerRowAttrsRaw.value,
111
117
  class: cx([
112
- config.value.headerRow,
118
+ headerRowAttrsRaw.value.class,
113
119
  selectedRows.value.length && !isHeaderSticky.value && config.value.stickyHeaderHidden,
114
120
  ]),
115
121
  }));
116
122
 
117
- const stickyHeaderAttrs = computed(() => {
118
- // console.log("hasSlotContentTheadActions", hasSlotContentTheadActions.value);
119
-
120
- return {
121
- class: cx([
122
- config.value.stickyHeader,
123
- selectedRows.value.length &&
124
- hasSlotContentTheadActions.value &&
125
- config.value.stickyHeaderActions,
126
- isHeaderSticky.value && props.stickyHeader && config.value.stickyHeaderActive,
127
- ]),
128
- };
129
- });
123
+ const stickyHeaderAttrs = computed(() => ({
124
+ ...stickyHeaderAttrsRaw.value,
125
+ class: cx([
126
+ stickyHeaderAttrsRaw.value.class,
127
+ selectedRows.value.length &&
128
+ hasSlotContentTheadActions.value &&
129
+ config.value.stickyHeaderActions,
130
+ isHeaderSticky.value && props.stickyHeader && config.value.stickyHeaderActive,
131
+ ]),
132
+ }));
130
133
 
131
134
  const footerClassesAttrs = computed(() => ({
132
- class: cx([config.value.footer, isFooterSticky.value && config.value.footerSticky]),
135
+ ...footerAttrsRaw.value,
136
+ class: cx([footerAttrsRaw.value.class, isFooterSticky.value && config.value.footerSticky]),
133
137
  }));
134
138
 
135
139
  const afterLastRowAttrs = computed(() => ({
@@ -141,8 +145,8 @@ export function useAttrs(
141
145
  }));
142
146
 
143
147
  const stickyHeaderColumnAttrs = computed(() => (thClass) => ({
144
- ...customHeaderItemAttrsRaw.value,
145
- class: cx([customHeaderItemAttrsRaw.value.class, thClass]),
148
+ ...stickyHeaderColumnAttrsRaw.value,
149
+ class: cx([stickyHeaderColumnAttrsRaw.value.class, thClass]),
146
150
  }));
147
151
 
148
152
  const dateSeparatorRowAttrs = (rowIndex) => {
@@ -158,6 +162,7 @@ export function useAttrs(
158
162
  };
159
163
 
160
164
  const toggleButtonWrapperAttrs = computed(() => (index) => ({
165
+ ...toggleButtonWrapperAttrsRaw.value,
161
166
  class: index === 0 && isNesting.value ? config.value.toggleButtonWrapper : "",
162
167
  }));
163
168
 
@@ -167,7 +172,8 @@ export function useAttrs(
167
172
  stickyHeaderColumnAttrs,
168
173
  stickyHeaderAttrs,
169
174
  tableWrapperAttrs,
170
- headerRowCellAttrs,
175
+ headerCellAttrs,
176
+ headerRowAttrs,
171
177
  bodyRowAttrs,
172
178
  footerClassesAttrs,
173
179
  afterLastRowAttrs,
@@ -176,7 +182,6 @@ export function useAttrs(
176
182
  beforeFirstRowCellAttrs,
177
183
  dateSeparatorRowAttrs,
178
184
  toggleButtonWrapperAttrs,
179
- headerCellAttrs,
180
185
  bodyCellAttrs,
181
186
  checkboxBodyWrapperCellAttrs,
182
187
  footerCellAttrs,
@@ -190,7 +195,6 @@ export function useAttrs(
190
195
  selectedRowsCountAttrs,
191
196
  stickyHeaderLoaderAttrs,
192
197
  tableAttrs,
193
- headerRowAttrs,
194
198
  tableLoaderAttrs,
195
199
  bodyAttrs,
196
200
  toggleItemButtonAttrs,
@@ -20,8 +20,8 @@ export default /*tw*/ {
20
20
  stickyHeaderColumn: "flex-none text-sm font-normal text-gray-500 p-4 whitespace-nowrap",
21
21
  selectedRowsCount: "absolute left-11 pr-2 font-medium text-sm text-gray-900 bg-gradient-to-r from-white from-80%",
22
22
  selectAllCheckbox: "flex-none text-sm font-normal text-gray-500 pr-[1.125rem] pl-4 py-4 group-[]/table-compact:!px-4",
23
- innerWrapper: "!overflow-x-auto rounded-lg border border-gray-200",
24
- innerWrapperSelected: "!rounded-t-none border-t-0",
23
+ tableWrapper: "!overflow-x-auto rounded-lg border border-gray-200",
24
+ tableWrapperSelected: "!rounded-t-none border-t-0",
25
25
  table: "min-w-full border-none text-sm w-full",
26
26
  tableLoader: "absolute !top-auto",
27
27
  moreRow: "!px-0 !py-4 first:mx-auto",
@@ -492,7 +492,7 @@ const {
492
492
  stickyHeaderColumnAttrs,
493
493
  stickyHeaderAttrs,
494
494
  tableWrapperAttrs,
495
- headerRowCellAttrs,
495
+ headerRowAttrs,
496
496
  afterLastRowAttrs,
497
497
  afterLastRowCellAttrs,
498
498
  beforeFirstRowAttrs,
@@ -515,7 +515,6 @@ const {
515
515
  selectedRowsCountAttrs,
516
516
  stickyHeaderLoaderAttrs,
517
517
  tableAttrs,
518
- headerRowAttrs,
519
518
  tableLoaderAttrs,
520
519
  bodyAttrs,
521
520
  toggleItemButtonAttrs,
@@ -6,7 +6,7 @@ import defaultConfig from "../configs/default.config";
6
6
 
7
7
  export function useAttrs(props) {
8
8
  const { config, getAttrs } = useUI(defaultConfig, () => props.config);
9
- const { dropzoneWrapper, placeholder, selectedItem } = config.value;
9
+ const { dropzoneWrapper, placeholder, contentWrapper } = config.value;
10
10
 
11
11
  const cvaDropzoneWrapper = cva({
12
12
  base: dropzoneWrapper.base,
@@ -20,10 +20,10 @@ export function useAttrs(props) {
20
20
  compoundVariants: placeholder.compoundVariants,
21
21
  });
22
22
 
23
- const cvaSelectedItem = cva({
24
- base: selectedItem.base,
25
- variants: selectedItem.variants,
26
- compoundVariants: selectedItem.compoundVariants,
23
+ const cvaContentWrapper = cva({
24
+ base: contentWrapper.base,
25
+ variants: contentWrapper.variants,
26
+ compoundVariants: contentWrapper.compoundVariants,
27
27
  });
28
28
 
29
29
  const dropzoneWrapperClasses = computed(() =>
@@ -41,9 +41,9 @@ export function useAttrs(props) {
41
41
  }),
42
42
  );
43
43
 
44
- const selectedItemClasses = computed(() =>
45
- cvaSelectedItem({
46
- size: props.size,
44
+ const contentWrapperClasses = computed(() =>
45
+ cvaContentWrapper({
46
+ multiple: props.multiple,
47
47
  }),
48
48
  );
49
49
 
@@ -51,14 +51,14 @@ export function useAttrs(props) {
51
51
  const buttonAttrs = getAttrs("button", { isComponent: true });
52
52
  const dropzoneWrapperAttrs = getAttrs("dropzoneWrapper", { classes: dropzoneWrapperClasses });
53
53
  const descriptionAttrs = getAttrs("description", { isComponent: true });
54
- const contentWrapperAttrs = getAttrs("contentWrapper");
55
54
  const buttonWrapperAttrs = getAttrs("buttonWrapper");
56
55
  const placeholderAttrs = getAttrs("placeholder", { classes: placeholderClasses });
57
56
  const clearIconAttrs = getAttrs("clearIcon", { isComponent: true });
57
+ const removeItemIconAttrs = getAttrs("removeItemIcon", { isComponent: true });
58
58
  const chooseFileIconAttrs = getAttrs("chooseFileIcon", { isComponent: true });
59
59
  const inputAttrs = getAttrs("input");
60
- const fileListAttrs = getAttrs("fileList");
61
- const selectedItemAttrs = getAttrs("selectedItem", { classes: selectedItemClasses });
60
+ const fileListAttrs = getAttrs("fileList", { isComponent: true });
61
+ const contentWrapperAttrs = getAttrs("contentWrapper", { classes: contentWrapperClasses });
62
62
 
63
63
  return {
64
64
  config,
@@ -72,7 +72,7 @@ export function useAttrs(props) {
72
72
  placeholderAttrs,
73
73
  fileListAttrs,
74
74
  buttonWrapperAttrs,
75
- selectedItemAttrs,
76
75
  chooseFileIconAttrs,
76
+ removeItemIconAttrs,
77
77
  };
78
78
  }
@@ -11,7 +11,7 @@ export default /*tw*/ {
11
11
  },
12
12
  dropzoneWrapper: {
13
13
  base: `
14
- size-auto w-full rounded border border-dashed border-gray-300 bg-white
14
+ size-auto w-full rounded border border-solid border-gray-300 bg-white
15
15
  p-4 px-5 py-6 hover:border-gray-400
16
16
  `,
17
17
  variants: {
@@ -21,24 +21,20 @@ export default /*tw*/ {
21
21
  },
22
22
  compoundVariants: [
23
23
  { labelAlign: "topInside", label: true, size: "sm", class: "pt-8" },
24
- { labelAlign: "topInside", label: true, size: "md", class: "py-10" },
24
+ { labelAlign: "topInside", label: true, size: "md", class: "pt-10" },
25
25
  { labelAlign: "topInside", label: true, size: "lg", class: "pt-12" },
26
26
  ],
27
27
  },
28
28
  description: "text-gray-700",
29
- contentWrapper: "relative mt-3 flex w-full gap-6 justify-between items-start rounded bg-brand-50 p-3",
30
- fileList: "pr-4 shrink-0 text-gray-700 flex-grow flex flex-col gap-4",
31
- placeholder: {
32
- base: "pr-4 text-gray-700 flex-grow",
29
+ contentWrapper: {
30
+ base: "relative mt-3 flex w-full gap-6 justify-between items-start rounded bg-brand-50 p-3",
33
31
  variants: {
34
- size: {
35
- sm: "text-sm",
36
- md: "text-base",
37
- lg: "text-lg",
32
+ multiple: {
33
+ false: "items-center",
38
34
  },
39
35
  },
40
36
  },
41
- selectedItem: {
37
+ placeholder: {
42
38
  base: "pr-4 text-gray-700 flex-grow",
43
39
  variants: {
44
40
  size: {
@@ -53,8 +49,10 @@ export default /*tw*/ {
53
49
  chooseFileIconName: "attach_file",
54
50
  clearIcon: "",
55
51
  clearIconName: "close",
56
- dropzoneWrapperHover: "border-gray-400",
57
- dropzoneWrapperError: "hover:border-red-400 border-red-300",
52
+ removeItemIcon: "",
53
+ removeItemIconName: "close",
54
+ dropzoneWrapperHover: "border-gray-400 border-dashed",
55
+ dropzoneWrapperError: "hover:border-red-400 border-dashed border-red-300",
58
56
  input: "sr-only pointer-events-none size-0 opacity-0",
59
57
  buttonWrapper: "flex gap-4 items-center",
60
58
  i18n: {
@@ -10,6 +10,9 @@ export default {
10
10
  id: "30232",
11
11
  title: "Form Inputs & Controls / Input File",
12
12
  component: UInputFile,
13
+ args: {
14
+ label: "Vueless file input",
15
+ },
13
16
  argTypes: {
14
17
  ...getArgTypes(UInputFile.name),
15
18
  },
@@ -37,6 +40,27 @@ const DefaultTemplate = (args) => ({
37
40
  `,
38
41
  });
39
42
 
43
+ const LabelAlignTemplate = (args, { argTypes } = {}) => ({
44
+ components: { UInputFile, URow },
45
+ setup() {
46
+ return {
47
+ args,
48
+ alignOptions: argTypes.labelAlign.options,
49
+ };
50
+ },
51
+ template: `
52
+ <URow class="!flex-col" gap="xl">
53
+ <div v-for="(align, index) in alignOptions" :key="index">
54
+ <UInputFile
55
+ :label-align="align"
56
+ v-bind="args"
57
+ :label="align"
58
+ />
59
+ </div>
60
+ </URow>
61
+ `,
62
+ });
63
+
40
64
  const SizesTemplate = (args, { argTypes } = {}) => ({
41
65
  components: { UInputFile, URow },
42
66
  setup() {
@@ -60,8 +84,17 @@ const SizesTemplate = (args, { argTypes } = {}) => ({
60
84
  export const Default = DefaultTemplate.bind({});
61
85
  Default.args = {};
62
86
 
87
+ export const multiple = DefaultTemplate.bind({});
88
+ multiple.args = { multiple: true };
89
+
90
+ export const allowedFileTypes = DefaultTemplate.bind({});
91
+ allowedFileTypes.args = { allowedFileTypes: ["png", "jpeg"], label: "Allow only png and jpeg" };
92
+
63
93
  export const error = DefaultTemplate.bind({});
64
94
  error.args = { error: "some error" };
65
95
 
66
96
  export const sizes = SizesTemplate.bind({});
67
97
  sizes.args = {};
98
+
99
+ export const labelAlign = LabelAlignTemplate.bind({});
100
+ labelAlign.args = {};
@@ -9,18 +9,21 @@
9
9
 
10
10
  <span v-if="!isValue" v-bind="placeholderAttrs" v-text="currentLocale.noFile" />
11
11
 
12
- <div v-else v-bind="fileListAttrs">
13
- <template v-if="props.multiple">
14
- <span
15
- v-for="(file, idx) in currentFiles"
16
- :key="idx"
17
- v-bind="selectedItemAttrs"
18
- v-text="file.name"
12
+ <UFiles :size="size" v-bind="fileListAttrs" :file-list="fileList">
13
+ <template #right="{ file }">
14
+ <UIcon
15
+ v-if="props.multiple"
16
+ pill
17
+ internal
18
+ interactive
19
+ size="2xs"
20
+ :name="config.removeItemIconName"
21
+ :data-cy="`${dataCy}-remove-item`"
22
+ v-bind="removeItemIconAttrs"
23
+ @click.stop.prevent="onClickRemoveItem(file.id)"
19
24
  />
20
25
  </template>
21
-
22
- <span v-else v-bind="selectedItemAttrs" v-text="currentFiles.name" />
23
- </div>
26
+ </UFiles>
24
27
 
25
28
  <div v-bind="buttonWrapperAttrs">
26
29
  <template v-if="Array.isArray(currentFiles) || !currentFiles">
@@ -31,6 +34,7 @@
31
34
  variant="thirdary"
32
35
  :size="nestedComponentSize"
33
36
  :label="currentLocale.uploadFile"
37
+ :data-cy="`${dataCy}-upload`"
34
38
  v-bind="buttonAttrs"
35
39
  >
36
40
  <template #right>
@@ -61,6 +65,7 @@
61
65
  interactive
62
66
  :size="nestedComponentSize"
63
67
  :name="config.clearIconName"
68
+ :data-cy="`${dataCy}-clear`"
64
69
  v-bind="clearIconAttrs"
65
70
  @click="onClickResetFiles"
66
71
  />
@@ -71,13 +76,14 @@
71
76
  </template>
72
77
 
73
78
  <script setup>
74
- import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
79
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
75
80
  import { merge } from "lodash-es";
76
81
 
77
82
  import UText from "../ui.text-block";
78
83
  import UIcon from "../ui.image-icon";
79
84
  import ULabel from "../ui.form-label";
80
85
  import UButton from "../ui.button";
86
+ import UFiles from "../ui.text-files";
81
87
 
82
88
  import UIService, { getRandomId } from "../service.ui";
83
89
  import { getFileMbSize } from "./services/fileForm.service";
@@ -178,6 +184,14 @@ const props = defineProps({
178
184
  type: Object,
179
185
  default: () => ({}),
180
186
  },
187
+
188
+ /**
189
+ * Data-cy attribute for automated testing.
190
+ */
191
+ dataCy: {
192
+ type: String,
193
+ default: "",
194
+ },
181
195
  });
182
196
 
183
197
  const emit = defineEmits(["update:modelValue", "update:error"]);
@@ -200,7 +214,7 @@ const {
200
214
  inputAttrs,
201
215
  fileListAttrs,
202
216
  buttonWrapperAttrs,
203
- selectedItemAttrs,
217
+ removeItemIconAttrs,
204
218
  } = useAttrs(props);
205
219
 
206
220
  const i18nGlobal = tm(UInputFile);
@@ -234,7 +248,7 @@ const accept = computed(() => {
234
248
 
235
249
  const isValue = computed(() => {
236
250
  return (
237
- (Array.isArray(currentFiles.value) && currentFiles.value.length) ||
251
+ (Array.isArray(currentFiles.value) && currentFiles.value?.length) ||
238
252
  (!Array.isArray(currentFiles.value) && currentFiles.value)
239
253
  );
240
254
  });
@@ -249,6 +263,14 @@ const nestedComponentSize = computed(() => {
249
263
  return size;
250
264
  });
251
265
 
266
+ const fileList = computed(() => {
267
+ if (Array.isArray(currentFiles.value)) {
268
+ return currentFiles.value;
269
+ }
270
+
271
+ return currentFiles.value ? [currentFiles.value] : [];
272
+ });
273
+
252
274
  onMounted(() => {
253
275
  dropZoneRef.value.addEventListener("dragover", onDragOver);
254
276
  dropZoneRef.value.addEventListener("dragleave", onDragLeave);
@@ -259,6 +281,23 @@ onBeforeUnmount(() => {
259
281
  dropZoneRef.value.removeEventListener("dragleave", onDragLeave);
260
282
  });
261
283
 
284
+ watch(
285
+ () => props.multiple,
286
+ () => {
287
+ if (!props.multiple && Array.isArray(currentFiles.value)) {
288
+ currentFiles.value = currentFiles.value[0];
289
+ }
290
+
291
+ if (props.multiple && !Array.isArray(currentFiles.value)) {
292
+ currentFiles.value = currentFiles.value ? [currentFiles.value] : [];
293
+ }
294
+ },
295
+ );
296
+
297
+ function removeDuplicates(files) {
298
+ return files.filter((file) => !fileList.value.find((item) => item.name === file.name));
299
+ }
300
+
262
301
  function validate(file) {
263
302
  const targetFileSize = getFileMbSize(file);
264
303
 
@@ -287,7 +326,7 @@ function onChangeFile(event) {
287
326
  }
288
327
 
289
328
  currentFiles.value = props.multiple
290
- ? [...currentFiles.value, Array.from(event.target.files).at(0)]
329
+ ? [...(currentFiles.value || []), ...removeDuplicates(Array.from(event.target.files))]
291
330
  : Array.from(event.target.files).at(0);
292
331
 
293
332
  if (fileInputRef.value) fileInputRef.value.value = "";
@@ -314,24 +353,32 @@ function onDragLeave(event) {
314
353
  function onDrop(event) {
315
354
  event.preventDefault();
316
355
 
317
- let targetFile = null;
356
+ let targetFiles = null;
318
357
 
319
358
  if (event.dataTransfer.items) {
320
- targetFile = [...event.dataTransfer.items].find((item) => item.kind === "file")?.getAsFile();
359
+ targetFiles = [...event.dataTransfer.items]
360
+ .filter((item) => item.kind === "file")
361
+ .map((item) => item.getAsFile());
321
362
  } else {
322
- targetFile = [...event.dataTransfer.files].at(0);
363
+ targetFiles = [...event.dataTransfer.files];
323
364
  }
324
365
 
325
- if (targetFile) validate(targetFile);
366
+ if (targetFiles.length) targetFiles.forEach(validate);
326
367
 
327
368
  nextTick(() => {
328
- if (currentError.value || !targetFile) {
369
+ if (currentError.value || !targetFiles.length) {
329
370
  onClickResetFiles();
330
371
 
331
372
  return;
332
373
  }
333
374
 
334
- currentFiles.value = props.multiple ? [...currentFiles.value, targetFile] : targetFile;
375
+ currentFiles.value = props.multiple
376
+ ? [...(currentFiles.value || []), ...removeDuplicates(targetFiles)]
377
+ : targetFiles[0];
335
378
  });
336
379
  }
380
+
381
+ function onClickRemoveItem(id) {
382
+ currentFiles.value = currentFiles.value?.filter((file) => file.name !== id);
383
+ }
337
384
  </script>
@@ -43,9 +43,5 @@ export default /*tw*/ {
43
43
  bordered: false,
44
44
  closeIcon: false,
45
45
  },
46
- safelist: (colors) => [
47
- { pattern: `bg-(${colors})-50` },
48
- { pattern: `text-(${colors})-700` },
49
- { pattern: `border-(${colors})-100` },
50
- ],
46
+ safelist: (colors) => [{ pattern: `bg-(${colors})-50` }, { pattern: `text-(${colors})-700` }, { pattern: `border-(${colors})-100` }],
51
47
  };
@@ -6,32 +6,33 @@ import defaultConfig from "../configs/default.config";
6
6
 
7
7
  export function useAttrs(props, { focus }) {
8
8
  const { config, getAttrs } = useUI(defaultConfig, () => props.config);
9
- const { text } = config.value;
9
+ const { label } = config.value;
10
10
 
11
- const cvaText = cva({
12
- base: text.base,
13
- variants: text.variants,
14
- compoundVariants: text.compoundVariants,
11
+ const cvaLabel = cva({
12
+ base: label.base,
13
+ variants: label.variants,
14
+ compoundVariants: label.compoundVariants,
15
15
  });
16
16
 
17
- const textClasses = computed(() =>
18
- cvaText({
17
+ const labelClasses = computed(() =>
18
+ cvaLabel({
19
19
  focus: Boolean(focus.value),
20
+ size: props.size,
20
21
  }),
21
22
  );
22
23
 
23
- const textAttrs = getAttrs("text", { classes: textClasses });
24
+ const labelAttrs = getAttrs("label", { classes: labelClasses });
24
25
  const fileAttrs = getAttrs("file", { isComponent: true });
25
26
  const infoAttrs = getAttrs("info");
26
27
  const iconAttrs = getAttrs("icon", { isComponent: true });
27
- const itemImageAttrs = getAttrs("itemImage");
28
+ const imageAttrs = getAttrs("image");
28
29
 
29
30
  return {
30
31
  config,
31
32
  fileAttrs,
32
33
  infoAttrs,
33
34
  iconAttrs,
34
- textAttrs,
35
- itemImageAttrs,
35
+ labelAttrs,
36
+ imageAttrs,
36
37
  };
37
38
  }
@@ -1,15 +1,20 @@
1
1
  export default /*tw*/ {
2
2
  file: "",
3
- info: "flex items-start",
4
- icon: "mr-3",
3
+ info: "flex items-center gap-2 ",
4
+ icon: "",
5
5
  iconName: "description",
6
- itemImage: "rounded-sm",
7
- text: {
8
- base: "whitespace-normal pt-0.5 hover:underline hover:decoration-dashed hover:underline-offset-4",
6
+ image: "rounded-sm max-w-7",
7
+ label: {
8
+ base: "text-gray-700 whitespace-normal hover:underline hover:decoration-dashed hover:underline-offset-4",
9
9
  variants: {
10
10
  focus: {
11
11
  true: "underline decoration-dashed underline-offset-4",
12
12
  },
13
+ size: {
14
+ sm: "text-sm",
15
+ md: "text-base",
16
+ lg: "text-lg",
17
+ },
13
18
  },
14
19
  },
15
20
  };
@@ -0,0 +1,5 @@
1
+ /*
2
+ This const is needed to prevent the issue in script setup:
3
+ `defineProps` is referencing locally declared variables. (vue/valid-define-props)
4
+ */
5
+ export const UFile = "UFile";
@@ -1,9 +1,9 @@
1
1
  <template>
2
2
  <ULink :url="url" no-ring target-blank :data-cy="dataCy" v-bind="fileAttrs">
3
- <slot name="left" :file="{ id, text, url, imageUrl }" />
3
+ <slot name="left" :file="{ id, label, url, imageUrl }" />
4
4
 
5
5
  <div v-bind="infoAttrs">
6
- <img v-if="imageUrl" :src="imageUrl" v-bind="itemImageAttrs" />
6
+ <img v-if="imageUrl" :src="imageUrl" v-bind="imageAttrs" />
7
7
 
8
8
  <UIcon
9
9
  v-else
@@ -16,7 +16,7 @@
16
16
  @blur="onBlur"
17
17
  />
18
18
 
19
- <span v-bind="textAttrs" v-text="label" />
19
+ <span v-bind="labelAttrs" v-text="label" />
20
20
  </div>
21
21
 
22
22
  <slot name="right" :file="{ id, label, url, imageUrl }" />
@@ -28,9 +28,12 @@ import { ref } from "vue";
28
28
 
29
29
  import ULink from "../ui.button-link";
30
30
  import UIcon from "../ui.image-icon";
31
- import { getRandomId } from "../service.ui";
31
+
32
+ import UIService, { getRandomId } from "../service.ui";
32
33
 
33
34
  import { useAttrs } from "./composables/attrs.composable";
35
+ import { UFile } from "./constants";
36
+ import defaultConfig from "./configs/default.config";
34
37
 
35
38
  /* Should be a string for correct web-types gen */
36
39
  defineOptions({ name: "UFile", inheritAttrs: false });
@@ -69,6 +72,15 @@ const props = defineProps({
69
72
  default: () => getRandomId(),
70
73
  },
71
74
 
75
+ /**
76
+ * Set size.
77
+ * @values sm, md, lg
78
+ */
79
+ size: {
80
+ type: String,
81
+ default: UIService.get(defaultConfig, UFile).default.size,
82
+ },
83
+
72
84
  /**
73
85
  * Sets data-cy attribute for automated testing.
74
86
  */
@@ -80,7 +92,7 @@ const props = defineProps({
80
92
 
81
93
  const focus = ref(false);
82
94
 
83
- const { config, fileAttrs, infoAttrs, iconAttrs, textAttrs, itemImageAttrs } = useAttrs(props, {
95
+ const { config, fileAttrs, infoAttrs, iconAttrs, labelAttrs, imageAttrs } = useAttrs(props, {
84
96
  focus,
85
97
  });
86
98
 
@@ -1,17 +1,26 @@
1
+ import { computed } from "vue";
1
2
  import useUI from "../../composable.ui";
3
+ import { cva } from "../../service.ui";
2
4
 
3
5
  import defaultConfig from "../configs/default.config";
4
6
 
5
7
  export function useAttrs(props) {
6
- const { getAttrs } = useUI(defaultConfig, () => props.config);
8
+ const { getAttrs, config } = useUI(defaultConfig, () => props.config);
9
+ const { body } = config.value;
10
+
11
+ const cvaBody = cva({
12
+ base: body.base,
13
+ variants: body.variants,
14
+ compoundVariants: body.compoundVariants,
15
+ });
16
+
17
+ const bodyClasses = computed(() => cvaBody({ label: Boolean(props.label) }));
7
18
 
8
- const filesAttrs = getAttrs("files");
9
19
  const labelAttrs = getAttrs("label");
10
- const bodyAttrs = getAttrs("body");
20
+ const bodyAttrs = getAttrs("body", { classes: bodyClasses });
11
21
  const fileAttrs = getAttrs("file", { isComponent: true });
12
22
 
13
23
  return {
14
- filesAttrs,
15
24
  labelAttrs,
16
25
  bodyAttrs,
17
26
  fileAttrs,
@@ -1,9 +1,16 @@
1
1
  export default /*tw*/ {
2
- files: "",
3
2
  label: "block text-sm font-normal text-gray-500",
4
- body: "space-y-2 mt-4",
3
+ body: {
4
+ base: "flex gap-4 flex-col",
5
+ variants: {
6
+ label: {
7
+ true: "mt-4",
8
+ },
9
+ },
10
+ },
5
11
  file: "block",
6
12
  defaultVariants: {
7
13
  placement: "topInside",
14
+ size: "md",
8
15
  },
9
16
  };
@@ -11,9 +11,9 @@ export default {
11
11
  component: UFiles,
12
12
  args: {
13
13
  label: "Label",
14
- options: [
15
- { text: "some file text 1", url: "https://storybook.js.org/" },
16
- { text: "some file text 2", url: "https://storybook.js.org/" },
14
+ fileList: [
15
+ new File(["foo"], "foo.txt", { type: "text/plain" }),
16
+ new File(["bar"], "foo.txt", { type: "text/plain" }),
17
17
  ],
18
18
  },
19
19
  argTypes: {
@@ -1,31 +1,31 @@
1
1
  <template>
2
- <div :data-cy="dataCy" v-bind="filesAttrs">
3
- <ULabel :label="label" :description="description" v-bind="labelAttrs">
4
- <div v-bind="bodyAttrs">
5
- <slot>
6
- <UFile
7
- v-for="(option, index) in options"
8
- :id="option.id"
9
- :key="option.text"
10
- :label="option.text"
11
- :url="option.url"
12
- :data-cy="`${dataCy}-item-${index}`"
13
- v-bind="fileAttrs"
14
- >
15
- <template #left="{ file }">
16
- <!-- @slot Use it to add something left. -->
17
- <slot name="left" :file="file" />
18
- </template>
2
+ <ULabel :label="label" :description="description" v-bind="labelAttrs">
3
+ <div v-bind="bodyAttrs">
4
+ <slot>
5
+ <UFile
6
+ v-for="file in formattedFileList"
7
+ :id="file.id"
8
+ :key="file.id"
9
+ :label="file.label"
10
+ :url="file.url"
11
+ :image-url="file.imageUrl"
12
+ :size="size"
13
+ v-bind="fileAttrs"
14
+ :data-cy="`${dataCy}-item`"
15
+ >
16
+ <template #left="{ file: currentFile }">
17
+ <!-- @slot Use it to add something left. -->
18
+ <slot name="left" :file="currentFile" />
19
+ </template>
19
20
 
20
- <template #right="{ file }">
21
- <!-- @slot Use it to add something right. -->
22
- <slot name="right" :file="file" />
23
- </template>
24
- </UFile>
25
- </slot>
26
- </div>
27
- </ULabel>
28
- </div>
21
+ <template #right="{ file: currentFile }">
22
+ <!-- @slot Use it to add something right. -->
23
+ <slot name="right" :file="currentFile" />
24
+ </template>
25
+ </UFile>
26
+ </slot>
27
+ </div>
28
+ </ULabel>
29
29
  </template>
30
30
 
31
31
  <script setup>
@@ -36,6 +36,7 @@ import UIService from "../service.ui";
36
36
  import { UFiles } from "./constants";
37
37
  import defaultConfig from "./configs/default.config";
38
38
  import { useAttrs } from "./composables/attrs.composable";
39
+ import { computed } from "vue";
39
40
 
40
41
  /* Should be a string for correct web-types gen */
41
42
  defineOptions({ name: "UFiles", inheritAttrs: false });
@@ -67,13 +68,22 @@ const props = defineProps({
67
68
  },
68
69
 
69
70
  /**
70
- * Set options for files.
71
+ * List of file objects.
71
72
  */
72
- options: {
73
+ fileList: {
73
74
  type: Array,
74
75
  default: () => [],
75
76
  },
76
77
 
78
+ /**
79
+ * Set size.
80
+ * @values sm, md, lg
81
+ */
82
+ size: {
83
+ type: String,
84
+ default: UIService.get(defaultConfig, UFiles).default.size,
85
+ },
86
+
77
87
  /**
78
88
  * Sets data-cy attribute for automated testing.
79
89
  */
@@ -83,5 +93,16 @@ const props = defineProps({
83
93
  },
84
94
  });
85
95
 
86
- const { filesAttrs, labelAttrs, bodyAttrs, fileAttrs } = useAttrs(props);
96
+ const { labelAttrs, bodyAttrs, fileAttrs } = useAttrs(props);
97
+
98
+ const formattedFileList = computed(() =>
99
+ props.fileList.map((file) => {
100
+ return {
101
+ id: file.name,
102
+ label: file.name,
103
+ url: URL.createObjectURL(file),
104
+ imageUrl: file.type.includes("image") ? URL.createObjectURL(file) : undefined,
105
+ };
106
+ }),
107
+ );
87
108
  </script>
package/web-types.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "framework": "vue",
3
3
  "name": "vueless",
4
- "version": "0.0.159",
4
+ "version": "0.0.161",
5
5
  "contributions": {
6
6
  "html": {
7
7
  "description-markup": "markdown",
@@ -2840,6 +2840,15 @@
2840
2840
  },
2841
2841
  "default": "() => getRandomId()"
2842
2842
  },
2843
+ {
2844
+ "name": "size",
2845
+ "description": "Set size.",
2846
+ "value": {
2847
+ "kind": "expression",
2848
+ "type": "'sm' | 'md' | 'lg'"
2849
+ },
2850
+ "default": "UIService.get(defaultConfig, UFile).default.size"
2851
+ },
2843
2852
  {
2844
2853
  "name": "dataCy",
2845
2854
  "description": "Sets data-cy attribute for automated testing.",
@@ -2895,14 +2904,23 @@
2895
2904
  "default": "topInside"
2896
2905
  },
2897
2906
  {
2898
- "name": "options",
2899
- "description": "Set options for files.",
2907
+ "name": "fileList",
2908
+ "description": "List of file objects.",
2900
2909
  "value": {
2901
2910
  "kind": "expression",
2902
2911
  "type": "array"
2903
2912
  },
2904
2913
  "default": "[]"
2905
2914
  },
2915
+ {
2916
+ "name": "size",
2917
+ "description": "Set size.",
2918
+ "value": {
2919
+ "kind": "expression",
2920
+ "type": "'sm' | 'md' | 'lg'"
2921
+ },
2922
+ "default": "md"
2923
+ },
2906
2924
  {
2907
2925
  "name": "dataCy",
2908
2926
  "description": "Sets data-cy attribute for automated testing.",
@@ -3595,6 +3613,15 @@
3595
3613
  "type": "object"
3596
3614
  },
3597
3615
  "default": "{}"
3616
+ },
3617
+ {
3618
+ "name": "dataCy",
3619
+ "description": "Data-cy attribute for automated testing.",
3620
+ "value": {
3621
+ "kind": "expression",
3622
+ "type": "string"
3623
+ },
3624
+ "default": "\"\""
3598
3625
  }
3599
3626
  ],
3600
3627
  "events": [