@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,60 @@
1
+ <script setup lang="ts">
2
+ // QItem — API Quasar : <q-item clickable active active-class="…" dense disable>
3
+ import { cn } from "../lib/utils"
4
+
5
+ interface Props {
6
+ /** Clic + hover (cursor pointer) */
7
+ clickable?: boolean
8
+ /** Hauteur réduite */
9
+ dense?: boolean
10
+ /** État actif */
11
+ active?: boolean
12
+ /** Classe ajoutée quand actif */
13
+ activeClass?: string
14
+ /** Désactivé */
15
+ disable?: boolean
16
+ tabindex?: string | number
17
+ /** Élément rendu (div par défaut) */
18
+ tag?: string
19
+ /** Indentation (multiples de 8px) */
20
+ insetLevel?: number
21
+ }
22
+
23
+ const props = withDefaults(defineProps<Props>(), {
24
+ clickable: false,
25
+ dense: false,
26
+ active: false,
27
+ activeClass: "",
28
+ disable: false,
29
+ tag: "div",
30
+ insetLevel: 0,
31
+ })
32
+
33
+ const emit = defineEmits<{ click: [event: MouseEvent] }>()
34
+
35
+ const itemClasses = cn(
36
+ "q-item",
37
+ props.clickable && "q-item--clickable",
38
+ props.dense && "q-item--dense",
39
+ props.active && "q-item--active",
40
+ props.active && props.activeClass,
41
+ props.disable && "q-item--disabled",
42
+ )
43
+
44
+ const itemStyle = props.insetLevel > 0 ? { paddingLeft: `${8 + props.insetLevel * 8}px` } : {}
45
+ </script>
46
+
47
+ <template>
48
+ <component
49
+ :is="tag"
50
+ class="q-item"
51
+ :class="itemClasses"
52
+ :style="itemStyle"
53
+ :tabindex="clickable ? (tabindex ?? 0) : undefined"
54
+ role="listitem"
55
+ :aria-disabled="disable || undefined"
56
+ @click="(e: MouseEvent) => { if (!disable) emit('click', e) }"
57
+ >
58
+ <slot />
59
+ </component>
60
+ </template>
@@ -0,0 +1,47 @@
1
+ <script setup lang="ts">
2
+ // QItemSection — API Quasar : <q-item-section avatar side top no-wrap>
3
+ // Sections d'un QItem : principale (flex:1), side (droite), avatar, thumbnail.
4
+ import { cn } from "../lib/utils"
5
+ import { colorValue } from "../lib/colors"
6
+
7
+ interface Props {
8
+ /** Zone avatar (largeur fixe, pas de stretch) */
9
+ avatar?: boolean
10
+ /** Zone vignette/image */
11
+ thumbnail?: boolean
12
+ /** Alignée à droite (actions…) */
13
+ side?: boolean
14
+ /** Alignée en haut */
15
+ top?: boolean
16
+ /** Pas de retour à la ligne */
17
+ noWrap?: boolean
18
+ /** Couleur du texte (token ou hex) */
19
+ color?: string
20
+ }
21
+
22
+ const props = withDefaults(defineProps<Props>(), {
23
+ avatar: false,
24
+ thumbnail: false,
25
+ side: false,
26
+ top: false,
27
+ noWrap: false,
28
+ color: "",
29
+ })
30
+
31
+ const sectionClasses = cn(
32
+ "q-item__section",
33
+ props.avatar && "q-item__section--avatar",
34
+ props.thumbnail && "q-item__section--thumbnail",
35
+ props.side && "q-item__section--side",
36
+ props.top && "q-item__section--top",
37
+ props.noWrap && "q-item__section--no-wrap",
38
+ )
39
+
40
+ const sectionStyle = props.color ? { color: colorValue(props.color) } : {}
41
+ </script>
42
+
43
+ <template>
44
+ <div class="q-item__section" :class="sectionClasses" :style="sectionStyle">
45
+ <slot />
46
+ </div>
47
+ </template>
@@ -0,0 +1,94 @@
1
+ <script setup lang="ts">
2
+ // QLinearProgress — barre de progression linéaire : <q-linear-progress value="0.7" color="secondary" stripe />
3
+ import { computed } from "vue"
4
+ import { cn } from "../lib/utils"
5
+ import { colorValue } from "../lib/colors"
6
+
7
+ interface Props {
8
+ /** Valeur (entre min et max, défaut 0-1) */
9
+ value?: number
10
+ min?: number
11
+ max?: number
12
+ /** Couleur de la barre (token ou hex) */
13
+ color?: string
14
+ /** Couleur de la piste */
15
+ trackColor?: string
16
+ /** Épaisseur (défaut 4px) */
17
+ size?: string
18
+ rounded?: boolean
19
+ /** Rayures animées */
20
+ stripe?: boolean
21
+ /** Sens inverse (remplit depuis la droite) */
22
+ reverse?: boolean
23
+ /** Mode indéterminé (bande qui traverse) */
24
+ indeterminate?: boolean
25
+ /** Mode recherche (bande qui traverse en sens inverse) */
26
+ query?: boolean
27
+ dark?: boolean
28
+ /** Sans transition sur width */
29
+ instantFeedback?: boolean
30
+ }
31
+
32
+ const props = withDefaults(defineProps<Props>(), {
33
+ value: 0,
34
+ min: 0,
35
+ max: 1,
36
+ color: "primary",
37
+ trackColor: "",
38
+ size: "4px",
39
+ rounded: false,
40
+ stripe: false,
41
+ reverse: false,
42
+ indeterminate: false,
43
+ query: false,
44
+ dark: false,
45
+ instantFeedback: false,
46
+ })
47
+
48
+ const ratio = computed(() => {
49
+ const span = props.max - props.min
50
+ if (span <= 0) return 0
51
+ return Math.min(1, Math.max(0, (props.value - props.min) / span))
52
+ })
53
+
54
+ const trackStyle = computed<Record<string, string>>(() => {
55
+ const style: Record<string, string> = { height: props.size }
56
+ if (props.trackColor) style.backgroundColor = colorValue(props.trackColor)
57
+ return style
58
+ })
59
+
60
+ const fillStyle = computed<Record<string, string>>(() => {
61
+ const style: Record<string, string> = { backgroundColor: colorValue(props.color) }
62
+ if (!props.indeterminate && !props.query) {
63
+ style.width = `${ratio.value * 100}%`
64
+ }
65
+ return style
66
+ })
67
+
68
+ const classes = computed(() =>
69
+ cn(
70
+ "q-linear-progress",
71
+ props.rounded && "q-linear-progress--rounded",
72
+ props.dark && "q-linear-progress--dark",
73
+ props.stripe && "q-linear-progress--stripe",
74
+ props.reverse && "q-linear-progress--reverse",
75
+ props.indeterminate && "q-linear-progress--indeterminate",
76
+ props.query && "q-linear-progress--query",
77
+ props.instantFeedback && "q-linear-progress--instant",
78
+ ),
79
+ )
80
+ </script>
81
+
82
+ <template>
83
+ <div
84
+ class="q-linear-progress"
85
+ :class="classes"
86
+ :style="trackStyle"
87
+ role="progressbar"
88
+ :aria-valuenow="value"
89
+ :aria-valuemin="min"
90
+ :aria-valuemax="max"
91
+ >
92
+ <div class="q-linear-progress__fill" :style="fillStyle" />
93
+ </div>
94
+ </template>
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ // QList — API Quasar : <q-list bordered dense separator dark padding>
3
+ interface Props {
4
+ /** Bordure fine */
5
+ bordered?: boolean
6
+ /** Hauteur réduite des items */
7
+ dense?: boolean
8
+ /** Séparateurs entre items */
9
+ separator?: boolean
10
+ /** Thème sombre */
11
+ dark?: boolean
12
+ /** Padding plus important */
13
+ padding?: boolean
14
+ }
15
+
16
+ const props = withDefaults(defineProps<Props>(), {
17
+ bordered: false,
18
+ dense: false,
19
+ separator: false,
20
+ dark: false,
21
+ padding: false,
22
+ })
23
+ </script>
24
+
25
+ <template>
26
+ <div
27
+ class="q-list"
28
+ :class="{
29
+ 'q-list--bordered': props.bordered,
30
+ 'q-list--dense': props.dense,
31
+ 'q-list--separator': props.separator,
32
+ 'q-list--dark': props.dark,
33
+ 'q-list--padding': props.padding,
34
+ }"
35
+ role="list"
36
+ >
37
+ <slot />
38
+ </div>
39
+ </template>
@@ -0,0 +1,77 @@
1
+ <script setup lang="ts">
2
+ // QLoading — overlay de chargement plein écran déclaratif : <q-loading v-model="busy" message="…" icon="…" />
3
+ // Alternative au plugin $q.loading (mêmes options visuelles) — piloté par v-model.
4
+ import { computed } from "vue"
5
+ import { Icon } from "@iconify/vue"
6
+ import { colorValue } from "../lib/colors"
7
+ import { useOverlayBack } from "../lib/overlayBack"
8
+
9
+ interface Props {
10
+ /** Affiche/masque l'overlay (v-model) */
11
+ modelValue?: boolean
12
+ /** Message sous le spinner */
13
+ message?: string
14
+ /** Taille du spinner en px (défaut : 48) */
15
+ spinnerSize?: number
16
+ /** Couleur du spinner : token (primary…) ou hex (défaut : primary) */
17
+ spinnerColor?: string
18
+ /** Couleur du message (défaut : #1d1d1d) */
19
+ messageColor?: string
20
+ /** Fond de l'overlay (défaut : rgb(0 0 0 / 0.3)) */
21
+ backgroundColor?: string
22
+ /** Icône Iconify affichée à la place du spinner (ex. : "lucide:loader-circle") */
23
+ icon?: string
24
+ /** Couleur de l'icône (défaut : spinnerColor) */
25
+ iconColor?: string
26
+ /** Garde la carte (fond blanc + ombre) même avec une icône (défaut : transparent) */
27
+ boxed?: boolean
28
+ }
29
+
30
+ const props = withDefaults(defineProps<Props>(), {
31
+ modelValue: false,
32
+ spinnerSize: 48,
33
+ spinnerColor: "primary",
34
+ messageColor: "#1d1d1d",
35
+ backgroundColor: "rgb(0 0 0 / 0.3)",
36
+ icon: "",
37
+ iconColor: "",
38
+ boxed: false,
39
+ })
40
+
41
+ const emit = defineEmits<{ "update:modelValue": [value: boolean] }>()
42
+
43
+ // « Retour » navigateur → ferme l'overlay de chargement au lieu de naviguer
44
+ const open = computed({
45
+ get: () => props.modelValue,
46
+ set: (v) => emit("update:modelValue", v),
47
+ })
48
+ useOverlayBack(open, () => { open.value = false }, "QLoading")
49
+
50
+ const spinnerStyle = computed(() => ({
51
+ width: `${props.spinnerSize}px`,
52
+ height: `${props.spinnerSize}px`,
53
+ borderWidth: `${Math.max(2, Math.round(props.spinnerSize / 8))}px`,
54
+ color: colorValue(props.spinnerColor),
55
+ }))
56
+
57
+ const iconStyle = computed(() => ({
58
+ width: `${props.spinnerSize}px`,
59
+ height: `${props.spinnerSize}px`,
60
+ color: colorValue(props.iconColor || props.spinnerColor),
61
+ }))
62
+
63
+ const messageStyle = computed(() => ({ color: colorValue(props.messageColor) }))
64
+ const overlayStyle = computed(() => ({ backgroundColor: props.backgroundColor }))
65
+ </script>
66
+
67
+ <template>
68
+ <Teleport to="body">
69
+ <div v-if="modelValue" class="q-loading" :style="overlayStyle" role="status" aria-live="polite">
70
+ <div class="q-loading__box" :class="{ 'q-loading__box--icon': icon && !boxed }">
71
+ <span v-if="!icon" class="q-loading__spinner" :style="spinnerStyle" aria-hidden="true" />
72
+ <Icon v-else :icon="icon" class="q-loading__icon" :style="iconStyle" aria-hidden="true" />
73
+ <div v-if="message" class="q-loading__message" :style="messageStyle">{{ message }}</div>
74
+ </div>
75
+ </div>
76
+ </Teleport>
77
+ </template>
@@ -0,0 +1,41 @@
1
+ <script setup lang="ts">
2
+ // QLoadingProvider — overlay de chargement plein écran ($q.loading).
3
+ // Intégré dans QConfigProvider (rendu automatiquement par le plus externe),
4
+ // utilisable aussi en autonome : <q-loading-provider>.
5
+ import { computed } from "vue"
6
+ import { Icon } from "@iconify/vue"
7
+ import { colorValue } from "../lib/colors"
8
+ import { loading } from "../lib/loading"
9
+
10
+ const visible = loading.isActive
11
+ const opts = loading._opts
12
+
13
+ const spinnerStyle = computed(() => ({
14
+ width: `${opts.spinnerSize}px`,
15
+ height: `${opts.spinnerSize}px`,
16
+ borderWidth: `${Math.max(2, Math.round((opts.spinnerSize ?? 48) / 8))}px`,
17
+ color: colorValue(opts.spinnerColor ?? "primary"),
18
+ }))
19
+
20
+ const iconStyle = computed(() => ({
21
+ width: `${opts.spinnerSize}px`,
22
+ height: `${opts.spinnerSize}px`,
23
+ color: colorValue(opts.iconColor ?? opts.spinnerColor ?? "primary"),
24
+ }))
25
+
26
+ const messageStyle = computed(() => ({ color: colorValue(opts.messageColor ?? "#1d1d1d") }))
27
+ const overlayStyle = computed(() => ({ backgroundColor: opts.backgroundColor }))
28
+ </script>
29
+
30
+ <template>
31
+ <slot />
32
+ <div v-if="visible" class="q-loading" :style="overlayStyle" role="status" aria-live="polite">
33
+ <div class="q-loading__box" :class="{ 'q-loading__box--icon': opts.icon && !opts.boxed }">
34
+ <span v-if="!opts.icon" class="q-loading__spinner" :style="spinnerStyle" aria-hidden="true" />
35
+ <Icon :icon="opts.icon" v-else class="q-loading__icon" :style="iconStyle" aria-hidden="true" />
36
+ <div v-if="opts.message" class="q-loading__message" :style="messageStyle">
37
+ {{ opts.message }}
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </template>
@@ -0,0 +1,56 @@
1
+ <script setup lang="ts">
2
+ // QMarquee — bandeau défilant infini : <q-marquee :text="…" :pause-on-hover="true" />.
3
+ // Deux copies du contenu (track animé translateX -50%) pour une boucle parfaite.
4
+ // Props : text (ou slot), direction (left|right), duration (s), gap, pause-on-hover.
5
+ import { computed } from "vue"
6
+ import { cn } from "../lib/utils"
7
+
8
+ interface Props {
9
+ /** Texte (sinon slot par défaut, rendu 2× pour la boucle) */
10
+ text?: string
11
+ /** Sens de défilement : "left" (vers la gauche) | "right" */
12
+ direction?: "left" | "right"
13
+ /** Durée d'un cycle complet, en secondes (défaut 20) */
14
+ duration?: number
15
+ /** Espace entre les deux copies (défaut 24px) */
16
+ gap?: string
17
+ /** Met en pause au survol */
18
+ pauseOnHover?: boolean
19
+ class?: string
20
+ }
21
+
22
+ const props = withDefaults(defineProps<Props>(), {
23
+ text: "",
24
+ direction: "left",
25
+ duration: 20,
26
+ gap: "24px",
27
+ pauseOnHover: false,
28
+ })
29
+
30
+ const marqueeStyle = computed<Record<string, string>>(() => ({
31
+ "--q-marquee-duration": `${props.duration}s`,
32
+ "--q-marquee-gap": props.gap,
33
+ }))
34
+
35
+ const marqueeClasses = computed(() =>
36
+ cn(
37
+ "q-marquee",
38
+ props.direction === "right" && "q-marquee--reverse",
39
+ props.pauseOnHover && "q-marquee--pause-on-hover",
40
+ props.class,
41
+ ),
42
+ )
43
+ </script>
44
+
45
+ <template>
46
+ <div class="q-marquee" :class="marqueeClasses" :style="marqueeStyle">
47
+ <div class="q-marquee__track">
48
+ <div class="q-marquee__copy">
49
+ <slot>{{ text }}</slot>
50
+ </div>
51
+ <div class="q-marquee__copy" aria-hidden="true">
52
+ <slot>{{ text }}</slot>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </template>
@@ -0,0 +1,10 @@
1
+ <script setup lang="ts">
2
+ // QMessageScroller — conteneur de la liste (équivalent MessageScroller).
3
+ // Enfant : QMessageScrollerViewport (+ QMessageScrollerButton en absolu).
4
+ </script>
5
+
6
+ <template>
7
+ <div class="q-msg-scroller">
8
+ <slot />
9
+ </div>
10
+ </template>
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ // QMessageScrollerButton — bouton flottant de retour au live edge (équivalent MessageScrollerButton).
3
+ // Visible uniquement quand il reste du contenu à atteindre dans cette direction.
4
+ import { computed, inject } from "vue"
5
+ import { Icon } from "@iconify/vue"
6
+ import { icons } from "../lib/icons"
7
+ import { qMessageScrollerKey } from "./QMessageScrollerProvider.vue"
8
+
9
+ interface Props {
10
+ /** Direction du saut */
11
+ direction?: "start" | "end"
12
+ label?: string
13
+ behavior?: ScrollBehavior
14
+ }
15
+
16
+ const props = withDefaults(defineProps<Props>(), {
17
+ direction: "end",
18
+ label: "Aller au plus récent",
19
+ behavior: "smooth",
20
+ })
21
+
22
+ const ctx = inject(qMessageScrollerKey, null)
23
+
24
+ const active = computed(() => {
25
+ const s = ctx?.scrollable.value
26
+ if (!s) return false
27
+ return props.direction === "end" ? s.end : s.start
28
+ })
29
+
30
+ const onClick = () => {
31
+ if (props.direction === "end") ctx?.scrollToEnd({ behavior: props.behavior })
32
+ else ctx?.scrollToStart({ behavior: props.behavior })
33
+ }
34
+ </script>
35
+
36
+ <template>
37
+ <button
38
+ type="button"
39
+ class="q-msg-scroller__button"
40
+ :class="{ 'q-msg-scroller__button--active': active }"
41
+ :tabindex="active ? 0 : -1"
42
+ :aria-hidden="active ? undefined : 'true'"
43
+ :aria-label="label"
44
+ @click="onClick"
45
+ >
46
+ <Icon :icon="direction === 'end' ? icons.chevronDown : icons.chevronUp" aria-hidden="true" />
47
+ </button>
48
+ </template>
@@ -0,0 +1,31 @@
1
+ <script setup lang="ts">
2
+ // QMessageScrollerContent — contenu du log (équivalent MessageScrollerContent).
3
+ // role="log" + aria-relevant="additions" : les lecteurs d'écran annoncent les nouveaux messages.
4
+ import { inject, ref, watch } from "vue"
5
+ import { qMessageScrollerKey } from "./QMessageScrollerProvider.vue"
6
+
7
+ interface Props {
8
+ /** État de streaming (aria-busy) */
9
+ ariaBusy?: boolean
10
+ }
11
+
12
+ const props = defineProps<Props>()
13
+ const ctx = inject(qMessageScrollerKey, null)
14
+ const el = ref<HTMLElement | null>(null)
15
+
16
+ watch(el, (v) => {
17
+ if (ctx) ctx.contentEl.value = v
18
+ }, { immediate: true })
19
+ </script>
20
+
21
+ <template>
22
+ <div
23
+ ref="el"
24
+ class="q-msg-scroller__content"
25
+ role="log"
26
+ aria-relevant="additions"
27
+ :aria-busy="ariaBusy || undefined"
28
+ >
29
+ <slot />
30
+ </div>
31
+ </template>
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ // QMessageScrollerItem — ligne du transcript (équivalent MessageScrollerItem).
3
+ // message-id : ancrage/visibilité/sauts ; scroll-anchor : marque le début d'un tour.
4
+ import { inject, onBeforeUnmount, onMounted, ref } from "vue"
5
+ import { qMessageScrollerKey } from "./QMessageScrollerProvider.vue"
6
+
7
+ let itemSeq = 0
8
+
9
+ /** Presets d'animation d'entrée d'un message (portés depuis ui-thing) */
10
+ type QMessageAnimationPreset =
11
+ | "fade"
12
+ | "slide-up"
13
+ | "slide-side"
14
+ | "pop"
15
+ | "spring-bounce"
16
+ | "blur-fade"
17
+ | "scale-fade"
18
+
19
+ interface Props {
20
+ /** Identifiant stable (ancrage, visibilité, sauts) — auto-généré si absent */
21
+ messageId?: string
22
+ /** Marque cette ligne comme le début d'un tour (positionnée près du haut à l'ajout) */
23
+ scrollAnchor?: boolean
24
+ /** Animation d'entrée du message à son ajout (fade, slide-up, pop, spring-bounce…) */
25
+ animationPreset?: QMessageAnimationPreset
26
+ }
27
+
28
+ const props = withDefaults(defineProps<Props>(), {
29
+ scrollAnchor: false,
30
+ })
31
+
32
+ const ctx = inject(qMessageScrollerKey, null)
33
+ const el = ref<HTMLElement | null>(null)
34
+
35
+ const resolvedId = props.messageId ?? `q-msg-item-${itemSeq++}`
36
+
37
+ onMounted(() => ctx?.registerItem({ id: resolvedId, el, anchor: props.scrollAnchor }))
38
+ onBeforeUnmount(() => ctx?.unregisterItem(resolvedId))
39
+ </script>
40
+
41
+ <template>
42
+ <div
43
+ ref="el"
44
+ class="q-msg-scroller__item"
45
+ :class="animationPreset && `q-msg-anim-${animationPreset}`"
46
+ >
47
+ <slot />
48
+ </div>
49
+ </template>