@witchcraft/ui 0.3.10 → 0.3.12

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.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "witchcraftUi",
3
3
  "configKey": "witchcraftUi",
4
- "version": "0.3.10",
4
+ "version": "0.3.12",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -1 +1 @@
1
- @custom-variant dark (&:where(.dark, .dark *));@utility focus-outline-within{@reference outlined-within:outline-2 outlined-within:outline-accent-500 outlined-within:outline-offset-2}@utility focus-outline{@reference outlined:outline-2 outlined:outline-accent-500 outlined:outline-offset-2}@utility focus-outline-no-offset{@reference outlined:outline-2 outlined:outline-accent-500}@utility focus-outline-hidden{@reference outlined:outline-none}@utility bg-squares-gradient{--_square:var(--squareSize,5px);--_double_square:calc(var(--_square)*2);--_light_square:var(--lightSquare,var(--color-white));--_dark_square:var(--darkSquare,var(--color-black));background-color:var(--_light_square);background:repeating-conic-gradient(var(--_dark_square) 0 25%,var(--_light_square) 0 50%) 50% /var(--_double_square) var(--_double_square)}@utility square-light-*{--lightSquare:--value(--color- *)}@utility square-dark-*{--darkSquare:--value(--color- *)}@utility square-size-*{--squareSize:calc(--value(integer) * 1px)}@utility bg-bars-gradient{--_bg_color:var(--bars-bg-color,var(--color-accent-700));--_fg_color:var(--bars-fg-color,var(--color-accent-800));--_angle:var(--bars-angle,45deg);--_fg_width:var(--bars-fg-width,50%);--_bg_width:calc(100% - var(--_fg_width));background-color:var(--_bg_color);--_pos_1:calc(var(--_bg_width)/2);--_pos_2:calc(var(--_bg_width)/2 + var(--_fg_width)/2);--_pos_3:calc(var(--_bg_width) + var(--_fg_width)/2);background-image:repeating-linear-gradient(var(--_angle),var(--_bg_color),var(--_bg_color) var(--_pos_1),var(--_fg_color) var(--_pos_1),var(--_fg_color) var(--_pos_2),var(--_bg_color) var(--_pos_2),var(--_bg_color) var(--_pos_3),var(--_fg_color) var(--_pos_3),var(--_fg_color))}@utility bars-angle-*{--bars-angle:var(--value(integer) * 1deg)}@utility bars-fg-*{--bars-fg-color:--value(--color-*)}@utility bars-bg-*{--bars-bg-color:--value(--color-*)}@utility bars-w-*{--bars-fg-width:calc(--value(integer) * 1%, 50%)}@utility scrollbar-hidden{-ms-overflow-style:none;scrollbar-width:none;&::-webkit-scrollbar{display:none}}@utility styled-scrollbar{--_scrollbar_width:var(--scrollbar-width,calc(var(--spacing)*3));--_scrollbar_border_width:var(--scrollbar-border-width,calc(var(--spacing)/2));--_scrollbar_color:var(--scrollbar-color,--alpha(var(--color-accent-500)/40%));--_scrollbar_hover_color:var(--scrollbar-hover-color,--alpha(var(--color-accent-500)/80%));--_scrollbar_bg_color:var(--scrollbar-bg-color,var(--color-bg));.dark &{--_scrollbar_bg_color:var(--scrollbar-bg-color,var(--color-fg))}&::-webkit-scrollbar{height:var(--_scrollbar_width);width:var(--_scrollbar_width)}&::-webkit-scrollbar-corner,&::-webkit-scrollbar-track{background-color:transparent}&::-webkit-scrollbar-thumb,&::-webkit-scrollbar-track{border-radius:var(--_scrollbar_width)}&::-webkit-scrollbar-thumb{background-color:var(--_scrollbar_color);border:var(--_scrollbar_border_width) solid var(--_scrollbar_bg_color)}&::-webkit-scrollbar-thumb:hover{cursor:pointer}&::-webkit-scrollbar-thumb:active,&::-webkit-scrollbar-thumb:hover{background-color:var(--_scrollbar_hover_color);border-radius:var(--_scrollbar_width)}}@utility styled-scrollbar-w-*{--scrollbar-width:--value(integer)}@utility styled-scrollbar-border-w-*{--scrollbar-border-width:--value(integer)}@utility styled-scrollbar-*{--scrollbar-color:--value(--color-*)}@utility styled-scrollbar-bg-*{--scrollbar-bg-color:--value(--color-*)}@utility styled-resizer{--_resizer_width:var(--resizer-width,8px);--_resizer_color:var(--resizer-color,var(--color-neutral-300));.dark &{--_resizer_color:var(--resizer-color,var(--color-neutral-700))}&::-webkit-resizer{border-bottom-color:var(--_resizer_color);border-left-color:transparent;border-right-color:var(--_resizer_color);border-style:solid;border-top-color:transparent;border-width:var(--_resizer_width)}}@utility styled-resizer-w-*{--resizer-width:--value(integer)}@utility styled-resizer-color-*{--resizer-color:--value(--color-*)}@utility content-vertical-holder{--tw-content:"\200b";content:var(--tw-content)}@utility no-touch-action{touch-action:none}@utility bg-transparency-squares{@apply bg-squares-gradient square-light-white square-dark-neutral-300 square-size-6}@utility link-like{@apply cursor-pointer hover:text-accent-500}
1
+ @custom-variant dark (&:where(.dark, .dark *));@utility focus-outline-within{@apply outlined-within:outline-2 outlined-within:outline-accent-500 outlined-within:outline-offset-2}@utility focus-outline{@apply outlined:outline-2 outlined:outline-accent-500 outlined:outline-offset-2}@utility focus-outline-no-offset{@apply outlined:outline-2 outlined:outline-accent-500}@utility focus-outline-hidden{@apply outlined:outline-none}@utility bg-squares-gradient{--_square:var(--squareSize,5px);--_double_square:calc(var(--_square)*2);--_light_square:var(--lightSquare,var(--color-white));--_dark_square:var(--darkSquare,var(--color-black));background-color:var(--_light_square);background:repeating-conic-gradient(var(--_dark_square) 0 25%,var(--_light_square) 0 50%) 50% /var(--_double_square) var(--_double_square)}@utility square-light-*{--lightSquare:--value(--color- *)}@utility square-dark-*{--darkSquare:--value(--color- *)}@utility square-size-*{--squareSize:calc(--value(integer) * 1px)}@utility bg-bars-gradient{--_bg_color:var(--bars-bg-color,var(--color-accent-700));--_fg_color:var(--bars-fg-color,var(--color-accent-800));--_angle:var(--bars-angle,45deg);--_fg_width:var(--bars-fg-width,50%);--_bg_width:calc(100% - var(--_fg_width));background-color:var(--_bg_color);--_pos_1:calc(var(--_bg_width)/2);--_pos_2:calc(var(--_bg_width)/2 + var(--_fg_width)/2);--_pos_3:calc(var(--_bg_width) + var(--_fg_width)/2);background-image:repeating-linear-gradient(var(--_angle),var(--_bg_color),var(--_bg_color) var(--_pos_1),var(--_fg_color) var(--_pos_1),var(--_fg_color) var(--_pos_2),var(--_bg_color) var(--_pos_2),var(--_bg_color) var(--_pos_3),var(--_fg_color) var(--_pos_3),var(--_fg_color))}@utility bars-angle-*{--bars-angle:var(--value(integer) * 1deg)}@utility bars-fg-*{--bars-fg-color:--value(--color-*)}@utility bars-bg-*{--bars-bg-color:--value(--color-*)}@utility bars-w-*{--bars-fg-width:calc(--value(integer) * 1%, 50%)}@utility scrollbar-hidden{-ms-overflow-style:none;scrollbar-width:none;&::-webkit-scrollbar{display:none}}@utility styled-scrollbar{--_scrollbar_width:var(--scrollbar-width,calc(var(--spacing)*3));--_scrollbar_border_width:var(--scrollbar-border-width,calc(var(--spacing)/2));--_scrollbar_color:var(--scrollbar-color,--alpha(var(--color-accent-500)/40%));--_scrollbar_hover_color:var(--scrollbar-hover-color,--alpha(var(--color-accent-500)/80%));--_scrollbar_bg_color:var(--scrollbar-bg-color,var(--color-bg));.dark &{--_scrollbar_bg_color:var(--scrollbar-bg-color,var(--color-fg))}&::-webkit-scrollbar{height:var(--_scrollbar_width);width:var(--_scrollbar_width)}&::-webkit-scrollbar-corner,&::-webkit-scrollbar-track{background-color:transparent}&::-webkit-scrollbar-thumb,&::-webkit-scrollbar-track{border-radius:var(--_scrollbar_width)}&::-webkit-scrollbar-thumb{background-color:var(--_scrollbar_color);border:var(--_scrollbar_border_width) solid var(--_scrollbar_bg_color)}&::-webkit-scrollbar-thumb:hover{cursor:pointer}&::-webkit-scrollbar-thumb:active,&::-webkit-scrollbar-thumb:hover{background-color:var(--_scrollbar_hover_color);border-radius:var(--_scrollbar_width)}}@utility styled-scrollbar-w-*{--scrollbar-width:--value(integer)}@utility styled-scrollbar-border-w-*{--scrollbar-border-width:--value(integer)}@utility styled-scrollbar-*{--scrollbar-color:--value(--color-*)}@utility styled-scrollbar-bg-*{--scrollbar-bg-color:--value(--color-*)}@utility styled-resizer{--_resizer_width:var(--resizer-width,8px);--_resizer_color:var(--resizer-color,var(--color-neutral-300));.dark &{--_resizer_color:var(--resizer-color,var(--color-neutral-700))}&::-webkit-resizer{border-bottom-color:var(--_resizer_color);border-left-color:transparent;border-right-color:var(--_resizer_color);border-style:solid;border-top-color:transparent;border-width:var(--_resizer_width)}}@utility styled-resizer-w-*{--resizer-width:--value(integer)}@utility styled-resizer-color-*{--resizer-color:--value(--color-*)}@utility content-vertical-holder{--tw-content:"\200b";content:var(--tw-content)}@utility no-touch-action{touch-action:none}@utility bg-transparency-squares{@apply bg-squares-gradient square-light-white square-dark-neutral-300 square-size-6}@utility link-like{@apply cursor-pointer hover:text-accent-500}
@@ -1,11 +1,13 @@
1
1
  import { type HTMLAttributes, type InputHTMLAttributes } from "vue";
