daisy-ui-kit 5.0.0-pre.21 → 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.
@@ -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 '~/composables/use-pikaday'
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 '~/composables/use-pikaday'
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 */
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
- import type { Toast } from '~/composables/use-toast'
2
+ import type { Toast } from '../composables/use-toast'
3
3
  import { computed } from 'vue'
4
- import { useToast } from '~/composables/use-toast'
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.21",
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
  ],