@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,250 @@
1
+ <script setup lang="ts">
2
+ // QDatePicker — sélecteur de date type DatePicker shadcn-vue, API Quasar :
3
+ // <q-date-picker v-model="date" mode="sheet" label="Échéance" clearable outlined />
4
+ // Modes : inline (calendrier en place) | modal (centré) | sheet (bottom sheet) | dialog (plein écran).
5
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"
6
+ import { Icon } from "@iconify/vue"
7
+ import { icons } from "../lib/icons"
8
+ import { cn } from "../lib/utils"
9
+ import { useOverlayBack } from "../lib/overlayBack"
10
+ import QDateCalendar from "./internal/QDateCalendar.vue"
11
+
12
+ interface Props {
13
+ /** Date sélectionnée */
14
+ modelValue?: Date | null
15
+ /** Mode d'affichage */
16
+ mode?: "inline" | "sheet" | "modal" | "dialog"
17
+ label?: string
18
+ stackLabel?: boolean
19
+ hint?: string
20
+ error?: boolean
21
+ errorMessage?: string
22
+ placeholder?: string
23
+ outlined?: boolean
24
+ filled?: boolean
25
+ borderless?: boolean
26
+ dense?: boolean
27
+ clearable?: boolean
28
+ disable?: boolean
29
+ readonly?: boolean
30
+ /** Formateur d'affichage de la date sélectionnée */
31
+ format?: (d: Date) => string
32
+ /** Date minimale sélectionnable */
33
+ minDate?: Date | string
34
+ /** Date maximale sélectionnable */
35
+ maxDate?: Date | string
36
+ /** Désactive des dates arbitraires */
37
+ disabledDates?: (d: Date) => boolean
38
+ /** Premier jour de la semaine : 0 = dimanche, 1 = lundi (défaut) */
39
+ firstDayOfWeek?: number
40
+ /** Largeur du panneau (modal/sheet) */
41
+ width?: string
42
+ /** Titre du panneau */
43
+ title?: string
44
+ }
45
+
46
+ const props = withDefaults(defineProps<Props>(), {
47
+ modelValue: null,
48
+ mode: "inline",
49
+ stackLabel: false,
50
+ error: false,
51
+ outlined: false,
52
+ filled: false,
53
+ borderless: false,
54
+ dense: false,
55
+ clearable: false,
56
+ disable: false,
57
+ readonly: false,
58
+ firstDayOfWeek: 1,
59
+ width: "",
60
+ title: "",
61
+ })
62
+
63
+ const emit = defineEmits<{
64
+ "update:modelValue": [value: Date | null]
65
+ focus: [event: FocusEvent]
66
+ blur: [event: FocusEvent]
67
+ clear: []
68
+ }>()
69
+
70
+ const isPanelMode = computed(() => props.mode !== "inline")
71
+
72
+ const open = ref(false)
73
+ const rootEl = ref<HTMLElement | null>(null)
74
+ const sheetRef = ref<HTMLElement | null>(null)
75
+
76
+ // « Retour » navigateur → ferme le panneau (sheet/modal/dialog) au lieu de naviguer
77
+ const panelOpen = computed(() => isPanelMode.value && open.value)
78
+ useOverlayBack(panelOpen, () => closePopup(), "QDatePicker")
79
+
80
+ // — Affichage du champ —
81
+ const formatFn = computed(
82
+ () =>
83
+ props.format ??
84
+ ((d: Date) =>
85
+ new Intl.DateTimeFormat(undefined, { day: "2-digit", month: "short", year: "numeric" }).format(d)),
86
+ )
87
+
88
+ const displayText = computed(() => (props.modelValue ? formatFn.value(props.modelValue) : ""))
89
+
90
+ const floatActive = computed(
91
+ () => open.value || props.modelValue !== null || props.stackLabel,
92
+ )
93
+
94
+ const fieldClasses = computed(() =>
95
+ cn(
96
+ "q-date-picker q-input",
97
+ props.outlined && "q-field--outlined",
98
+ props.filled && "q-field--filled",
99
+ props.borderless && "q-field--borderless",
100
+ props.dense && "q-field--dense",
101
+ props.error && "q-field--error",
102
+ props.disable && "q-field--disabled",
103
+ floatActive.value && "q-field--float",
104
+ ),
105
+ )
106
+
107
+ const onSelect = (d: Date) => {
108
+ emit("update:modelValue", d)
109
+ if (isPanelMode.value) open.value = false
110
+ }
111
+
112
+ const onClear = () => {
113
+ emit("update:modelValue", null)
114
+ emit("clear")
115
+ }
116
+
117
+ const openPopup = () => {
118
+ if (props.disable || props.readonly) return
119
+ open.value = true
120
+ }
121
+
122
+ const closePopup = () => {
123
+ open.value = false
124
+ }
125
+
126
+ const sheetTitle = computed(() => props.title || props.label || "Sélectionner une date")
127
+
128
+ const sheetStyle = computed<Record<string, string> | undefined>(() =>
129
+ props.width ? { width: props.width, maxWidth: props.width } : undefined,
130
+ )
131
+
132
+ // — Fermetures : clic extérieur (hors panneau téléporté), Échap, scroll lock —
133
+ const onDocMousedown = (e: MouseEvent) => {
134
+ const target = e.target as Node
135
+ if (rootEl.value?.contains(target)) return
136
+ if (sheetRef.value?.contains(target)) return
137
+ closePopup()
138
+ }
139
+
140
+ const onDocKeydown = (e: KeyboardEvent) => {
141
+ if (e.key === "Escape" && open.value) closePopup()
142
+ }
143
+
144
+ watch(open, (v) => {
145
+ if (typeof document !== "undefined" && isPanelMode.value) {
146
+ document.body.style.overflow = v ? "hidden" : ""
147
+ }
148
+ })
149
+
150
+ onMounted(() => {
151
+ if (typeof document !== "undefined") {
152
+ document.addEventListener("mousedown", onDocMousedown)
153
+ document.addEventListener("keydown", onDocKeydown)
154
+ }
155
+ })
156
+ onBeforeUnmount(() => {
157
+ if (typeof document !== "undefined") {
158
+ document.removeEventListener("mousedown", onDocMousedown)
159
+ document.removeEventListener("keydown", onDocKeydown)
160
+ document.body.style.overflow = ""
161
+ }
162
+ })
163
+ </script>
164
+
165
+ <template>
166
+ <div ref="rootEl" :class="isPanelMode ? fieldClasses : 'q-date-picker'">
167
+ <!-- Mode inline : calendrier en place -->
168
+ <div v-if="mode === 'inline'">
169
+ <q-date-calendar
170
+ :model-value="modelValue"
171
+ :min-date="minDate"
172
+ :max-date="maxDate"
173
+ :disabled-dates="disabledDates"
174
+ :first-day-of-week="firstDayOfWeek"
175
+ @select="onSelect"
176
+ />
177
+ </div>
178
+
179
+ <!-- Modes panneau : champ déclencheur -->
180
+ <template v-else>
181
+ <label v-if="label" class="q-field__label-stack">{{ label }}</label>
182
+ <div class="q-field__control" @click="openPopup">
183
+ <Icon :icon="icons.calendarDays" class="q-date-picker__field-icon" aria-hidden="true" />
184
+ <span class="q-select__display" :class="{ 'q-select__display--placeholder': !displayText }">
185
+ {{ displayText || placeholder }}
186
+ </span>
187
+ <button
188
+ v-if="clearable && modelValue && !disable && !readonly"
189
+ class="q-field__clear"
190
+ type="button"
191
+ aria-label="Effacer"
192
+ @click.stop="onClear"
193
+ >
194
+ <Icon :icon="icons.x" aria-hidden="true" />
195
+ </button>
196
+ <ChevronDown class="q-select__arrow" :class="{ 'q-select__arrow--rotated': open }" aria-hidden="true" />
197
+ </div>
198
+ <div v-if="error" class="q-field__bottom">
199
+ <div class="q-field__error">{{ errorMessage }}</div>
200
+ </div>
201
+ <div v-else-if="hint" class="q-field__bottom">
202
+ <div class="q-field__hint">{{ hint }}</div>
203
+ </div>
204
+
205
+ <Teleport to="body">
206
+ <Transition name="q-date-modal">
207
+ <div
208
+ v-if="open"
209
+ class="q-date-picker__overlay"
210
+ :class="`q-date-picker__overlay--${mode}`"
211
+ role="presentation"
212
+ @mousedown.self="closePopup"
213
+ >
214
+ <div
215
+ ref="sheetRef"
216
+ class="q-date-picker__sheet"
217
+ :class="`q-date-picker__sheet--${mode}`"
218
+ :style="sheetStyle"
219
+ role="dialog"
220
+ aria-modal="true"
221
+ :aria-label="sheetTitle"
222
+ >
223
+ <div class="q-date-picker__sheet-header">
224
+ <span class="q-date-picker__sheet-title">{{ sheetTitle }}</span>
225
+ <button
226
+ type="button"
227
+ class="q-date-picker__sheet-close"
228
+ :aria-label="`Fermer ${sheetTitle}`"
229
+ @click="closePopup"
230
+ >
231
+ <Icon :icon="icons.x" aria-hidden="true" />
232
+ </button>
233
+ </div>
234
+ <div class="q-date-picker__sheet-body">
235
+ <q-date-calendar
236
+ :model-value="modelValue"
237
+ :min-date="minDate"
238
+ :max-date="maxDate"
239
+ :disabled-dates="disabledDates"
240
+ :first-day-of-week="firstDayOfWeek"
241
+ @select="onSelect"
242
+ />
243
+ </div>
244
+ </div>
245
+ </div>
246
+ </Transition>
247
+ </Teleport>
248
+ </template>
249
+ </div>
250
+ </template>
@@ -0,0 +1,163 @@
1
+ <script lang="ts">
2
+ // QDialog — modale type Dialog shadcn-vue (reka-ui), API Quasar.
3
+ // <q-dialog v-model="open" persistent position="bottom"> + QDialogTrigger/Header/Footer
4
+ import type { InjectionKey } from "vue"
5
+
6
+ export interface DialogContext {
7
+ setOpen: (v: boolean) => void
8
+ }
9
+
10
+ export const qDialogKey: InjectionKey<DialogContext> = Symbol("q-dialog")
11
+ </script>
12
+
13
+ <script setup lang="ts">
14
+ import { computed, onBeforeUnmount, onMounted, provide, watch } from "vue"
15
+ import type { StyleValue } from "vue"
16
+ import { cn } from "../lib/utils"
17
+ import { radiusStyle, useRadius } from "../lib/useComponentProps"
18
+ import type { RadiusProp } from "../lib/useComponentProps"
19
+ import { useOverlayBack } from "../lib/overlayBack"
20
+
21
+ type DialogPosition = "standard" | "top" | "right" | "bottom" | "left"
22
+
23
+ interface Props {
24
+ /** Ouvert (v-model) */
25
+ modelValue?: boolean
26
+ /** Ne se ferme ni au backdrop ni à Échap */
27
+ persistent?: boolean
28
+ /** Plein écran */
29
+ maximized?: boolean
30
+ /** Position : standard (centré) | top | right | bottom | left */
31
+ position?: DialogPosition
32
+ /** Coins droits */
33
+ square?: boolean
34
+ /** Coins arrondis : true = md, ou échelle xs|sm|md|lg (none = carré) */
35
+ radius?: RadiusProp
36
+ fullWidth?: boolean
37
+ fullHeight?: boolean
38
+ contentClass?: string
39
+ /** Style(s) du panneau — string, objet ou tableau (comme la prop style de Vue) */
40
+ contentStyle?: StyleValue
41
+ noBackdropDismiss?: boolean
42
+ noEscDismiss?: boolean
43
+ /** Animation : fade | zoom | slide-up | slide-down | slide-left | slide-right (sinon basée sur position) */
44
+ transition?: "fade" | "zoom" | "slide-up" | "slide-down" | "slide-left" | "slide-right"
45
+ /** Durée des transitions d'entrée/sortie en ms (défaut CSS : ~200ms entrée, ~150ms sortie) */
46
+ transitionDuration?: number
47
+ }
48
+
49
+ const props = withDefaults(defineProps<Props>(), {
50
+ modelValue: false,
51
+ persistent: false,
52
+ maximized: false,
53
+ position: "standard",
54
+ square: false,
55
+ fullWidth: false,
56
+ fullHeight: false,
57
+ contentClass: "",
58
+ contentStyle: "",
59
+ noBackdropDismiss: false,
60
+ noEscDismiss: false,
61
+ transitionDuration: undefined,
62
+ })
63
+
64
+ const emit = defineEmits<{
65
+ "update:modelValue": [value: boolean]
66
+ show: []
67
+ hide: []
68
+ }>()
69
+
70
+ const open = computed({
71
+ get: () => props.modelValue,
72
+ set: (v) => emit("update:modelValue", v),
73
+ })
74
+
75
+ provide<DialogContext>(qDialogKey, {
76
+ setOpen: (v) => {
77
+ open.value = v
78
+ },
79
+ })
80
+
81
+ // Échap + verrouillage du scroll du body
82
+ const onDocKeydown = (e: KeyboardEvent) => {
83
+ if (e.key === "Escape" && open.value && !props.persistent && !props.noEscDismiss) {
84
+ open.value = false
85
+ }
86
+ }
87
+
88
+ // « Retour » navigateur → ferme le dialog au lieu de naviguer
89
+ useOverlayBack(open, () => { open.value = false }, "QDialog")
90
+
91
+ watch(open, (v) => {
92
+ if (typeof document !== "undefined") document.body.style.overflow = v ? "hidden" : ""
93
+ })
94
+
95
+ onMounted(() => {
96
+ if (typeof document !== "undefined") document.addEventListener("keydown", onDocKeydown)
97
+ })
98
+ onBeforeUnmount(() => {
99
+ if (typeof document !== "undefined") {
100
+ document.removeEventListener("keydown", onDocKeydown)
101
+ document.body.style.overflow = ""
102
+ }
103
+ })
104
+
105
+ const overlayClasses = computed(() => cn("q-dialog__overlay", `q-dialog__overlay--${props.position}`))
106
+
107
+ // radius : prop explicite > composantProps.QDialog.radius ; échelle → --q-radius
108
+ const effectiveRadius = useRadius("QDialog", () => props.radius)
109
+ const roundedStyle = computed(() => radiusStyle(effectiveRadius.value))
110
+
111
+ const contentClasses = computed(() =>
112
+ cn(
113
+ "q-dialog__content",
114
+ `q-dialog__content--${props.position}`,
115
+ props.maximized && "q-dialog__content--maximized",
116
+ props.square && "q-dialog__content--square",
117
+ props.fullWidth && "q-dialog__content--full-width",
118
+ props.fullHeight && "q-dialog__content--full-height",
119
+ props.contentClass,
120
+ ),
121
+ )
122
+
123
+ // Nom de la transition : custom (prop) ou défaut basé sur position
124
+ const transitionName = computed(() =>
125
+ props.transition ? `q-dialog-${props.transition}` : "q-dialog",
126
+ )
127
+
128
+ // Durée des transitions (enter/leave) via variables CSS sur l'overlay
129
+ const durationStyle = computed<StyleValue>(() =>
130
+ props.transitionDuration
131
+ ? {
132
+ "--q-dialog-duration-enter": `${props.transitionDuration}ms`,
133
+ "--q-dialog-duration-leave": `${props.transitionDuration}ms`,
134
+ }
135
+ : {},
136
+ )
137
+ </script>
138
+
139
+ <template>
140
+ <Teleport to="body">
141
+ <Transition :name="transitionName" @after-enter="emit('show')" @after-leave="emit('hide')">
142
+ <div
143
+ v-if="open"
144
+ class="q-dialog__overlay"
145
+ :class="overlayClasses"
146
+ :style="durationStyle"
147
+ @mousedown.self="!persistent && !noBackdropDismiss && (open = false)"
148
+ >
149
+ <div
150
+ v-if="open"
151
+ class="q-dialog__content"
152
+ :class="contentClasses"
153
+ :style="[contentStyle, roundedStyle]"
154
+ role="dialog"
155
+ aria-modal="true"
156
+ @mousedown.stop
157
+ >
158
+ <slot />
159
+ </div>
160
+ </div>
161
+ </Transition>
162
+ </Teleport>
163
+ </template>
@@ -0,0 +1,9 @@
1
+ <script setup lang="ts">
2
+ // QDialogFooter — zone d'actions en bas de la modale (équivalent DialogFooter).
3
+ </script>
4
+
5
+ <template>
6
+ <div class="q-dialog__footer">
7
+ <slot />
8
+ </div>
9
+ </template>
@@ -0,0 +1,41 @@
1
+ <script setup lang="ts">
2
+ // QDialogHeader — header de la modale : titre + description + bouton fermer (équivalent DialogHeader/Title/Close).
3
+ import { inject } from "vue"
4
+ import { Icon } from "@iconify/vue"
5
+ import { icons } from "../lib/icons"
6
+ import { qDialogKey } from "./QDialog.vue"
7
+
8
+ interface Props {
9
+ title?: string
10
+ description?: string
11
+ /** Bouton fermer */
12
+ showClose?: boolean
13
+ }
14
+
15
+ const props = withDefaults(defineProps<Props>(), {
16
+ showClose: true,
17
+ })
18
+
19
+ const dialog = inject(qDialogKey, null)
20
+ </script>
21
+
22
+ <template>
23
+ <div class="q-dialog__header">
24
+ <div class="q-dialog__header-text">
25
+ <h2 v-if="title" class="q-dialog__title">{{ title }}</h2>
26
+ <slot name="title" />
27
+ <p v-if="description" class="q-dialog__description">{{ description }}</p>
28
+ <slot name="description" />
29
+ </div>
30
+ <button
31
+ v-if="showClose"
32
+ type="button"
33
+ class="q-dialog__close"
34
+ aria-label="Fermer"
35
+ @click="dialog?.setOpen(false)"
36
+ >
37
+ <Icon :icon="icons.x" aria-hidden="true" />
38
+ </button>
39
+ <slot />
40
+ </div>
41
+ </template>
@@ -0,0 +1,65 @@
1
+ <script setup lang="ts">
2
+ // QDialogProvider — rend la pile de dialogues programmatiques ($q.dialog).
3
+ // Intégré dans QConfigProvider (rendu automatiquement par le plus externe),
4
+ // mais utilisable aussi en autonome : <q-dialog-provider><slot /></q-dialog-provider>.
5
+ // Chaque entrée de la pile est rendue dans un <q-dialog> ; quand le composant
6
+ // émet ok / cancel / dismiss / close, le résolveur correspondant est appelé
7
+ // puis l'entrée est retirée de la pile.
8
+ import { computed, inject } from "vue"
9
+ import { dialogStack, closeDialog } from "../lib/q"
10
+ import type { DialogController } from "../lib/q"
11
+ import { qConfigKey } from "../lib/config"
12
+ import { themeVarsStyle } from "../lib/themeVars"
13
+
14
+ const dialogs = dialogStack
15
+
16
+ const config = inject(qConfigKey, null)
17
+
18
+ // Les dialogs sont téléportés au body (QDialog) : on repose les tokens du thème
19
+ // sur leur contenu pour qu'ils héritent des couleurs/radius du provider.
20
+ const dialogThemeStyle = themeVarsStyle(computed(() => config?.theme.value ?? {}))
21
+
22
+ const contentStyle = (ctrl: DialogController) =>
23
+ typeof ctrl._opts.fullscreen === "string" ? { width: ctrl._opts.fullscreen } : undefined
24
+
25
+ const handleOk = (ctrl: DialogController, data?: unknown) => {
26
+ ctrl._resolvers.ok?.(data)
27
+ closeDialog(ctrl)
28
+ }
29
+ const handleCancel = (ctrl: DialogController) => {
30
+ ctrl._resolvers.cancel?.()
31
+ closeDialog(ctrl)
32
+ }
33
+ const handleDismiss = (ctrl: DialogController) => {
34
+ ctrl._resolvers.dismiss?.()
35
+ closeDialog(ctrl)
36
+ }
37
+ </script>
38
+
39
+ <template>
40
+ <slot />
41
+ <q-dialog
42
+ v-for="(dialog, idx) in dialogs"
43
+ :key="idx"
44
+ :model-value="true"
45
+ :maximized="dialog._opts.fullscreen === true"
46
+ :content-style="[contentStyle(dialog), dialogThemeStyle]"
47
+ :content-class="dialog._opts.class"
48
+ :persistent="dialog._opts.persistent"
49
+ @update:model-value="(v: boolean) => !v && handleDismiss(dialog)"
50
+ >
51
+ <q-dialog-header
52
+ v-if="dialog._opts.title || dialog._opts.description"
53
+ :title="dialog._opts.title"
54
+ :description="dialog._opts.description"
55
+ />
56
+ <component
57
+ :is="dialog._opts.component"
58
+ v-bind="dialog._opts.componentProps"
59
+ @ok="(data?: unknown) => handleOk(dialog, data)"
60
+ @cancel="handleCancel(dialog)"
61
+ @dismiss="handleDismiss(dialog)"
62
+ @close="handleDismiss(dialog)"
63
+ />
64
+ </q-dialog>
65
+ </template>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ // QDialogTrigger — bouton d'ouverture (équivalent DialogTrigger).
3
+ import { inject } from "vue"
4
+ import { qDialogKey } from "./QDialog.vue"
5
+
6
+ interface Props {
7
+ label?: string
8
+ }
9
+
10
+ const props = defineProps<Props>()
11
+ const dialog = inject(qDialogKey, null)
12
+ </script>
13
+
14
+ <template>
15
+ <button type="button" class="q-dialog__trigger" @click="dialog?.setOpen(true)">
16
+ <slot>{{ label }}</slot>
17
+ </button>
18
+ </template>
@@ -0,0 +1,80 @@
1
+ <script lang="ts">
2
+ // QFab — bouton d'action flottant (FAB) avec actions dépliables, API Quasar.
3
+ import type { InjectionKey } from "vue"
4
+
5
+ export interface QFabContext {
6
+ close: () => void
7
+ }
8
+
9
+ export const qFabKey: InjectionKey<QFabContext> = Symbol("q-fab")
10
+ </script>
11
+
12
+ <script setup lang="ts">
13
+ import { computed, provide, ref } from "vue"
14
+ import { Icon } from "@iconify/vue"
15
+ import { icons } from "../lib/icons"
16
+ import { colorValue, foregroundFor } from "../lib/colors"
17
+
18
+ type FabPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right"
19
+
20
+ interface Props {
21
+ /** Ouvert (v-model) */
22
+ modelValue?: boolean
23
+ /** Position du FAB à l'écran */
24
+ position?: FabPosition
25
+ /** Icône Iconify du bouton principal (défaut : lucide:plus — rotation 45° à l'ouverture) */
26
+ icon?: string
27
+ /** Couleur du bouton principal (token ou hex) */
28
+ color?: string
29
+ disable?: boolean
30
+ }
31
+
32
+ const props = withDefaults(defineProps<Props>(), {
33
+ modelValue: false,
34
+ position: "bottom-right",
35
+ icon: icons.plus,
36
+ color: "primary",
37
+ disable: false,
38
+ })
39
+
40
+ const emit = defineEmits<{ "update:modelValue": [value: boolean] }>()
41
+
42
+ const open = computed({
43
+ get: () => props.modelValue,
44
+ set: (v) => emit("update:modelValue", v),
45
+ })
46
+
47
+ const toggle = () => {
48
+ if (!props.disable) open.value = !open.value
49
+ }
50
+
51
+ provide<QFabContext>(qFabKey, {
52
+ close: () => {
53
+ open.value = false
54
+ },
55
+ })
56
+
57
+ const mainStyle = computed<Record<string, string>>(() => ({
58
+ backgroundColor: colorValue(props.color),
59
+ color: foregroundFor(props.color),
60
+ }))
61
+ </script>
62
+
63
+ <template>
64
+ <div class="q-fab" :class="[`q-fab--${position}`, open && 'q-fab--open']">
65
+ <div class="q-fab__actions" role="menu">
66
+ <slot />
67
+ </div>
68
+ <button
69
+ type="button"
70
+ class="q-fab__main"
71
+ :style="mainStyle"
72
+ :aria-expanded="open ? 'true' : 'false'"
73
+ :aria-label="open ? 'Fermer les actions' : 'Ouvrir les actions'"
74
+ :disabled="disable"
75
+ @click="toggle"
76
+ >
77
+ <Icon :icon="icon" aria-hidden="true" />
78
+ </button>
79
+ </div>
80
+ </template>
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ // QFabAction — action individuelle du QFab (équivalent QFabAction Quasar).
3
+ // Au clic : émet "click" et ferme le QFab parent.
4
+ import { computed, inject } from "vue"
5
+ import { Icon } from "@iconify/vue"
6
+ import { qFabKey } from "./QFab.vue"
7
+ import { colorValue, foregroundFor } from "../lib/colors"
8
+
9
+ interface Props {
10
+ /** Libellé affiché à côté du bouton */
11
+ label?: string
12
+ /** Icône Iconify (ex. : "lucide:share") */
13
+ icon?: string
14
+ /** Couleur du bouton rond (token ou hex) */
15
+ color?: string
16
+ disable?: boolean
17
+ }
18
+
19
+ const props = withDefaults(defineProps<Props>(), {
20
+ color: "",
21
+ disable: false,
22
+ })
23
+
24
+ const emit = defineEmits<{ click: [event: MouseEvent] }>()
25
+
26
+ const fab = inject(qFabKey, null)
27
+
28
+ const onClick = (e: MouseEvent) => {
29
+ if (props.disable) return
30
+ fab?.close()
31
+ emit("click", e)
32
+ }
33
+
34
+ const btnStyle = computed<Record<string, string> | undefined>(() =>
35
+ props.color
36
+ ? { backgroundColor: colorValue(props.color), color: foregroundFor(props.color) }
37
+ : undefined,
38
+ )
39
+ </script>
40
+
41
+ <template>
42
+ <button type="button" class="q-fab__action" role="menuitem" :disabled="disable" @click="onClick">
43
+ <span v-if="label" class="q-fab__action-label">{{ label }}</span>
44
+ <span class="q-fab__action-btn" :style="btnStyle">
45
+ <Icon v-if="icon" :icon="icon" aria-hidden="true" />
46
+ </span>
47
+ </button>
48
+ </template>