@witchcraft/ui 0.3.1 → 0.3.3

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 (96) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/assets/animations.css +1 -0
  3. package/dist/runtime/assets/base.css +1 -1
  4. package/dist/runtime/assets/tailwind.css +1 -1
  5. package/dist/runtime/components/LibButton/LibButton.d.vue.ts +1 -1
  6. package/dist/runtime/components/LibButton/LibButton.vue.d.ts +1 -1
  7. package/dist/runtime/components/LibCheckbox/LibCheckbox.d.vue.ts +1 -1
  8. package/dist/runtime/components/LibCheckbox/LibCheckbox.vue.d.ts +1 -1
  9. package/dist/runtime/components/LibColorInput/LibColorInput.d.vue.ts +2 -2
  10. package/dist/runtime/components/LibColorInput/LibColorInput.vue.d.ts +2 -2
  11. package/dist/runtime/components/LibColorPicker/LibColorPicker.d.vue.ts +2 -2
  12. package/dist/runtime/components/LibColorPicker/LibColorPicker.vue.d.ts +2 -2
  13. package/dist/runtime/components/LibColorPicker/utils/safeConvertToHsva.d.ts +1 -1
  14. package/dist/runtime/components/LibColorPicker/utils/safeConvertToRgba.d.ts +1 -1
  15. package/dist/runtime/components/LibColorPicker/utils/toLowPrecisionRgbaString.d.ts +1 -1
  16. package/dist/runtime/components/LibDarkModeSwitcher/LibDarkModeSwitcher.d.vue.ts +1 -1
  17. package/dist/runtime/components/LibDarkModeSwitcher/LibDarkModeSwitcher.vue.d.ts +1 -1
  18. package/dist/runtime/components/LibDatePicker/LibDatePicker.d.vue.ts +1 -1
  19. package/dist/runtime/components/LibDatePicker/LibDatePicker.vue.d.ts +1 -1
  20. package/dist/runtime/components/LibDatePicker/LibRangeDatePicker.d.vue.ts +1 -1
  21. package/dist/runtime/components/LibDatePicker/LibRangeDatePicker.vue.d.ts +1 -1
  22. package/dist/runtime/components/LibDatePicker/LibSingleDatePicker.d.vue.ts +1 -1
  23. package/dist/runtime/components/LibDatePicker/LibSingleDatePicker.vue.d.ts +1 -1
  24. package/dist/runtime/components/LibFileInput/LibFileInput.d.vue.ts +2 -2
  25. package/dist/runtime/components/LibFileInput/LibFileInput.vue.d.ts +2 -2
  26. package/dist/runtime/components/LibInputDeprecated/LibInputDeprecated.d.vue.ts +1 -1
  27. package/dist/runtime/components/LibInputDeprecated/LibInputDeprecated.vue.d.ts +1 -1
  28. package/dist/runtime/components/LibLabel/LibLabel.d.vue.ts +1 -1
  29. package/dist/runtime/components/LibLabel/LibLabel.vue.d.ts +1 -1
  30. package/dist/runtime/components/LibMultiValues/LibMultiValues.d.vue.ts +1 -1
  31. package/dist/runtime/components/LibMultiValues/LibMultiValues.vue.d.ts +1 -1
  32. package/dist/runtime/components/LibNotifications/LibNotification.d.vue.ts +17 -4
  33. package/dist/runtime/components/LibNotifications/LibNotification.vue +84 -25
  34. package/dist/runtime/components/LibNotifications/LibNotification.vue.d.ts +17 -4
  35. package/dist/runtime/components/LibNotifications/LibNotifications.d.vue.ts +2 -2
  36. package/dist/runtime/components/LibNotifications/LibNotifications.vue +110 -87
  37. package/dist/runtime/components/LibNotifications/LibNotifications.vue.d.ts +2 -2
  38. package/dist/runtime/components/LibPagination/LibPagination.d.vue.ts +1 -1
  39. package/dist/runtime/components/LibPagination/LibPagination.vue.d.ts +1 -1
  40. package/dist/runtime/components/LibPalette/LibPalette.d.vue.ts +1 -1
  41. package/dist/runtime/components/LibPalette/LibPalette.vue.d.ts +1 -1
  42. package/dist/runtime/components/LibPopup/LibPopup.d.vue.ts +4 -4
  43. package/dist/runtime/components/LibPopup/LibPopup.vue +2 -6
  44. package/dist/runtime/components/LibPopup/LibPopup.vue.d.ts +4 -4
  45. package/dist/runtime/components/LibProgressBar/LibProgressBar.d.vue.ts +2 -1
  46. package/dist/runtime/components/LibProgressBar/LibProgressBar.vue +1 -1
  47. package/dist/runtime/components/LibProgressBar/LibProgressBar.vue.d.ts +2 -1
  48. package/dist/runtime/components/LibRecorder/LibRecorder.d.vue.ts +1 -1
  49. package/dist/runtime/components/LibRecorder/LibRecorder.vue +1 -1
  50. package/dist/runtime/components/LibRecorder/LibRecorder.vue.d.ts +1 -1
  51. package/dist/runtime/components/LibRoot/LibRoot.d.vue.ts +2 -2
  52. package/dist/runtime/components/LibRoot/LibRoot.vue.d.ts +2 -2
  53. package/dist/runtime/components/LibSimpleInput/LibSimpleInput.d.vue.ts +1 -1
  54. package/dist/runtime/components/LibSimpleInput/LibSimpleInput.vue.d.ts +1 -1
  55. package/dist/runtime/components/LibSuggestions/LibSuggestions.d.vue.ts +1 -1
  56. package/dist/runtime/components/LibSuggestions/LibSuggestions.vue.d.ts +1 -1
  57. package/dist/runtime/components/LibTable/LibTable.d.vue.ts +2 -2
  58. package/dist/runtime/components/LibTable/LibTable.vue.d.ts +2 -2
  59. package/dist/runtime/components/Template/TemplateStory.d.ts +1 -1
  60. package/dist/runtime/components/index.d.ts +20 -20
  61. package/dist/runtime/components/shared/props.d.ts +1 -1
  62. package/dist/runtime/composables/index.d.ts +17 -15
  63. package/dist/runtime/composables/index.js +2 -0
  64. package/dist/runtime/composables/useDragWithThreshold.d.ts +1 -1
  65. package/dist/runtime/composables/useInjectedDarkMode.d.ts +1 -1
  66. package/dist/runtime/composables/useInjectedI18n.d.ts +1 -1
  67. package/dist/runtime/composables/useInjectedLocale.d.ts +1 -1
  68. package/dist/runtime/composables/useNotificationHandler.d.ts +1 -1
  69. package/dist/runtime/composables/useScrollNearContainerEdges.d.ts +1 -1
  70. package/dist/runtime/composables/useSetupDarkMode.d.ts +1 -1
  71. package/dist/runtime/composables/useSlotVars.d.ts +32 -0
  72. package/dist/runtime/composables/useSlotVars.js +12 -0
  73. package/dist/runtime/composables/useSuggestions.d.ts +1 -1
  74. package/dist/runtime/directives/index.d.ts +4 -4
  75. package/dist/runtime/globalResizeObserver.d.ts +1 -1
  76. package/dist/runtime/helpers/NotificationHandler.d.ts +11 -4
  77. package/dist/runtime/helpers/NotificationHandler.js +34 -16
  78. package/dist/runtime/helpers/index.d.ts +10 -10
  79. package/dist/runtime/helpers/resizeObserverWrapper.d.ts +1 -1
  80. package/dist/runtime/injectionKeys.d.ts +2 -2
  81. package/dist/runtime/main.lib.d.ts +10 -10
  82. package/dist/runtime/tailwind/index.d.ts +1 -1
  83. package/package.json +2 -2
  84. package/src/runtime/assets/animations.css +75 -0
  85. package/src/runtime/assets/base.css +0 -27
  86. package/src/runtime/assets/tailwind.css +1 -0
  87. package/src/runtime/components/LibColorPicker/LibColorPicker.stories.ts +1 -1
  88. package/src/runtime/components/LibNotifications/LibNotification.vue +86 -25
  89. package/src/runtime/components/LibNotifications/LibNotifications.stories.ts +4 -4
  90. package/src/runtime/components/LibNotifications/LibNotifications.vue +112 -90
  91. package/src/runtime/components/LibPopup/LibPopup.vue +2 -6
  92. package/src/runtime/components/LibProgressBar/LibProgressBar.vue +2 -1
  93. package/src/runtime/components/LibRecorder/LibRecorder.vue +1 -1
  94. package/src/runtime/composables/index.ts +2 -0
  95. package/src/runtime/composables/useSlotVars.ts +41 -0
  96. package/src/runtime/helpers/NotificationHandler.ts +44 -22
