@dnax/ui 0.0.1

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 (150) hide show
  1. package/README.md +15 -0
  2. package/components/QAccordion.vue +68 -0
  3. package/components/QAccordionContent.vue +62 -0
  4. package/components/QAccordionItem.vue +24 -0
  5. package/components/QAccordionTrigger.vue +51 -0
  6. package/components/QActionSheet.vue +159 -0
  7. package/components/QApp.vue +8 -0
  8. package/components/QAutocomplete.vue +316 -0
  9. package/components/QAvatar.vue +92 -0
  10. package/components/QBackHeader.vue +69 -0
  11. package/components/QBackTop.vue +125 -0
  12. package/components/QBadge.vue +55 -0
  13. package/components/QBottomSheet.vue +177 -0
  14. package/components/QBottomSheetFooter.vue +9 -0
  15. package/components/QBottomSheetHeader.vue +35 -0
  16. package/components/QBottomSheetProvider.vue +62 -0
  17. package/components/QBottomSheetTrigger.vue +18 -0
  18. package/components/QBtn.vue +144 -0
  19. package/components/QBtnGroup.vue +31 -0
  20. package/components/QBubble.vue +36 -0
  21. package/components/QBubbleContent.vue +28 -0
  22. package/components/QBubbleGroup.vue +25 -0
  23. package/components/QBubbleReactions.vue +40 -0
  24. package/components/QCard.vue +63 -0
  25. package/components/QCardActions.vue +27 -0
  26. package/components/QCardContent.vue +29 -0
  27. package/components/QCarousel.vue +115 -0
  28. package/components/QCarouselContent.vue +23 -0
  29. package/components/QCarouselItem.vue +20 -0
  30. package/components/QCarouselNav.vue +51 -0
  31. package/components/QCheckbox.vue +146 -0
  32. package/components/QChip.vue +91 -0
  33. package/components/QCircularProgress.vue +122 -0
  34. package/components/QCol.vue +52 -0
  35. package/components/QCollapse.vue +141 -0
  36. package/components/QConfigProvider.vue +164 -0
  37. package/components/QContainer.vue +201 -0
  38. package/components/QCountDown.vue +164 -0
  39. package/components/QCountryPicker.vue +593 -0
  40. package/components/QDataGrid.vue +491 -0
  41. package/components/QDatePicker.vue +250 -0
  42. package/components/QDialog.vue +163 -0
  43. package/components/QDialogFooter.vue +9 -0
  44. package/components/QDialogHeader.vue +41 -0
  45. package/components/QDialogProvider.vue +65 -0
  46. package/components/QDialogTrigger.vue +18 -0
  47. package/components/QFab.vue +80 -0
  48. package/components/QFabAction.vue +48 -0
  49. package/components/QFilePicker.vue +294 -0
  50. package/components/QFooter.vue +72 -0
  51. package/components/QGallery.vue +280 -0
  52. package/components/QGrid.vue +54 -0
  53. package/components/QHeader.vue +38 -0
  54. package/components/QIcon.vue +53 -0
  55. package/components/QImagePicker.vue +248 -0
  56. package/components/QImagePreview.vue +225 -0
  57. package/components/QImagePreviewProvider.vue +35 -0
  58. package/components/QImg.vue +142 -0
  59. package/components/QInfiniteScroll.vue +109 -0
  60. package/components/QInnerLoading.vue +67 -0
  61. package/components/QInput.vue +289 -0
  62. package/components/QInputOtp.vue +238 -0
  63. package/components/QInputPassword.vue +67 -0
  64. package/components/QInputTag.vue +281 -0
  65. package/components/QIntersection.vue +119 -0
  66. package/components/QItem.vue +60 -0
  67. package/components/QItemSection.vue +47 -0
  68. package/components/QLinearProgress.vue +94 -0
  69. package/components/QList.vue +39 -0
  70. package/components/QLoading.vue +77 -0
  71. package/components/QLoadingProvider.vue +41 -0
  72. package/components/QMarquee.vue +56 -0
  73. package/components/QMessageScroller.vue +10 -0
  74. package/components/QMessageScrollerButton.vue +48 -0
  75. package/components/QMessageScrollerContent.vue +31 -0
  76. package/components/QMessageScrollerItem.vue +49 -0
  77. package/components/QMessageScrollerProvider.vue +198 -0
  78. package/components/QMessageScrollerViewport.vue +109 -0
  79. package/components/QNavMenu.vue +75 -0
  80. package/components/QNavMenuContent.vue +10 -0
  81. package/components/QNavMenuItem.vue +60 -0
  82. package/components/QNavMenuTrigger.vue +96 -0
  83. package/components/QNotifyProvider.vue +33 -0
  84. package/components/QNotifyToast.vue +60 -0
  85. package/components/QPage.vue +56 -0
  86. package/components/QPagination.vue +195 -0
  87. package/components/QParallax.vue +124 -0
  88. package/components/QPullToRefresh.vue +138 -0
  89. package/components/QRadio.vue +99 -0
  90. package/components/QRating.vue +105 -0
  91. package/components/QReorder.vue +191 -0
  92. package/components/QRollingText.vue +225 -0
  93. package/components/QRow.vue +23 -0
  94. package/components/QSafeArea.vue +39 -0
  95. package/components/QScrollArea.vue +118 -0
  96. package/components/QSelect.vue +698 -0
  97. package/components/QSeparator.vue +39 -0
  98. package/components/QSidebar.vue +153 -0
  99. package/components/QSidebarContent.vue +14 -0
  100. package/components/QSidebarFooter.vue +14 -0
  101. package/components/QSidebarHeader.vue +14 -0
  102. package/components/QSidebarMenu.vue +9 -0
  103. package/components/QSidebarMenuButton.vue +59 -0
  104. package/components/QSidebarMenuItem.vue +9 -0
  105. package/components/QSidebarTrigger.vue +32 -0
  106. package/components/QSkeleton.vue +95 -0
  107. package/components/QSlider.vue +205 -0
  108. package/components/QSpace.vue +10 -0
  109. package/components/QSpinner.vue +172 -0
  110. package/components/QSplitter.vue +145 -0
  111. package/components/QSticky.vue +181 -0
  112. package/components/QSwipeCell.vue +217 -0
  113. package/components/QSwiper.vue +102 -0
  114. package/components/QSwiperSlide.vue +19 -0
  115. package/components/QSyntax.vue +171 -0
  116. package/components/QTab.vue +175 -0
  117. package/components/QTabPanel.vue +50 -0
  118. package/components/QTabPanels.vue +106 -0
  119. package/components/QTable.vue +575 -0
  120. package/components/QTabs.vue +188 -0
  121. package/components/QText.vue +316 -0
  122. package/components/QTextCaption.vue +122 -0
  123. package/components/QToolbar.vue +28 -0
  124. package/components/QTooltip.vue +313 -0
  125. package/components/QUploader.vue +473 -0
  126. package/components/QVideo.vue +289 -0
  127. package/components/QVirtualScroll.vue +190 -0
  128. package/components/internal/QDateCalendar.vue +153 -0
  129. package/index.ts +151 -0
  130. package/lib/breakpoints.ts +44 -0
  131. package/lib/colors.ts +24 -0
  132. package/lib/config.ts +34 -0
  133. package/lib/countries.ts +261 -0
  134. package/lib/formula.ts +630 -0
  135. package/lib/icons.ts +38 -0
  136. package/lib/loading.ts +75 -0
  137. package/lib/overlayBack.ts +124 -0
  138. package/lib/platform.ts +146 -0
  139. package/lib/primitive.ts +25 -0
  140. package/lib/q.ts +287 -0
  141. package/lib/screen.ts +105 -0
  142. package/lib/search.ts +35 -0
  143. package/lib/themeVars.ts +22 -0
  144. package/lib/useComponentProps.ts +54 -0
  145. package/lib/utils.ts +7 -0
  146. package/module.ts +89 -0
  147. package/package.json +41 -0
  148. package/scripts/generate-exports.ts +56 -0
  149. package/styles/main.css +7627 -0
  150. package/tsconfig.json +33 -0
