@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,56 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QPage — zone de contenu. Prop `virtual` : active le virtual scroll
|
|
3
|
+
// (rendu fenêtré via QVirtualScroll) quand on a beaucoup d'items.
|
|
4
|
+
import QVirtualScroll from "./QVirtualScroll.vue"
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
/** Active le virtual scroll (rendu fenêtré des items) */
|
|
8
|
+
virtual?: boolean
|
|
9
|
+
/** Données à afficher (requis si virtual) */
|
|
10
|
+
items?: any[]
|
|
11
|
+
/** Clé d'item : string (propriété) ou fonction */
|
|
12
|
+
itemKey?: string | ((item: any) => unknown)
|
|
13
|
+
/** Nombre d'items rendus autour de la zone visible */
|
|
14
|
+
virtualScrollSliceSize?: number
|
|
15
|
+
/** Ratio d'items rendus avant la zone visible */
|
|
16
|
+
virtualScrollSliceRatioBefore?: number
|
|
17
|
+
/** Ratio d'items rendus après la zone visible */
|
|
18
|
+
virtualScrollSliceRatioAfter?: number
|
|
19
|
+
/** Taille moyenne d'un item en px (estimation initiale) */
|
|
20
|
+
virtualScrollItemSize?: number
|
|
21
|
+
/** Espace réservé en haut (contenu sticky) */
|
|
22
|
+
virtualScrollStickySizeStart?: number
|
|
23
|
+
/** Espace réservé en bas (contenu sticky) */
|
|
24
|
+
virtualScrollStickySizeEnd?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
defineProps<Props>()
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<q-virtual-scroll
|
|
32
|
+
v-if="virtual"
|
|
33
|
+
class="q-page"
|
|
34
|
+
:items="items"
|
|
35
|
+
:item-key="itemKey"
|
|
36
|
+
:virtual-scroll-slice-size="virtualScrollSliceSize"
|
|
37
|
+
:virtual-scroll-slice-ratio-before="virtualScrollSliceRatioBefore"
|
|
38
|
+
:virtual-scroll-slice-ratio-after="virtualScrollSliceRatioAfter"
|
|
39
|
+
:virtual-scroll-item-size="virtualScrollItemSize"
|
|
40
|
+
:virtual-scroll-sticky-size-start="virtualScrollStickySizeStart"
|
|
41
|
+
:virtual-scroll-sticky-size-end="virtualScrollStickySizeEnd"
|
|
42
|
+
>
|
|
43
|
+
<template #default="scope">
|
|
44
|
+
<slot v-bind="scope" />
|
|
45
|
+
</template>
|
|
46
|
+
<template #before>
|
|
47
|
+
<slot name="before" />
|
|
48
|
+
</template>
|
|
49
|
+
<template #after>
|
|
50
|
+
<slot name="after" />
|
|
51
|
+
</template>
|
|
52
|
+
</q-virtual-scroll>
|
|
53
|
+
<div v-else class="q-page">
|
|
54
|
+
<slot />
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QPagination — pagination : <q-pagination v-model="page" :max="10" boundary-links direction-links active-color="secondary" />
|
|
3
|
+
// Fenêtre de pages avec ellipses, premier/dernier et précédent/suivant optionnels.
|
|
4
|
+
import { computed } from "vue"
|
|
5
|
+
import { Icon } from "@iconify/vue"
|
|
6
|
+
import { icons } from "../lib/icons"
|
|
7
|
+
import { cn } from "../lib/utils"
|
|
8
|
+
import { colorValue, foregroundFor } from "../lib/colors"
|
|
9
|
+
|
|
10
|
+
const SIZE_MAP: Record<string, string> = { xs: "24px", sm: "28px", md: "32px", lg: "40px", xl: "48px" }
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
/** Page courante (v-model) */
|
|
14
|
+
modelValue?: number
|
|
15
|
+
/** Première page (défaut 1) */
|
|
16
|
+
min?: number
|
|
17
|
+
/** Dernière page */
|
|
18
|
+
max: number
|
|
19
|
+
/** Nombre max de boutons de pages (0 = illimité) */
|
|
20
|
+
maxPages?: number
|
|
21
|
+
/** Boutons première/dernière */
|
|
22
|
+
boundaryLinks?: boolean
|
|
23
|
+
/** Boutons précédent/suivant */
|
|
24
|
+
directionLinks?: boolean
|
|
25
|
+
/** Icônes Iconify des boutons (défauts lucide) */
|
|
26
|
+
iconFirst?: string
|
|
27
|
+
iconPrev?: string
|
|
28
|
+
iconNext?: string
|
|
29
|
+
iconLast?: string
|
|
30
|
+
/** Couleur de la page active (token ou hex) */
|
|
31
|
+
activeColor?: string
|
|
32
|
+
/** Couleur des autres pages (token ou hex) */
|
|
33
|
+
color?: string
|
|
34
|
+
/** Taille : xs | sm | md | lg | xl ou valeur CSS */
|
|
35
|
+
size?: string
|
|
36
|
+
/** Affiche … pour les trous */
|
|
37
|
+
ellipses?: boolean
|
|
38
|
+
/** Variantes de boutons */
|
|
39
|
+
flat?: boolean
|
|
40
|
+
outline?: boolean
|
|
41
|
+
unelevated?: boolean
|
|
42
|
+
rounded?: boolean
|
|
43
|
+
square?: boolean
|
|
44
|
+
dense?: boolean
|
|
45
|
+
disable?: boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
49
|
+
modelValue: 1,
|
|
50
|
+
min: 1,
|
|
51
|
+
maxPages: 0,
|
|
52
|
+
boundaryLinks: false,
|
|
53
|
+
directionLinks: false,
|
|
54
|
+
size: "md",
|
|
55
|
+
ellipses: true,
|
|
56
|
+
activeColor: "primary",
|
|
57
|
+
color: "",
|
|
58
|
+
flat: false,
|
|
59
|
+
outline: false,
|
|
60
|
+
unelevated: false,
|
|
61
|
+
rounded: false,
|
|
62
|
+
square: false,
|
|
63
|
+
dense: false,
|
|
64
|
+
disable: false,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const emit = defineEmits<{ "update:modelValue": [value: number] }>()
|
|
68
|
+
|
|
69
|
+
const total = computed(() => Math.max(props.min, props.max))
|
|
70
|
+
const clampedPage = computed(() => Math.min(Math.max(props.modelValue, props.min), total.value))
|
|
71
|
+
|
|
72
|
+
const setPage = (p: number) => {
|
|
73
|
+
if (props.disable) return
|
|
74
|
+
emit("update:modelValue", Math.min(Math.max(p, props.min), total.value))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// — Fenêtre de pages avec ellipses —
|
|
78
|
+
const pages = computed<(number | "ellipsis")[]>(() => {
|
|
79
|
+
const totalPages = total.value
|
|
80
|
+
if (totalPages <= 1) return [1]
|
|
81
|
+
const maxPages = props.maxPages
|
|
82
|
+
if (maxPages <= 0 || totalPages <= maxPages + 2) {
|
|
83
|
+
return Array.from({ length: totalPages }, (_, i) => i + 1)
|
|
84
|
+
}
|
|
85
|
+
const cur = clampedPage.value
|
|
86
|
+
let start = Math.max(1, cur - Math.floor((maxPages - 1) / 2))
|
|
87
|
+
const end = Math.min(totalPages, start + maxPages - 1)
|
|
88
|
+
start = Math.max(1, end - maxPages + 1)
|
|
89
|
+
|
|
90
|
+
const list: (number | "ellipsis")[] = []
|
|
91
|
+
if (start > 2) list.push(1, "ellipsis")
|
|
92
|
+
else if (start === 2) list.push(1)
|
|
93
|
+
for (let i = start; i <= end; i++) list.push(i)
|
|
94
|
+
if (end < totalPages - 1) list.push("ellipsis", totalPages)
|
|
95
|
+
else if (end === totalPages - 1) list.push(totalPages)
|
|
96
|
+
return list
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const btnClasses = computed(() =>
|
|
100
|
+
cn(
|
|
101
|
+
"q-pagination__btn",
|
|
102
|
+
props.flat && "q-pagination__btn--flat",
|
|
103
|
+
props.outline && "q-pagination__btn--outline",
|
|
104
|
+
props.unelevated && "q-pagination__btn--unelevated",
|
|
105
|
+
props.rounded && "q-pagination__btn--rounded",
|
|
106
|
+
props.square && "q-pagination__btn--square",
|
|
107
|
+
props.dense && "q-pagination__btn--dense",
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
const btnSize = computed<Record<string, string>>(() => {
|
|
112
|
+
const s = SIZE_MAP[props.size] ?? props.size
|
|
113
|
+
return { minWidth: s, height: s }
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const activeStyle = computed<Record<string, string>>(() => ({
|
|
117
|
+
backgroundColor: colorValue(props.activeColor),
|
|
118
|
+
color: foregroundFor(props.activeColor),
|
|
119
|
+
borderColor: "transparent",
|
|
120
|
+
}))
|
|
121
|
+
|
|
122
|
+
const colorStyle = computed<Record<string, string> | undefined>(() =>
|
|
123
|
+
props.color ? { color: colorValue(props.color) } : undefined,
|
|
124
|
+
)
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<template>
|
|
128
|
+
<div class="q-pagination" :class="{ 'q-pagination--disabled': disable }" role="navigation" aria-label="Pagination">
|
|
129
|
+
<button
|
|
130
|
+
v-if="boundaryLinks"
|
|
131
|
+
type="button"
|
|
132
|
+
class="q-pagination__btn"
|
|
133
|
+
:class="btnClasses"
|
|
134
|
+
:style="[btnSize, colorStyle]"
|
|
135
|
+
:disabled="disable || clampedPage <= min"
|
|
136
|
+
aria-label="Première page"
|
|
137
|
+
@click="setPage(min)"
|
|
138
|
+
>
|
|
139
|
+
<Icon :icon="iconFirst || icons.chevronsLeft" aria-hidden="true" />
|
|
140
|
+
</button>
|
|
141
|
+
<button
|
|
142
|
+
v-if="directionLinks"
|
|
143
|
+
type="button"
|
|
144
|
+
class="q-pagination__btn"
|
|
145
|
+
:class="btnClasses"
|
|
146
|
+
:style="[btnSize, colorStyle]"
|
|
147
|
+
:disabled="disable || clampedPage <= min"
|
|
148
|
+
aria-label="Page précédente"
|
|
149
|
+
@click="setPage(clampedPage - 1)"
|
|
150
|
+
>
|
|
151
|
+
<Icon :icon="iconPrev || icons.chevronLeft" aria-hidden="true" />
|
|
152
|
+
</button>
|
|
153
|
+
|
|
154
|
+
<button
|
|
155
|
+
v-for="(p, i) in pages"
|
|
156
|
+
:key="i"
|
|
157
|
+
type="button"
|
|
158
|
+
class="q-pagination__btn"
|
|
159
|
+
:class="[btnClasses, p === clampedPage && 'q-pagination__btn--active']"
|
|
160
|
+
:style="[btnSize, p === clampedPage ? activeStyle : colorStyle]"
|
|
161
|
+
:disabled="disable || p === 'ellipsis'"
|
|
162
|
+
:aria-label="p === 'ellipsis' ? 'Pages intermédiaires' : `Page ${p}`"
|
|
163
|
+
:aria-current="p === clampedPage ? 'page' : undefined"
|
|
164
|
+
@click="p !== 'ellipsis' && setPage(p)"
|
|
165
|
+
>
|
|
166
|
+
<span v-if="p === 'ellipsis'" class="q-pagination__ellipsis" aria-hidden="true">…</span>
|
|
167
|
+
<span v-else>{{ p }}</span>
|
|
168
|
+
</button>
|
|
169
|
+
|
|
170
|
+
<button
|
|
171
|
+
v-if="directionLinks"
|
|
172
|
+
type="button"
|
|
173
|
+
class="q-pagination__btn"
|
|
174
|
+
:class="btnClasses"
|
|
175
|
+
:style="[btnSize, colorStyle]"
|
|
176
|
+
:disabled="disable || clampedPage >= total"
|
|
177
|
+
aria-label="Page suivante"
|
|
178
|
+
@click="setPage(clampedPage + 1)"
|
|
179
|
+
>
|
|
180
|
+
<Icon :icon="iconNext || icons.chevronRight" aria-hidden="true" />
|
|
181
|
+
</button>
|
|
182
|
+
<button
|
|
183
|
+
v-if="boundaryLinks"
|
|
184
|
+
type="button"
|
|
185
|
+
class="q-pagination__btn"
|
|
186
|
+
:class="btnClasses"
|
|
187
|
+
:style="[btnSize, colorStyle]"
|
|
188
|
+
:disabled="disable || clampedPage >= total"
|
|
189
|
+
aria-label="Dernière page"
|
|
190
|
+
@click="setPage(total)"
|
|
191
|
+
>
|
|
192
|
+
<Icon :icon="iconLast || icons.chevronsRight" aria-hidden="true" />
|
|
193
|
+
</button>
|
|
194
|
+
</div>
|
|
195
|
+
</template>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QParallax — effet parallaxe type Quasar : <q-parallax :src="img" :height="300" :speed="0.6">
|
|
3
|
+
// L'image de fond se déplace plus lentement que le scroll de la fenêtre (ou du
|
|
4
|
+
// conteneur scrollable parent), en restant toujours couvrante. Content via slot.
|
|
5
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/** Image de fond (URL) */
|
|
10
|
+
src?: string
|
|
11
|
+
/** Hauteur du bloc (px ou chaîne CSS) — défaut 200 */
|
|
12
|
+
height?: number | string
|
|
13
|
+
/** Intensité du parallaxe 0.1–1 (défaut 1) */
|
|
14
|
+
speed?: number
|
|
15
|
+
/** Désactive l'effet (image statique, cover) */
|
|
16
|
+
disable?: boolean
|
|
17
|
+
contentClass?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
21
|
+
height: 200,
|
|
22
|
+
speed: 1,
|
|
23
|
+
disable: false,
|
|
24
|
+
contentClass: "",
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const containerEl = ref<HTMLElement | null>(null)
|
|
28
|
+
const mediaEl = ref<HTMLElement | null>(null)
|
|
29
|
+
const mediaStyle = ref<Record<string, string>>({})
|
|
30
|
+
|
|
31
|
+
let scrollParent: HTMLElement | Window | null = null
|
|
32
|
+
let raf = 0
|
|
33
|
+
|
|
34
|
+
const reducedMotion =
|
|
35
|
+
typeof window !== "undefined" && !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
|
|
36
|
+
|
|
37
|
+
const update = () => {
|
|
38
|
+
const el = containerEl.value
|
|
39
|
+
const media = mediaEl.value
|
|
40
|
+
if (!el || !media) return
|
|
41
|
+
const h = el.offsetHeight
|
|
42
|
+
const speed = Math.min(1, Math.max(0.1, props.speed))
|
|
43
|
+
const mediaH = h * (1 + 2 * 0.15 * speed) // 1.3× à speed 1
|
|
44
|
+
const travel = (mediaH - h) / 2
|
|
45
|
+
|
|
46
|
+
// Viewport visible : la fenêtre, ou le conteneur scrollable détecté
|
|
47
|
+
let vpTop = 0
|
|
48
|
+
let vpH = window.innerHeight
|
|
49
|
+
if (scrollParent && scrollParent !== window) {
|
|
50
|
+
const r = (scrollParent as HTMLElement).getBoundingClientRect()
|
|
51
|
+
vpTop = r.top
|
|
52
|
+
vpH = r.height
|
|
53
|
+
}
|
|
54
|
+
const vpCenter = vpTop + vpH / 2
|
|
55
|
+
const elCenter = el.getBoundingClientRect().top + h / 2
|
|
56
|
+
const maxDist = vpH / 2 + h / 2
|
|
57
|
+
const percent = Math.max(-1, Math.min(1, (elCenter - vpCenter) / maxDist))
|
|
58
|
+
const parallax = -(travel * percent)
|
|
59
|
+
|
|
60
|
+
mediaStyle.value = {
|
|
61
|
+
top: -travel + "px",
|
|
62
|
+
height: mediaH + "px",
|
|
63
|
+
transform:
|
|
64
|
+
props.disable || reducedMotion ? "none" : `translate3d(0, ${parallax}px, 0)`,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const schedule = () => {
|
|
69
|
+
if (props.disable || reducedMotion) return
|
|
70
|
+
if (raf) cancelAnimationFrame(raf)
|
|
71
|
+
raf = requestAnimationFrame(update)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const findScrollParent = (el: HTMLElement): HTMLElement | Window => {
|
|
75
|
+
let node = el.parentElement
|
|
76
|
+
while (node) {
|
|
77
|
+
const s = getComputedStyle(node)
|
|
78
|
+
if (/(auto|scroll|overlay)/.test(s.overflowY) && node.scrollHeight > node.clientHeight)
|
|
79
|
+
return node
|
|
80
|
+
node = node.parentElement
|
|
81
|
+
}
|
|
82
|
+
return window
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
onMounted(() => {
|
|
86
|
+
if (typeof window === "undefined") return
|
|
87
|
+
scrollParent = findScrollParent(containerEl.value!)
|
|
88
|
+
const target = scrollParent === window ? window : scrollParent
|
|
89
|
+
target.addEventListener("scroll", schedule, { passive: true })
|
|
90
|
+
window.addEventListener("resize", schedule)
|
|
91
|
+
update()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
onBeforeUnmount(() => {
|
|
95
|
+
if (raf) cancelAnimationFrame(raf)
|
|
96
|
+
if (scrollParent) {
|
|
97
|
+
const target = scrollParent === window ? window : scrollParent
|
|
98
|
+
target.removeEventListener("scroll", schedule)
|
|
99
|
+
}
|
|
100
|
+
window.removeEventListener("resize", schedule)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
watch(() => props.disable, update)
|
|
104
|
+
|
|
105
|
+
const containerStyle = computed<Record<string, string>>(() => ({
|
|
106
|
+
height: typeof props.height === "number" ? props.height + "px" : props.height,
|
|
107
|
+
}))
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<template>
|
|
111
|
+
<div
|
|
112
|
+
ref="containerEl"
|
|
113
|
+
class="q-parallax"
|
|
114
|
+
:class="cn(props.disable && 'q-parallax--disabled')"
|
|
115
|
+
:style="containerStyle"
|
|
116
|
+
>
|
|
117
|
+
<div ref="mediaEl" class="q-parallax__media" :style="mediaStyle">
|
|
118
|
+
<img v-if="src" :src="src" alt="" loading="lazy" draggable="false" />
|
|
119
|
+
</div>
|
|
120
|
+
<div class="q-parallax__content" :class="contentClass">
|
|
121
|
+
<slot />
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</template>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QPullToRefresh — tirer pour rafraîchir : <q-pull-to-refresh @refresh="(done) => { ...; done() }">
|
|
3
|
+
// Conteneur scrollable ; geste de traction quand scrollTop = 0 → indicateur + refresh.
|
|
4
|
+
import { computed, ref } from "vue"
|
|
5
|
+
import { Icon } from "@iconify/vue"
|
|
6
|
+
import { icons } from "../lib/icons"
|
|
7
|
+
import { colorValue } from "../lib/colors"
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
/** État de rafraîchissement (v-model) */
|
|
11
|
+
modelValue?: boolean
|
|
12
|
+
/** Couleur de l'indicateur (token ou hex) */
|
|
13
|
+
color?: string
|
|
14
|
+
/** Distance (px) de traction avant déclenchement */
|
|
15
|
+
pullBack?: number
|
|
16
|
+
/** Icône Iconify de l'indicateur (défaut : lucide:refresh-cw) */
|
|
17
|
+
icon?: string
|
|
18
|
+
/** Taille de l'icône / du spinner de l'indicateur (CSS, défaut 28px) */
|
|
19
|
+
size?: string
|
|
20
|
+
disable?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
24
|
+
modelValue: false,
|
|
25
|
+
color: "primary",
|
|
26
|
+
pullBack: 40,
|
|
27
|
+
size: "28px",
|
|
28
|
+
disable: false,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const emit = defineEmits<{
|
|
32
|
+
"update:modelValue": [value: boolean]
|
|
33
|
+
/** Appelé quand le seuil est atteint ; appeler done() pour terminer */
|
|
34
|
+
refresh: [done: () => void]
|
|
35
|
+
}>()
|
|
36
|
+
|
|
37
|
+
const internalRefreshing = ref(false)
|
|
38
|
+
const refreshing = computed({
|
|
39
|
+
get: () => props.modelValue ?? internalRefreshing.value,
|
|
40
|
+
set: (v) => {
|
|
41
|
+
internalRefreshing.value = v
|
|
42
|
+
emit("update:modelValue", v)
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const done = () => {
|
|
47
|
+
refreshing.value = false
|
|
48
|
+
pullDistance.value = 0
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// — Geste de traction (Pointer Events : tactile + souris) —
|
|
52
|
+
const contentRef = ref<HTMLElement | null>(null)
|
|
53
|
+
const pulling = ref(false)
|
|
54
|
+
const startY = ref(0)
|
|
55
|
+
const pullDistance = ref(0)
|
|
56
|
+
const RESISTANCE = 0.45
|
|
57
|
+
|
|
58
|
+
const progress = computed(() => Math.min(1, pullDistance.value / props.pullBack))
|
|
59
|
+
|
|
60
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
61
|
+
if (props.disable || refreshing.value) return
|
|
62
|
+
if (contentRef.value && contentRef.value.scrollTop > 0) return
|
|
63
|
+
startY.value = e.clientY
|
|
64
|
+
pulling.value = true
|
|
65
|
+
;(e.target as HTMLElement).setPointerCapture(e.pointerId)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const onPointerMove = (e: PointerEvent) => {
|
|
69
|
+
if (!pulling.value) return
|
|
70
|
+
const dy = e.clientY - startY.value
|
|
71
|
+
if (dy <= 0 || (contentRef.value && contentRef.value.scrollTop > 0)) {
|
|
72
|
+
pullDistance.value = 0
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
pullDistance.value = Math.min(props.pullBack * 3, dy * RESISTANCE)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const onPointerUp = () => {
|
|
79
|
+
if (!pulling.value) return
|
|
80
|
+
pulling.value = false
|
|
81
|
+
if (pullDistance.value >= props.pullBack) {
|
|
82
|
+
refreshing.value = true
|
|
83
|
+
emit("refresh", done)
|
|
84
|
+
}
|
|
85
|
+
pullDistance.value = 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const indicatorY = computed(() => (refreshing.value ? 60 : pullDistance.value))
|
|
89
|
+
|
|
90
|
+
const indicatorStyle = computed<Record<string, string>>(() => ({
|
|
91
|
+
transform: `translateY(${indicatorY.value}px)`,
|
|
92
|
+
}))
|
|
93
|
+
|
|
94
|
+
const iconStyle = computed<Record<string, string>>(() => ({
|
|
95
|
+
color: colorValue(props.color),
|
|
96
|
+
width: props.size,
|
|
97
|
+
height: props.size,
|
|
98
|
+
transform: `rotate(${progress.value * 180}deg)`,
|
|
99
|
+
}))
|
|
100
|
+
|
|
101
|
+
const rootClasses = computed(() => [
|
|
102
|
+
"q-pull-to-refresh",
|
|
103
|
+
pulling.value && "q-pull-to-refresh--pulling",
|
|
104
|
+
refreshing.value && "q-pull-to-refresh--refreshing",
|
|
105
|
+
])
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<template>
|
|
109
|
+
<div
|
|
110
|
+
ref="contentRef"
|
|
111
|
+
class="q-pull-to-refresh"
|
|
112
|
+
:class="rootClasses"
|
|
113
|
+
@pointerdown="onPointerDown"
|
|
114
|
+
@pointermove="onPointerMove"
|
|
115
|
+
@pointerup="onPointerUp"
|
|
116
|
+
@pointercancel="onPointerUp"
|
|
117
|
+
>
|
|
118
|
+
<div class="q-pull-to-refresh__indicator" :style="indicatorStyle">
|
|
119
|
+
<template v-if="refreshing">
|
|
120
|
+
<slot name="refreshing">
|
|
121
|
+
<span
|
|
122
|
+
class="q-spinner-ring"
|
|
123
|
+
:style="{ color: colorValue(color), width: size, height: size }"
|
|
124
|
+
aria-hidden="true"
|
|
125
|
+
/>
|
|
126
|
+
</slot>
|
|
127
|
+
</template>
|
|
128
|
+
<template v-else>
|
|
129
|
+
<slot name="pulling" :position="pullDistance">
|
|
130
|
+
<Icon :icon="icon || icons.refreshCw" class="q-pull-to-refresh__icon" :style="iconStyle" aria-hidden="true" />
|
|
131
|
+
</slot>
|
|
132
|
+
</template>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="q-pull-to-refresh__inner">
|
|
135
|
+
<slot />
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</template>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QRadio — API Quasar : <q-radio v-model="val" val="a" label="Option A" color="secondary" dense left-label />
|
|
3
|
+
// Sélectionné quand modelValue === val (ou trueValue si pas de val).
|
|
4
|
+
import { computed } from "vue"
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
import { colorValue } from "../lib/colors"
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
modelValue?: unknown
|
|
10
|
+
/** Valeur que ce radio représente */
|
|
11
|
+
val?: unknown
|
|
12
|
+
trueValue?: unknown
|
|
13
|
+
falseValue?: unknown
|
|
14
|
+
/** Couleur (token ou hex) */
|
|
15
|
+
color?: string
|
|
16
|
+
/** Applique la couleur aussi à l'état non sélectionné */
|
|
17
|
+
keepColor?: boolean
|
|
18
|
+
dense?: boolean
|
|
19
|
+
dark?: boolean
|
|
20
|
+
disable?: boolean
|
|
21
|
+
readonly?: boolean
|
|
22
|
+
label?: string
|
|
23
|
+
/** Label à gauche du contrôle */
|
|
24
|
+
leftLabel?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
28
|
+
color: "primary",
|
|
29
|
+
keepColor: false,
|
|
30
|
+
dense: false,
|
|
31
|
+
dark: false,
|
|
32
|
+
disable: false,
|
|
33
|
+
readonly: false,
|
|
34
|
+
leftLabel: false,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Defaults manuels (withDefaults ne gère pas les defaults sur props typées unknown)
|
|
38
|
+
const trueValue = computed(() => props.trueValue ?? true)
|
|
39
|
+
const falseValue = computed(() => props.falseValue ?? false)
|
|
40
|
+
|
|
41
|
+
const emit = defineEmits<{
|
|
42
|
+
"update:modelValue": [value: unknown]
|
|
43
|
+
focus: [event: FocusEvent]
|
|
44
|
+
blur: [event: FocusEvent]
|
|
45
|
+
}>()
|
|
46
|
+
|
|
47
|
+
const selectedValue = computed(() => props.val ?? trueValue.value)
|
|
48
|
+
const isSelected = computed(() => props.modelValue === selectedValue.value)
|
|
49
|
+
const isDisabled = computed(() => props.disable || props.readonly)
|
|
50
|
+
|
|
51
|
+
const select = () => {
|
|
52
|
+
if (isDisabled.value) return
|
|
53
|
+
emit("update:modelValue", selectedValue.value)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const circleStyle = computed<Record<string, string>>(() => {
|
|
57
|
+
const style: Record<string, string> = {}
|
|
58
|
+
const color = colorValue(props.color)
|
|
59
|
+
if (isSelected.value || props.keepColor) style.borderColor = color
|
|
60
|
+
return style
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const dotStyle = computed<Record<string, string>>((): Record<string, string> =>
|
|
64
|
+
isSelected.value ? { backgroundColor: colorValue(props.color) } : {},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const rootClasses = computed(() =>
|
|
68
|
+
cn(
|
|
69
|
+
"q-radio",
|
|
70
|
+
props.dense && "q-radio--dense",
|
|
71
|
+
props.dark && "q-radio--dark",
|
|
72
|
+
props.disable && "q-radio--disabled",
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<template>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
role="radio"
|
|
81
|
+
:aria-checked="isSelected ? 'true' : 'false'"
|
|
82
|
+
:disabled="isDisabled"
|
|
83
|
+
class="q-radio"
|
|
84
|
+
:class="rootClasses"
|
|
85
|
+
@click="select"
|
|
86
|
+
@focus="(e) => emit('focus', e)"
|
|
87
|
+
@blur="(e) => emit('blur', e)"
|
|
88
|
+
>
|
|
89
|
+
<span v-if="leftLabel && (label !== undefined || $slots.default)" class="q-radio__label">
|
|
90
|
+
<slot>{{ label }}</slot>
|
|
91
|
+
</span>
|
|
92
|
+
<span class="q-radio__circle" :style="circleStyle" aria-hidden="true">
|
|
93
|
+
<span v-if="isSelected" class="q-radio__dot" :style="dotStyle" />
|
|
94
|
+
</span>
|
|
95
|
+
<span v-if="!leftLabel && (label !== undefined || $slots.default)" class="q-radio__label">
|
|
96
|
+
<slot>{{ label }}</slot>
|
|
97
|
+
</span>
|
|
98
|
+
</button>
|
|
99
|
+
</template>
|