@@ -1,69 +1,131 @@
1
1
  <template>
2
+ <!-- using custom toasts, reka-ui toasts still have issues, like like of control over pause, and I can't get the leave event to animate or transition with vue transitions to work -->
2
3
  <TransitionGroup
3
4
  name="list"
4
5
  tag="div"
5
- :class="twMerge(`notifications
6
- absolute
7
- z-50
8
- inset-y-0 right-0
9
- w-1/3
10
- min-w-[300px]
11
- pointer-events-none
12
- overflow-hidden
13
- flex flex-col
14
- `, ($attrs as any).class)"
6
+ :class="twMerge(`
7
+ notifications
8
+ [--notification-width:300px]
9
+ fixed
10
+ top-0
11
+ z-50
12
+ right-[calc(var(--notification-width)*-1)]
13
+ py-2
14
+ w-[calc(var(--spacing)*2+var(--notification-width)*2)]
15
+ [&_.notification]:w-[var(--notification-width)]
16
+ max-h-[100dvh]
17
+ flex
18
+ flex-col
19
+ [&_.notification]:shrink-0
20
+ gap-1
21
+ list-none
22
+ outline-none
23
+ overflow-y-auto
24
+ overflow-x-clip
25
+ scrollbar-hidden
26
+ `, ($attrs as any).class)"
15
27
  v-bind="{ ...$attrs, class: undefined }"
16
28
  >
17
29
  <lib-notification
18
- class="pointer-events-auto"
19
30
  :handler="handler"
20
31
  tabindex="0"
21
32
  :notification="notification"
33
+ class="overflow-hidden"
22
34
  v-for="notification of notifications"
23
35
  :key="notification.id"
24
- />
25
- </TransitionGroup>
26
- <Transition>
27
- <div
28
- v-show="topNotifications.length > 0"
29
- :class="twMerge(`notifications--none`, ($attrs as any).class)"
30
- />
31
- </Transition>
32
- <Transition>
33
- <dialog
34
- v-show="topNotifications.length > 0"
35
- :id="id"
36
- :class="twMerge(`notifications-modal
37
- bg-transparent
38
- p-0
39
- backdrop:bg-black/50
40
- backdrop:p-5
41
- `, ($attrs as any).class)"
42
- ref="dialogEl"
43
- @click.self.prevent="topNotifications[0] && NotificationHandler.dismiss(topNotifications[0])"
36
+ @pointerenter="notification.timeout && !notification.isPaused && handler.pause(notification)"
37
+ @blur="notification.timeout && notification.isPaused && handler.resume(notification)"
44
38
  >
45
- <form>
39
+ <template #top>
40
+ <LibProgressBar
41
+ v-if="notification.timeout !== undefined"
42
+ class="
43
+ w-full
44
+ h-1
45
+ before:duration-[10ms]
46
+ -mt-1
47
+ -mx-[calc(var(--spacing)*2+2px)]
48
+ rounded-none
49
+ "
50
+ :progress="100 - (((notification.isPaused ? (notification._timer.elapsedBeforePause): (notification._timer.elapsedBeforePause + (time - notification.startTime))) / notification.timeout) * 100)"
51
+ />
52
+ </template>
53
+ </lib-notification>
54
+ </TransitionGroup>
55
+ <!-- we don't need to worry about the user accidentally closing a non-closable dialog as keeping open=true (which the handler handles when the component tries to close) is enough to keep it open without issues -->
56
+ <AlertDialogRoot
57
+ :open="topNotifications.length > 0 && topNotifications[0] !== undefined"
58
+ @update:open="topNotifications[0] && NotificationHandler.dismiss(topNotifications[0])"
59
+ >
60
+ <AlertDialogPortal :to="'#root'">
61
+ <AlertDialogOverlay
62
+ class="
63
+ fixed inset-0 z-30
64
+ bg-neutral-950/20
65
+ data-[state=open]:animate-overlayShow
66
+ "
67
+ />
68
+ <AlertDialogContent
69
+ class="
70
+ data-[state=open]:animate-contentShow
71
+ fixed
72
+ top-[50%]
73
+ left-[50%]
74
+ translate-x-[-50%]
75
+ translate-y-[-50%]
76
+ max-h-[80dvh]
77
+ max-w-[700px]
78
+ z-100
79
+ "
80
+ >
46
81
  <lib-notification
47
- v-if="topNotifications.length > 0 && topNotifications[0]"
82
+ class="
83
+ top-notification
84
+ text-md
85
+ gap-2
86
+ p-2
87
+ [&_.notification--button]:p-2
88
+ [&_.notification--button]:py-1
89
+ [&_.notification--header]:text-lg
90
+ [&_.notification--message]:py-3
91
+ "
48
92
  :handler="handler"
49
- class="top-notification"
50
- :notification="topNotifications[0]"
93
+ :notification="topNotifications[0]!"
51
94
  ref="topNotificationComp"
52
- />
53
- </form>
54
- </dialog>
55
- </Transition>
95
+ >
96
+ <template #title="slotProps">
97
+ <AlertDialogTitle v-bind="slotProps">
98
+ {{ slotProps.title }}
99
+ </AlertDialogTitle>
100
+ </template>
101
+ <template #message="slotProps">
102
+ <AlertDialogDescription v-bind="slotProps">
103
+ {{ slotProps.message }}
104
+ </AlertDialogDescription>
105
+ </template>
106
+ </lib-notification>
107
+ </AlertDialogContent>
108
+ </AlertDialogPortal>
109
+ </AlertDialogRoot>
56
110
  </template>
57
111
 
58
112
  <script setup lang="ts">
59
- import { removeIfIn } from "@alanscodelog/utils/removeIfIn"
60
- import { nextTick, onBeforeUnmount, ref, shallowReactive } from "vue"
113
+ import {
114
+ AlertDialogContent,
115
+ AlertDialogDescription,
116
+ AlertDialogOverlay,
117
+ AlertDialogPortal,
118
+ AlertDialogRoot,
119
+ AlertDialogTitle
120
+ } from "reka-ui"
121
+ import { computed, ref } from "vue"
61
122
 
62
123
  import LibNotification from "./LibNotification.vue"
63
124
 
64
125
  import { useNotificationHandler } from "../../composables/useNotificationHandler.js"
65
- import { type NotificationEntry, NotificationHandler } from "../../helpers/NotificationHandler.js"
126
+ import { NotificationHandler } from "../../helpers/NotificationHandler.js"
66
127
  import { twMerge } from "../../utils/twMerge.js"
128
+ import LibProgressBar from "../LibProgressBar/LibProgressBar.vue"
67
129
  import type { LinkableByIdProps, TailwindClassProp } from "../shared/props.js"
68
130
 
69
131
  defineOptions({
@@ -73,58 +135,18 @@ defineOptions({
73
135
 
74
136
  const props = defineProps<Props>()
75
137
 
76
- const dialogEl = ref<HTMLDialogElement | null>(null)
138
+ const topNotifications = computed(() => handler.queue.filter(entry => entry.requiresAction).reverse())
139
+ const notifications = computed(() => handler.queue.filter(entry => !entry.requiresAction))
77
140
 
78
- const isOpen = ref(false)
79
- const notifications = shallowReactive<NotificationEntry[]>([])
80
- const topNotifications = shallowReactive<NotificationEntry[]>([])
81
- const open = () => {
82
- if (!isOpen.value) {
83
- void nextTick(() => {
84
- dialogEl.value!.showModal()
85
- isOpen.value = true
86
- })
87
- }
88
- }
89
- const close = () => {
90
- if (isOpen.value && topNotifications.length === 0) {
91
- dialogEl.value!.close()
92
- isOpen.value = false
93
- }
94
- }
141
+ const time = ref(Date.now())
142
+ setInterval(() => {
143
+ requestAnimationFrame(() => {
144
+ time.value = Date.now()
145
+ })
146
+ }, 50)
95
147
 
96
- const addNotification = (entry: NotificationEntry) => {
97
- if (entry.resolution === undefined) {
98
- if (entry.requiresAction) {
99
- topNotifications.push(entry)
100
- open()
101
- entry.promise.then(() => {
102
- removeIfIn(topNotifications, entry)
103
- close()
104
- })
105
- } else {
106
- notifications.splice(0, 0, entry)
107
- entry.promise.then(() => {
108
- removeIfIn(notifications, entry)
109
- })
110
- }
111
- }
112
- }
113
-
114
- const notificationListener = (entry: NotificationEntry, type: "added" | "resolved" | "deleted"): void => {
115
- if (type === "added") {
116
- addNotification(entry)
117
- }
118
- }
119
148
 
120
149
  const handler = props.handler ?? useNotificationHandler()
121
-
122
- handler.addNotificationListener(notificationListener)
123
-
124
- for (const entry of handler.queue) { addNotification(entry) }
125
- onBeforeUnmount(() => {
126
- handler.removeNotificationListener(notificationListener)
127
- })
128
150
  </script>
129
151
 
130
152
  <script lang="ts">
@@ -86,7 +86,7 @@ const backgroundEl = ref<IPopupReference | null>(null)
86
86
 
87
87
  const pos = ref<PopupPosition>({} as any)
88
88
  const modelValue = defineModel<boolean>({ default: false })
89
- let isOpen = false
89
+ let isOpen = modelValue.value
90
90
 
91
91
  /**
92
92
  * We don't have access to the dialog backdrop and without extra styling, it's of 0 width/height, positioned in the center of the screen, with margins taking up all the space.
@@ -341,10 +341,6 @@ const close = () => {
341
341
  }
342
342
  }
343
343
 
344
- const toggle = () => {
345
- if (!isOpen) show()
346
- else close()
347
- }
348
344
 
349
345
  const recomputeListener = () => recompute()
350
346
 
@@ -369,7 +365,7 @@ watch([modelValue, popupEl], () => {
369
365
 
370
366
  const handleMouseup = ($event: MouseEvent) => {
371
367
  $event.preventDefault()
372
- toggle()
368
+ close()
373
369
  }
374
370
  onMounted(() => {
375
371
  recompute()
@@ -28,7 +28,7 @@
28
28
  before:shadow-black/50
29
29
  before:rounded-sm
30
30
  before:bg-bars-gradient
31
- before:animate-[slide_10s_linear_infinite]
31
+ before:animate-slideBgInf
32
32
  before:[background-size:15px_15px]
33
33
  before:absolute
34
34
  before:w-[var(--progress)]
@@ -171,6 +171,7 @@ type RealProps
171
171
  & BaseInteractiveProps
172
172
  & LabelProps
173
173
  & {
174
+ /** A number from 0-100. It is auto-clamped. */
174
175
  progress: number
175
176
  /** Will auto hide after this given time if progress is 100% or more or less than 0% until progress is set to something else. Disabled (-1) by default. */
176
177
  autohideOnComplete?: number
@@ -53,7 +53,7 @@
53
53
  hover:bg-red-500
54
54
  `,
