daisy-ui-kit 5.0.0-pre.21 → 5.0.0-pre.25
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 +149 -63
- package/app/components/CalendarInput.vue +229 -128
- package/app/components/Toast.vue +2 -2
- package/app/composables/__tests__/use-calendar.test.ts +239 -0
- package/app/composables/use-calendar.ts +288 -0
- package/app/composables/use-daisy-theme.ts +131 -0
- package/app/composables/use-toast.ts +345 -0
- package/app/composables/useSearch.ts +22 -0
- package/package.json +8 -4
|
@@ -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.25",
|
|
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
|
],
|
|
@@ -58,7 +60,6 @@
|
|
|
58
60
|
"@vueuse/integrations": "^13.1.0",
|
|
59
61
|
"focus-trap": "^7.6.4",
|
|
60
62
|
"nuxt": "^4.2.1",
|
|
61
|
-
"pikaday": "^1.8.2",
|
|
62
63
|
"shiki": "^3.3.0",
|
|
63
64
|
"typescript": "^5.8.3",
|
|
64
65
|
"vue": "^3.5.13",
|
|
@@ -70,11 +71,14 @@
|
|
|
70
71
|
"@stylistic/eslint-plugin": "^4.2.0",
|
|
71
72
|
"@tailwindcss/typography": "^0.5.16",
|
|
72
73
|
"@tailwindcss/vite": "^4.1.5",
|
|
73
|
-
"@
|
|
74
|
+
"@vitejs/plugin-vue": "^6.0.3",
|
|
75
|
+
"@vue/test-utils": "^2.4.6",
|
|
74
76
|
"daisyui": "^5.5.5",
|
|
75
77
|
"eslint": "^9.26.0",
|
|
76
78
|
"eslint-config-prettier": "^10.1.8",
|
|
77
79
|
"eslint-plugin-vue": "^10.5.1",
|
|
78
|
-
"
|
|
80
|
+
"happy-dom": "^20.0.11",
|
|
81
|
+
"tailwindcss": "^4.1.5",
|
|
82
|
+
"vitest": "^4.0.16"
|
|
79
83
|
}
|
|
80
84
|
}
|