@dnax/ui 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +15 -0
  2. package/components/QAccordion.vue +68 -0
  3. package/components/QAccordionContent.vue +62 -0
  4. package/components/QAccordionItem.vue +24 -0
  5. package/components/QAccordionTrigger.vue +51 -0
  6. package/components/QActionSheet.vue +159 -0
  7. package/components/QApp.vue +8 -0
  8. package/components/QAutocomplete.vue +316 -0
  9. package/components/QAvatar.vue +92 -0
  10. package/components/QBackHeader.vue +69 -0
  11. package/components/QBackTop.vue +125 -0
  12. package/components/QBadge.vue +55 -0
  13. package/components/QBottomSheet.vue +177 -0
  14. package/components/QBottomSheetFooter.vue +9 -0
  15. package/components/QBottomSheetHeader.vue +35 -0
  16. package/components/QBottomSheetProvider.vue +62 -0
  17. package/components/QBottomSheetTrigger.vue +18 -0
  18. package/components/QBtn.vue +144 -0
  19. package/components/QBtnGroup.vue +31 -0
  20. package/components/QBubble.vue +36 -0
  21. package/components/QBubbleContent.vue +28 -0
  22. package/components/QBubbleGroup.vue +25 -0
  23. package/components/QBubbleReactions.vue +40 -0
  24. package/components/QCard.vue +63 -0
  25. package/components/QCardActions.vue +27 -0
  26. package/components/QCardContent.vue +29 -0
  27. package/components/QCarousel.vue +115 -0
  28. package/components/QCarouselContent.vue +23 -0
  29. package/components/QCarouselItem.vue +20 -0
  30. package/components/QCarouselNav.vue +51 -0
  31. package/components/QCheckbox.vue +146 -0
  32. package/components/QChip.vue +91 -0
  33. package/components/QCircularProgress.vue +122 -0
  34. package/components/QCol.vue +52 -0
  35. package/components/QCollapse.vue +141 -0
  36. package/components/QConfigProvider.vue +164 -0
  37. package/components/QContainer.vue +201 -0
  38. package/components/QCountDown.vue +164 -0
  39. package/components/QCountryPicker.vue +593 -0
  40. package/components/QDataGrid.vue +491 -0
  41. package/components/QDatePicker.vue +250 -0
  42. package/components/QDialog.vue +163 -0
  43. package/components/QDialogFooter.vue +9 -0
  44. package/components/QDialogHeader.vue +41 -0
  45. package/components/QDialogProvider.vue +65 -0
  46. package/components/QDialogTrigger.vue +18 -0
  47. package/components/QFab.vue +80 -0
  48. package/components/QFabAction.vue +48 -0
  49. package/components/QFilePicker.vue +294 -0
  50. package/components/QFooter.vue +72 -0
  51. package/components/QGallery.vue +280 -0
  52. package/components/QGrid.vue +54 -0
  53. package/components/QHeader.vue +38 -0
  54. package/components/QIcon.vue +53 -0
  55. package/components/QImagePicker.vue +248 -0
  56. package/components/QImagePreview.vue +225 -0
  57. package/components/QImagePreviewProvider.vue +35 -0
  58. package/components/QImg.vue +142 -0
  59. package/components/QInfiniteScroll.vue +109 -0
  60. package/components/QInnerLoading.vue +67 -0
  61. package/components/QInput.vue +289 -0
  62. package/components/QInputOtp.vue +238 -0
  63. package/components/QInputPassword.vue +67 -0
  64. package/components/QInputTag.vue +281 -0
  65. package/components/QIntersection.vue +119 -0
  66. package/components/QItem.vue +60 -0
  67. package/components/QItemSection.vue +47 -0
  68. package/components/QLinearProgress.vue +94 -0
  69. package/components/QList.vue +39 -0
  70. package/components/QLoading.vue +77 -0
  71. package/components/QLoadingProvider.vue +41 -0
  72. package/components/QMarquee.vue +56 -0
  73. package/components/QMessageScroller.vue +10 -0
  74. package/components/QMessageScrollerButton.vue +48 -0
  75. package/components/QMessageScrollerContent.vue +31 -0
  76. package/components/QMessageScrollerItem.vue +49 -0
  77. package/components/QMessageScrollerProvider.vue +198 -0
  78. package/components/QMessageScrollerViewport.vue +109 -0
  79. package/components/QNavMenu.vue +75 -0
  80. package/components/QNavMenuContent.vue +10 -0
  81. package/components/QNavMenuItem.vue +60 -0
  82. package/components/QNavMenuTrigger.vue +96 -0
  83. package/components/QNotifyProvider.vue +33 -0
  84. package/components/QNotifyToast.vue +60 -0
  85. package/components/QPage.vue +56 -0
  86. package/components/QPagination.vue +195 -0
  87. package/components/QParallax.vue +124 -0
  88. package/components/QPullToRefresh.vue +138 -0
  89. package/components/QRadio.vue +99 -0
  90. package/components/QRating.vue +105 -0
  91. package/components/QReorder.vue +191 -0
  92. package/components/QRollingText.vue +225 -0
  93. package/components/QRow.vue +23 -0
  94. package/components/QSafeArea.vue +39 -0
  95. package/components/QScrollArea.vue +118 -0
  96. package/components/QSelect.vue +698 -0
  97. package/components/QSeparator.vue +39 -0
  98. package/components/QSidebar.vue +153 -0
  99. package/components/QSidebarContent.vue +14 -0
  100. package/components/QSidebarFooter.vue +14 -0
  101. package/components/QSidebarHeader.vue +14 -0
  102. package/components/QSidebarMenu.vue +9 -0
  103. package/components/QSidebarMenuButton.vue +59 -0
  104. package/components/QSidebarMenuItem.vue +9 -0
  105. package/components/QSidebarTrigger.vue +32 -0
  106. package/components/QSkeleton.vue +95 -0
  107. package/components/QSlider.vue +205 -0
  108. package/components/QSpace.vue +10 -0
  109. package/components/QSpinner.vue +172 -0
  110. package/components/QSplitter.vue +145 -0
  111. package/components/QSticky.vue +181 -0
  112. package/components/QSwipeCell.vue +217 -0
  113. package/components/QSwiper.vue +102 -0
  114. package/components/QSwiperSlide.vue +19 -0
  115. package/components/QSyntax.vue +171 -0
  116. package/components/QTab.vue +175 -0
  117. package/components/QTabPanel.vue +50 -0
  118. package/components/QTabPanels.vue +106 -0
  119. package/components/QTable.vue +575 -0
  120. package/components/QTabs.vue +188 -0
  121. package/components/QText.vue +316 -0
  122. package/components/QTextCaption.vue +122 -0
  123. package/components/QToolbar.vue +28 -0
  124. package/components/QTooltip.vue +313 -0
  125. package/components/QUploader.vue +473 -0
  126. package/components/QVideo.vue +289 -0
  127. package/components/QVirtualScroll.vue +190 -0
  128. package/components/internal/QDateCalendar.vue +153 -0
  129. package/index.ts +151 -0
  130. package/lib/breakpoints.ts +44 -0
  131. package/lib/colors.ts +24 -0
  132. package/lib/config.ts +34 -0
  133. package/lib/countries.ts +261 -0
  134. package/lib/formula.ts +630 -0
  135. package/lib/icons.ts +38 -0
  136. package/lib/loading.ts +75 -0
  137. package/lib/overlayBack.ts +124 -0
  138. package/lib/platform.ts +146 -0
  139. package/lib/primitive.ts +25 -0
  140. package/lib/q.ts +287 -0
  141. package/lib/screen.ts +105 -0
  142. package/lib/search.ts +35 -0
  143. package/lib/themeVars.ts +22 -0
  144. package/lib/useComponentProps.ts +54 -0
  145. package/lib/utils.ts +7 -0
  146. package/module.ts +89 -0
  147. package/package.json +41 -0
  148. package/scripts/generate-exports.ts +56 -0
  149. package/styles/main.css +7627 -0
  150. package/tsconfig.json +33 -0
@@ -0,0 +1,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>