55
55
  recording && `
56
- animate-[blink_1s_infinite]
56
+ animate-blinkInf
57
57
  bg-red-500
58
58
  `,
59
59
  (disabled || readonly) && `
@@ -13,5 +13,7 @@ export { useNotificationHandler } from "./useNotificationHandler.js"
13
13
  export { usePreHydrationValue } from "./usePreHydrationValue.js"
14
14
  export { useScrollNearContainerEdges } from "./useScrollNearContainerEdges.js"
15
15
  export { useSetupDarkMode } from "./useSetupDarkMode.js"
16
+ export { useSetupLocale } from "./useSetupLocale.js"
16
17
  export { useShowDevOnlyKey } from "./useShowDevOnlyKey.js"
18
+ export { useSlotVars } from "./useSlotVars.js"
17
19
  export { useSuggestions } from "./useSuggestions.js"
@@ -0,0 +1,41 @@
1
+ import { reactive } from "vue"
2
+
3
+ /**
4
+ * Helper for managing props passed to slots and their fallbacks.
5
+ *
6
+ * Most slots have a default style but vue makes it hard to pass them both to the slot and the fallback content without repitition.
7
+ *
8
+ * This helper allows setting the variables from the template when creating the slot WHILE also using the created state without having to use any wrapper components, {@link https://github.com/vuejs/core/issues/1172 | see also this issue}.
9
+ *
10
+ * @example
11
+ * ```vue
12
+ * <template>
13
+ * <div>
14
+ * <slot
15
+ * v-bind="setSlotVar('title', {
16
+ * class: 'title focus-outline flex rounded-sm font-bold'
17
+ * someOtherProp: true
18
+ * })"
19
+ * >
20
+ * <FallbackComponent v-bind="slotVars.title"/>
21
+ * <slot/>
22
+ * </div>
23
+ * </template>
24
+ * <script setup lang="ts">
25
+ * import { useSlotVars } from "@witchcraft/ui/composables/useSlotVars"
26
+ * const { slotVars, setSlotVar } = useSlotVars()
27
+ * </script>
28
+ * ```
29
+ * The magic is that setSlotVar both sets and returns the value for the slot name passed. You can then access the state in a fallback component by accessing slotVars[slotName]. Unfortunately this is untyped unless you set the generic in useSlotsVars, but we usually don't need them to be typed.
30
+ */
31
+ export function useSlotVars<T extends Record<string, Record<string, any>>, TKey extends keyof T>() {
32
+ const state = reactive<Record<TKey, Record<string, any>>>({} as any)
33
+ function setSlotVar<T extends Record<string, any>>(name: TKey, obj: T): T {
34
+ state[name as keyof typeof state] = obj as any
35
+ return state[name as keyof typeof state] as T
36
+ }
37
+ return {
38
+ slotVars: state,
39
+ setSlotVar
40
+ }
41
+ }
@@ -5,6 +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
9
 
9
10
  export class NotificationHandler<
10
11
  TRawEntry extends RawNotificationEntry<any, any> = RawNotificationEntry<any, any>,
@@ -16,9 +17,9 @@ export class NotificationHandler<
16
17
 
17
18
  private id: number = 0
18
19
 
19
- readonly queue: TEntry[] = []
20
+ readonly queue: Reactive<TEntry[]>
20
21
 
21
- readonly history: TEntry[] = []
22
+ readonly history: Readonly<TEntry[]>
22
23
 
23
24
  maxHistory: number = 100
24
25
 
@@ -35,6 +36,8 @@ export class NotificationHandler<
35
36
  stringifier?: NotificationHandler<TRawEntry>["stringifier"]
36
37
  maxHistory?: NotificationHandler<TRawEntry>["maxHistory"]
37
38
  } = {}) {
39
+ this.queue = reactive([])
40
+ this.history = reactive([])
38
41
  if (timeout) this.timeout = timeout
39
42
  if (maxHistory) this.maxHistory = maxHistory
40
43
  if (stringifier) this.stringifier = stringifier
@@ -126,11 +129,12 @@ export class NotificationHandler<
126
129
  }) as NotificationPromise
127
130
 
128
131
  if (entry.timeout !== undefined) {
129
- setTimeout(() => {
130
- entry.resolve(entry.cancellable)
131
- }, entry.timeout)
132
+ entry._timer = {
133
+ elapsedBeforePause: 0
134
+ }
135
+ this.resume(entry as any)
132
136
  }
133
- this.queue.push(entry)
137
+ this.queue.push(entry as any)
134
138
  for (const listener of this.listeners) {
135
139
  listener(entry, "added")
136
140
  }
@@ -140,18 +144,43 @@ export class NotificationHandler<
140
144
  for (const listener of this.listeners) {
141
145
  listener(entry, "resolved")
142
146
  }
143
- this.history.push(entry)
147
+ ;(this.history as any).push(entry)
144
148
  if (this.history.length > this.maxHistory) {
145
- this.history.splice(0, 1)
149
+ ;(this.history as any).splice(0, 1)
146
150
  for (const listener of this.listeners) {
147
151
  listener(entry, "deleted")
148
152
  }
149
153
  }
150
- this.queue.splice(this.queue.indexOf(entry), 1)
154
+ this.queue.splice(this.queue.indexOf(entry as any), 1)
151
155
  return res
152
156
  }) satisfies NotificationPromise as any
153
157
  }
154
158
 
159
+ pause(notification: NotificationEntry): void {
160
+ if (notification.timeout === undefined) {
161
+ throw new Error(`Cannot pause notification with no timeout: ${notification.id}`)
162
+ }
163
+ if (notification.isPaused) {
164
+ throw new Error(`Cannot pause notification that is already paused: ${notification.id}`)
165
+ }
166
+ notification.isPaused = true
167
+ clearTimeout(notification._timer.id)
168
+ notification._timer.elapsedBeforePause += (Date.now() - notification.startTime)
169
+ }
170
+
171
+ resume(notification: NotificationEntry): void {
172
+ if (notification.timeout === undefined) {
173
+ throw new Error(`Cannot resume notification with no timeout: ${notification.id}`)
174
+ }
175
+ notification.isPaused = false
176
+ notification.startTime = Date.now()
177
+ const remaining = notification.timeout - notification._timer.elapsedBeforePause
178
+ clearTimeout(notification._timer.id)
179
+ notification._timer.id = setTimeout(() => {
180
+ notification.resolve(notification.cancellable)
181
+ }, remaining)
182
+ }
183
+
155
184
  static resolveToDefault(notification: NotificationEntry): void {
156
185
  notification.resolve(notification.default)
157
186
  }
@@ -174,19 +203,6 @@ export class NotificationHandler<
174
203
  clear(): void {
175
204
  setReadOnly(this, "history", [])
176
205
  }
177
-
178
- addNotificationListener(cb: NotificationListener<TEntry>): void {
179
- this.listeners.push(cb)
180
- }
181
-
182
- removeNotificationListener(cb: NotificationListener<TEntry>): void {
183
- const exists = this.listeners.indexOf(cb)
184
- if (exists > -1) {
185
- this.listeners.splice(exists, 1)
186
- } else {
187
- throw new Error(`Listener does not exist: ${cb.toString()}`)
188
- }
189
- }
190
206
  }
191
207
 
192
208
  export type NotificationPromise<TOption extends string = string> = Promise<TOption>
@@ -221,6 +237,12 @@ export type NotificationEntry<
221
237
  timeout?: number
222
238
  resolution?: string
223
239
  id: number
240
+ startTime: number
241
+ isPaused: boolean
242
+ _timer: {
243
+ id?: ReturnType<typeof setTimeout>
244
+ elapsedBeforePause: number
245
+ }
224
246
  }
225
247
 
226
248
  export type NotificationListener<TEntry extends NotificationEntry<any>> = (notification: TEntry, type: "added" | "resolved" | "deleted") => void