2
2
  import type { FileInputError } from "../../types/index.js";
3
3
  import { type LinkableByIdProps, type TailwindClassProp, type WrapperProps } from "../shared/props.js";
4
- declare const _default: __VLS_WithSlots<import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
5
- input: (val: File[]) => any;
4
+ declare const _default: __VLS_WithSlots<import("vue").DefineComponent<Props, {
5
+ clearFiles: () => void;
6
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
7
+ input: (val: File[], clearFiles: () => void) => any;
6
8
  errors: (val: FileInputError[]) => any;
7
9
  }, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
8
- onInput?: ((val: File[]) => any) | undefined;
10
+ onInput?: ((val: File[], clearFiles: () => void) => any) | undefined;
9
11
  onErrors?: ((val: FileInputError[]) => any) | undefined;
10
12
  }>, {
11
13
  multiple: boolean;
@@ -2,30 +2,52 @@
2
2
  <!-- todo aria errors -->
3
3
  <div
4
4
  :class="twMerge(
5
- `file-input
5
+ `
6
+ file-input
6
7
  justify-center
7
8
  border-2
8
9
  border-dashed
9
10
  border-accent-500/80
10
11
  focus-outline-within
11
12
  transition-[border-color,box-shadow]
12
- ease-out`,
13
+ ease-out
14
+ hover:bg-accent-500/10
15
+ outlined-focus-within
16
+ `,
13
17
  compact && `rounded-sm`,
14
- !compact && `flex w-full flex-col items-center gap-2 rounded-xl p-2 `,
18
+ !compact && `
19
+ flex
20
+ w-full
21
+ flex-col
22
+ items-stretch
23
+ gap-2
24
+ rounded-xl
25
+ p-2
26
+ `,
15
27
  errors.length > 0 && errorFlashing && `border-danger-400`,
28
+ isHovered && `bg-accent-500/10`,
16
29
  $.wrapperAttrs.class
17
30
  )"
18
31
  v-bind="{ ...$.wrapperAttrs, class: void 0 }"
