@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,105 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QRating — notation par étoiles : <q-rating v-model="note" :max="5" color="warning" size="lg" />
|
|
3
|
+
// Étoiles remplies jusqu'à la valeur, hover (bureau), dimming des non sélectionnées (no-dimming pour le désactiver).
|
|
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
|
+
const SIZE_MAP: Record<string, string> = { sm: "18px", md: "22px", lg: "30px", xl: "36px" }
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** Note actuelle */
|
|
13
|
+
modelValue?: number
|
|
14
|
+
/** Nombre d'étoiles (défaut 5) */
|
|
15
|
+
max?: number
|
|
16
|
+
/** Icône Iconify de l'étoile (défaut : lucide:star) */
|
|
17
|
+
icon?: string
|
|
18
|
+
/** Couleur (token ou hex) — défaut warning (ambre) */
|
|
19
|
+
color?: string
|
|
20
|
+
/** Couleur du texte (remplace color) */
|
|
21
|
+
textColor?: string
|
|
22
|
+
/** Taille : sm | md | lg | xl ou valeur CSS */
|
|
23
|
+
size?: string
|
|
24
|
+
/** Pas d'assombrissement des étoiles non sélectionnées */
|
|
25
|
+
noDimming?: boolean
|
|
26
|
+
/** Lecture seule (pas d'interaction) */
|
|
27
|
+
readonly?: boolean
|
|
28
|
+
disable?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
32
|
+
modelValue: 0,
|
|
33
|
+
max: 5,
|
|
34
|
+
icon: icons.star,
|
|
35
|
+
color: "warning",
|
|
36
|
+
textColor: "",
|
|
37
|
+
size: "md",
|
|
38
|
+
noDimming: false,
|
|
39
|
+
readonly: false,
|
|
40
|
+
disable: false,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const emit = defineEmits<{ "update:modelValue": [value: number] }>()
|
|
44
|
+
|
|
45
|
+
const isDisabled = computed(() => props.readonly || props.disable)
|
|
46
|
+
const hoverValue = ref(0)
|
|
47
|
+
|
|
48
|
+
const displayValue = computed(() => (hoverValue.value > 0 ? hoverValue.value : props.modelValue))
|
|
49
|
+
|
|
50
|
+
const iconStyle = computed<Record<string, string>>(() => {
|
|
51
|
+
const style: Record<string, string> = {}
|
|
52
|
+
style.width = SIZE_MAP[props.size] ?? props.size
|
|
53
|
+
style.height = SIZE_MAP[props.size] ?? props.size
|
|
54
|
+
style.color = props.textColor ? colorValue(props.textColor) : colorValue(props.color)
|
|
55
|
+
return style
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const isOn = (n: number) => displayValue.value >= n
|
|
59
|
+
const isDimmed = (n: number) => !props.noDimming && !isOn(n)
|
|
60
|
+
|
|
61
|
+
const setRating = (n: number) => {
|
|
62
|
+
if (isDisabled.value) return
|
|
63
|
+
emit("update:modelValue", n)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const setHover = (n: number) => {
|
|
67
|
+
if (!isDisabled.value) hoverValue.value = n
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<template>
|
|
72
|
+
<div
|
|
73
|
+
class="q-rating"
|
|
74
|
+
:class="{ 'q-rating--disabled': isDisabled }"
|
|
75
|
+
role="radiogroup"
|
|
76
|
+
:aria-label="`Évaluation sur ${max}`"
|
|
77
|
+
>
|
|
78
|
+
<button
|
|
79
|
+
v-for="n in max"
|
|
80
|
+
:key="n"
|
|
81
|
+
type="button"
|
|
82
|
+
role="radio"
|
|
83
|
+
class="q-rating__star"
|
|
84
|
+
:class="{
|
|
85
|
+
'q-rating__star--on': isOn(n),
|
|
86
|
+
'q-rating__star--dimmed': isDimmed(n),
|
|
87
|
+
}"
|
|
88
|
+
:aria-label="`${n} étoile${n > 1 ? 's' : ''} sur ${max}`"
|
|
89
|
+
:aria-checked="n === modelValue ? 'true' : 'false'"
|
|
90
|
+
:disabled="isDisabled"
|
|
91
|
+
@mouseenter="setHover(n)"
|
|
92
|
+
@mouseleave="setHover(0)"
|
|
93
|
+
@focus="setHover(n)"
|
|
94
|
+
@blur="setHover(0)"
|
|
95
|
+
@click="setRating(n)"
|
|
96
|
+
>
|
|
97
|
+
<Icon
|
|
98
|
+
:icon="icon"
|
|
99
|
+
:style="iconStyle"
|
|
100
|
+
:fill="isOn(n) ? 'currentColor' : 'none'"
|
|
101
|
+
aria-hidden="true"
|
|
102
|
+
/>
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
</template>
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QReorder — liste réordonnable par glisser-déposer :
|
|
3
|
+
// <q-reorder v-model="items" row-key="id"> <template #item="{ item, index }">…</template> </q-reorder>
|
|
4
|
+
// Drag sur toute la ligne (ou seulement la poignée avec `handle`), clavier ↑/↓, pointer events.
|
|
5
|
+
import { computed, nextTick, ref, watch } from "vue"
|
|
6
|
+
import { Icon } from "@iconify/vue"
|
|
7
|
+
import { icons } from "../lib/icons"
|
|
8
|
+
import { cn } from "../lib/utils"
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
/** Items (v-model) — l'ordre est mis à jour au drop */
|
|
12
|
+
modelValue?: any[]
|
|
13
|
+
/** Clé stable par item pour les animations (string ou fonction) — défaut : l'index */
|
|
14
|
+
rowKey?: string | ((item: any) => any)
|
|
15
|
+
/** Le drag ne démarre que depuis la poignée (sinon toute la ligne) */
|
|
16
|
+
handle?: boolean
|
|
17
|
+
disable?: boolean
|
|
18
|
+
itemClass?: string
|
|
19
|
+
handleClass?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
23
|
+
modelValue: () => [],
|
|
24
|
+
handle: false,
|
|
25
|
+
disable: false,
|
|
26
|
+
itemClass: "",
|
|
27
|
+
handleClass: "",
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const emit = defineEmits<{
|
|
31
|
+
"update:modelValue": [value: any[]]
|
|
32
|
+
/** Émis quand un item change de position : { from, to } */
|
|
33
|
+
reorder: [payload: { from: number; to: number }]
|
|
34
|
+
}>()
|
|
35
|
+
|
|
36
|
+
const list = ref<any[]>([...props.modelValue])
|
|
37
|
+
watch(
|
|
38
|
+
() => props.modelValue,
|
|
39
|
+
(v) => {
|
|
40
|
+
if (v && v !== list.value) list.value = [...v]
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const keyOf = (item: any, index: number) => {
|
|
45
|
+
if (!props.rowKey) return typeof item === "object" && item !== null ? index : item
|
|
46
|
+
return typeof props.rowKey === "function" ? props.rowKey(item) : item?.[props.rowKey]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// — Drag (pointer events sur window) —
|
|
50
|
+
const containerEl = ref<HTMLElement | null>(null)
|
|
51
|
+
const itemEls = ref<(HTMLElement | null)[]>([])
|
|
52
|
+
const heights = ref<number[]>([])
|
|
53
|
+
|
|
54
|
+
const draggingIndex = ref<number | null>(null)
|
|
55
|
+
const pointerIndex = ref<number | null>(null)
|
|
56
|
+
const startY = ref(0)
|
|
57
|
+
const dragDy = ref(0)
|
|
58
|
+
|
|
59
|
+
let cleanup: (() => void) | null = null
|
|
60
|
+
|
|
61
|
+
const interactive = (target: EventTarget | null) =>
|
|
62
|
+
target instanceof Element && !!target.closest("button, input, select, textarea, a, [contenteditable]")
|
|
63
|
+
|
|
64
|
+
const onPointerDown = (i: number, e: PointerEvent) => {
|
|
65
|
+
if (props.disable) return
|
|
66
|
+
if (props.handle && !(e.target as Element).closest(".q-reorder__handle")) return
|
|
67
|
+
if (!props.handle && interactive(e.target)) return
|
|
68
|
+
e.preventDefault()
|
|
69
|
+
heights.value = itemEls.value.map((el) => el?.offsetHeight ?? 0)
|
|
70
|
+
draggingIndex.value = i
|
|
71
|
+
pointerIndex.value = i
|
|
72
|
+
startY.value = e.clientY
|
|
73
|
+
dragDy.value = 0
|
|
74
|
+
|
|
75
|
+
const onMove = (ev: PointerEvent) => {
|
|
76
|
+
dragDy.value = ev.clientY - startY.value
|
|
77
|
+
const el = document.elementFromPoint(ev.clientX, ev.clientY) as HTMLElement | null
|
|
78
|
+
const row = el?.closest?.(".q-reorder__item") as HTMLElement | null
|
|
79
|
+
const idx = row ? Number(row.dataset.index) : -1
|
|
80
|
+
if (!Number.isNaN(idx) && idx >= 0) pointerIndex.value = idx
|
|
81
|
+
}
|
|
82
|
+
const onUp = () => {
|
|
83
|
+
commit()
|
|
84
|
+
if (cleanup) cleanup()
|
|
85
|
+
}
|
|
86
|
+
window.addEventListener("pointermove", onMove)
|
|
87
|
+
window.addEventListener("pointerup", onUp)
|
|
88
|
+
window.addEventListener("pointercancel", onUp)
|
|
89
|
+
cleanup = () => {
|
|
90
|
+
window.removeEventListener("pointermove", onMove)
|
|
91
|
+
window.removeEventListener("pointerup", onUp)
|
|
92
|
+
window.removeEventListener("pointercancel", onUp)
|
|
93
|
+
cleanup = null
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const commit = () => {
|
|
98
|
+
const from = draggingIndex.value
|
|
99
|
+
const to = pointerIndex.value
|
|
100
|
+
draggingIndex.value = null
|
|
101
|
+
pointerIndex.value = null
|
|
102
|
+
dragDy.value = 0
|
|
103
|
+
if (from === null || to === null || from === to) return
|
|
104
|
+
const next = [...list.value]
|
|
105
|
+
const [moved] = next.splice(from, 1)
|
|
106
|
+
next.splice(to, 0, moved)
|
|
107
|
+
list.value = next
|
|
108
|
+
emit("update:modelValue", next)
|
|
109
|
+
emit("reorder", { from, to })
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const itemStyle = (i: number): Record<string, string> | undefined => {
|
|
113
|
+
if (draggingIndex.value === null) return undefined
|
|
114
|
+
if (i === draggingIndex.value) {
|
|
115
|
+
return { transform: `translateY(${dragDy.value}px)`, transition: "none" }
|
|
116
|
+
}
|
|
117
|
+
const h = heights.value[draggingIndex.value] ?? 0
|
|
118
|
+
if (pointerIndex.value !== null && pointerIndex.value > draggingIndex.value && i > draggingIndex.value && i <= pointerIndex.value)
|
|
119
|
+
return { transform: `translateY(-${h}px)` }
|
|
120
|
+
if (pointerIndex.value !== null && pointerIndex.value < draggingIndex.value && i >= pointerIndex.value && i < draggingIndex.value)
|
|
121
|
+
return { transform: `translateY(${h}px)` }
|
|
122
|
+
return undefined
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// — Clavier : ↑ / ↓ déplace l'item —
|
|
126
|
+
const move = (from: number, to: number) => {
|
|
127
|
+
if (props.disable) return
|
|
128
|
+
to = Math.max(0, Math.min(list.value.length - 1, to))
|
|
129
|
+
if (to === from) return
|
|
130
|
+
const next = [...list.value]
|
|
131
|
+
const [m] = next.splice(from, 1)
|
|
132
|
+
next.splice(to, 0, m)
|
|
133
|
+
list.value = next
|
|
134
|
+
emit("update:modelValue", next)
|
|
135
|
+
emit("reorder", { from, to })
|
|
136
|
+
nextTick(() => itemEls.value[to]?.focus())
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const onKeydown = (i: number, e: KeyboardEvent) => {
|
|
140
|
+
if (e.key === "ArrowUp") {
|
|
141
|
+
e.preventDefault()
|
|
142
|
+
move(i, i - 1)
|
|
143
|
+
} else if (e.key === "ArrowDown") {
|
|
144
|
+
e.preventDefault()
|
|
145
|
+
move(i, i + 1)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const rootClasses = computed(() =>
|
|
150
|
+
cn("q-reorder", props.disable && "q-reorder--disabled"),
|
|
151
|
+
)
|
|
152
|
+
</script>
|
|
153
|
+
|
|
154
|
+
<template>
|
|
155
|
+
<div ref="containerEl" class="q-reorder" :class="rootClasses" role="list">
|
|
156
|
+
<div
|
|
157
|
+
v-for="(item, i) in list"
|
|
158
|
+
:key="keyOf(item, i)"
|
|
159
|
+
:ref="(el) => (itemEls[i] = el as HTMLElement | null)"
|
|
160
|
+
class="q-reorder__item"
|
|
161
|
+
:class="[
|
|
162
|
+
itemClass,
|
|
163
|
+
i === draggingIndex && 'q-reorder__item--dragging',
|
|
164
|
+
!handle && 'q-reorder__item--drag-all',
|
|
165
|
+
]"
|
|
166
|
+
:data-index="i"
|
|
167
|
+
:style="itemStyle(i)"
|
|
168
|
+
role="listitem"
|
|
169
|
+
:tabindex="disable ? -1 : 0"
|
|
170
|
+
:aria-grabbed="i === draggingIndex || undefined"
|
|
171
|
+
@pointerdown="onPointerDown(i, $event)"
|
|
172
|
+
@keydown="onKeydown(i, $event)"
|
|
173
|
+
>
|
|
174
|
+
<div class="q-reorder__content">
|
|
175
|
+
<slot name="item" :item="item" :index="i" :dragging="i === draggingIndex">
|
|
176
|
+
<span class="q-reorder__label">{{ String(item) }}</span>
|
|
177
|
+
</slot>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<span
|
|
181
|
+
class="q-reorder__handle"
|
|
182
|
+
:class="handleClass"
|
|
183
|
+
aria-hidden="true"
|
|
184
|
+
>
|
|
185
|
+
<slot name="handle" :index="i" :dragging="i === draggingIndex">
|
|
186
|
+
<Icon :icon="icons.gripVertical" class="q-reorder__grip" />
|
|
187
|
+
</slot>
|
|
188
|
+
</span>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</template>
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QRollingText — compteur à rouleaux (type machine à sous) :
|
|
3
|
+
// <q-rolling-text :start-num="0" :target-num="123" />.
|
|
4
|
+
// Chaque chiffre défile verticalement de start-num vers target-num (2 tours complets
|
|
5
|
+
// entre les deux), avec un effet cascade via stop-order. API inspirée de Vant
|
|
6
|
+
// RollingText : start-num / target-num / text-list / duration / direction /
|
|
7
|
+
// auto-start / stop-order / height + méthodes exposées start() / reset().
|
|
8
|
+
import { computed, ref, watch } from "vue"
|
|
9
|
+
|
|
10
|
+
type RollingTextDirection = "up" | "down"
|
|
11
|
+
type RollingTextStopOrder = "ltr" | "rtl"
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
/** Valeur de départ affichée avant l'animation (défaut 0) */
|
|
15
|
+
startNum?: number
|
|
16
|
+
/** Valeur cible — les rouleaux s'arrêtent sur celle-ci */
|
|
17
|
+
targetNum?: number
|
|
18
|
+
/** Texte personnalisé : tableau de chaînes de MÊME longueur — le rouleau défile du premier au dernier élément */
|
|
19
|
+
textList?: string[]
|
|
20
|
+
/** Durée de l'animation, en secondes (défaut 2) */
|
|
21
|
+
duration?: number
|
|
22
|
+
/** Sens de défilement : "up" compte en montant, "down" en descendant (défaut down) */
|
|
23
|
+
direction?: RollingTextDirection
|
|
24
|
+
/** Démarre l'animation automatiquement au montage (défaut true) */
|
|
25
|
+
autoStart?: boolean
|
|
26
|
+
/** Ordre d'arrêt des rouleaux : "ltr" (rang de gauche d'abord) | "rtl" (unités d'abord) */
|
|
27
|
+
stopOrder?: RollingTextStopOrder
|
|
28
|
+
/** Hauteur d'un chiffre, en px (défaut 40) */
|
|
29
|
+
height?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
33
|
+
startNum: 0,
|
|
34
|
+
targetNum: 0,
|
|
35
|
+
textList: () => [],
|
|
36
|
+
duration: 2,
|
|
37
|
+
direction: "down",
|
|
38
|
+
autoStart: true,
|
|
39
|
+
stopOrder: "ltr",
|
|
40
|
+
height: 40,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
/** Nombre de tours complets entre le chiffre de départ et la cible */
|
|
44
|
+
const CIRCLE_NUM = 2
|
|
45
|
+
|
|
46
|
+
const isCustom = computed(() => props.textList.length > 0)
|
|
47
|
+
|
|
48
|
+
/** Nombre de colonnes (chiffres) affichées */
|
|
49
|
+
const itemLength = computed(() => {
|
|
50
|
+
if (isCustom.value) return props.textList[0]?.length ?? 0
|
|
51
|
+
return String(Math.max(props.startNum, props.targetNum)).length
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const targetNumArr = computed(() => {
|
|
55
|
+
if (isCustom.value) return new Array(itemLength.value).fill("")
|
|
56
|
+
return String(props.targetNum).padStart(itemLength.value, "0").split("")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const startNumArr = computed(() =>
|
|
60
|
+
String(props.startNum).padStart(itemLength.value, "0").split(""),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
/** Colonne i : la liste des figures qui défilent sous la fenêtre */
|
|
64
|
+
const columns = computed<(string | number)[][]>(() => {
|
|
65
|
+
const cols: (string | number)[][] = []
|
|
66
|
+
for (let i = 0; i < itemLength.value; i++) {
|
|
67
|
+
if (isCustom.value) {
|
|
68
|
+
const arr: (string | number)[] = []
|
|
69
|
+
for (let k = 0; k < props.textList.length; k++) {
|
|
70
|
+
arr.push(props.textList[k]?.[i] ?? "")
|
|
71
|
+
}
|
|
72
|
+
cols.push(arr)
|
|
73
|
+
} else {
|
|
74
|
+
const start = Number(startNumArr.value[i])
|
|
75
|
+
const target = Number(targetNumArr.value[i])
|
|
76
|
+
const arr: (string | number)[] = []
|
|
77
|
+
for (let n = start; n <= 9; n++) arr.push(n)
|
|
78
|
+
for (let c = 0; c < CIRCLE_NUM; c++) for (let n = 0; n <= 9; n++) arr.push(n)
|
|
79
|
+
for (let n = 0; n <= target; n++) arr.push(n)
|
|
80
|
+
cols.push(arr)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Sens "down" : figures inversées — le rouleau démarre pré-translaté et descend vers la cible
|
|
84
|
+
return props.direction === "down" ? cols.map((c) => c.slice().reverse()) : cols
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const rolling = ref(props.autoStart)
|
|
88
|
+
|
|
89
|
+
/** Démarre (ou relance) l'animation */
|
|
90
|
+
const start = () => {
|
|
91
|
+
rolling.value = true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Réinitialise l'animation, puis la relance si auto-start */
|
|
95
|
+
const reset = () => {
|
|
96
|
+
rolling.value = false
|
|
97
|
+
if (props.autoStart) requestAnimationFrame(() => start())
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
defineExpose({ start, reset })
|
|
101
|
+
|
|
102
|
+
watch(
|
|
103
|
+
() => props.autoStart,
|
|
104
|
+
(v) => {
|
|
105
|
+
if (v) start()
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
/** Délai de démarrage par colonne (effet cascade) */
|
|
110
|
+
const getDelay = (i: number, len: number) =>
|
|
111
|
+
props.stopOrder === "ltr" ? 0.2 * i : 0.2 * (len - 1 - i)
|
|
112
|
+
|
|
113
|
+
const digitStyle = computed(() => ({ lineHeight: `${props.height}px` }))
|
|
114
|
+
|
|
115
|
+
const itemStyle = (i: number) => {
|
|
116
|
+
const col = columns.value[i] ?? []
|
|
117
|
+
return {
|
|
118
|
+
height: `${props.height}px`,
|
|
119
|
+
"--q-translate": `-${props.height * Math.max(0, col.length - 1)}px`,
|
|
120
|
+
"--q-duration": `${props.duration}s`,
|
|
121
|
+
"--q-delay": `${getDelay(i, itemLength.value)}s`,
|
|
122
|
+
} as Record<string, string>
|
|
123
|
+
}
|
|
124
|
+
</script>
|
|
125
|
+
|
|
126
|
+
<template>
|
|
127
|
+
<div class="q-rolling-text" :class="`q-rolling-text--${direction}`">
|
|
128
|
+
<div
|
|
129
|
+
v-for="(col, i) in columns"
|
|
130
|
+
:key="i"
|
|
131
|
+
class="q-rolling-text__item"
|
|
132
|
+
:style="itemStyle(i)"
|
|
133
|
+
>
|
|
134
|
+
<div
|
|
135
|
+
class="q-rolling-text__box"
|
|
136
|
+
:class="{ 'q-rolling-text__box--animate': rolling }"
|
|
137
|
+
>
|
|
138
|
+
<div
|
|
139
|
+
v-for="(digit, j) in col"
|
|
140
|
+
:key="j"
|
|
141
|
+
class="q-rolling-text__digit"
|
|
142
|
+
:style="digitStyle"
|
|
143
|
+
>
|
|
144
|
+
<slot
|
|
145
|
+
name="digit"
|
|
146
|
+
:value="digit"
|
|
147
|
+
:index="i"
|
|
148
|
+
:total="columns.length"
|
|
149
|
+
:active="j === col.length - 1"
|
|
150
|
+
>
|
|
151
|
+
{{ digit }}
|
|
152
|
+
</slot>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</template>
|
|
158
|
+
|
|
159
|
+
<style scoped>
|
|
160
|
+
.q-rolling-text {
|
|
161
|
+
display: inline-flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
justify-content: center;
|
|
164
|
+
color: var(--q-rolling-text-color, var(--foreground));
|
|
165
|
+
font-size: var(--q-rolling-text-font-size, 18px);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.q-rolling-text__item {
|
|
169
|
+
overflow: hidden;
|
|
170
|
+
width: var(--q-rolling-text-item-width, 1.2em);
|
|
171
|
+
border-radius: var(--q-rolling-text-item-border-radius, 6px);
|
|
172
|
+
background: var(--q-rolling-text-background, transparent);
|
|
173
|
+
}
|
|
174
|
+
.q-rolling-text__item + .q-rolling-text__item {
|
|
175
|
+
margin-left: var(--q-rolling-text-gap, 2px);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.q-rolling-text__box {
|
|
179
|
+
overflow: hidden;
|
|
180
|
+
}
|
|
181
|
+
.q-rolling-text__box--animate {
|
|
182
|
+
animation: q-rolling-up var(--q-duration, 2s) ease-in-out var(--q-delay, 0s) both;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* Sens "down" : rouleau pré-positionné en bas, il remonte vers la cible */
|
|
186
|
+
.q-rolling-text--down .q-rolling-text__box {
|
|
187
|
+
transform: translateY(var(--q-translate, 0px));
|
|
188
|
+
}
|
|
189
|
+
.q-rolling-text--down .q-rolling-text__box--animate {
|
|
190
|
+
animation-name: q-rolling-down;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.q-rolling-text__digit {
|
|
194
|
+
text-align: center;
|
|
195
|
+
font-weight: 600;
|
|
196
|
+
font-variant-numeric: tabular-nums;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@keyframes q-rolling-up {
|
|
200
|
+
from {
|
|
201
|
+
transform: translateY(0);
|
|
202
|
+
}
|
|
203
|
+
to {
|
|
204
|
+
transform: translateY(var(--q-translate, 0px));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
@keyframes q-rolling-down {
|
|
208
|
+
from {
|
|
209
|
+
transform: translateY(var(--q-translate, 0px));
|
|
210
|
+
}
|
|
211
|
+
to {
|
|
212
|
+
transform: translateY(0);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@media (prefers-reduced-motion: reduce) {
|
|
217
|
+
.q-rolling-text__box--animate {
|
|
218
|
+
animation: none;
|
|
219
|
+
transform: translateY(var(--q-translate, 0px));
|
|
220
|
+
}
|
|
221
|
+
.q-rolling-text--down .q-rolling-text__box--animate {
|
|
222
|
+
transform: translateY(0);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QRow — alias sémantique de QGrid (même API) : <q-row gap="12px"><q-col :span="6">…
|
|
3
|
+
import QGrid from "./QGrid.vue"
|
|
4
|
+
|
|
5
|
+
// Même interface que QGrid (dupliquée — pas d'import de type cross-SFC, qui exige fs)
|
|
6
|
+
interface Props {
|
|
7
|
+
cols?: number
|
|
8
|
+
gap?: string | number
|
|
9
|
+
rowGap?: string | number
|
|
10
|
+
columnGap?: string | number
|
|
11
|
+
colsSm?: number
|
|
12
|
+
colsMd?: number
|
|
13
|
+
colsLg?: number
|
|
14
|
+
colsXl?: number
|
|
15
|
+
align?: "start" | "center" | "end" | "stretch"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
defineProps<Props>()
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<q-grid v-bind="$props"><slot /></q-grid>
|
|
23
|
+
</template>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QSafeArea — applique les safe-area insets (encoche, barre d'accueil) sur les côtés demandés.
|
|
3
|
+
// <q-safe-area top bottom>…</q-safe-area> — padding = env(safe-area-inset-*), fallback constant() (iOS 11.0-11.2).
|
|
4
|
+
interface Props {
|
|
5
|
+
/** padding-top = env(safe-area-inset-top) */
|
|
6
|
+
top?: boolean
|
|
7
|
+
/** padding-right = env(safe-area-inset-right) */
|
|
8
|
+
right?: boolean
|
|
9
|
+
/** padding-bottom = env(safe-area-inset-bottom) */
|
|
10
|
+
bottom?: boolean
|
|
11
|
+
/** padding-left = env(safe-area-inset-left) */
|
|
12
|
+
left?: boolean
|
|
13
|
+
/** Applique les 4 insets (équivalent top + right + bottom + left) */
|
|
14
|
+
all?: boolean
|
|
15
|
+
class?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
19
|
+
top: false,
|
|
20
|
+
right: false,
|
|
21
|
+
bottom: false,
|
|
22
|
+
left: false,
|
|
23
|
+
all: false,
|
|
24
|
+
})
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<div
|
|
29
|
+
class="q-safe-area"
|
|
30
|
+
:class="{
|
|
31
|
+
'q-safe-area--top': props.all || props.top,
|
|
32
|
+
'q-safe-area--right': props.all || props.right,
|
|
33
|
+
'q-safe-area--bottom': props.all || props.bottom,
|
|
34
|
+
'q-safe-area--left': props.all || props.left,
|
|
35
|
+
}"
|
|
36
|
+
>
|
|
37
|
+
<slot />
|
|
38
|
+
</div>
|
|
39
|
+
</template>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QScrollArea — API Quasar : <q-scroll-area visible :thumb-style="…" :content-style="…" delay="350">
|
|
3
|
+
// Zone scrollable avec barre de défilement personnalisée (native masquée, thumb draggable).
|
|
4
|
+
import { computed, onBeforeUnmount, onMounted, ref } from "vue"
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
barStyle?: string
|
|
9
|
+
barClass?: string
|
|
10
|
+
thumbStyle?: string
|
|
11
|
+
thumbClass?: string
|
|
12
|
+
contentStyle?: string
|
|
13
|
+
contentClass?: string
|
|
14
|
+
/** Toujours afficher la barre */
|
|
15
|
+
visible?: boolean
|
|
16
|
+
/** Délai de masquage après scroll (ms) */
|
|
17
|
+
delay?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
21
|
+
visible: false,
|
|
22
|
+
delay: 350,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const contentRef = ref<HTMLElement | null>(null)
|
|
26
|
+
const viewportH = ref(0)
|
|
27
|
+
const scrollH = ref(0)
|
|
28
|
+
const scrollTop = ref(0)
|
|
29
|
+
const thumbH = ref(0)
|
|
30
|
+
const show = ref(false)
|
|
31
|
+
const dragging = ref(false)
|
|
32
|
+
let hideTimer: ReturnType<typeof setTimeout> | null = null
|
|
33
|
+
|
|
34
|
+
const scrollable = computed(() => scrollH.value > viewportH.value + 1)
|
|
35
|
+
|
|
36
|
+
const measure = () => {
|
|
37
|
+
const c = contentRef.value
|
|
38
|
+
if (!c) return
|
|
39
|
+
viewportH.value = c.clientHeight
|
|
40
|
+
scrollH.value = c.scrollHeight
|
|
41
|
+
thumbH.value = Math.max(24, (viewportH.value / scrollH.value) * viewportH.value)
|
|
42
|
+
scrollTop.value = c.scrollTop
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const onScroll = () => {
|
|
46
|
+
const c = contentRef.value
|
|
47
|
+
if (c) scrollTop.value = c.scrollTop
|
|
48
|
+
show.value = true
|
|
49
|
+
if (hideTimer) clearTimeout(hideTimer)
|
|
50
|
+
hideTimer = setTimeout(() => {
|
|
51
|
+
if (!dragging.value) show.value = false
|
|
52
|
+
}, props.delay)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const thumbTop = computed(() =>
|
|
56
|
+
scrollH.value > 0 ? (scrollTop.value / scrollH.value) * viewportH.value : 0,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const barVisible = computed(() => show.value || props.visible || dragging.value)
|
|
60
|
+
|
|
61
|
+
// Drag du thumb
|
|
62
|
+
const onThumbDown = (e: PointerEvent) => {
|
|
63
|
+
const c = contentRef.value
|
|
64
|
+
if (!c) return
|
|
65
|
+
dragging.value = true
|
|
66
|
+
const startY = e.clientY
|
|
67
|
+
const startTop = c.scrollTop
|
|
68
|
+
const onMove = (ev: PointerEvent) => {
|
|
69
|
+
const dy = ev.clientY - startY
|
|
70
|
+
c.scrollTop = startTop + (dy / viewportH.value) * scrollH.value
|
|
71
|
+
scrollTop.value = c.scrollTop
|
|
72
|
+
}
|
|
73
|
+
const onUp = () => {
|
|
74
|
+
dragging.value = false
|
|
75
|
+
window.removeEventListener("pointermove", onMove)
|
|
76
|
+
window.removeEventListener("pointerup", onUp)
|
|
77
|
+
}
|
|
78
|
+
window.addEventListener("pointermove", onMove)
|
|
79
|
+
window.addEventListener("pointerup", onUp)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
onMounted(() => {
|
|
83
|
+
measure()
|
|
84
|
+
if (typeof window !== "undefined") window.addEventListener("resize", measure)
|
|
85
|
+
})
|
|
86
|
+
onBeforeUnmount(() => {
|
|
87
|
+
if (typeof window !== "undefined") window.removeEventListener("resize", measure)
|
|
88
|
+
if (hideTimer) clearTimeout(hideTimer)
|
|
89
|
+
})
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<template>
|
|
93
|
+
<div class="q-scroll-area">
|
|
94
|
+
<div
|
|
95
|
+
ref="contentRef"
|
|
96
|
+
class="q-scroll-area__content"
|
|
97
|
+
:class="contentClass"
|
|
98
|
+
:style="contentStyle"
|
|
99
|
+
@scroll="onScroll"
|
|
100
|
+
>
|
|
101
|
+
<slot />
|
|
102
|
+
</div>
|
|
103
|
+
<div
|
|
104
|
+
v-if="scrollable"
|
|
105
|
+
class="q-scroll-area__bar"
|
|
106
|
+
:class="cn(barClass, barVisible && 'q-scroll-area__bar--visible')"
|
|
107
|
+
:style="barStyle"
|
|
108
|
+
aria-hidden="true"
|
|
109
|
+
>
|
|
110
|
+
<div
|
|
111
|
+
class="q-scroll-area__thumb"
|
|
112
|
+
:class="thumbClass"
|
|
113
|
+
:style="[{ height: `${thumbH}px`, transform: `translateY(${thumbTop}px)` }, thumbStyle]"
|
|
114
|
+
@pointerdown="onThumbDown"
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</template>
|