@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,593 @@
1
+ <script setup lang="ts">
2
+ // QCountryPicker — sélecteur de pays préfait : drapeau, nom, indicatif, recherche.
3
+ // <q-country-picker v-model="code" label="Country" mode="sheet" outlined />
4
+ // v-model = code ISO 3166-1 alpha-2 (ex. "FR").
5
+ // Modes : inline (champ + liste déroulante) | modal (centré) | sheet (bottom sheet) | dialog (plein écran).
6
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
7
+ import { Icon } from "@iconify/vue"
8
+ import { icons } from "../lib/icons"
9
+ import { cn } from "../lib/utils"
10
+ import { useOverlayBack } from "../lib/overlayBack"
11
+ import { COUNTRIES, flagOf } from "../lib/countries"
12
+ import type { Country } from "../lib/countries"
13
+
14
+ /** Options spécifiques à un mode d'ouverture (sheet / modal) */
15
+ export interface QCountryPickerModeOptions {
16
+ /** Largeur du panneau (remplace width) */
17
+ width?: string
18
+ /** Style appliqué au panneau */
19
+ style?: Record<string, string>
20
+ /** Classes ajoutées au panneau */
21
+ class?: string
22
+ /** Hauteur de la liste scrollable (ex. "45vh", "400px") */
23
+ height?: string
24
+ /** Placeholder du champ de recherche pour ce mode */
25
+ searchPlaceholder?: string
26
+ }
27
+
28
+ interface Props {
29
+ /** Code ISO 3166-1 alpha-2 sélectionné (ex. "FR") — v-model */
30
+ modelValue?: string
31
+ /** Liste des pays proposés (défaut : tous) */
32
+ countries?: Country[]
33
+ /** Langue des noms de pays : "en" | "fr" (défaut en) */
34
+ language?: "en" | "fr"
35
+ /** Mode d'affichage : inline (liste sous le champ) | sheet | modal | dialog */
36
+ mode?: "inline" | "sheet" | "modal" | "dialog"
37
+ /** Libellé du champ vide (défaut "Select a country") */
38
+ placeholder?: string
39
+ /** Champ de recherche dans la liste (défaut true) */
40
+ searchable?: boolean
41
+ /** Affiche l'indicatif téléphonique (défaut true) */
42
+ showDial?: boolean
43
+ /** Label au-dessus du champ */
44
+ label?: string
45
+ /** Titre du panneau (sheet/modal/dialog) — défaut : label ou placeholder */
46
+ title?: string
47
+ /** Largeur du panneau (sheet/modal/dialog) */
48
+ width?: string
49
+ /** Seuil de drag (px) au-delà duquel on ferme en swipant vers le bas (mode sheet, défaut 80) */
50
+ dragThreshold?: number
51
+ /** Options spécifiques au mode sheet (width, style, class, height de la liste, searchPlaceholder) */
52
+ sheetOptions?: QCountryPickerModeOptions
53
+ /** Options spécifiques au mode modal */
54
+ modalOptions?: QCountryPickerModeOptions
55
+ /** Ne se ferme ni au backdrop ni à Échap (le drag ferme toujours) */
56
+ persistent?: boolean
57
+ /** Fond translucide du panneau (frosted glass) : true = 70%, ou valeur % */
58
+ translucent?: boolean | number
59
+ /** Arrondi du panneau : true (défaut) | false | valeur CSS */
60
+ rounded?: boolean | string
61
+ /** Hauteur du panneau (sinon max-height 80vh) */
62
+ height?: string
63
+ /** Style du panneau (variables de thème pour les contenus téléportés) */
64
+ contentStyle?: Record<string, string>
65
+ /** Fond transparent (style des champs Quasar) */
66
+ outlined?: boolean
67
+ /** Fond grisé sans bordure (style Quasar) */
68
+ filled?: boolean
69
+ /** Aucune bordure ni fond */
70
+ borderless?: boolean
71
+ /** Champ compact */
72
+ dense?: boolean
73
+ /** Champ désactivé */
74
+ disable?: boolean
75
+ /** Lecture seule (le popup ne s'ouvre pas) */
76
+ readonly?: boolean
77
+ /** Thème sombre */
78
+ dark?: boolean
79
+ /** Texte quand aucun pays ne correspond (défaut "No matching countries") */
80
+ noOptionsLabel?: string
81
+ }
82
+
83
+ const props = withDefaults(defineProps<Props>(), {
84
+ modelValue: "",
85
+ countries: () => COUNTRIES,
86
+ language: "en",
87
+ mode: "inline",
88
+ placeholder: "Select a country",
89
+ searchable: true,
90
+ showDial: true,
91
+ label: "",
92
+ title: "",
93
+ width: "",
94
+ dragThreshold: 80,
95
+ outlined: false,
96
+ filled: false,
97
+ borderless: false,
98
+ dense: false,
99
+ disable: false,
100
+ readonly: false,
101
+ dark: false,
102
+ persistent: false,
103
+ translucent: false,
104
+ rounded: true,
105
+ height: "",
106
+ contentStyle: undefined,
107
+ noOptionsLabel: "No matching countries",
108
+ })
109
+
110
+ const emit = defineEmits<{ "update:modelValue": [value: string] }>()
111
+
112
+ const isPanelMode = computed(() => props.mode !== "inline")
113
+
114
+ const rootEl = ref<HTMLElement | null>(null)
115
+ const sheetRef = ref<HTMLElement | null>(null)
116
+ const searchInput = ref<HTMLInputElement | null>(null)
117
+ const open = ref(false)
118
+ const query = ref("")
119
+ const activeIndex = ref(0)
120
+
121
+ // « Retour » navigateur → ferme le panneau (sheet/modal/dialog) au lieu de naviguer
122
+ const panelOpen = computed(() => isPanelMode.value && open.value)
123
+ useOverlayBack(panelOpen, () => close(), "QCountryPicker")
124
+
125
+ const selected = computed(() =>
126
+ props.countries.find((c) => c.code === props.modelValue),
127
+ )
128
+
129
+ /** Nom du pays dans la langue courante (fallback : nom anglais) */
130
+ const countryName = (c: Country): string =>
131
+ props.language === "fr" ? (c.nameFr ?? c.name) : c.name
132
+
133
+ const filtered = computed(() => {
134
+ const q = query.value.trim().toLowerCase()
135
+ if (!q) return props.countries
136
+ return props.countries.filter(
137
+ (c) =>
138
+ countryName(c).toLowerCase().includes(q) ||
139
+ (props.language === "fr"
140
+ ? c.name.toLowerCase().includes(q)
141
+ : (c.nameFr ?? "").toLowerCase().includes(q)) ||
142
+ c.code.toLowerCase().includes(q) ||
143
+ c.dial.replace(/\s+/g, "").includes(q.replace(/\s+/g, "")),
144
+ )
145
+ })
146
+
147
+ const select = (c: Country) => {
148
+ emit("update:modelValue", c.code)
149
+ close()
150
+ }
151
+
152
+ const close = () => {
153
+ open.value = false
154
+ query.value = ""
155
+ }
156
+
157
+ const openPopup = () => {
158
+ if (props.disable || props.readonly) return
159
+ open.value = true
160
+ }
161
+
162
+ const onFieldClick = () => {
163
+ if (isPanelMode.value) openPopup()
164
+ else if (open.value) close()
165
+ else openPopup()
166
+ }
167
+
168
+ // — clavier : flèches + entrée (liste ouverte), échap pour fermer —
169
+ const onKeydown = (e: KeyboardEvent) => {
170
+ if (!open.value) return
171
+ if (e.key === "Escape") {
172
+ e.preventDefault()
173
+ close()
174
+ return
175
+ }
176
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
177
+ e.preventDefault()
178
+ const list = filtered.value
179
+ if (!list.length) return
180
+ const dir = e.key === "ArrowDown" ? 1 : -1
181
+ activeIndex.value = (activeIndex.value + dir + list.length) % list.length
182
+ return
183
+ }
184
+ if (e.key === "Enter" && filtered.value[activeIndex.value]) {
185
+ e.preventDefault()
186
+ select(filtered.value[activeIndex.value]!)
187
+ }
188
+ }
189
+
190
+ // — Fermetures : clic extérieur, Échap, scroll lock (modes panneau) —
191
+ const onDocMousedown = (e: MouseEvent) => {
192
+ const target = e.target as Node
193
+ if (rootEl.value?.contains(target)) return
194
+ if (sheetRef.value?.contains(target)) return
195
+ if (isPanelMode.value && props.persistent) return
196
+ close()
197
+ }
198
+
199
+ const onDocKeydown = (e: KeyboardEvent) => {
200
+ if (e.key === "Escape" && open.value && !props.persistent) close()
201
+ }
202
+
203
+ watch(open, (v) => {
204
+ activeIndex.value = 0
205
+ if (v) nextTick(() => searchInput.value?.focus())
206
+ if (isPanelMode.value) {
207
+ document.body.style.overflow = v ? "hidden" : ""
208
+ }
209
+ })
210
+
211
+ onMounted(() => {
212
+ if (typeof document !== "undefined") {
213
+ document.addEventListener("mousedown", onDocMousedown)
214
+ document.addEventListener("keydown", onDocKeydown)
215
+ }
216
+ })
217
+ onBeforeUnmount(() => {
218
+ if (typeof document !== "undefined") {
219
+ document.removeEventListener("mousedown", onDocMousedown)
220
+ document.removeEventListener("keydown", onDocKeydown)
221
+ document.body.style.overflow = ""
222
+ }
223
+ })
224
+
225
+ const sheetTitle = computed(() => props.title || props.label || props.placeholder)
226
+
227
+ /** Options du mode courant (sheet → sheetOptions, modal → modalOptions) */
228
+ const modeOptions = computed<QCountryPickerModeOptions | undefined>(() => {
229
+ if (props.mode === "sheet") return props.sheetOptions
230
+ if (props.mode === "modal") return props.modalOptions
231
+ return undefined
232
+ })
233
+
234
+ const sheetStyle = computed<Record<string, string> | undefined>(() => {
235
+ const w = modeOptions.value?.width ?? props.width
236
+ const style: Record<string, string> = {}
237
+ if (w) {
238
+ style.width = w
239
+ style.maxWidth = w
240
+ }
241
+ if (props.height) style.height = props.height
242
+ if (typeof props.rounded === "string") style.borderRadius = props.rounded
243
+ if (props.translucent === true || typeof props.translucent === "number") {
244
+ style["--q-translucent-opacity"] = `${typeof props.translucent === "number" ? props.translucent : 70}%`
245
+ }
246
+ return Object.keys(style).length ? style : undefined
247
+ })
248
+
249
+ /** Hauteur de la liste scrollable (options du mode courant) */
250
+ const listHeightStyle = computed<Record<string, string> | undefined>(() =>
251
+ modeOptions.value?.height ? { height: modeOptions.value.height } : undefined,
252
+ )
253
+
254
+ /** Classes additionnelles du panneau (options du mode + modifiers bottom-sheet) */
255
+ const sheetClass = computed(() =>
256
+ cn(
257
+ modeOptions.value?.class,
258
+ props.rounded === false && "q-country-picker__sheet--square",
259
+ props.dark && "q-country-picker__sheet--dark",
260
+ (props.translucent === true || typeof props.translucent === "number") &&
261
+ "q-country-picker__sheet--translucent",
262
+ ),
263
+ )
264
+
265
+ const searchPlaceholder = computed(
266
+ () => modeOptions.value?.searchPlaceholder ?? props.placeholder,
267
+ )
268
+
269
+ // — Drag vers le bas pour fermer (mode sheet, pattern bottom-sheet) —
270
+ const dragging = ref(false)
271
+ let startY = 0
272
+ let currentDy = 0
273
+
274
+ const onHandleDown = (e: PointerEvent) => {
275
+ if (props.mode !== "sheet") return
276
+ ;(e.target as HTMLElement).setPointerCapture(e.pointerId)
277
+ startY = e.clientY
278
+ currentDy = 0
279
+ dragging.value = true
280
+ }
281
+
282
+ const onHandleMove = (e: PointerEvent) => {
283
+ if (!dragging.value) return
284
+ currentDy = Math.max(0, e.clientY - startY)
285
+ if (sheetRef.value) sheetRef.value.style.transform = `translateY(${currentDy}px)`
286
+ }
287
+
288
+ const onHandleUp = () => {
289
+ if (!dragging.value) return
290
+ dragging.value = false
291
+ if (currentDy > props.dragThreshold) close()
292
+ else if (sheetRef.value) sheetRef.value.style.transform = ""
293
+ currentDy = 0
294
+ }
295
+
296
+ const fieldClasses = computed(() =>
297
+ cn(
298
+ "q-country-picker q-input",
299
+ props.outlined && "q-field--outlined",
300
+ props.filled && "q-field--filled",
301
+ props.borderless && "q-field--borderless",
302
+ props.dense && "q-field--dense",
303
+ props.disable && "q-field--disabled",
304
+ ),
305
+ )
306
+ </script>
307
+
308
+ <template>
309
+ <div ref="rootEl" :class="fieldClasses">
310
+ <label v-if="label" class="q-field__label-stack">{{ label }}</label>
311
+ <div
312
+ class="q-field__control q-country-picker__field"
313
+ :class="{ 'q-country-picker__field--open': open }"
314
+ role="combobox"
315
+ tabindex="0"
316
+ :aria-expanded="open ? 'true' : 'false'"
317
+ aria-haspopup="listbox"
318
+ @click="onFieldClick"
319
+ @keydown="onKeydown"
320
+ >
321
+ <span v-if="selected" class="q-country-picker__flag" aria-hidden="true">
322
+ {{ flagOf(selected.code) }}
323
+ </span>
324
+ <span
325
+ class="q-country-picker__label"
326
+ :class="{ 'q-country-picker__label--placeholder': !selected }"
327
+ >
328
+ {{ selected ? countryName(selected) : placeholder }}
329
+ </span>
330
+ <span v-if="selected && showDial" class="q-country-picker__dial">
331
+ {{ selected.dial }}
332
+ </span>
333
+ <Icon
334
+ :icon="icons.chevronDown"
335
+ class="q-country-picker__arrow"
336
+ :class="{ 'q-country-picker__arrow--rotated': open }"
337
+ aria-hidden="true"
338
+ />
339
+ </div>
340
+
341
+ <!-- Mode inline : liste sous le champ -->
342
+ <div v-if="open && mode === 'inline'" class="q-country-picker__popup" role="listbox">
343
+ <div v-if="searchable" class="q-country-picker__search">
344
+ <Icon :icon="icons.search" class="q-country-picker__search-icon" aria-hidden="true" />
345
+ <input
346
+ ref="searchInput"
347
+ v-model="query"
348
+ class="q-country-picker__search-input"
349
+ :placeholder="searchPlaceholder"
350
+ @keydown="onKeydown"
351
+ />
352
+ </div>
353
+ <div class="q-country-picker__list" :style="listHeightStyle">
354
+ <button
355
+ v-for="(c, i) in filtered"
356
+ :key="c.code"
357
+ type="button"
358
+ role="option"
359
+ :aria-selected="c.code === modelValue ? 'true' : 'false'"
360
+ class="q-country-picker__option"
361
+ :class="{
362
+ 'q-country-picker__option--active': i === activeIndex,
363
+ 'q-country-picker__option--selected': c.code === modelValue,
364
+ }"
365
+ @mouseenter="activeIndex = i"
366
+ @mousedown.prevent="select(c)"
367
+ >
368
+ <span class="q-country-picker__flag" aria-hidden="true">{{ flagOf(c.code) }}</span>
369
+ <span class="q-country-picker__name">{{ countryName(c) }}</span>
370
+ <span v-if="showDial" class="q-country-picker__dial">{{ c.dial }}</span>
371
+ </button>
372
+ <div v-if="!filtered.length" class="q-country-picker__empty">
373
+ {{ noOptionsLabel }}
374
+ </div>
375
+ </div>
376
+ </div>
377
+
378
+ <!-- Modes panneau : overlay téléporté (modal / sheet / dialog) -->
379
+ <Teleport to="body">
380
+ <Transition name="q-country-modal">
381
+ <div
382
+ v-if="open && mode !== 'inline'"
383
+ class="q-country-picker__overlay"
384
+ :class="`q-country-picker__overlay--${mode}`"
385
+ role="presentation"
386
+ >
387
+ <div
388
+ ref="sheetRef"
389
+ class="q-country-picker__sheet"
390
+ :class="[
391
+ `q-country-picker__sheet--${mode}`,
392
+ sheetClass,
393
+ dragging && 'q-country-picker__sheet--dragging',
394
+ ]"
395
+ :style="[sheetStyle, modeOptions?.style, props.contentStyle]"
396
+ role="dialog"
397
+ aria-modal="true"
398
+ :aria-label="sheetTitle"
399
+ >
400
+ <div
401
+ v-if="mode === 'sheet'"
402
+ class="q-country-picker__handle"
403
+ @pointerdown="onHandleDown"
404
+ @pointermove="onHandleMove"
405
+ @pointerup="onHandleUp"
406
+ @pointercancel="onHandleUp"
407
+ >
408
+ <div class="q-country-picker__handle-bar" />
409
+ </div>
410
+ <div class="q-country-picker__sheet-header">
411
+ <span class="q-country-picker__sheet-title">{{ sheetTitle }}</span>
412
+ <button
413
+ type="button"
414
+ class="q-country-picker__sheet-close"
415
+ :aria-label="`Close ${sheetTitle}`"
416
+ @click="close"
417
+ >
418
+ <Icon :icon="icons.x" aria-hidden="true" />
419
+ </button>
420
+ </div>
421
+ <div class="q-country-picker__sheet-body">
422
+ <div v-if="searchable" class="q-country-picker__search">
423
+ <Icon :icon="icons.search" class="q-country-picker__search-icon" aria-hidden="true" />
424
+ <input
425
+ ref="searchInput"
426
+ v-model="query"
427
+ class="q-country-picker__search-input"
428
+ :placeholder="searchPlaceholder"
429
+ @keydown="onKeydown"
430
+ />
431
+ </div>
432
+ <div class="q-country-picker__list" :style="listHeightStyle">
433
+ <button
434
+ v-for="(c, i) in filtered"
435
+ :key="c.code"
436
+ type="button"
437
+ role="option"
438
+ :aria-selected="c.code === modelValue ? 'true' : 'false'"
439
+ class="q-country-picker__option"
440
+ :class="{
441
+ 'q-country-picker__option--active': i === activeIndex,
442
+ 'q-country-picker__option--selected': c.code === modelValue,
443
+ }"
444
+ @mouseenter="activeIndex = i"
445
+ @mousedown.prevent="select(c)"
446
+ >
447
+ <span class="q-country-picker__flag" aria-hidden="true">{{ flagOf(c.code) }}</span>
448
+ <span class="q-country-picker__name">{{ countryName(c) }}</span>
449
+ <span v-if="showDial" class="q-country-picker__dial">{{ c.dial }}</span>
450
+ </button>
451
+ <div v-if="!filtered.length" class="q-country-picker__empty">
452
+ {{ noOptionsLabel }}
453
+ </div>
454
+ </div>
455
+ </div>
456
+ </div>
457
+ </div>
458
+ </Transition>
459
+ </Teleport>
460
+ </div>
461
+ </template>
462
+
463
+ <style scoped>
464
+ .q-country-picker {
465
+ position: relative;
466
+ }
467
+
468
+ .q-country-picker__field {
469
+ cursor: pointer;
470
+ }
471
+ .q-country-picker__field--open,
472
+ .q-country-picker__field:focus-within {
473
+ border-color: var(--primary);
474
+ box-shadow: 0 0 0 1px var(--primary);
475
+ }
476
+
477
+ .q-country-picker__flag {
478
+ font-size: 18px;
479
+ line-height: 1;
480
+ }
481
+
482
+ .q-country-picker__label {
483
+ flex: 1;
484
+ min-width: 0;
485
+ overflow: hidden;
486
+ text-overflow: ellipsis;
487
+ white-space: nowrap;
488
+ font-size: 14px;
489
+ color: var(--foreground);
490
+ }
491
+ .q-country-picker__label--placeholder {
492
+ color: #8b93a1;
493
+ }
494
+
495
+ .q-country-picker__dial {
496
+ font-size: 12.5px;
497
+ color: #8b93a1;
498
+ font-variant-numeric: tabular-nums;
499
+ white-space: nowrap;
500
+ }
501
+
502
+ .q-country-picker__arrow {
503
+ font-size: 16px;
504
+ color: #8b93a1;
505
+ transition: transform 0.2s ease;
506
+ }
507
+ .q-country-picker__arrow--rotated {
508
+ transform: rotate(180deg);
509
+ }
510
+
511
+ /* — liste inline — */
512
+ .q-country-picker__popup {
513
+ position: absolute;
514
+ top: calc(100% + 4px);
515
+ left: 0;
516
+ right: 0;
517
+ z-index: 1000;
518
+ overflow: hidden;
519
+ background-color: #fff;
520
+ border: 1px solid rgb(0 0 0 / 0.12);
521
+ border-radius: var(--q-radius);
522
+ box-shadow: 0 8px 24px rgb(0 0 0 / 0.14);
523
+ }
524
+ .dark .q-country-picker__popup {
525
+ background-color: var(--card);
526
+ border-color: var(--border);
527
+ color: var(--foreground);
528
+ }
529
+
530
+ .q-country-picker__search {
531
+ display: flex;
532
+ align-items: center;
533
+ gap: 8px;
534
+ padding: 8px 10px;
535
+ border-bottom: 1px solid rgb(0 0 0 / 0.08);
536
+ }
537
+ .dark .q-country-picker__search {
538
+ border-bottom-color: var(--border);
539
+ }
540
+ .q-country-picker__search-icon {
541
+ color: #8b93a1;
542
+ font-size: 15px;
543
+ }
544
+ .q-country-picker__search-input {
545
+ flex: 1;
546
+ min-width: 0;
547
+ border: none;
548
+ outline: none;
549
+ background: transparent;
550
+ color: var(--foreground);
551
+ font-size: 13.5px;
552
+ }
553
+
554
+ .q-country-picker__list {
555
+ max-height: 260px;
556
+ overflow-y: auto;
557
+ padding: 4px;
558
+ }
559
+ .q-country-picker__option {
560
+ display: flex;
561
+ align-items: center;
562
+ gap: 10px;
563
+ width: 100%;
564
+ padding: 7px 10px;
565
+ border: none;
566
+ border-radius: 6px;
567
+ background: transparent;
568
+ color: var(--foreground);
569
+ font-size: 13.5px;
570
+ text-align: left;
571
+ cursor: pointer;
572
+ }
573
+ .q-country-picker__option--active {
574
+ background: rgb(25 118 210 / 0.08);
575
+ }
576
+ .q-country-picker__option--selected {
577
+ color: var(--primary);
578
+ font-weight: 600;
579
+ }
580
+ .q-country-picker__name {
581
+ flex: 1;
582
+ min-width: 0;
583
+ overflow: hidden;
584
+ text-overflow: ellipsis;
585
+ white-space: nowrap;
586
+ }
587
+ .q-country-picker__empty {
588
+ padding: 14px 10px;
589
+ font-size: 13px;
590
+ color: #8b93a1;
591
+ text-align: center;
592
+ }
593
+ </style>