32
+ @drop="onDrop"
33
+ @dragover.prevent="isHovered = true"
34
+ @dragleave="isHovered = false"
19
35
  >
20
36
  <div
21
37
  :class="twMerge(
22
38
  `
23
- file-input--wrapper
24
- relative justify-center`,
39
+ file-input--wrapper
40
+ relative
41
+ justify-center
42
+ @container
43
+ `,
25
44
  compact && `flex gap-2`,
26
- !compact && `input-wrapper
27
- flex flex-col items-center
28
- `
45
+ !compact && `
46
+ file-input
47
+ flex
48
+ flex-col
49
+ items-center
50
+ `
29
51
  )"
30
52
  >
31
53
  <label
@@ -36,7 +58,9 @@
36
58
  flex
37
59
  gap-1
38
60
  items-center
61
+ justify-center
39
62
  whitespace-nowrap
63
+ max-w-full
40
64
  `)"
41
65
  >
42
66
  <slot
@@ -46,23 +70,37 @@
46
70
  <icon><i-fa6-solid-arrow-up-from-bracket/></icon>
47
71
  </slot>
48
72
  <slot name="label">
49
- {{
50
- compact ? multiple ? t("file-input.compact-choose-file-plural") : t("file-input.compact-choose-file") : multiple ? t("file-input.non-compact-choose-file-plural") : t("file-input.non-compact-choose-file")
51
- }}
73
+ <div class="text-ellipsis overflow-hidden shrink-1 hidden @min-[15ch]:block">
74
+ {{
75
+ compact ? multiple ? t("file-input.compact-choose-file-plural") : t("file-input.compact-choose-file") : multiple ? t("file-input.non-compact-choose-file-plural") : t("file-input.non-compact-choose-file")
76
+ }}
77
+ </div>
52
78
  </slot>
53
- <span
79
+ <div
54
80
  v-if="compact && multiple"
55
81
  class="file-input--label-count"
56
82
  >
57
83
  {{ ` (${files.length})` }}
58
- </span>
84
+ </div>
85
+ <div
86
+ v-if="compact && !multiple && files.length > 0"
87
+ class="file-input--label-name text-ellipsis overflow-hidden shrink-9999 hidden @3xs:block"
88
+ >
89
+ {{ ` (${files[0]?.file.name})` }}
90
+ </div>
91
+ <div
92
+ v-if="compact && !multiple && files.length > 0"
93
+ class="file-input--label-name text-ellipsis overflow-hidden shrink-9999 @3xs:hidden"
94
+ >
95
+ {{ ` (...)` }}
96
+ </div>
59
97
  </label>
60
98
  <label
61
99
  v-if="!compact && formats?.length > 0"
62
- class="file-input--formats-label flex flex-col items-center text-sm"
100
+ class="file-input--formats-label flex-col items-center text-sm max-w-full hidden @min-[15ch]:flex"
63
101
  >
64
- <slot name="formats">{{ t("file-input.accepted-formats") }}: </slot>
65
- <div class="file-input--formats-list">
102
+ <slot name="formats"><div class="text-ellipsis overflow-hidden max-w-full">{{ t("file-input.accepted-formats") }}:</div> </slot>
103
+ <div class="file-input--formats-list overflow-hidden text-ellipsis max-w-full">
66
104
  {{ extensions.join(", ") }}
67
105
  </div>
68
106
  </label>
@@ -70,14 +108,14 @@
70
108
  :id="id ?? fallbackId"
71
109
  :class="twMerge(
72
110
  `
73
- file-input--input
74
- absolute
75
- inset-0
76
- z-0
77
- cursor-pointer
78
- text-[0]
79
- opacity-0
80
- `,
111
+ file-input--input
112
+ absolute
113
+ inset-[calc(var(--spacing)*-2)]
114
+ cursor-pointer
115
+ z-0
116
+ text-[0]
117
+ opacity-0
118
+ `,
81
119
  $.inputAttrs?.class
82
120
  )"
83
121
  type="file"
@@ -94,7 +132,7 @@
94
132
  v-if="!compact && files.length > 0"
95
133
  :class="twMerge(
96
134
  `file-input--previews
97
- flex items-stretch justify-center gap-2 flex-wrap
135
+ flex items-stretch justify-center gap-4 flex-wrap
98
136
  `,
99
137
  multiple && `
100
138
  w-full
@@ -102,32 +140,47 @@
102
140
  $.previewsAttrs?.class
103
141
  )"
104
142
  >
105
- <div class="file-input--preview-spacer flex-1"/>
106
143
  <div
107
- class="file-input--preview-wrapper
144
+ class="
145
+ file-input--preview-wrapper
108
146
  z-1
109
147
  relative
110
148
  flex
111
149
  min-w-0
112
150
  max-w-[150px]
113
151
  flex-initial
114
- flex-wrap
152
+ flex-col
115
153
  items-center
116
- gap-2 rounded-sm border border-neutral-400
117
- shadow-xs
118
- shadow-neutral-800/20
154
+ gap-1
155
+ p-1
156
+ rounded-sm
157
+ border
158
+ border-neutral-300
159
+ dark:border-neutral-800
160
+ shadow-md
161
+ shadow-neutral-800/30
162
+ bg-neutral-100
163
+ dark:bg-neutral-900
164
+ [&:hover_.file-input--remove-button]:opacity-100
119
165
  "
120
166
  v-for="entry of files"
121
167
  :key="entry.file.name"
122
168
  >
123
- <div class="file-input--remove-button flex flex-initial basis-full justify-start">
169
+ <div class="flex flex-initial basis-full justify-start items-center max-w-full gap-2 px-1">
124
170
  <lib-button
125
171
  :border="false"
172
+ class="file-input--remove-button rounded-full p-0"
126
173
  :aria-label="`Remove file ${entry.file.name}`"
127
174
  @click="removeFile(entry)"
128
175
  >
129
176
  <icon><i-fa6-solid-xmark/></icon>
130
177
  </lib-button>
178
+ <div
179
+ class="file-input--preview-filename min-w-0 flex-1 basis-0 truncate break-all rounded-sm text-sm"
180
+ :title="entry.file.name"
181
+ >
182
+ {{ entry.file.name }}
183
+ </div>
131
184
  </div>
132
185
 
133
186
  <div class="file-input--preview flex flex-initial basis-full justify-center">