@@ -0,0 +1,198 @@
1
+ <script lang="ts">
2
+ // QMessageScrollerProvider — possède l'état de scroll (équivalent MessageScrollerProvider shadcn-vue).
3
+ // Contexte : autoScroll, ancrage des tours, visibilité, commandes de scroll.
4
+ import { computed, inject } from "vue"
5
+ import type { InjectionKey, Ref } from "vue"
6
+
7
+ export interface MessageScrollerItemReg {
8
+ id: string
9
+ el: Ref<HTMLElement | null>
10
+ anchor: boolean
11
+ }
12
+
13
+ export interface ScrollToOptions {
14
+ align?: "start" | "center" | "end"
15
+ behavior?: ScrollBehavior
16
+ scrollMargin?: number
17
+ }
18
+
19
+ export interface MessageScrollerContext {
20
+ autoScroll: Readonly<Ref<boolean>>
21
+ defaultPosition: Readonly<Ref<"start" | "end" | "last-anchor">>
22
+ peek: Readonly<Ref<number>>
23
+ margin: Readonly<Ref<number>>
24
+ threshold: Readonly<Ref<number>>
25
+ viewportEl: Ref<HTMLElement | null>
26
+ contentEl: Ref<HTMLElement | null>
27
+ pinned: Readonly<Ref<boolean>>
28
+ scrollable: Readonly<Ref<{ start: boolean; end: boolean }>>
29
+ currentAnchorId: Readonly<Ref<string | null>>
30
+ visibleIds: Readonly<Ref<string[]>>
31
+ items: Readonly<Ref<MessageScrollerItemReg[]>>
32
+ registerItem: (item: MessageScrollerItemReg) => void
33
+ unregisterItem: (id: string) => void
34
+ updateScrollState: () => void
35
+ scrollToEnd: (opts?: { behavior?: ScrollBehavior }) => void
36
+ scrollToStart: (opts?: { behavior?: ScrollBehavior }) => void
37
+ scrollToMessage: (id: string, opts?: ScrollToOptions) => boolean
38
+ }
39
+
40
+ export const qMessageScrollerKey: InjectionKey<MessageScrollerContext> = Symbol("q-message-scroller")
41
+
42
+ /** Commande le transcript depuis l'extérieur (saut vers un message, début, fin). */
43
+ export function useMessageScroller() {
44
+ const ctx = inject(qMessageScrollerKey, null)
45
+ if (!ctx) throw new Error("useMessageScroller doit être utilisé dans un QMessageScrollerProvider")
46
+ return {
47
+ scrollToMessage: ctx.scrollToMessage,
48
+ scrollToEnd: ctx.scrollToEnd,
49
+ scrollToStart: ctx.scrollToStart,
50
+ }
51
+ }
52
+
53
+ /** Position du lecteur : anchor courant + messages visibles. */
54
+ export function useMessageScrollerVisibility() {
55
+ const ctx = inject(qMessageScrollerKey, null)
56
+ return computed(() => ({
57
+ currentAnchorId: ctx?.currentAnchorId.value ?? null,
58
+ visibleMessageIds: ctx?.visibleIds.value ?? [],
59
+ }))
60
+ }
61
+
62
+ /** Bords encore scrollables (lecture du scroll en JS). */
63
+ export function useMessageScrollerScrollable() {
64
+ const ctx = inject(qMessageScrollerKey, null)
65
+ return computed(() => ctx?.scrollable.value ?? { start: false, end: false })
66
+ }
67
+ </script>
68
+
69
+ <script setup lang="ts">
70
+ // QMessageScrollerProvider — :auto-scroll :default-scroll-position :scroll-previous-item-peek :scroll-margin
71
+ import { nextTick, provide, ref, shallowRef } from "vue"
72
+
73
+ interface Props {
74
+ /** Suit le live edge tant que le lecteur est en bas */
75
+ autoScroll?: boolean
76
+ /** Position d'ouverture : start | end | last-anchor */
77
+ defaultScrollPosition?: "start" | "end" | "last-anchor"
78
+ /** Distance (px) d'un bord avant d'être considéré scrollable */
79
+ scrollEdgeThreshold?: number
80
+ /** Aperçu (px) du message précédent gardé visible lors de l'ancrage */
81
+ scrollPreviousItemPeek?: number
82
+ /** Décalage (px) appliqué en plus lors d'un scrollToMessage */
83
+ scrollMargin?: number
84
+ }
85
+
86
+ const props = withDefaults(defineProps<Props>(), {
87
+ autoScroll: false,
88
+ defaultScrollPosition: "end",
89
+ scrollEdgeThreshold: 8,
90
+ scrollPreviousItemPeek: 64,
91
+ scrollMargin: 0,
92
+ })
93
+
94
+ const viewportEl = ref<HTMLElement | null>(null)
95
+ const contentEl = ref<HTMLElement | null>(null)
96
+ const pinned = ref(true)
97
+ const scrollable = ref({ start: false, end: false })
98
+ // shallowRef : ne pas dé-unwrappe les Ref internes des enregistrements
99
+ const items = shallowRef<MessageScrollerItemReg[]>([])
100
+ const currentAnchorId = ref<string | null>(null)
101
+ const visibleIds = ref<string[]>([])
102
+
103
+ const updateScrollState = () => {
104
+ const v = viewportEl.value
105
+ if (!v) return
106
+ const atEnd = v.scrollHeight - v.scrollTop - v.clientHeight <= props.scrollEdgeThreshold
107
+ pinned.value = atEnd
108
+ scrollable.value = {
109
+ start: v.scrollTop > props.scrollEdgeThreshold,
110
+ end: !atEnd,
111
+ }
112
+ // Visibilité (coordonnées relatives au content, positionné)
113
+ const top = v.scrollTop
114
+ const bottom = top + v.clientHeight
115
+ const vis: string[] = []
116
+ let lastAnchor: string | null = null
117
+ for (const it of items.value) {
118
+ const el = it.el.value
119
+ if (!el) continue
120
+ const t = el.offsetTop
121
+ const b = t + el.offsetHeight
122
+ if (b >= top && t <= bottom) vis.push(it.id)
123
+ if (it.anchor && t <= bottom) lastAnchor = it.id
124
+ }
125
+ visibleIds.value = vis
126
+ if (lastAnchor) currentAnchorId.value = lastAnchor
127
+ }
128
+
129
+ const scrollToEnd = (opts: { behavior?: ScrollBehavior } = {}) => {
130
+ const v = viewportEl.value
131
+ if (!v) return
132
+ v.scrollTo({ top: v.scrollHeight, behavior: opts.behavior ?? "smooth" })
133
+ }
134
+
135
+ const scrollToStart = (opts: { behavior?: ScrollBehavior } = {}) => {
136
+ const v = viewportEl.value
137
+ if (!v) return
138
+ v.scrollTo({ top: 0, behavior: opts.behavior ?? "smooth" })
139
+ }
140
+
141
+ const scrollToMessage = (id: string, opts: ScrollToOptions = {}): boolean => {
142
+ const v = viewportEl.value
143
+ const reg = items.value.find((i) => i.id === id)
144
+ const el = reg?.el.value
145
+ if (!v || !el) return false
146
+ const margin = opts.scrollMargin ?? props.scrollMargin
147
+ let top = el.offsetTop - margin
148
+ if (opts.align === "center") top = el.offsetTop - (v.clientHeight - el.offsetHeight) / 2
149
+ else if (opts.align === "end") top = el.offsetTop + el.offsetHeight - v.clientHeight
150
+ v.scrollTo({ top, behavior: opts.behavior ?? "smooth" })
151
+ return true
152
+ }
153
+
154
+ // Ancrage d'un tour : quand un nouvel item scrollAnchor est ajouté (pas au montage initial),
155
+ // le viewport le place près du haut avec un aperçu du message précédent.
156
+ const registerItem = (item: MessageScrollerItemReg) => {
157
+ const wasEmpty = items.value.length === 0
158
+ items.value = [...items.value, item]
159
+ if (!wasEmpty && item.anchor) {
160
+ nextTick(() => {
161
+ const v = viewportEl.value
162
+ const el = item.el.value
163
+ if (v && el) {
164
+ v.scrollTo({ top: Math.max(0, el.offsetTop - props.scrollPreviousItemPeek), behavior: "auto" })
165
+ }
166
+ })
167
+ }
168
+ }
169
+
170
+ const unregisterItem = (id: string) => {
171
+ items.value = items.value.filter((i) => i.id !== id)
172
+ }
173
+
174
+ provide<MessageScrollerContext>(qMessageScrollerKey, {
175
+ autoScroll: computed(() => props.autoScroll),
176
+ defaultPosition: computed(() => props.defaultScrollPosition),
177
+ peek: computed(() => props.scrollPreviousItemPeek),
178
+ margin: computed(() => props.scrollMargin),
179
+ threshold: computed(() => props.scrollEdgeThreshold),
180
+ viewportEl,
181
+ contentEl,
182
+ pinned,
183
+ scrollable,
184
+ currentAnchorId,
185
+ visibleIds,
186
+ items,
187
+ registerItem,
188
+ unregisterItem,
189
+ updateScrollState,
190
+ scrollToEnd,
191
+ scrollToStart,
192
+ scrollToMessage,
193
+ })
194
+ </script>
195
+
196
+ <template>
197
+ <slot />
198
+ </template>
@@ -0,0 +1,109 @@
1
+ <script setup lang="ts">
2
+ // QMessageScrollerViewport — zone scrollable native (équivalent MessageScrollerViewport).
3
+ // Preserve la position quand l'historique est prépandu (MutationObserver), suit le live edge
4
+ // quand autoScroll est actif, applique la position d'ouverture par défaut.
5
+ import { inject, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
6
+ import { qMessageScrollerKey } from "./QMessageScrollerProvider.vue"
7
+
8
+ interface Props {
9
+ /** Garde la vue quand des messages sont ajoutés au-dessus */
10
+ preserveScrollOnPrepend?: boolean
11
+ }
12
+
13
+ const props = withDefaults(defineProps<Props>(), {
14
+ preserveScrollOnPrepend: true,
15
+ })
16
+
17
+ const ctx = inject(qMessageScrollerKey, null)
18
+ const viewportRef = ref<HTMLElement | null>(null)
19
+
20
+ let resizeObserver: ResizeObserver | null = null
21
+ let mutationObserver: MutationObserver | null = null
22
+ let preservedScrollHeight = 0
23
+
24
+ const onScroll = () => ctx?.updateScrollState()
25
+
26
+ // Prépend (historique chargé au-dessus) : garde le lecteur en place.
27
+ const onMutation = (records: MutationRecord[]) => {
28
+ const content = ctx?.contentEl.value
29
+ const viewport = ctx?.viewportEl.value
30
+ if (!content || !viewport || !ctx) return
31
+
32
+ const prepend = records.some(
33
+ (r) => r.addedNodes.length > 0 && r.previousSibling === null && r.target === content,
34
+ )
35
+
36
+ if (prepend) {
37
+ preservedScrollHeight = content.scrollHeight
38
+ requestAnimationFrame(() => {
39
+ if (ctx.pinned.value && ctx.autoScroll.value) {
40
+ ctx.scrollToEnd({ behavior: "auto" })
41
+ }
42
+ else if (props.preserveScrollOnPrepend) {
43
+ const delta = content.scrollHeight - preservedScrollHeight
44
+ if (delta > 0) viewport.scrollTop += delta
45
+ }
46
+ ctx.updateScrollState()
47
+ })
48
+ }
49
+ else if (ctx.pinned.value && ctx.autoScroll.value) {
50
+ viewport.scrollTop = viewport.scrollHeight
51
+ ctx.updateScrollState()
52
+ }
53
+ }
54
+
55
+ // Position d'ouverture par défaut (start / end / last-anchor)
56
+ const applyDefaultPosition = async () => {
57
+ await nextTick()
58
+ requestAnimationFrame(() => {
59
+ if (!ctx) return
60
+ const pos = ctx.defaultPosition.value
61
+ if (pos === "end") ctx.scrollToEnd({ behavior: "auto" })
62
+ else if (pos === "start") ctx.scrollToStart({ behavior: "auto" })
63
+ else if (pos === "last-anchor") {
64
+ const anchors = ctx.items.value.filter((i) => i.anchor)
65
+ const last = anchors[anchors.length - 1]
66
+ if (last) ctx.scrollToMessage(last.id, { align: "start", behavior: "auto", scrollMargin: ctx.margin.value })
67
+ }
68
+ ctx.updateScrollState()
69
+ })
70
+ }
71
+
72
+ onMounted(() => {
73
+ if (!ctx) return
74
+ ctx.viewportEl.value = viewportRef.value
75
+ applyDefaultPosition()
76
+
77
+ watch(
78
+ () => ctx.contentEl.value,
79
+ (el) => {
80
+ if (!el) return
81
+ resizeObserver = new ResizeObserver(() => {
82
+ if (ctx && ctx.pinned.value && ctx.autoScroll.value) ctx.scrollToEnd({ behavior: "auto" })
83
+ ctx?.updateScrollState()
84
+ })
85
+ resizeObserver.observe(el)
86
+ mutationObserver = new MutationObserver(onMutation)
87
+ mutationObserver.observe(el, { childList: true })
88
+ },
89
+ )
90
+ })
91
+
92
+ onBeforeUnmount(() => {
93
+ resizeObserver?.disconnect()
94
+ mutationObserver?.disconnect()
95
+ })
96
+ </script>
97
+
98
+ <template>
99
+ <div
100
+ ref="viewportRef"
101
+ class="q-msg-scroller__viewport"
102
+ role="region"
103
+ aria-label="Messages"
104
+ tabindex="0"
105
+ @scroll="onScroll"
106
+ >
107
+ <slot />
108
+ </div>
109
+ </template>
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ // QNavMenu — barre de navigation type NavigationMenu shadcn-vue, style Quasar.
3
+ // Fournit le contexte (item ouvert, enregistrement des triggers) via provide/inject.
4
+ import type { InjectionKey, Ref } from "vue"
5
+
6
+ export interface NavMenuContext {
7
+ openName: Readonly<Ref<string | null>>
8
+ setOpen: (name: string | null) => void
9
+ registerTrigger: (name: string, el: Ref<HTMLElement | null>) => void
10
+ }
11
+
12
+ export const qNavMenuKey: InjectionKey<NavMenuContext> = Symbol("q-nav-menu")
13
+ </script>
14
+
15
+ <script setup lang="ts">
16
+ import { onBeforeUnmount, onMounted, provide, ref, shallowRef } from "vue"
17
+
18
+ const openName = ref<string | null>(null)
19
+ const triggers = shallowRef<{ name: string; el: Ref<HTMLElement | null> }[]>([])
20
+ const rootEl = ref<HTMLElement | null>(null)
21
+
22
+ const context: NavMenuContext = {
23
+ openName,
24
+ setOpen: (name) => {
25
+ openName.value = name
26
+ },
27
+ registerTrigger: (name, el) => {
28
+ triggers.value = [...triggers.value, { name, el }]
29
+ },
30
+ }
31
+ provide(qNavMenuKey, context)
32
+
33
+ // Fermeture au clic extérieur
34
+ const onDocMousedown = (e: MouseEvent) => {
35
+ if (rootEl.value && !rootEl.value.contains(e.target as Node)) openName.value = null
36
+ }
37
+
38
+ // Clavier : flèches/Home/End déplacent le focus, Échap ferme
39
+ const onKeydown = (e: KeyboardEvent) => {
40
+ if (e.key === "Escape") {
41
+ openName.value = null
42
+ return
43
+ }
44
+ if (e.key !== "ArrowRight" && e.key !== "ArrowLeft" && e.key !== "Home" && e.key !== "End") return
45
+ if (!rootEl.value) return
46
+ const list = Array.from(
47
+ rootEl.value.querySelectorAll(".q-nav-menu__trigger, .q-nav-menu__item"),
48
+ ) as HTMLElement[]
49
+ if (list.length === 0) return
50
+ const active = document.activeElement as HTMLElement | null
51
+ const index = active ? list.indexOf(active) : -1
52
+ let target: HTMLElement | undefined
53
+ if (e.key === "ArrowRight") target = list[(index + 1) % list.length]
54
+ else if (e.key === "ArrowLeft") target = list[(index - 1 + list.length) % list.length]
55
+ else if (e.key === "Home") target = list[0]
56
+ else if (e.key === "End") target = list[list.length - 1]
57
+ if (target) {
58
+ e.preventDefault()
59
+ target.focus()
60
+ }
61
+ }
62
+
63
+ onMounted(() => {
64
+ if (typeof document !== "undefined") document.addEventListener("mousedown", onDocMousedown)
65
+ })
66
+ onBeforeUnmount(() => {
67
+ if (typeof document !== "undefined") document.removeEventListener("mousedown", onDocMousedown)
68
+ })
69
+ </script>
70
+
71
+ <template>
72
+ <nav ref="rootEl" class="q-nav-menu" @keydown="onKeydown">
73
+ <slot />
74
+ </nav>
75
+ </template>
@@ -0,0 +1,10 @@
1
+ <script setup lang="ts">
2
+ // QNavMenuContent — panneau du menu déroulant (équivalent NavigationMenuContent).
3
+ // À placer dans le slot par défaut d'un QNavMenuTrigger.
4
+ </script>
5
+
6
+ <template>
7
+ <div class="q-nav-menu__panel">
8
+ <slot />
9
+ </div>
10
+ </template>
@@ -0,0 +1,60 @@
1
+ <script setup lang="ts">
2
+ // QNavMenuItem — lien/bouton de navigation (équivalent NavigationMenuLink).
3
+ import { computed, inject } from "vue"
4
+ import { Icon } from "@iconify/vue"
5
+ import { cn } from "../lib/utils"
6
+ import { qNavMenuKey } from "./QNavMenu.vue"
7
+
8
+ interface Props {
9
+ label?: string
10
+ /** Icône Iconify (ex. : "lucide:home") */
11
+ icon?: string
12
+ /** Lien natif (<a>) ; sinon bouton */
13
+ href?: string
14
+ /** État actif (aria-current="page") */
15
+ active?: boolean
16
+ activeClass?: string
17
+ disable?: boolean
18
+ }
19
+
20
+ const props = withDefaults(defineProps<Props>(), {
21
+ active: false,
22
+ activeClass: "",
23
+ disable: false,
24
+ })
25
+
26
+ const emit = defineEmits<{ click: [event: MouseEvent] }>()
27
+
28
+ const menu = inject(qNavMenuKey, null)
29
+
30
+ const onClick = (e: MouseEvent) => {
31
+ if (props.disable) return
32
+ menu?.setOpen(null) // ferme les dropdowns après navigation
33
+ emit("click", e)
34
+ }
35
+
36
+ const itemClasses = computed(() =>
37
+ cn(
38
+ "q-nav-menu__item",
39
+ props.active && "q-nav-menu__item--active",
40
+ props.active && props.activeClass,
41
+ props.disable && "q-nav-menu__item--disabled",
42
+ ),
43
+ )
44
+ </script>
45
+
46
+ <template>
47
+ <component
48
+ :is="href ? 'a' : 'button'"
49
+ :href="href || undefined"
50
+ :disabled="href ? undefined : disable"
51
+ class="q-nav-menu__item"
52
+ :class="itemClasses"
53
+ :aria-current="active ? 'page' : undefined"
54
+ @click="onClick"
55
+ >
56
+ <Icon :icon="icon" v-if="icon" class="q-nav-menu__item-icon" aria-hidden="true" />
57
+ <span v-if="label" class="q-nav-menu__item-label">{{ label }}</span>
58
+ <slot v-else />
59
+ </component>
60
+ </template>
@@ -0,0 +1,96 @@
1
+ <script setup lang="ts">
2
+ // QNavMenuTrigger — bouton de menu avec dropdown ; le contenu du slot par défaut
3
+ // est le panneau déroulant (positionné sous le trigger, ouvert au hover/clic).
4
+ import { computed, inject, onBeforeUnmount, onMounted, ref } from "vue"
5
+ import { Icon } from "@iconify/vue"
6
+ import { icons } from "../lib/icons"
7
+ import { cn } from "../lib/utils"
8
+ import { qNavMenuKey } from "./QNavMenu.vue"
9
+
10
+ let triggerSeq = 0
11
+
12
+ interface Props {
13
+ /** Identifiant (auto-généré si absent) */
14
+ name?: string
15
+ label?: string
16
+ /** Icône Iconify à gauche (ex. : "lucide:star") */
17
+ icon?: string
18
+ disable?: boolean
19
+ }
20
+
21
+ const props = withDefaults(defineProps<Props>(), {
22
+ disable: false,
23
+ })
24
+
25
+ const menu = inject(qNavMenuKey, null)
26
+ const resolvedName = props.name ?? `q-nav-${triggerSeq++}`
27
+ const el = ref<HTMLElement | null>(null)
28
+
29
+ onMounted(() => menu?.registerTrigger(resolvedName, el))
30
+
31
+ const isOpen = computed(() => menu?.openName.value === resolvedName)
32
+
33
+ const open = () => {
34
+ if (closeTimer) {
35
+ clearTimeout(closeTimer)
36
+ closeTimer = undefined
37
+ }
38
+ if (!props.disable) menu?.setOpen(resolvedName)
39
+ }
40
+ const close = () => {
41
+ if (isOpen.value) menu?.setOpen(null)
42
+ }
43
+ const toggle = () => {
44
+ if (props.disable) return
45
+ menu?.setOpen(isOpen.value ? null : resolvedName)
46
+ }
47
+
48
+ // Fermeture différée : laisse le temps de traverser l'espace (6px) entre le
49
+ // trigger et le panneau — le mouseenter (re-entrée via le panneau) l'annule.
50
+ let closeTimer: ReturnType<typeof setTimeout> | undefined
51
+ const scheduleClose = () => {
52
+ if (closeTimer) clearTimeout(closeTimer)
53
+ closeTimer = setTimeout(() => {
54
+ closeTimer = undefined
55
+ close()
56
+ }, 150)
57
+ }
58
+
59
+ onBeforeUnmount(() => {
60
+ if (closeTimer) clearTimeout(closeTimer)
61
+ })
62
+
63
+ const triggerClasses = computed(() =>
64
+ cn("q-nav-menu__trigger", isOpen.value && "q-nav-menu__trigger--open"),
65
+ )
66
+ </script>
67
+
68
+ <template>
69
+ <div ref="el" class="q-nav-menu__trigger-wrap" @mouseenter="open" @mouseleave="scheduleClose">
70
+ <button
71
+ type="button"
72
+ class="q-nav-menu__trigger"
73
+ :class="triggerClasses"
74
+ :disabled="disable"
75
+ :aria-expanded="isOpen ? 'true' : 'false'"
76
+ @click="toggle"
77
+ >
78
+ <Icon :icon="icon" v-if="icon" class="q-nav-menu__trigger-icon" aria-hidden="true" />
79
+ <span v-if="label" class="q-nav-menu__trigger-label">{{ label }}</span>
80
+ <slot name="trigger" />
81
+ <Icon
82
+ :icon="icons.chevronDown"
83
+ class="q-nav-menu__trigger-arrow"
84
+ :class="{ 'q-nav-menu__trigger-arrow--rotated': isOpen }"
85
+ aria-hidden="true"
86
+ />
87
+ </button>
88
+ <div
89
+ v-if="$slots.default"
90
+ class="q-nav-menu__content"
91
+ :class="{ 'q-nav-menu__content--open': isOpen }"
92
+ >
93
+ <slot />
94
+ </div>
95
+ </div>
96
+ </template>
@@ -0,0 +1,33 @@
1
+ <script setup lang="ts">
2
+ // QNotifyProvider — notifications $q.notify (vue-sonner / shadcn Sonner).
3
+ // Intégré dans QConfigProvider (rendu automatiquement par le plus externe),
4
+ // mais utilisable aussi en autonome : <q-notify-provider />.
5
+ import { Toaster } from "vue-sonner"
6
+
7
+ interface Props {
8
+ /** Position des toasts (défaut : bottom-right) */
9
+ position?: "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right"
10
+ /** Couleurs riches par type (success/error/…) */
11
+ richColors?: boolean
12
+ /** Bouton fermer sur chaque toast */
13
+ closeButton?: boolean
14
+ /** Thème (défaut : light) */
15
+ theme?: "light" | "dark" | "system"
16
+ }
17
+
18
+ const props = withDefaults(defineProps<Props>(), {
19
+ position: "bottom-right",
20
+ richColors: true,
21
+ closeButton: true,
22
+ theme: "light",
23
+ })
24
+ </script>
25
+
26
+ <template>
27
+ <Toaster
28
+ :position="props.position"
29
+ :rich-colors="props.richColors"
30
+ :close-button="props.closeButton"
31
+ :theme="props.theme"
32
+ />
33
+ </template>
@@ -0,0 +1,60 @@
1
+ <script setup lang="ts">
2
+ // QNotifyToast — rendu custom d'une notification $q.notify avec couleur Quasar
3
+ // (utilisé par lib/q.ts via toast.custom de vue-sonner).
4
+ import { computed } from "vue"
5
+ import { Icon } from "@iconify/vue"
6
+ import { colorValue, contrastText } from "../lib/colors"
7
+
8
+ interface Props {
9
+ message?: string | number
10
+ caption?: string
11
+ icon?: string
12
+ /** Couleur de fond (token ou hex) — sinon neutre */
13
+ color?: string
14
+ /** Libellé de l'action principale */
15
+ actionLabel?: string
16
+ /** Callback de l'action (fourni par $q.notify) */
17
+ onAction?: () => void
18
+ /** Ferme la notification (fourni par $q.notify) */
19
+ onDismiss?: () => void
20
+ }
21
+
22
+ const props = withDefaults(defineProps<Props>(), {})
23
+
24
+ const bg = computed(() => (props.color ? colorValue(props.color) : undefined))
25
+ const fg = computed(() => (props.color ? contrastText(props.color) : "#1d1d1d"))
26
+ </script>
27
+
28
+ <template>
29
+ <div
30
+ class="q-notify"
31
+ :style="bg ? { backgroundColor: bg, color: fg } : undefined"
32
+ role="status"
33
+ >
34
+ <Icon :icon="icon" v-if="icon" class="q-notify__icon" aria-hidden="true" />
35
+ <div class="q-notify__body">
36
+ <div v-if="message" class="q-notify__message">{{ message }}</div>
37
+ <div v-if="caption" class="q-notify__caption" :style="bg ? { color: fg } : undefined">
38
+ {{ caption }}
39
+ </div>
40
+ </div>
41
+ <button
42
+ v-if="actionLabel"
43
+ type="button"
44
+ class="q-notify__action"
45
+ :style="bg ? { color: fg } : undefined"
46
+ @click="onAction?.()"
47
+ >
48
+ {{ actionLabel }}
49
+ </button>
50
+ <button
51
+ v-if="onDismiss"
52
+ type="button"
53
+ class="q-notify__close"
54
+ aria-label="Fermer"
55
+ @click="onDismiss()"
56
+ >
57
+
58
+ </button>
59
+ </div>
60
+ </template>