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.
@@ -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.21",
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
- "@types/pikaday": "^1.7.9",
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
- "tailwindcss": "^4.1.5"
80
+ "happy-dom": "^20.0.11",
81
+ "tailwindcss": "^4.1.5",
82
+ "vitest": "^4.0.16"
79
83
  }
80
84
  }