@@ -154,25 +207,7 @@
154
207
  <icon><i-fa6-regular-file class="text-4xl opacity-50"/></icon>
155
208
  </div>
156
209
  </div>
157
- <div
158
- class="
159
- file-input--preview-filename
160
- min-w-0
161
- flex-1
162
- basis-0
163
- truncate
164
- break-all
165
- rounded-sm
166
- p-1
167
- text-sm
168
- "
169
- :title="entry.file.name"
170
- >
171
- {{ entry.file.name }}
172
- </div>
173
210
  </div>
174
-
175
- <div class="flex-1"/>
176
211
  </div>
177
212
  </div>
178
213
  </template>
@@ -193,8 +228,13 @@ const el = ref(null);
193
228
  const files = shallowReactive([]);
194
229
  const errors = shallowReactive([]);
195
230
  const errorFlashing = ref(false);
231
+ const isHovered = ref(false);
232
+ function clearFiles() {
233
+ el.value.value = "";
234
+ files.splice(0, files.length);
235
+ }
196
236
  watch(files, () => {
197
- emits("input", files.map((entry) => entry.file));
237
+ emits("input", files.map((entry) => entry.file), clearFiles);
198
238
  });
199
239
  watch(errors, () => {
200
240
  if (errors.length > 0) {
@@ -202,7 +242,8 @@ watch(errors, () => {
202
242
  setTimeout(() => {
203
243
  errorFlashing.value = false;
204
244
  }, 500);
205
- emits("errors", errors);
245
+ emits("errors", [...errors]);
246
+ errors.splice(0, errors.length);
206
247
  }
207
248
  });
208
249
  defineOptions({
@@ -229,44 +270,60 @@ const removeFile = (entry) => {
229
270
  files.splice(index, 1);
230
271
  };
231
272
  const extensionsList = computed(() => extensions.value.join(", "));
232
- const inputFile = async (e) => {
273
+ function onDrop(e) {
274
+ if ("dataTransfer" in e && e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
275
+ el.value.files = e.dataTransfer.files;
276
+ e.preventDefault();
277
+ isHovered.value = false;
278
+ return updateFiles(el.value.files);
279
+ }
280
+ return void 0;
281
+ }
282
+ async function inputFile(e) {
233
283
  e.preventDefault();
234
284
  if (el.value.files) {
235
- const errs = [];
236
- for (const file of el.value.files) {
237
- const isImg = file.type.startsWith("image");
238
- const byPassValidation = props.formats.length === 0;
239
- const isValidMimeType = mimeTypes.value.find((_) => _.endsWith("/*") ? file.type.startsWith(_.slice(0, -2)) : _ === file.type) !== void 0;
240
- const isValidExtension = extensions.value.find((_) => file.name.endsWith(_)) !== void 0;
241
- if (!byPassValidation && (!isValidMimeType || !isValidExtension)) {
242
- const extension = file.name.match(/.*(\..*)/)?.[1] ?? "Unknown";
243
- const type = file.type === "" ? "" : ` (${file.type})`;
244
- const message = `File type ${extension}${type} is not allowed. Allowed file types are: ${extensionsList.value}.`;
245
- const err = new Error(message);
246
- err.file = file;
247
- err.isValidExtension = isValidExtension;
248
- err.isValidMimeType = isValidMimeType;
249
- errs.push(err);
250
- continue;
251
- }
252
- if (errs.length > 0) continue;
253
- if (!files.find((_) => _.file === file)) {
254
- if (props.multiple || files.length < 1) {
255
- files.push({ file, isImg });
256
- } else {
257
- files.splice(0, files.length, { file, isImg });
258
- }
259
- }
285
+ return updateFiles(el.value.files);
286
+ }
287
+ return void 0;
288
+ }
289
+ function updateFiles(filesList) {
290
+ const errs = [];
291
+ for (const file of filesList) {
292
+ const isImg = file.type.startsWith("image");
293
+ const byPassValidation = props.formats.length === 0;
294
+ const isValidMimeType = mimeTypes.value.find((_) => _.endsWith("/*") ? file.type.startsWith(_.slice(0, -2)) : _ === file.type) !== void 0;
295
+ const isValidExtension = extensions.value.find((_) => file.name.endsWith(_)) !== void 0;
296
+ if (!byPassValidation && (!isValidMimeType || !isValidExtension)) {
297
+ const extension = file.name.match(/.*(\..*)/)?.[1] ?? "Unknown";
298
+ const type = file.type === "" ? "" : ` (${file.type})`;
299
+ const message = `File type ${extension}${type} is not allowed. Allowed file types are: ${extensionsList.value}.`;
300
+ const err = new Error(message);
301
+ err.file = file;
302
+ err.isValidExtension = isValidExtension;
303
+ err.isValidMimeType = isValidMimeType;
304
+ errs.push(err);
305
+ continue;
260
306
  }
261
- if (errs.length > 0) {
262
- errors.splice(0, errors.length, ...errs);
263
- return false;
264
- } else if (errors.length > 0) {
265
- errors.splice(0, errors.length);
307
+ if (errs.length > 0) continue;
308
+ if (!files.find((_) => _.file === file)) {
309
+ if (props.multiple || files.length < 1) {
310
+ files.push({ file, isImg });
311
+ } else {
312
+ files.splice(0, files.length, { file, isImg });
313
+ }
266
314
  }
267
315
  }
316
+ if (errs.length > 0) {
317
+ errors.splice(0, errors.length, ...errs);
318
+ return false;
319
+ } else if (errors.length > 0) {
320
+ errors.splice(0, errors.length);
321
+ }
268
322
  return void 0;
269
- };
323
+ }
324
+ defineExpose({
325
+ clearFiles
326
+ });
270
327
  </script>
271
328
 
272
329
  <script>
@@ -1,11 +1,13 @@
1
1
  import { type HTMLAttributes, type InputHTMLAttributes } from "vue";
2
2
  import type { FileInputError } from "../../types/index.js";
3
3
  import { type LinkableByIdProps, type TailwindClassProp, type WrapperProps } from "../shared/props.js";
4
- declare const _default: __VLS_WithSlots<import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
5
- input: (val: File[]) => any;
4
+ declare const _default: __VLS_WithSlots<import("vue").DefineComponent<Props, {
5
+ clearFiles: () => void;
6
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
7
+ input: (val: File[], clearFiles: () => void) => any;
6
8
  errors: (val: FileInputError[]) => any;
7
9
  }, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
8
- onInput?: ((val: File[]) => any) | undefined;
10
+ onInput?: ((val: File[], clearFiles: () => void) => any) | undefined;
9
11
  onErrors?: ((val: FileInputError[]) => any) | undefined;
10
12
  }>, {
11
13
  multiple: boolean;
@@ -11,7 +11,13 @@ Partial<Omit<HTMLAttributes, "class"> & TailwindClassProp>, RealProps {
11
11
  }
12
12
  declare const _default: __VLS_WithSlots<import("vue").DefineComponent<Props, {
13
13
  focus: () => void;
14
- }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{}>, {
14
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
15
+ pause: (notification: NotificationEntry) => any;
16
+ resume: (notification: NotificationEntry) => any;
17
+ }, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
18
+ onPause?: ((notification: NotificationEntry) => any) | undefined;
19
+ onResume?: ((notification: NotificationEntry) => any) | undefined;
20
+ }>, {
15
21
  handler: NotificationHandler;
16
22
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>, {
17
23
  top?: (props: {
@@ -20,13 +20,16 @@
20
20
  p-1
21
21
  text-sm
22
22
  focus:border-accent-500
23
+ focus-within:border-accent-500
23
24
  `,
24
25
  $attrs.class
25
26
  )"
26
27
  v-bind="{ ...$attrs, class: void 0 }"
27
28
  tabindex="0"
29
+ :data-id="notification.id"
28
30
  ref="notificationEl"
29
31
  @keydown.enter.self="NotificationHandler.resolveToDefault(notification)"
32
+ @pointerenter="notification.timeout && !notification.isPaused && emit('pause', notification)"
30
33
  >
31
34
  <slot
32
35
  name="top"
@@ -148,7 +151,7 @@
148
151
  </template>
149
152
 
150
153
  <script setup>
151
- import { computed, onMounted, ref, useAttrs } from "vue";
154
+ import { computed, onBeforeUnmount, onMounted, ref, useAttrs } from "vue";
152
155
  import IFa6RegularCopy from "~icons/fa6-regular/copy";
153
156
  import IFa6SolidXmark from "~icons/fa6-solid/xmark";
154
157
  import { useSlotVars } from "../../composables/useSlotVars.js";
@@ -167,13 +170,29 @@ const props = defineProps({
167
170
  notification: { type: null, required: true },
168
171
  handler: { type: Object, required: false, default: void 0 }
169
172
  });
173
+ const emit = defineEmits(["pause", "resume"]);
170
174
  const getColor = (notification, option) => {
171
175
  return notification.dangerous.includes(option) ? "danger" : notification.default === option ? "primary" : "secondary";
172
176
  };
173
177
  const buttonColors = computed(() => props.notification.options.map((option) => getColor(props.notification, option)));
174
178
  const notificationEl = ref(null);
179
+ const mousedownAbortController = new AbortController();
175
180
  onMounted(() => {
176
181
  notificationEl.value?.focus();
182
+ if (props.notification.timeout) {
183
+ window.addEventListener("pointerdown", (e) => {
184
+ if (!e.target || !(e.target instanceof HTMLElement)) return;
185
+ if (e.target === notificationEl.value || notificationEl.value?.contains(e.target)) {
186
+ if (props.notification.isPaused) return;
187
+ emit("pause", props.notification);
188
+ } else {
189
+ emit("resume", props.notification);
190
+ }
191
+ }, { signal: mousedownAbortController.signal });
192
+ }
193
+ });
194
+ onBeforeUnmount(() => {
195
+ mousedownAbortController.abort();
177
196
  });
178
197
  defineExpose({
179
198
  focus: () => {
@@ -11,7 +11,13 @@ Partial<Omit<HTMLAttributes, "class"> & TailwindClassProp>, RealProps {
11
11
  }
12
12
  declare const _default: __VLS_WithSlots<import("vue").DefineComponent<Props, {
13
13
  focus: () => void;
14
- }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{}>, {
14
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
15
+ pause: (notification: NotificationEntry) => any;
16
+ resume: (notification: NotificationEntry) => any;
17
+ }, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
18
+ onPause?: ((notification: NotificationEntry) => any) | undefined;
19
+ onResume?: ((notification: NotificationEntry) => any) | undefined;
20
+ }>, {
15
21
  handler: NotificationHandler;
16
22
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>, {
17
23
  top?: (props: {
@@ -32,8 +32,8 @@
32
32
  class="overflow-hidden my-2"
33
33
  v-for="notification of notifications"
34
34
  :key="notification.id"
35
- @pointerenter="notification.timeout && !notification.isPaused && handler.pause(notification)"
36
- @blur="notification.timeout && notification.isPaused && handler.resume(notification)"
35
+ @pause="handler.pause(notification)"
36
+ @resume="handler.resume(notification)"
37
37
  >
38
38
  <template #top>
39
39
  <LibProgressBar
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@witchcraft/ui",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Vue component library.",
5
5
  "type": "module",
6
6
  "main": "./dist/runtime/main.lib.js",
@@ -121,7 +121,7 @@
121
121
  "husky": "^9.1.7",
122
122
  "indexit": "2.1.0-beta.3",
123
123
  "madge": "^7.0.0",
124
- "nuxt": "^4.0.3",
124
+ "nuxt": "^4.2.1",
125
125
  "playwright": "=1.54.0",
126
126
  "playwright-core": "=1.54.0",
127
127
  "semantic-release": "^24.2.7",
@@ -37,17 +37,17 @@
37
37
  }
38
38
 
39
39
  @utility focus-outline-within {
40
- @reference outlined-within:outline-2 outlined-within:outline-accent-500 outlined-within:outline-offset-2;
40
+ @apply outlined-within:outline-2 outlined-within:outline-accent-500 outlined-within:outline-offset-2;
41
41
  }
42
42
 
43
43
  @utility focus-outline {
44
- @reference outlined:outline-2 outlined:outline-accent-500 outlined:outline-offset-2;
44
+ @apply outlined:outline-2 outlined:outline-accent-500 outlined:outline-offset-2;
45
45
  }
46
46
  @utility focus-outline-no-offset {
47
- @reference outlined:outline-2 outlined:outline-accent-500;
47
+ @apply outlined:outline-2 outlined:outline-accent-500;
48
48
  }
49
49
  @utility focus-outline-hidden {
50
- @reference outlined:outline-none;
50
+ @apply outlined:outline-none;
51
51
  }
52
52
 
53
53
  /* .bg-squares-gradient { */
@@ -21,14 +21,24 @@ export const SingleFile: Story = {
21
21
  render: args => ({
22
22
  components,
23
23
  setup: () => {
24
- function errorHandler(e: any): void {
24
+ const errors = ref([])
25
+ function errorHandler(errs: any) {
25
26
  // eslint-disable-next-line no-console
26
- console.log(e)
27
+ console.log(errs)
28
+ errors.value = errs
27
29
  }
28
- return { args, errorHandler }
30
+ return { args, errorHandler, errors }
29
31
  },
30
32
  template: `
31
33
  <lib-file-input v-bind="{...args}" @errors="errorHandler"></lib-file-input>
34
+ <div
35
+ v-if="errors.length > 0"
36
+ class="border-2 border-red-500 rounded-lg p-2 mt-2 w-full"
37
+ >
38
+ <div v-for="err of errors" :key="err">
39
+ {{err}}
40
+ </div>
41
+ </div>
32
42
  `
33
43
  })
34
44
  }
@@ -1,29 +1,51 @@
1
1
  <template>
2
2
  <!-- todo aria errors -->
3
3
  <div
4
- :class="twMerge(`file-input
4
+ :class="twMerge(`
5
+ file-input
5
6
  justify-center
6
7
  border-2
7
8
  border-dashed
8
9
  border-accent-500/80
9
10
  focus-outline-within
10
11
  transition-[border-color,box-shadow]
11
- ease-out`,
12
+ ease-out
13
+ hover:bg-accent-500/10
14
+ outlined-focus-within
15
+ `,
12
16
  compact && `rounded-sm`,
13
- !compact && `flex w-full flex-col items-center gap-2 rounded-xl p-2 `,
17
+ !compact && `
18
+ flex
19
+ w-full
20
+ flex-col
21
+ items-stretch
22
+ gap-2
23
+ rounded-xl
24
+ p-2
25
+ `,
14
26
  errors.length > 0 && errorFlashing && `border-danger-400`,
27
+ isHovered && `bg-accent-500/10`,
15
28
  ($.wrapperAttrs as any).class
16
29
  )"
17
30
  v-bind="{ ...$.wrapperAttrs, class: undefined }"
31
+ @drop="onDrop"
32
+ @dragover.prevent="isHovered = true"
33
+ @dragleave="isHovered = false"
18
34
  >
19
35
  <div
20
36
  :class="twMerge(`
21
- file-input--wrapper
22
- relative justify-center`,
37
+ file-input--wrapper
38
+ relative
39
+ justify-center
40
+ @container
41
+ `,
23
42
  compact && `flex gap-2`,
24
- !compact && `input-wrapper
25
- flex flex-col items-center
26
- `
43
+ !compact && `
44
+ file-input
45
+ flex
46
+ flex-col
47
+ items-center
48
+ `
27
49
  )"
28
50
  >
29
51
  <label
@@ -34,7 +56,9 @@
34
56
  flex
35
57
  gap-1
36
58
  items-center
59
+ justify-center
37
60
  whitespace-nowrap
61
+ max-w-full
38
62
  `)"
39
63
  >
40
64
  <slot
@@ -44,44 +68,58 @@
44
68
  <icon><i-fa6-solid-arrow-up-from-bracket/></icon>
45
69
  </slot>
46
70
  <slot name="label">
47
- {{
48
- (compact
49
- ? multiple
50
- ? t("file-input.compact-choose-file-plural")
51
- : t("file-input.compact-choose-file")
52
- : multiple
53
- ? t("file-input.non-compact-choose-file-plural")
54
- : t("file-input.non-compact-choose-file")
55
- )
56
- }}
71
+ <div class="text-ellipsis overflow-hidden shrink-1 hidden @min-[15ch]:block">
72
+ {{
73
+ (compact
74
+ ? multiple
75
+ ? t("file-input.compact-choose-file-plural")
76
+ : t("file-input.compact-choose-file")
77
+ : multiple
78
+ ? t("file-input.non-compact-choose-file-plural")
79
+ : t("file-input.non-compact-choose-file")
80
+ )
81
+ }}
82
+ </div>
57
83
  </slot>
58
- <span
84
+ <div
59
85
  v-if="compact && multiple"
60
86
  class="file-input--label-count"
61
87
  >
62
88
  {{ ` (${files.length})` }}
63
- </span>
89
+ </div>
90
+ <div
91
+ v-if="compact && !multiple && files.length > 0"
92
+ class="file-input--label-name text-ellipsis overflow-hidden shrink-9999 hidden @3xs:block"
93
+ >
94
+ {{ ` (${files[0]?.file.name})` }}
95
+ </div>
96
+ <div
97
+ v-if="compact && !multiple && files.length > 0"
98
+ class="file-input--label-name text-ellipsis overflow-hidden shrink-9999 @3xs:hidden"
99
+ >
100
+ {{ ` (...)` }}
101
+ </div>
64
102
  </label>
65
103
  <label
66
104
  v-if="!compact && formats?.length > 0"
67
- class="file-input--formats-label flex flex-col items-center text-sm"
105
+ class="file-input--formats-label flex-col items-center text-sm max-w-full hidden @min-[15ch]:flex"
68
106
  >
69
- <slot name="formats">{{ t("file-input.accepted-formats") }}: </slot>
70
- <div class="file-input--formats-list">
107
+ <slot name="formats"><div class="text-ellipsis overflow-hidden max-w-full">{{ t("file-input.accepted-formats") }}:</div> </slot>
108
+ <div class="file-input--formats-list overflow-hidden text-ellipsis max-w-full">
71
109
  {{ extensions.join(", ") }}
72
110
  </div>
73
111
  </label>
74
112
  <input
75
113
  :id="id ?? fallbackId"
76
114
  :class="twMerge(`
77
- file-input--input
78
- absolute
79
- inset-0
80
- z-0
81
- cursor-pointer
82
- text-[0]
83
- opacity-0
84
- `,
115
+ file-input--input
116
+ absolute
117
+ inset-[calc(var(--spacing)*-2)]
118
+ cursor-pointer
119
+ z-0
120
+ text-[0]
121
+ opacity-0
122
+ `,
85
123
  ($.inputAttrs as any)?.class
86
124
  )"
87
125
  type="file"
@@ -97,7 +135,7 @@
97
135
  <div
98
136
  v-if="!compact && files.length > 0"
99
137
  :class="twMerge(`file-input--previews
100
- flex items-stretch justify-center gap-2 flex-wrap
138
+ flex items-stretch justify-center gap-4 flex-wrap
101
139
  `,
102
140
  multiple && `
103
141
  w-full
@@ -105,32 +143,47 @@
105
143
  ($.previewsAttrs as any)?.class
106
144
  )"
107
145
  >
108
- <div class="file-input--preview-spacer flex-1"/>
109
146
  <div
110
- class="file-input--preview-wrapper
147
+ class="
148
+ file-input--preview-wrapper
111
149
  z-1
112
150
  relative
113
151
  flex
114
152
  min-w-0
115
153
  max-w-[150px]
116
154
  flex-initial
117
- flex-wrap
155
+ flex-col
118
156
  items-center
119
- gap-2 rounded-sm border border-neutral-400
120
- shadow-xs
121
- shadow-neutral-800/20
157
+ gap-1
158
+ p-1
159
+ rounded-sm
160
+ border
161
+ border-neutral-300
162
+ dark:border-neutral-800
163
+ shadow-md
164
+ shadow-neutral-800/30
165
+ bg-neutral-100
166
+ dark:bg-neutral-900
167
+ [&:hover_.file-input--remove-button]:opacity-100
122
168
  "
123
169
  v-for="entry of files"
124
170
  :key="entry.file.name"
125
171
  >
126
- <div class="file-input--remove-button flex flex-initial basis-full justify-start">
172
+ <div class="flex flex-initial basis-full justify-start items-center max-w-full gap-2 px-1">
127
173
  <lib-button
128
174
  :border="false"
175
+ class="file-input--remove-button rounded-full p-0"
129
176
  :aria-label="`Remove file ${entry.file.name}`"
130
177
  @click="removeFile(entry)"
131
178
  >
132
179
  <icon><i-fa6-solid-xmark/></icon>
133
180
  </lib-button>
181
+ <div
182
+ class="file-input--preview-filename min-w-0 flex-1 basis-0 truncate break-all rounded-sm text-sm"
183
+ :title="entry.file.name"
184
+ >
185
+ {{ entry.file.name }}
186
+ </div>
134
187
  </div>
135
188
 
136
189
  <div class="file-input--preview flex flex-initial basis-full justify-center">
@@ -157,25 +210,7 @@
157
210
  <icon><i-fa6-regular-file class="text-4xl opacity-50"/></icon>
158
211
  </div>
159
212
  </div>
160
- <div
161
- class="
162
- file-input--preview-filename
163
- min-w-0
164
- flex-1
165
- basis-0
166
- truncate
167
- break-all
168
- rounded-sm
169
- p-1
170
- text-sm
171
- "
172
- :title="entry.file.name"
173
- >
174
- {{ entry.file.name }}
175
- </div>
176
213
  </div>
177
-
178
- <div class="flex-1"/>
179
214
  </div>
180
215
  </div>
181
216
  </template>
@@ -202,9 +237,16 @@ type Entry = { file: File, isImg: boolean }
202
237
  const files = shallowReactive<(Entry)[]>([])
203
238
  const errors = shallowReactive<(FileInputError)[]>([])
204
239
  const errorFlashing = ref(false)
240
+ const isHovered = ref(false)
241
+
242
+
243
+ function clearFiles() {
244
+ el.value!.value = ""
245
+ files.splice(0, files.length)
246
+ }
205
247
 
206
248
  watch(files, () => {
207
- emits("input", files.map(entry => entry.file))
249
+ emits("input", files.map(entry => entry.file), clearFiles)
208
250
  })
209
251
  watch(errors, () => {
210
252
  if (errors.length > 0) {
@@ -212,7 +254,8 @@ watch(errors, () => {
212
254
  setTimeout(() => {
213
255
  errorFlashing.value = false
214
256
  }, 500)
215
- emits("errors", errors)
257
+ emits("errors", [...errors])
258
+ errors.splice(0, errors.length)
216
259
  }
217
260
  })
218
261
 
@@ -223,7 +266,7 @@ defineOptions({
223
266
  const $ = useDivideAttrs(["wrapper", "input", "previews"] as const)
224
267
 
225
268
  const emits = defineEmits<{
226
- (e: "input", val: File[]): void
269
+ (e: "input", val: File[], clearFiles: () => void): void
227
270
  (e: "errors", val: FileInputError[]): void
228
271
  }>()
229
272
 
@@ -247,46 +290,65 @@ const removeFile = (entry: Entry) => {
247
290
  files.splice(index, 1)
248
291
  }
249
292
  const extensionsList = computed(() => extensions.value.join(", "))
250
- const inputFile = async (e: InputEvent): Promise<undefined | boolean> => {
293
+
294
+ function onDrop(e: DragEvent) {
295
+ if ("dataTransfer" in e && e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
296
+ el.value!.files = e.dataTransfer.files
297
+ e.preventDefault()
298
+ isHovered.value = false
299
+ return updateFiles(el.value!.files)
300
+ }
301
+ return undefined
302
+ }
303
+ async function inputFile(e: InputEvent): Promise<undefined | boolean> {
251
304
  e.preventDefault()
252
305
  if (el.value!.files) {
253
- const errs = []
254
- for (const file of el.value!.files) {
255
- const isImg = file.type.startsWith("image")
306
+ return updateFiles(el.value!.files)
307
+ }
308
+ return undefined
309
+ }
256
310
 
257
- const byPassValidation = props.formats.length === 0
258
- const isValidMimeType = mimeTypes.value.find(_ => _.endsWith("/*") ? file.type.startsWith(_.slice(0, -2)) : _ === file.type) !== undefined
259
- const isValidExtension = extensions.value.find(_ => file.name.endsWith(_)) !== undefined
260
- if (!byPassValidation && (!isValidMimeType || !isValidExtension)) {
261
- const extension = file.name.match(/.*(\..*)/)?.[1] ?? "Unknown"
262
- const type = file.type === "" ? "" : ` (${file.type})`
263
- const message = `File type ${extension}${type} is not allowed. Allowed file types are: ${extensionsList.value}.`
264
- const err = new Error(message) as FileInputError
265
- err.file = file
266
- err.isValidExtension = isValidExtension
267
- err.isValidMimeType = isValidMimeType
268
- errs.push(err)
269
- continue
270
- }
271
- if (errs.length > 0) continue
272
- if (!files.find(_ => _.file === file)) {
273
- if ((props.multiple || files.length < 1)
274
- ) {
275
- files.push({ file, isImg })
276
- } else {
277
- files.splice(0, files.length, { file, isImg })
278
- }
279
- }
311
+ function updateFiles(filesList: FileList): boolean | undefined {
312
+ const errs = []
313
+ for (const file of filesList) {
314
+ const isImg = file.type.startsWith("image")
315
+
316
+ const byPassValidation = props.formats.length === 0
317
+ const isValidMimeType = mimeTypes.value.find(_ => _.endsWith("/*") ? file.type.startsWith(_.slice(0, -2)) : _ === file.type) !== undefined
318
+ const isValidExtension = extensions.value.find(_ => file.name.endsWith(_)) !== undefined
319
+ if (!byPassValidation && (!isValidMimeType || !isValidExtension)) {
320
+ const extension = file.name.match(/.*(\..*)/)?.[1] ?? "Unknown"
321
+ const type = file.type === "" ? "" : ` (${file.type})`
322
+ const message = `File type ${extension}${type} is not allowed. Allowed file types are: ${extensionsList.value}.`
323
+ const err = new Error(message) as FileInputError
324
+ err.file = file
325
+ err.isValidExtension = isValidExtension
326
+ err.isValidMimeType = isValidMimeType
327
+ errs.push(err)
328
+ continue
280
329
  }
281
- if (errs.length > 0) {
282
- errors.splice(0, errors.length, ...errs)
283
- return false
284
- } else if (errors.length > 0) {
285
- errors.splice(0, errors.length)
330
+ if (errs.length > 0) continue
331
+ if (!files.find(_ => _.file === file)) {
332
+ if ((props.multiple || files.length < 1)
333
+ ) {
334
+ files.push({ file, isImg })
335
+ } else {
336
+ files.splice(0, files.length, { file, isImg })
337
+ }
286
338
  }
287
339
  }
340
+ if (errs.length > 0) {
341
+ errors.splice(0, errors.length, ...errs)
342
+ return false
343
+ } else if (errors.length > 0) {
344
+ errors.splice(0, errors.length)
345
+ }
288
346
  return undefined
289
347
  }
348
+
349
+ defineExpose({
350
+ clearFiles
351
+ })
290
352
  </script>
291
353
 
292
354
  <script lang="ts">
@@ -19,12 +19,15 @@
19
19
  p-1
20
20
  text-sm
21
21
  focus:border-accent-500
22
+ focus-within:border-accent-500
22
23
  `,
23
24
  ($attrs as any).class)"
24
25
  v-bind="{ ...$attrs, class: undefined }"
25
26
  tabindex="0"
27
+ :data-id="notification.id"
26
28
  ref="notificationEl"
27
29
  @keydown.enter.self="NotificationHandler.resolveToDefault(notification)"
30
+ @pointerenter="notification.timeout && !notification.isPaused && emit('pause', notification)"
28
31
  >
29
32
  <slot
30
33
  name="top"
@@ -145,7 +148,7 @@
145
148
  </template>
146
149
 
147
150
  <script setup lang="ts">
148
- import { computed, type HTMLAttributes, onMounted, ref, useAttrs } from "vue"
151
+ import { computed, type HTMLAttributes, onBeforeUnmount, onMounted, ref, useAttrs } from "vue"
149
152
 
150
153
  import IFa6RegularCopy from "~icons/fa6-regular/copy"
151
154
  import IFa6SolidXmark from "~icons/fa6-solid/xmark"
@@ -171,6 +174,11 @@ const props = withDefaults(defineProps<Props>(), {
171
174
  handler: undefined
172
175
  })
173
176
 
177
+ const emit = defineEmits<{
178
+ (e: "pause", notification: NotificationEntry): void
179
+ (e: "resume", notification: NotificationEntry): void
180
+ }>()
181
+
174
182
  const getColor = (notification: NotificationEntry, option: string): "ok" | "primary" | "danger" | "secondary" => {
175
183
  return notification.dangerous.includes(option)
176
184
  ? "danger"
@@ -184,9 +192,28 @@ const getColor = (notification: NotificationEntry, option: string): "ok" | "prim
184
192
  const buttonColors = computed(() => props.notification.options.map((option: any /* what ??? */) => getColor(props.notification, option)))
185
193
 
186
194
  const notificationEl = ref<null | HTMLElement>(null)
195
+
196
+ const mousedownAbortController = new AbortController()
197
+
187
198
  onMounted(() => {
188
199
  notificationEl.value?.focus()
200
+ if (props.notification.timeout) {
201
+ window.addEventListener("pointerdown", e => {
202
+ if (!e.target || !(e.target instanceof HTMLElement)) return
203
+ if (e.target === notificationEl.value || notificationEl.value?.contains(e.target)) {
204
+ if (props.notification.isPaused) return
205
+ emit("pause", props.notification)
206
+ } else {
207
+ emit("resume", props.notification)
208
+ }
209
+ }, { signal: mousedownAbortController.signal })
210
+ }
189
211
  })
212
+
213
+ onBeforeUnmount(() => {
214
+ mousedownAbortController.abort()
215
+ })
216
+
190
217
  defineExpose({
191
218
  focus: () => {
192
219
  notificationEl.value?.focus()
@@ -32,8 +32,8 @@
32
32
  class="overflow-hidden my-2"
33
33
  v-for="notification of notifications"
34
34
  :key="notification.id"
35
- @pointerenter="notification.timeout && !notification.isPaused && handler.pause(notification)"
36
- @blur="notification.timeout && notification.isPaused && handler.resume(notification)"
35
+ @pause="handler.pause(notification)"
36
+ @resume="handler.resume(notification)"
37
37
  >
38
38
  <template #top>
39
39
  <LibProgressBar