daisy-ui-kit 5.0.0-pre.20 → 5.0.0-pre.24
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/app/components/Calendar.vue +1 -1
- package/app/components/CalendarInput.vue +1 -1
- package/app/components/Toast.vue +2 -2
- package/app/composables/use-daisy-theme.ts +131 -0
- package/app/composables/use-pikaday.ts +35 -0
- package/app/composables/use-toast.ts +345 -0
- package/app/composables/useSearch.ts +22 -0
- package/package.json +3 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { PikadayOptions } from 'pikaday'
|
|
3
3
|
import { onMounted, ref } from 'vue'
|
|
4
|
-
import { usePikaday } from '
|
|
4
|
+
import { usePikaday } from '../composables/use-pikaday'
|
|
5
5
|
import CalendarSkeleton from './CalendarSkeleton.vue'
|
|
6
6
|
|
|
7
7
|
const props = defineProps<{
|
|
@@ -3,7 +3,7 @@ import type { PikadayOptions } from 'pikaday'
|
|
|
3
3
|
import type { ComponentPublicInstance } from 'vue'
|
|
4
4
|
import { onMounted, ref, watch } from 'vue'
|
|
5
5
|
|
|
6
|
-
import { usePikaday } from '
|
|
6
|
+
import { usePikaday } from '../composables/use-pikaday'
|
|
7
7
|
|
|
8
8
|
const props = defineProps<{
|
|
9
9
|
/** Bound value: Date object or ISO string or null */
|
package/app/components/Toast.vue
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Toast } from '
|
|
2
|
+
import type { Toast } from '../composables/use-toast'
|
|
3
3
|
import { computed } from 'vue'
|
|
4
|
-
import { useToast } from '
|
|
4
|
+
import { useToast } from '../composables/use-toast'
|
|
5
5
|
|
|
6
6
|
// Explicit slot typing (Vue 3.4+ / Volar)
|
|
7
7
|
interface ToastSlotProps {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { Ref } from 'vue'
|
|
2
|
+
import { usePreferredDark } from '@vueuse/core'
|
|
3
|
+
import { computed, ref } from 'vue'
|
|
4
|
+
|
|
5
|
+
// Type for a theme object
|
|
6
|
+
export interface DaisyThemeMeta {
|
|
7
|
+
theme: string
|
|
8
|
+
cssVars?: string
|
|
9
|
+
name?: string
|
|
10
|
+
[key: string]: any
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type DaisyThemeInput = string | DaisyThemeMeta
|
|
14
|
+
|
|
15
|
+
interface DaisyThemeOptions {
|
|
16
|
+
themes: DaisyThemeInput[]
|
|
17
|
+
defaultTheme?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Global state (client only)
|
|
21
|
+
let globalThemes: Ref<DaisyThemeMeta[]> | null = null
|
|
22
|
+
let globalTheme: Ref<string> | null = null
|
|
23
|
+
|
|
24
|
+
function normalizeTheme(input: DaisyThemeInput): DaisyThemeMeta {
|
|
25
|
+
if (typeof input === 'string') {
|
|
26
|
+
return { theme: input }
|
|
27
|
+
}
|
|
28
|
+
return { ...input }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Provide a default storage implementation (plain ref)
|
|
32
|
+
function defaultStorage<T>(_: string, initial: T): Ref<T> {
|
|
33
|
+
return ref(initial) as Ref<T>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* useDaisyTheme composable
|
|
38
|
+
* @param storage Optional. Ref factory for persistence (e.g., useCookie, useLocalStorage, or ref). Defaults to ref.
|
|
39
|
+
* @param options Optional. Theme options (themes, defaultTheme, etc.).
|
|
40
|
+
*
|
|
41
|
+
* Calling with arguments (in app.vue/root): initializes global state (client) or per-call state (server).
|
|
42
|
+
* Calling with no arguments (in any component): reuses global state (client) or per-call state (server).
|
|
43
|
+
*/
|
|
44
|
+
export function useDaisyTheme(storage?: <T>(key: string, initial: T) => Ref<T>, options?: DaisyThemeOptions) {
|
|
45
|
+
// On client, always use global state for reactivity across consumers
|
|
46
|
+
// On server, always use per-call state
|
|
47
|
+
const isClient = typeof window !== 'undefined'
|
|
48
|
+
|
|
49
|
+
// Only initialize global state if provided options (themes, etc.)
|
|
50
|
+
if (isClient && options) {
|
|
51
|
+
globalThemes = ref(options.themes?.map(normalizeTheme) ?? [])
|
|
52
|
+
globalTheme = null // reset theme so it will be re-initialized below
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Themes list
|
|
56
|
+
const themes = isClient
|
|
57
|
+
? (globalThemes ??= ref(options?.themes?.map(normalizeTheme) ?? []))
|
|
58
|
+
: ref(options?.themes?.map(normalizeTheme) ?? [])
|
|
59
|
+
|
|
60
|
+
// Theme name (persisted)
|
|
61
|
+
const _storage = storage ?? defaultStorage
|
|
62
|
+
const theme = isClient
|
|
63
|
+
? (globalTheme ??= _storage('theme', options?.defaultTheme ?? themes.value[0]?.theme ?? 'light'))
|
|
64
|
+
: _storage('theme', options?.defaultTheme ?? themes.value[0]?.theme ?? 'light')
|
|
65
|
+
|
|
66
|
+
// System dark mode
|
|
67
|
+
const preferredDark = usePreferredDark()
|
|
68
|
+
|
|
69
|
+
// Compute the effective theme for UI/DOM
|
|
70
|
+
const effectiveTheme = computed(() => {
|
|
71
|
+
if (theme.value === 'system') {
|
|
72
|
+
return preferredDark.value ? 'dark' : 'light'
|
|
73
|
+
}
|
|
74
|
+
return theme.value
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Set theme by name
|
|
78
|
+
function setTheme(name: string) {
|
|
79
|
+
if (themes.value.some(t => t.theme === name) || name === 'system') {
|
|
80
|
+
theme.value = name
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Cycle to next theme
|
|
85
|
+
function cycleTheme() {
|
|
86
|
+
const names = themes.value.map(t => t.theme)
|
|
87
|
+
if (!names.length) {
|
|
88
|
+
return // Guard: no themes
|
|
89
|
+
}
|
|
90
|
+
const idx = names.indexOf(theme.value)
|
|
91
|
+
const nextIdx = (idx + 1) % names.length
|
|
92
|
+
// Only set if defined (TypeScript safety)
|
|
93
|
+
if (typeof names[nextIdx] === 'string') {
|
|
94
|
+
setTheme(names[nextIdx]!)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Register a new theme
|
|
99
|
+
function registerTheme(newTheme: DaisyThemeInput) {
|
|
100
|
+
const meta = normalizeTheme(newTheme)
|
|
101
|
+
if (!themes.value.some(t => t.theme === meta.theme)) {
|
|
102
|
+
themes.value.push(meta)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Remove a theme by name
|
|
107
|
+
function removeTheme(name: string) {
|
|
108
|
+
const idx = themes.value.findIndex(t => t.theme === name)
|
|
109
|
+
if (idx !== -1) {
|
|
110
|
+
themes.value.splice(idx, 1)
|
|
111
|
+
// If current theme was removed, fallback to first
|
|
112
|
+
if (theme.value === name) {
|
|
113
|
+
theme.value = themes.value[0]?.theme ?? 'light'
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Get the current theme object
|
|
119
|
+
const themeInfo = computed(() => themes.value.find(t => t.theme === theme.value))
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
themes,
|
|
123
|
+
theme,
|
|
124
|
+
effectiveTheme,
|
|
125
|
+
themeInfo,
|
|
126
|
+
setTheme,
|
|
127
|
+
cycleTheme,
|
|
128
|
+
registerTheme,
|
|
129
|
+
removeTheme,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type Pikaday from 'pikaday'
|
|
2
|
+
import type { PikadayOptions } from 'pikaday'
|
|
3
|
+
import { onBeforeUnmount, ref } from 'vue'
|
|
4
|
+
|
|
5
|
+
export function usePikaday(
|
|
6
|
+
options: PikadayOptions,
|
|
7
|
+
onSelect: (date: Date) => void,
|
|
8
|
+
onOpen?: () => void,
|
|
9
|
+
onClose?: () => void,
|
|
10
|
+
) {
|
|
11
|
+
const picker = ref<Pikaday | null>(null)
|
|
12
|
+
|
|
13
|
+
async function createPicker(fieldOrContainer: HTMLElement) {
|
|
14
|
+
const { default: PikadayLib } = await import('pikaday')
|
|
15
|
+
picker.value = new PikadayLib({
|
|
16
|
+
...options,
|
|
17
|
+
field: options.bound !== false ? fieldOrContainer : undefined,
|
|
18
|
+
container: options.bound === false ? fieldOrContainer : undefined,
|
|
19
|
+
onSelect,
|
|
20
|
+
onOpen,
|
|
21
|
+
onClose,
|
|
22
|
+
})
|
|
23
|
+
return picker.value
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
onBeforeUnmount(() => {
|
|
27
|
+
picker.value?.destroy()
|
|
28
|
+
picker.value = null
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
picker,
|
|
33
|
+
createPicker,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import type { Ref } from 'vue'
|
|
2
|
+
import { computed, reactive, ref, toRef } from 'vue'
|
|
3
|
+
|
|
4
|
+
export type ToastType = 'success' | 'error' | 'info' | 'warning' | string
|
|
5
|
+
|
|
6
|
+
export type ToastPosition =
|
|
7
|
+
| 'top-start'
|
|
8
|
+
| 'top-center'
|
|
9
|
+
| 'top-end'
|
|
10
|
+
| 'middle-start'
|
|
11
|
+
| 'middle-center'
|
|
12
|
+
| 'middle-end'
|
|
13
|
+
| 'bottom-start'
|
|
14
|
+
| 'bottom-center'
|
|
15
|
+
| 'bottom-end'
|
|
16
|
+
|
|
17
|
+
export type ToastStatus = 'pending' | 'success' | 'error' | 'info' | 'warning' | 'default'
|
|
18
|
+
|
|
19
|
+
export interface Toast {
|
|
20
|
+
id: number
|
|
21
|
+
message: string
|
|
22
|
+
name?: string // toast channel name, optional but always set by logic
|
|
23
|
+
type?: ToastType
|
|
24
|
+
duration?: number
|
|
25
|
+
position: ToastPosition
|
|
26
|
+
countdown?: number
|
|
27
|
+
status?: ToastStatus
|
|
28
|
+
progress?: number // 0-1 for progress bar
|
|
29
|
+
promiseId?: string // for async/promise support
|
|
30
|
+
ariaLive?: 'polite' | 'assertive' // accessibility
|
|
31
|
+
[key: string]: any
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* State for a single toast channel (internal use)
|
|
36
|
+
*/
|
|
37
|
+
export interface ToastChannelState {
|
|
38
|
+
toasts: Ref<Toast[]>
|
|
39
|
+
toastQueue: Toast[]
|
|
40
|
+
toastLimit: number | null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let globalToastChannels: Ref<Record<string, ToastChannelState & { defaults?: Partial<Toast> }>> | null = null
|
|
44
|
+
let globalNextToastId: Ref<number> | null = null
|
|
45
|
+
|
|
46
|
+
function getGlobalToastChannels() {
|
|
47
|
+
if (!globalToastChannels) {
|
|
48
|
+
globalToastChannels = ref({})
|
|
49
|
+
}
|
|
50
|
+
return globalToastChannels
|
|
51
|
+
}
|
|
52
|
+
function getGlobalNextToastId() {
|
|
53
|
+
if (!globalNextToastId) {
|
|
54
|
+
globalNextToastId = ref(1)
|
|
55
|
+
}
|
|
56
|
+
return globalNextToastId
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getOrCreateChannel(name: string, defaults?: Partial<Toast>, limit?: number | null) {
|
|
60
|
+
const channels = getGlobalToastChannels().value
|
|
61
|
+
if (!channels[name]) {
|
|
62
|
+
channels[name] = {
|
|
63
|
+
toasts: ref<Toast[]>([]),
|
|
64
|
+
toastQueue: [],
|
|
65
|
+
toastLimit: typeof limit === 'number' ? limit : null,
|
|
66
|
+
defaults,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return toRef(channels[name])
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Clear all toast queues for all channels (useful for logout or channel switch)
|
|
74
|
+
*/
|
|
75
|
+
export function clearAllToastQueues() {
|
|
76
|
+
const channels = getGlobalToastChannels().value
|
|
77
|
+
Object.values(channels).forEach(channel => {
|
|
78
|
+
channel.toastQueue.length = 0
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Global toast notification composable supporting named channels.
|
|
84
|
+
*
|
|
85
|
+
* - Each toast has an optional `name` property (defaults to 'default').
|
|
86
|
+
* - All toast state (toasts, queue, limit) is isolated per channel.
|
|
87
|
+
* - UI and logic can independently manage/display different channels.
|
|
88
|
+
* - Defensive dev warnings if name is missing/invalid.
|
|
89
|
+
*
|
|
90
|
+
* Example usage:
|
|
91
|
+
* const { toasts, addToast } = useToast({ name: 'admin' })
|
|
92
|
+
* addToast({ message: 'Hi', name: 'admin' })
|
|
93
|
+
* // In UI: <Toast name="admin" />
|
|
94
|
+
*/
|
|
95
|
+
/**
|
|
96
|
+
* Options for useToast composable.
|
|
97
|
+
* - name: channel name (default: 'default')
|
|
98
|
+
* - defaults: default toast settings for this channel (merged into each toast)
|
|
99
|
+
*/
|
|
100
|
+
export interface UseToastOptions {
|
|
101
|
+
name?: string
|
|
102
|
+
defaults?: Partial<Toast>
|
|
103
|
+
limit?: number // per-channel toast limit
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Global toast notification composable supporting named channels and customizable defaults.
|
|
108
|
+
*
|
|
109
|
+
* - Each toast has an optional `name` property (defaults to 'default').
|
|
110
|
+
* - All toast state (toasts, queue, limit) is isolated per channel.
|
|
111
|
+
* - UI and logic can independently manage/display different channels.
|
|
112
|
+
* - Defensive dev warnings if name is missing/invalid.
|
|
113
|
+
* - You can provide `defaults` to set default toast settings for all toasts in this channel.
|
|
114
|
+
*
|
|
115
|
+
* Example usage:
|
|
116
|
+
* const { toasts, addToast } = useToast({ name: 'admin', defaults: { duration: 6000, type: 'info' } })
|
|
117
|
+
* addToast({ message: 'Hi' }) // will use defaults
|
|
118
|
+
* // In UI: <Toast name="admin" />
|
|
119
|
+
*/
|
|
120
|
+
function normalizeToast(toast: any): Toast & { countdown: number; originalDuration: number; intervalId?: number } {
|
|
121
|
+
if (toast.originalDuration == null) {
|
|
122
|
+
toast.originalDuration = toast.duration ?? 0
|
|
123
|
+
}
|
|
124
|
+
if (toast.countdown == null) {
|
|
125
|
+
toast.countdown = toast.originalDuration
|
|
126
|
+
}
|
|
127
|
+
if (typeof toast.intervalId === 'undefined') {
|
|
128
|
+
toast.intervalId = undefined
|
|
129
|
+
}
|
|
130
|
+
return toast as Toast & { countdown: number; originalDuration: number; intervalId?: number }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function useToast(options?: UseToastOptions) {
|
|
134
|
+
const name = options?.name?.trim() || 'default'
|
|
135
|
+
const defaults = options?.defaults || {}
|
|
136
|
+
const limit = typeof options?.limit === 'number' ? options.limit : null
|
|
137
|
+
|
|
138
|
+
// Always get or create the channel (init only once)
|
|
139
|
+
const channel = getOrCreateChannel(name, defaults, limit)
|
|
140
|
+
|
|
141
|
+
function addToast(
|
|
142
|
+
toast: { message: string; position?: ToastPosition; name?: string } & Partial<Omit<Toast, 'id' | 'name'>>,
|
|
143
|
+
) {
|
|
144
|
+
const toastName = toast.name?.trim() || name
|
|
145
|
+
const nextToastIdRef = getGlobalNextToastId()
|
|
146
|
+
// Always get or create the target channel for this toast, using the correct limit if provided
|
|
147
|
+
const channelLimit = typeof options?.limit === 'number' ? options.limit : null
|
|
148
|
+
const channel = getOrCreateChannel(toastName, defaults, channelLimit)
|
|
149
|
+
const merged = { ...channel.value.defaults, ...defaults, ...toast }
|
|
150
|
+
const position = merged.position ?? 'bottom-center'
|
|
151
|
+
const duration = merged.duration ?? 0
|
|
152
|
+
const status = merged.status ?? 'default'
|
|
153
|
+
if (import.meta.env.NODE_ENV !== 'production' && !toastName) {
|
|
154
|
+
console.warn('[addToast] Toast channel name is empty or invalid. Falling back to "default".')
|
|
155
|
+
}
|
|
156
|
+
const id = nextToastIdRef.value++
|
|
157
|
+
const newToast = normalizeToast(
|
|
158
|
+
reactive({
|
|
159
|
+
id,
|
|
160
|
+
...merged,
|
|
161
|
+
name: toastName,
|
|
162
|
+
position,
|
|
163
|
+
countdown: duration,
|
|
164
|
+
originalDuration: duration,
|
|
165
|
+
status,
|
|
166
|
+
progress: typeof merged.progress === 'number' ? merged.progress : undefined,
|
|
167
|
+
ariaLive: merged.ariaLive ?? 'polite',
|
|
168
|
+
intervalId: undefined as number | undefined,
|
|
169
|
+
}),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
// If limit is set and reached, queue the toast
|
|
173
|
+
if (channel.value.toastLimit && channel.value.toasts.length >= channel.value.toastLimit) {
|
|
174
|
+
;(channel.value.toastQueue ??= []).push(newToast)
|
|
175
|
+
return id
|
|
176
|
+
}
|
|
177
|
+
channel.value.toasts.push(newToast)
|
|
178
|
+
// Ensure timer always starts for visible toasts
|
|
179
|
+
if (channel.value.toasts.includes(newToast)) {
|
|
180
|
+
startToastTimer(newToast)
|
|
181
|
+
}
|
|
182
|
+
return id
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function removeToast(id: number) {
|
|
186
|
+
const idx = channel.value.toasts.findIndex(t => t.id === id)
|
|
187
|
+
if (idx !== -1) {
|
|
188
|
+
// Defensive: clear countdown timer if present
|
|
189
|
+
const toast = channel.value.toasts[idx] as Toast & { intervalId?: number }
|
|
190
|
+
if (toast.intervalId) {
|
|
191
|
+
clearInterval(toast.intervalId)
|
|
192
|
+
}
|
|
193
|
+
channel.value.toasts.splice(idx, 1)
|
|
194
|
+
// If queue exists, pop next toast
|
|
195
|
+
if (channel.value.toastLimit && (channel.value.toastQueue?.length ?? 0) > 0) {
|
|
196
|
+
const next = channel.value.toastQueue!.shift()
|
|
197
|
+
if (next) {
|
|
198
|
+
const norm = normalizeToast(next)
|
|
199
|
+
channel.value.toasts.push(norm)
|
|
200
|
+
// Ensure timer always starts for visible toasts
|
|
201
|
+
if (channel.value.toasts.includes(norm)) {
|
|
202
|
+
startToastTimer(norm)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function clearToasts() {
|
|
210
|
+
channel.value.toasts = []
|
|
211
|
+
if (channel.value.toastQueue) {
|
|
212
|
+
channel.value.toastQueue.length = 0
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Update or replace a toast by id
|
|
218
|
+
*/
|
|
219
|
+
function updateToast(id: number, updates: Partial<Toast>) {
|
|
220
|
+
const toast = channel.value.toasts.find(t => t.id === id)
|
|
221
|
+
if (toast) {
|
|
222
|
+
Object.assign(toast, updates)
|
|
223
|
+
normalizeToast(toast)
|
|
224
|
+
// If updating countdown/duration, recalculate progress
|
|
225
|
+
if (typeof toast.countdown === 'number' && typeof toast.duration === 'number' && toast.duration > 0) {
|
|
226
|
+
toast.progress = Math.max(0, toast.countdown / toast.duration)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Show a toast for the duration of a promise
|
|
233
|
+
* Sets status to 'pending', then 'success' or 'error' on resolve/reject
|
|
234
|
+
*/
|
|
235
|
+
function toastPromise<T>(
|
|
236
|
+
promise: Promise<T>,
|
|
237
|
+
options: {
|
|
238
|
+
pending: Omit<Toast, 'id'>
|
|
239
|
+
success: Omit<Toast, 'id'>
|
|
240
|
+
error: Omit<Toast, 'id'>
|
|
241
|
+
name?: string
|
|
242
|
+
},
|
|
243
|
+
): Promise<T> {
|
|
244
|
+
const toastName = options.name?.trim() || name
|
|
245
|
+
if (import.meta.env.NODE_ENV !== 'production' && !toastName) {
|
|
246
|
+
console.warn('[toastPromise] Toast channel name is empty or invalid. Falling back to "default".')
|
|
247
|
+
}
|
|
248
|
+
const promiseId = `promise-${Date.now()}-${Math.random()}`
|
|
249
|
+
const pendingId = addToast({
|
|
250
|
+
...options.pending,
|
|
251
|
+
status: 'pending',
|
|
252
|
+
promiseId,
|
|
253
|
+
message: options.pending.message,
|
|
254
|
+
name: toastName,
|
|
255
|
+
})
|
|
256
|
+
return promise.then(
|
|
257
|
+
result => {
|
|
258
|
+
updateToast(pendingId, {
|
|
259
|
+
...options.success,
|
|
260
|
+
status: 'success',
|
|
261
|
+
promiseId,
|
|
262
|
+
message: options.success.message,
|
|
263
|
+
name: toastName,
|
|
264
|
+
})
|
|
265
|
+
return result
|
|
266
|
+
},
|
|
267
|
+
err => {
|
|
268
|
+
updateToast(pendingId, {
|
|
269
|
+
...options.error,
|
|
270
|
+
status: 'error',
|
|
271
|
+
promiseId,
|
|
272
|
+
message: options.error.message,
|
|
273
|
+
name: toastName,
|
|
274
|
+
})
|
|
275
|
+
throw err
|
|
276
|
+
},
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Set a maximum number of visible toasts (queue extras)
|
|
282
|
+
*/
|
|
283
|
+
function setToastLimit(limit: number) {
|
|
284
|
+
channel.value.toastLimit = limit
|
|
285
|
+
if (channel.value.toastLimit) {
|
|
286
|
+
// If over limit, move extras to queue
|
|
287
|
+
while (channel.value.toasts.length > channel.value.toastLimit) {
|
|
288
|
+
const removed = channel.value.toasts.pop()
|
|
289
|
+
if (removed) {
|
|
290
|
+
// Stop timer for toast leaving visible list
|
|
291
|
+
if (removed.intervalId) {
|
|
292
|
+
clearInterval(removed.intervalId)
|
|
293
|
+
removed.intervalId = undefined
|
|
294
|
+
}
|
|
295
|
+
;(channel.value.toastQueue ??= []).unshift(removed)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// If under limit and queue has items, fill up
|
|
299
|
+
while (channel.value.toasts.length < channel.value.toastLimit && (channel.value.toastQueue?.length ?? 0) > 0) {
|
|
300
|
+
const next = channel.value.toastQueue!.shift()
|
|
301
|
+
if (next) {
|
|
302
|
+
const norm = normalizeToast(next)
|
|
303
|
+
channel.value.toasts.push(norm)
|
|
304
|
+
// Ensure timer always starts for visible toasts
|
|
305
|
+
if (channel.value.toasts.includes(norm)) {
|
|
306
|
+
startToastTimer(norm)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function startToastTimer(
|
|
314
|
+
toast: Toast & { countdown: number; originalDuration: number; intervalId?: number; startTime?: number },
|
|
315
|
+
) {
|
|
316
|
+
if (toast.originalDuration > 0) {
|
|
317
|
+
if (toast.intervalId) {
|
|
318
|
+
clearInterval(toast.intervalId)
|
|
319
|
+
}
|
|
320
|
+
const start = Date.now()
|
|
321
|
+
toast.startTime = start
|
|
322
|
+
|
|
323
|
+
toast.intervalId = setInterval(() => {
|
|
324
|
+
const elapsed = Date.now() - (toast.startTime ?? start)
|
|
325
|
+
toast.countdown = Math.max(0, toast.originalDuration - elapsed)
|
|
326
|
+
toast.progress = Math.max(0, toast.countdown / toast.originalDuration)
|
|
327
|
+
if (toast.countdown <= 0) {
|
|
328
|
+
clearInterval(toast.intervalId)
|
|
329
|
+
removeToast(toast.id)
|
|
330
|
+
}
|
|
331
|
+
}, 16) as unknown as number // 60fps for smooth animation
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
toasts: computed(() => channel.value.toasts),
|
|
337
|
+
addToast,
|
|
338
|
+
removeToast,
|
|
339
|
+
clearToasts,
|
|
340
|
+
updateToast,
|
|
341
|
+
toastPromise,
|
|
342
|
+
setToastLimit,
|
|
343
|
+
name,
|
|
344
|
+
}
|
|
345
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const isSearchOpen = ref(false)
|
|
2
|
+
|
|
3
|
+
export function useSearch() {
|
|
4
|
+
function openSearch() {
|
|
5
|
+
isSearchOpen.value = true
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function closeSearch() {
|
|
9
|
+
isSearchOpen.value = false
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function toggleSearch() {
|
|
13
|
+
isSearchOpen.value = !isSearchOpen.value
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
isSearchOpen,
|
|
18
|
+
openSearch,
|
|
19
|
+
closeSearch,
|
|
20
|
+
toggleSearch,
|
|
21
|
+
}
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "daisy-ui-kit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "5.0.0-pre.
|
|
4
|
+
"version": "5.0.0-pre.24",
|
|
5
5
|
"packageManager": "pnpm@10.10.0",
|
|
6
6
|
"author": "feathers.dev",
|
|
7
7
|
"exports": {
|
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
"require": "./nuxt.js"
|
|
15
15
|
},
|
|
16
16
|
"./components/*": "./app/components/*",
|
|
17
|
+
"./composables/*": "./app/composables/*",
|
|
17
18
|
"./utils/*": "./app/utils/*"
|
|
18
19
|
},
|
|
19
20
|
"main": "./nuxt.js",
|
|
20
21
|
"module": "./nuxt.js",
|
|
21
22
|
"files": [
|
|
22
23
|
"app/components/*.vue",
|
|
24
|
+
"app/composables/*",
|
|
23
25
|
"app/utils/*",
|
|
24
26
|
"nuxt.js"
|
|
25
27
|
],
|