@witchcraft/ui 0.3.11 → 0.3.13

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 (32) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/assets/utils.css +1 -1
  3. package/dist/runtime/components/LibColorInput/LibColorInput.d.vue.ts +2 -2
  4. package/dist/runtime/components/LibColorInput/LibColorInput.vue.d.ts +2 -2
  5. package/dist/runtime/components/LibColorPicker/LibColorPicker.d.vue.ts +3 -3
  6. package/dist/runtime/components/LibColorPicker/LibColorPicker.vue.d.ts +3 -3
  7. package/dist/runtime/components/LibFileInput/LibFileInput.d.vue.ts +5 -3
  8. package/dist/runtime/components/LibFileInput/LibFileInput.vue +141 -84
  9. package/dist/runtime/components/LibFileInput/LibFileInput.vue.d.ts +5 -3
  10. package/dist/runtime/components/LibInputDeprecated/LibInputDeprecated.d.vue.ts +3 -3
  11. package/dist/runtime/components/LibInputDeprecated/LibInputDeprecated.vue.d.ts +3 -3
  12. package/dist/runtime/components/LibNotifications/LibNotification.vue +16 -1
  13. package/dist/runtime/components/LibNotifications/LibNotificationTestMessageComponent.d.vue.ts +6 -0
  14. package/dist/runtime/components/LibNotifications/LibNotificationTestMessageComponent.vue +29 -0
  15. package/dist/runtime/components/LibNotifications/LibNotificationTestMessageComponent.vue.d.ts +6 -0
  16. package/dist/runtime/components/LibPopup/LibPopup.d.vue.ts +1 -1
  17. package/dist/runtime/components/LibPopup/LibPopup.vue.d.ts +1 -1
  18. package/dist/runtime/helpers/NotificationHandler.d.ts +5 -2
  19. package/dist/runtime/helpers/NotificationHandler.js +2 -1
  20. package/dist/runtime/types/index.d.ts +4 -0
  21. package/dist/runtime/utils/notifyIfError.d.ts +3 -1
  22. package/dist/runtime/utils/notifyIfError.js +4 -2
  23. package/package.json +2 -2
  24. package/src/runtime/assets/utils.css +4 -4
  25. package/src/runtime/components/LibFileInput/LibFileInput.stories.ts +13 -3
  26. package/src/runtime/components/LibFileInput/LibFileInput.vue +154 -92
  27. package/src/runtime/components/LibNotifications/LibNotification.stories.ts +22 -1
  28. package/src/runtime/components/LibNotifications/LibNotification.vue +16 -1
  29. package/src/runtime/components/LibNotifications/LibNotificationTestMessageComponent.vue +27 -0
  30. package/src/runtime/helpers/NotificationHandler.ts +6 -2
  31. package/src/runtime/types/index.ts +5 -0
  32. package/src/runtime/utils/notifyIfError.ts +6 -2
@@ -1,5 +1,5 @@
1
1
  import type { AnyFunction, MakeRequired } from "@alanscodelog/utils";
2
- import { type Reactive } from "vue";
2
+ import { type Component, type Reactive } from "vue";
3
3
  export declare class NotificationHandler<TRawEntry extends RawNotificationEntry<any, any> = RawNotificationEntry<any, any>, TEntry extends NotificationEntry<TRawEntry> = NotificationEntry<TRawEntry>> {
4
4
  timeout: number;
5
5
  debug: boolean;
@@ -26,7 +26,6 @@ export declare class NotificationHandler<TRawEntry extends RawNotificationEntry<
26
26
  }
27
27
  export type NotificationPromise<TOption extends string = string> = Promise<TOption>;
28
28
  export type RawNotificationEntry<TOptions extends string[] = ["Ok", "Cancel"], TCancellable extends boolean | TOptions[number] = "Cancel"> = {
29
- message: string;
30
29
  title?: string;
31
30
  code?: string;
32
31
  /** @default ["Ok", "Cancel"] */
@@ -41,6 +40,10 @@ export type RawNotificationEntry<TOptions extends string[] = ["Ok", "Cancel"], T
41
40
  /** @default false if cancellable, otherwise the default timeout */
42
41
  timeout?: number | boolean;
43
42
  icon?: string;
43
+ message: string;
44
+ component?: string | Component;
45
+ /** By default the component is passed the message and the messageClasses. Both will be overriden if you set them on componentProps. */
46
+ componentProps?: Record<string, any>;
44
47
  };
