@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.
- package/README.md +15 -0
- package/components/QAccordion.vue +68 -0
- package/components/QAccordionContent.vue +62 -0
- package/components/QAccordionItem.vue +24 -0
- package/components/QAccordionTrigger.vue +51 -0
- package/components/QActionSheet.vue +159 -0
- package/components/QApp.vue +8 -0
- package/components/QAutocomplete.vue +316 -0
- package/components/QAvatar.vue +92 -0
- package/components/QBackHeader.vue +69 -0
- package/components/QBackTop.vue +125 -0
- package/components/QBadge.vue +55 -0
- package/components/QBottomSheet.vue +177 -0
- package/components/QBottomSheetFooter.vue +9 -0
- package/components/QBottomSheetHeader.vue +35 -0
- package/components/QBottomSheetProvider.vue +62 -0
- package/components/QBottomSheetTrigger.vue +18 -0
- package/components/QBtn.vue +144 -0
- package/components/QBtnGroup.vue +31 -0
- package/components/QBubble.vue +36 -0
- package/components/QBubbleContent.vue +28 -0
- package/components/QBubbleGroup.vue +25 -0
- package/components/QBubbleReactions.vue +40 -0
- package/components/QCard.vue +63 -0
- package/components/QCardActions.vue +27 -0
- package/components/QCardContent.vue +29 -0
- package/components/QCarousel.vue +115 -0
- package/components/QCarouselContent.vue +23 -0
- package/components/QCarouselItem.vue +20 -0
- package/components/QCarouselNav.vue +51 -0
- package/components/QCheckbox.vue +146 -0
- package/components/QChip.vue +91 -0
- package/components/QCircularProgress.vue +122 -0
- package/components/QCol.vue +52 -0
- package/components/QCollapse.vue +141 -0
- package/components/QConfigProvider.vue +164 -0
- package/components/QContainer.vue +201 -0
- package/components/QCountDown.vue +164 -0
- package/components/QCountryPicker.vue +593 -0
- package/components/QDataGrid.vue +491 -0
- package/components/QDatePicker.vue +250 -0
- package/components/QDialog.vue +163 -0
- package/components/QDialogFooter.vue +9 -0
- package/components/QDialogHeader.vue +41 -0
- package/components/QDialogProvider.vue +65 -0
- package/components/QDialogTrigger.vue +18 -0
- package/components/QFab.vue +80 -0
- package/components/QFabAction.vue +48 -0
- package/components/QFilePicker.vue +294 -0
- package/components/QFooter.vue +72 -0
- package/components/QGallery.vue +280 -0
- package/components/QGrid.vue +54 -0
- package/components/QHeader.vue +38 -0
- package/components/QIcon.vue +53 -0
- package/components/QImagePicker.vue +248 -0
- package/components/QImagePreview.vue +225 -0
- package/components/QImagePreviewProvider.vue +35 -0
- package/components/QImg.vue +142 -0
- package/components/QInfiniteScroll.vue +109 -0
- package/components/QInnerLoading.vue +67 -0
- package/components/QInput.vue +289 -0
- package/components/QInputOtp.vue +238 -0
- package/components/QInputPassword.vue +67 -0
- package/components/QInputTag.vue +281 -0
- package/components/QIntersection.vue +119 -0
- package/components/QItem.vue +60 -0
- package/components/QItemSection.vue +47 -0
- package/components/QLinearProgress.vue +94 -0
- package/components/QList.vue +39 -0
- package/components/QLoading.vue +77 -0
- package/components/QLoadingProvider.vue +41 -0
- package/components/QMarquee.vue +56 -0
- package/components/QMessageScroller.vue +10 -0
- package/components/QMessageScrollerButton.vue +48 -0
- package/components/QMessageScrollerContent.vue +31 -0
- package/components/QMessageScrollerItem.vue +49 -0
- package/components/QMessageScrollerProvider.vue +198 -0
- package/components/QMessageScrollerViewport.vue +109 -0
- package/components/QNavMenu.vue +75 -0
- package/components/QNavMenuContent.vue +10 -0
- package/components/QNavMenuItem.vue +60 -0
- package/components/QNavMenuTrigger.vue +96 -0
- package/components/QNotifyProvider.vue +33 -0
- package/components/QNotifyToast.vue +60 -0
- package/components/QPage.vue +56 -0
- package/components/QPagination.vue +195 -0
- package/components/QParallax.vue +124 -0
- package/components/QPullToRefresh.vue +138 -0
- package/components/QRadio.vue +99 -0
- package/components/QRating.vue +105 -0
- package/components/QReorder.vue +191 -0
- package/components/QRollingText.vue +225 -0
- package/components/QRow.vue +23 -0
- package/components/QSafeArea.vue +39 -0
- package/components/QScrollArea.vue +118 -0
- package/components/QSelect.vue +698 -0
- package/components/QSeparator.vue +39 -0
- package/components/QSidebar.vue +153 -0
- package/components/QSidebarContent.vue +14 -0
- package/components/QSidebarFooter.vue +14 -0
- package/components/QSidebarHeader.vue +14 -0
- package/components/QSidebarMenu.vue +9 -0
- package/components/QSidebarMenuButton.vue +59 -0
- package/components/QSidebarMenuItem.vue +9 -0
- package/components/QSidebarTrigger.vue +32 -0
- package/components/QSkeleton.vue +95 -0
- package/components/QSlider.vue +205 -0
- package/components/QSpace.vue +10 -0
- package/components/QSpinner.vue +172 -0
- package/components/QSplitter.vue +145 -0
- package/components/QSticky.vue +181 -0
- package/components/QSwipeCell.vue +217 -0
- package/components/QSwiper.vue +102 -0
- package/components/QSwiperSlide.vue +19 -0
- package/components/QSyntax.vue +171 -0
- package/components/QTab.vue +175 -0
- package/components/QTabPanel.vue +50 -0
- package/components/QTabPanels.vue +106 -0
- package/components/QTable.vue +575 -0
- package/components/QTabs.vue +188 -0
- package/components/QText.vue +316 -0
- package/components/QTextCaption.vue +122 -0
- package/components/QToolbar.vue +28 -0
- package/components/QTooltip.vue +313 -0
- package/components/QUploader.vue +473 -0
- package/components/QVideo.vue +289 -0
- package/components/QVirtualScroll.vue +190 -0
- package/components/internal/QDateCalendar.vue +153 -0
- package/index.ts +151 -0
- package/lib/breakpoints.ts +44 -0
- package/lib/colors.ts +24 -0
- package/lib/config.ts +34 -0
- package/lib/countries.ts +261 -0
- package/lib/formula.ts +630 -0
- package/lib/icons.ts +38 -0
- package/lib/loading.ts +75 -0
- package/lib/overlayBack.ts +124 -0
- package/lib/platform.ts +146 -0
- package/lib/primitive.ts +25 -0
- package/lib/q.ts +287 -0
- package/lib/screen.ts +105 -0
- package/lib/search.ts +35 -0
- package/lib/themeVars.ts +22 -0
- package/lib/useComponentProps.ts +54 -0
- package/lib/utils.ts +7 -0
- package/module.ts +89 -0
- package/package.json +41 -0
- package/scripts/generate-exports.ts +56 -0
- package/styles/main.css +7627 -0
- 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,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>
|