45
48
  export type NotificationEntry<TRawEntry extends RawNotificationEntry<any, any> = RawNotificationEntry<any, any>> = Omit<MakeRequired<TRawEntry, "options" | "requiresAction" | "default" | "dangerous">, "cancellable"> & {
46
49
  promise: NotificationPromise;
@@ -4,7 +4,7 @@ import { indent } from "@alanscodelog/utils/indent";
4
4
  import { isBlank } from "@alanscodelog/utils/isBlank";
5
5
  import { pretty } from "@alanscodelog/utils/pretty";
6
6
  import { setReadOnly } from "@alanscodelog/utils/setReadOnly";
7
- import { reactive } from "vue";
7
+ import { markRaw, reactive } from "vue";
8
8
  export class NotificationHandler {
9
9
  timeout = 5e3;
10
10
  debug = false;
@@ -87,6 +87,7 @@ export class NotificationHandler {
87
87
  default: "Ok",
88
88
  cancellable: rawEntry.cancellable,
89
89
  ...rawEntry,
90
+ component: rawEntry.component && typeof rawEntry.component !== "string" ? markRaw(rawEntry.component) : void 0,
90
91
  dangerous: rawEntry.dangerous ?? [],
91
92
  timeout: rawEntry.timeout === true ? this.timeout : rawEntry.timeout !== void 0 && rawEntry.timeout !== false ? rawEntry.timeout : void 0
92
93
  };
@@ -123,3 +123,7 @@ export type RangeDate = {
123
123
  start?: SingleDate;
124
124
  end?: SingleDate;
125
125
  };
126
+ export type CustomNotificationComponentProps = {
127
+ message: string;
128
+ messageClasses?: string;
129
+ };
@@ -1,3 +1,4 @@
1
+ import type { NotificationEntry } from "../helpers/NotificationHandler.js";
1
2
  /**
2
3
  * Notifies the user if the given value is an error. Useful for making non-critical errors don't go unnoticed.
3
4
  *
@@ -5,10 +6,11 @@
5
6
  *
6
7
  * If the value is not an error, it is returned.
7
8
  */
8
- export declare function notifyIfError<T>(err: T, { logger, ns, force }?: {
9
+ export declare function notifyIfError<T>(err: T, { logger, ns, force, entry }?: {
9
10
  logger?: {
10
11
  debug: (...args: any[]) => void;
11
12
  };
12
13
  ns?: string;
13
14
  force?: boolean;
15
+ entry?: Partial<NotificationEntry<any>>;
14
16
  }): T;
@@ -3,7 +3,8 @@ import { useNotificationHandler } from "../composables/useNotificationHandler.js
3
3
  export function notifyIfError(err, {
4
4
  logger,
5
5
  ns,
6
- force = false
6
+ force = false,
7
+ entry
7
8
  } = {}) {
8
9
  if (force || err instanceof Error) {
9
10
  const errMessage = {
@@ -22,7 +23,8 @@ export function notifyIfError(err, {
22
23
  ...errMessage,
23
24
  options: ["Ok"],
24
25
  cancellable: "Ok",
25
- timeout: true
26
+ timeout: true,
27
+ ...entry
26
28
  });
27
29
  }
28
30
  return err;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@witchcraft/ui",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
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">
@@ -2,6 +2,7 @@
2
2
  import type { Meta, StoryObj } from "@storybook/vue3"
3
3
 
4
4
  import LibNotification from "./LibNotification.vue"
5
+ import LibNotificationTestMessageComponent from "./LibNotificationTestMessageComponent.vue"
5
6
 
6
7
  import { NotificationHandler } from "../../helpers/NotificationHandler.js"
7
8
  import * as components from "../index.js"
@@ -21,7 +22,11 @@ type Story = StoryObj<typeof LibNotification>
21
22
 
22
23
  export const Primary: Story = {
23
24
  render: args => ({
24
- components: { ...components, LibNotification },
25
+ components: {
26
+ ...components,
27
+ LibNotification,
28
+ LibNotificationTestMessageComponent
29
+ },
25
30
  setup() {
26
31
  return { args }
27
32
  },
@@ -136,3 +141,19 @@ export const CustomDefaultAndDangerousOption: Story = {
136
141
  })
137
142
  }
138
143
  }
144
+
145
+
146
+ export const CustomMessageComponent: Story = {
147
+ ...Primary,
148
+ args: {
149
+ ...Primary.args,
150
+ // @ts-expect-error calling protected method
151
+ notification: handler._createEntry({
152
+ ...Primary.args!.notification,
153
+ component: LibNotificationTestMessageComponent,
154
+ componentProps: {
155
+ customProp: "Custom Prop"
156
+ }
157
+ })
158
+ }
159
+ }
@@ -90,7 +90,7 @@
90
90
  </div>
91
91
  </div>
92
92
  <slot
93
- v-if="notification.message"
93
+ v-if="notification.message && !notification.component"
94
94
  name="message"
95
95
  v-bind="setSlotVar('message', {
96
96
  class: `
@@ -110,6 +110,21 @@
110
110
  {{ notification.message }}
111
111
  </div>
112
112
  </slot>
113
+ <Component
114
+ v-if="notification.component"
115
+ :is="notification.component"
116
+ v-bind="{
117
+ message: notification.message,
118
+ messageClasses: `
119
+ notification--message
120
+ whitespace-pre-wrap
121
+ text-neutral-800
122
+ dark:text-neutral-200
123
+ mb-1
124
+ `,
125
+ ...(notification.componentProps ?? {})
126
+ }"
127
+ />
113
128
  <div class="notification--footer flex items-end justify-between">
114
129
  <div
115
130
  v-if="notification.code"
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <div class="border-2 border-accent-500 p-1 px-2 rounded-sm bg-accent-500/10">
3
+ <div class="text-lg">
4
+ Custom Message Component
5
+ </div>
6
+ <div class="font-bold">
7
+ Original message:
8
+ </div>
9
+ <div
10
+ :class="props.messageClasses"
11
+ >
12
+ {{ props.message }}
13
+ </div>
14
+ <div class="font-bold">
15
+ Custom Prop:
16
+ </div>
17
+ <div>
18
+ {{ props.customProp }}
19
+ </div>
20
+ </div>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ import type { CustomNotificationComponentProps } from "../../types/index.js"
25
+
26
+ const props = defineProps<CustomNotificationComponentProps & { customProp: string }>()
27
+ </script>
@@ -5,7 +5,7 @@ import { indent } from "@alanscodelog/utils/indent"
5
5
  import { isBlank } from "@alanscodelog/utils/isBlank"
6
6
  import { pretty } from "@alanscodelog/utils/pretty"
7
7
  import { setReadOnly } from "@alanscodelog/utils/setReadOnly"
8
- import { type Reactive, reactive } from "vue"
8
+ import { type Component, markRaw, type Reactive, reactive } from "vue"
9
9
 
10
10
  export class NotificationHandler<
11
11
  TRawEntry extends RawNotificationEntry<any, any> = RawNotificationEntry<any, any>,
@@ -99,6 +99,7 @@ export class NotificationHandler<
99
99
  default: "Ok",
100
100
  cancellable: rawEntry.cancellable,
101
101
  ...rawEntry,
102
+ component: rawEntry.component && typeof rawEntry.component !== "string" ? markRaw(rawEntry.component) : undefined,
102
103
  dangerous: rawEntry.dangerous ?? [],
103
104
  timeout: rawEntry.timeout === true
104
105
  ? this.timeout
@@ -211,7 +212,6 @@ export type RawNotificationEntry<
211
212
  TOptions extends string[] = ["Ok", "Cancel"],
212
213
  TCancellable extends boolean | TOptions[number] = "Cancel"
213
214
  > = {
214
- message: string
215
215
  title?: string
216
216
  code?: string
217
217
  /** @default ["Ok", "Cancel"] */
@@ -226,6 +226,10 @@ export type RawNotificationEntry<
226
226
  /** @default false if cancellable, otherwise the default timeout */
227
227
  timeout?: number | boolean
228
228
  icon?: string
229
+ message: string
230
+ component?: string | Component
231
+ /** By default the component is passed the message and the messageClasses. Both will be overriden if you set them on componentProps. */
232
+ componentProps?: Record<string, any>
229
233
  }
230
234
 
231
235
  export type NotificationEntry<
@@ -118,3 +118,8 @@ export type RangeDate = {
118
118
  start?: SingleDate
119
119
  end?: SingleDate
120
120
  }
121
+
122
+ export type CustomNotificationComponentProps = {
123
+ message: string
124
+ messageClasses?: string
125
+ }