@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,188 @@
1
+ <script lang="ts">
2
+ // QTabs — API Quasar (source officielle) : <q-tabs v-model="tab" align="justify" active-color="secondary">
3
+ // Fournit le contexte (tab actif, enregistrement) aux QTab enfants via provide/inject.
4
+ import type { InjectionKey, Ref } from "vue"
5
+
6
+ export interface QTabRegistration {
7
+ name: string | number
8
+ el: Ref<HTMLElement | null>
9
+ }
10
+
11
+ export interface QTabContext {
12
+ activeName: Readonly<Ref<string | number | null>>
13
+ setActive: (name: string | number) => void
14
+ register: (tab: QTabRegistration) => void
15
+ unregister: (name: string | number) => void
16
+ inlineLabel: Readonly<Ref<boolean>>
17
+ noCaps: Readonly<Ref<boolean>>
18
+ activeClass: Readonly<Ref<string | undefined>>
19
+ }
20
+
21
+ export const qTabsKey: InjectionKey<QTabContext> = Symbol("q-tabs")
22
+ </script>
23
+
24
+ <script setup lang="ts">
25
+ import { computed, nextTick, onBeforeUnmount, onMounted, provide, ref, shallowRef, watch } from "vue"
26
+ import { cva } from "class-variance-authority"
27
+ import { cn } from "../lib/utils"
28
+
29
+ const tabsVariants = cva("q-tabs", {
30
+ variants: {
31
+ align: {
32
+ left: "q-tabs--left",
33
+ center: "q-tabs--center",
34
+ right: "q-tabs--right",
35
+ justify: "q-tabs--justify",
36
+ },
37
+ dense: { true: "q-tabs--dense" },
38
+ },
39
+ defaultVariants: { align: "center" },
40
+ })
41
+
42
+ interface Props {
43
+ /** Nom du tab actif */
44
+ modelValue?: string | number | null
45
+ /** Alignement des tabs (défaut Quasar : center) */
46
+ align?: "left" | "center" | "right" | "justify"
47
+ /** Couleur du tab actif (token ou hex) */
48
+ activeColor?: string
49
+ /** Couleur de fond du tab actif */
50
+ activeBgColor?: string
51
+ /** Classe ajoutée au tab actif */
52
+ activeClass?: string
53
+ /** Couleur de l'indicateur (défaut : activeColor → primary) */
54
+ indicatorColor?: string
55
+ /** Hauteur réduite */
56
+ dense?: boolean
57
+ /** Icône + label sur la même ligne */
58
+ inlineLabel?: boolean
59
+ /** Pas de majuscules sur les tabs */
60
+ noCaps?: boolean
61
+ /** Indicateur en haut au lieu du bas */
62
+ switchIndicator?: boolean
63
+ /** Tabs en colonne (verticales) */
64
+ vertical?: boolean
65
+ /** Ne pas s'étendre (usage dans un QToolbar) */
66
+ shrink?: boolean
67
+ /** S'étendre sur l'axe croisé du parent flex */
68
+ stretch?: boolean
69
+ /** Classe du contenu interne */
70
+ contentClass?: string
71
+ /** Anime les tabs au clic (appui scale + pop du tab actif) */
72
+ animated?: boolean
73
+ }
74
+
75
+ const props = withDefaults(defineProps<Props>(), {
76
+ modelValue: null,
77
+ align: "center",
78
+ activeColor: "",
79
+ activeBgColor: "",
80
+ activeClass: "",
81
+ indicatorColor: "",
82
+ dense: false,
83
+ inlineLabel: false,
84
+ noCaps: false,
85
+ switchIndicator: false,
86
+ vertical: false,
87
+ shrink: false,
88
+ stretch: false,
89
+ contentClass: "",
90
+ animated: false,
91
+ })
92
+
93
+ const emit = defineEmits<{ "update:modelValue": [value: string | number | null] }>()
94
+
95
+ // shallowRef : on ne veut PAS que Vue dé-unwrappe les Ref contenues dans les enregistrements
96
+ const tabRegs = shallowRef<QTabRegistration[]>([])
97
+ const tick = ref(0)
98
+
99
+ // Recalcule la position de l'indicateur après chaque changement de DOM
100
+ const measure = async () => {
101
+ await nextTick()
102
+ tick.value++
103
+ }
104
+
105
+ watch([() => props.modelValue, tabRegs], measure)
106
+
107
+ onMounted(() => {
108
+ measure()
109
+ if (typeof window !== "undefined") window.addEventListener("resize", measure)
110
+ })
111
+ onBeforeUnmount(() => {
112
+ if (typeof window !== "undefined") window.removeEventListener("resize", measure)
113
+ })
114
+
115
+ const context: QTabContext = {
116
+ activeName: computed(() => props.modelValue),
117
+ setActive: (name) => emit("update:modelValue", name),
118
+ register: (tab) => {
119
+ tabRegs.value = [...tabRegs.value, tab]
120
+ },
121
+ unregister: (name) => {
122
+ tabRegs.value = tabRegs.value.filter((t) => t.name !== name)
123
+ },
124
+ inlineLabel: computed(() => props.inlineLabel),
125
+ noCaps: computed(() => props.noCaps),
126
+ activeClass: computed(() => (props.activeClass ? props.activeClass : undefined)),
127
+ }
128
+
129
+ provide(qTabsKey, context)
130
+
131
+ const activeTabEl = computed(
132
+ () => tabRegs.value.find((t) => t.name === props.modelValue)?.el.value ?? null,
133
+ )
134
+
135
+ const indicatorStyle = computed<Record<string, string>>(() => {
136
+ void tick.value // réactivité sur les mesures DOM
137
+ const el = activeTabEl.value
138
+ const style: Record<string, string> = {}
139
+ if (el) {
140
+ if (props.vertical) {
141
+ style.transform = `translateY(${el.offsetTop}px)`
142
+ style.height = `${el.offsetHeight}px`
143
+ }
144
+ else {
145
+ style.transform = `translateX(${el.offsetLeft}px)`
146
+ style.width = `${el.offsetWidth}px`
147
+ }
148
+ }
149
+ else {
150
+ style.opacity = "0"
151
+ }
152
+ if (props.indicatorColor) style.backgroundColor = props.indicatorColor
153
+ return style
154
+ })
155
+
156
+ const containerStyle = computed<Record<string, string>>(() => {
157
+ const style: Record<string, string> = {}
158
+ if (props.activeColor) style["--q-tabs-active-color"] = props.activeColor
159
+ if (props.activeBgColor) style["--q-tabs-active-bg"] = props.activeBgColor
160
+ return style
161
+ })
162
+
163
+ const tabsClasses = computed(() =>
164
+ cn(
165
+ tabsVariants({ align: props.align, dense: props.dense }),
166
+ props.vertical && "q-tabs--vertical",
167
+ props.switchIndicator && "q-tabs--switch-indicator",
168
+ props.shrink && "q-tabs--shrink",
169
+ props.stretch && "q-tabs--stretch",
170
+ props.animated && "q-tabs--animated",
171
+ ),
172
+ )
173
+ </script>
174
+
175
+ <template>
176
+ <div
177
+ class="q-tabs"
178
+ :class="tabsClasses"
179
+ :style="containerStyle"
180
+ role="tablist"
181
+ :aria-orientation="vertical ? 'vertical' : 'horizontal'"
182
+ >
183
+ <div class="q-tabs__content" :class="contentClass">
184
+ <slot />
185
+ <div class="q-tabs__indicator" :style="indicatorStyle" aria-hidden="true" />
186
+ </div>
187
+ </div>
188
+ </template>
@@ -0,0 +1,316 @@
1
+ <script setup lang="ts">
2
+ // QText — texte avec troncature multi-lignes + transitions :
3
+ // <q-text text="…" :lines="2" transition="fade" :transition-duration="400" />.
4
+ // lines > 0 → line-clamp (… à la fin), title = texte complet au survol.
5
+ // transition (fade, fade-up, zoom, slide-down…) : l'ancien texte sort et le
6
+ // nouveau entre quand la prop `text` change (mode out-in).
7
+ import { computed } from "vue"
8
+ import { cn } from "../lib/utils"
9
+
10
+ /** Effets prédéfinis — ou n'importe quel nom custom (classes `q-text-{nom}-enter-active`…) */
11
+ type QTextTransition =
12
+ | "fade"
13
+ | "fade-up"
14
+ | "fade-down"
15
+ | "fade-left"
16
+ | "fade-right"
17
+ | "zoom"
18
+ | "blur"
19
+ | "slide-up"
20
+ | "slide-down"
21
+ | "slide-left"
22
+ | "slide-right"
23
+ | (string & {})
24
+
25
+ interface Props {
26
+ /** Texte (sinon slot par défaut) */
27
+ text?: string | number
28
+ /** Nombre de lignes avant troncature (1, 2, 3…) ; absent = pas de clamp */
29
+ lines?: number
30
+ /** Élément rendu (p par défaut) */
31
+ tag?: string
32
+ /** Transition jouée quand `text` change : fade, fade-up, fade-down,
33
+ * fade-left, fade-right, zoom, blur, slide-up, slide-down, slide-left,
34
+ * slide-right — ou un nom custom (classes `q-text-{nom}-enter-active`…) */
35
+ transition?: QTextTransition
36
+ /** Durée de la transition en ms (défaut 300) */
37
+ transitionDuration?: number
38
+ /** Respiration douce du texte : chaque mot s'estompe en boucle (stagger) */
39
+ breathing?: boolean
40
+ /** Apparition « générée » : chaque mot se révèle en séquence (blur + fondu) */
41
+ generate?: boolean
42
+ /** Sous-chaîne de `text` à surligner (marqueur qui se dessine, façon Inspira) */
43
+ highlight?: string
44
+ class?: string
45
+ }
46
+
47
+ const props = withDefaults(defineProps<Props>(), {
48
+ tag: "p",
49
+ transition: "",
50
+ transitionDuration: 300,
51
+ })
52
+
53
+ const clampStyle = computed<Record<string, string> | undefined>(() =>
54
+ props.lines ? { "--q-text-lines": String(props.lines) } : undefined,
55
+ )
56
+
57
+ const transitionStyle = computed<Record<string, string> | undefined>(() =>
58
+ props.transition
59
+ ? { "--q-text-duration": `${props.transitionDuration}ms` }
60
+ : undefined,
61
+ )
62
+
63
+ /** Les effets slide sortent le texte du cadre → masquer le débordement */
64
+ const overflow = computed(() => props.transition.startsWith("slide"))
65
+
66
+ /** Mots du texte (prop text) pour l'effet breathing */
67
+ const words = computed(() => String(props.text).split(" ").filter(Boolean))
68
+
69
+ /** Délai de respiration par mot (cascade) */
70
+ const wordDelay = (i: number) => ({ animationDelay: `${i * 0.12}s` })
71
+
72
+ /** Parties du texte autour du mot surligné : [avant, mot, après] ou null */
73
+ const hlParts = computed<[string, string, string] | null>(() => {
74
+ if (!props.highlight || props.text === undefined) return null
75
+ const t = String(props.text)
76
+ const idx = t.indexOf(props.highlight)
77
+ if (idx === -1) return null
78
+ return [t.slice(0, idx), props.highlight, t.slice(idx + props.highlight.length)]
79
+ })
80
+ </script>
81
+
82
+ <template>
83
+ <component
84
+ :is="tag"
85
+ class="q-text"
86
+ :class="cn(
87
+ lines && 'q-text--clamp',
88
+ transition && 'q-text--transition',
89
+ overflow && 'q-text--overflow',
90
+ props.class,
91
+ )"
92
+ :style="[clampStyle, transitionStyle]"
93
+ :title="lines && text !== undefined ? String(text) : undefined"
94
+ >
95
+ <Transition
96
+ v-if="transition"
97
+ :name="`q-text-${transition}`"
98
+ mode="out-in"
99
+ :duration="transitionDuration"
100
+ >
101
+ <span :key="String(text)" class="q-text__content">
102
+ <template v-if="hlParts">
103
+ <template v-for="(part, i) in hlParts" :key="i">
104
+ <span v-if="i === 1" class="q-text__highlight">{{ part }}</span>
105
+ <template v-else>{{ part }}</template>
106
+ </template>
107
+ </template>
108
+ <template v-else-if="generate && text !== undefined">
109
+ <span
110
+ v-for="(w, i) in words"
111
+ :key="i"
112
+ class="q-text__gen-word"
113
+ :style="{ animationDelay: `${i * 0.08}s` }"
114
+ >{{ w }}</span>
115
+ </template>
116
+ <template v-else-if="breathing && text !== undefined">
117
+ <span
118
+ v-for="(w, i) in words"
119
+ :key="i"
120
+ class="q-text__word"
121
+ :style="wordDelay(i)"
122
+ >{{ w }}</span>
123
+ </template>
124
+ <slot v-else>{{ text }}</slot>
125
+ </span>
126
+ </Transition>
127
+ <template v-if="!transition">
128
+ <template v-if="hlParts">
129
+ <template v-for="(part, i) in hlParts" :key="i">
130
+ <span v-if="i === 1" class="q-text__highlight">{{ part }}</span>
131
+ <template v-else>{{ part }}</template>
132
+ </template>
133
+ </template>
134
+ <template v-else-if="generate && text !== undefined">
135
+ <span
136
+ v-for="(w, i) in words"
137
+ :key="i"
138
+ class="q-text__gen-word"
139
+ :style="{ animationDelay: `${i * 0.08}s` }"
140
+ >{{ w }}</span>
141
+ </template>
142
+ <template v-else-if="breathing && text !== undefined">
143
+ <span
144
+ v-for="(w, i) in words"
145
+ :key="i"
146
+ class="q-text__word"
147
+ :style="wordDelay(i)"
148
+ >{{ w }}</span>
149
+ </template>
150
+ <slot v-else>{{ text }}</slot>
151
+ </template>
152
+ </component>
153
+ </template>
154
+
155
+ <style scoped>
156
+ /* Contenu animé : inline-block pour que transform fonctionne */
157
+ .q-text__content {
158
+ display: inline-block;
159
+ }
160
+
161
+ /* Les effets slide masquent le texte qui sort du cadre */
162
+ .q-text--overflow {
163
+ overflow: hidden;
164
+ }
165
+
166
+ /* ─── fade ─── */
167
+ .q-text-fade-enter-active,
168
+ .q-text-fade-leave-active {
169
+ transition: opacity var(--q-text-duration, 300ms) ease;
170
+ }
171
+ .q-text-fade-enter-from,
172
+ .q-text-fade-leave-to {
173
+ opacity: 0;
174
+ }
175
+
176
+ /* ─── fade + direction ─── */
177
+ .q-text-fade-up-enter-active,
178
+ .q-text-fade-up-leave-active,
179
+ .q-text-fade-down-enter-active,
180
+ .q-text-fade-down-leave-active,
181
+ .q-text-fade-left-enter-active,
182
+ .q-text-fade-left-leave-active,
183
+ .q-text-fade-right-enter-active,
184
+ .q-text-fade-right-leave-active {
185
+ transition:
186
+ opacity var(--q-text-duration, 300ms) ease,
187
+ transform var(--q-text-duration, 300ms) ease;
188
+ }
189
+ .q-text-fade-up-enter-from {
190
+ opacity: 0;
191
+ transform: translateY(12px);
192
+ }
193
+ .q-text-fade-up-leave-to {
194
+ opacity: 0;
195
+ transform: translateY(-12px);
196
+ }
197
+ .q-text-fade-down-enter-from {
198
+ opacity: 0;
199
+ transform: translateY(-12px);
200
+ }
201
+ .q-text-fade-down-leave-to {
202
+ opacity: 0;
203
+ transform: translateY(12px);
204
+ }
205
+ .q-text-fade-left-enter-from {
206
+ opacity: 0;
207
+ transform: translateX(12px);
208
+ }
209
+ .q-text-fade-left-leave-to {
210
+ opacity: 0;
211
+ transform: translateX(-12px);
212
+ }
213
+ .q-text-fade-right-enter-from {
214
+ opacity: 0;
215
+ transform: translateX(-12px);
216
+ }
217
+ .q-text-fade-right-leave-to {
218
+ opacity: 0;
219
+ transform: translateX(12px);
220
+ }
221
+
222
+ /* ─── zoom ─── */
223
+ .q-text-zoom-enter-active,
224
+ .q-text-zoom-leave-active {
225
+ transition:
226
+ opacity var(--q-text-duration, 300ms) ease,
227
+ transform var(--q-text-duration, 300ms) ease;
228
+ }
229
+ .q-text-zoom-enter-from {
230
+ opacity: 0;
231
+ transform: scale(0.92);
232
+ }
233
+ .q-text-zoom-leave-to {
234
+ opacity: 0;
235
+ transform: scale(1.05);
236
+ }
237
+
238
+ /* ─── blur ─── */
239
+ .q-text-blur-enter-active,
240
+ .q-text-blur-leave-active {
241
+ transition:
242
+ opacity var(--q-text-duration, 300ms) ease,
243
+ filter var(--q-text-duration, 300ms) ease;
244
+ }
245
+ .q-text-blur-enter-from {
246
+ opacity: 0;
247
+ filter: blur(6px);
248
+ }
249
+ .q-text-blur-leave-to {
250
+ opacity: 0;
251
+ filter: blur(6px);
252
+ }
253
+
254
+ /* ─── slide (sans fondu, le texte glisse) ─── */
255
+ .q-text-slide-up-enter-active,
256
+ .q-text-slide-up-leave-active,
257
+ .q-text-slide-down-enter-active,
258
+ .q-text-slide-down-leave-active,
259
+ .q-text-slide-left-enter-active,
260
+ .q-text-slide-left-leave-active,
261
+ .q-text-slide-right-enter-active,
262
+ .q-text-slide-right-leave-active {
263
+ transition: transform var(--q-text-duration, 300ms) ease;
264
+ }
265
+ .q-text-slide-up-enter-from {
266
+ transform: translateY(100%);
267
+ }
268
+ .q-text-slide-up-leave-to {
269
+ transform: translateY(-100%);
270
+ }
271
+ .q-text-slide-down-enter-from {
272
+ transform: translateY(-100%);
273
+ }
274
+ .q-text-slide-down-leave-to {
275
+ transform: translateY(100%);
276
+ }
277
+ .q-text-slide-left-enter-from {
278
+ transform: translateX(100%);
279
+ }
280
+ .q-text-slide-left-leave-to {
281
+ transform: translateX(-100%);
282
+ }
283
+ .q-text-slide-right-enter-from {
284
+ transform: translateX(-100%);
285
+ }
286
+ .q-text-slide-right-leave-to {
287
+ transform: translateX(100%);
288
+ }
289
+
290
+ @media (prefers-reduced-motion: reduce) {
291
+ .q-text-fade-enter-active,
292
+ .q-text-fade-leave-active,
293
+ .q-text-fade-up-enter-active,
294
+ .q-text-fade-up-leave-active,
295
+ .q-text-fade-down-enter-active,
296
+ .q-text-fade-down-leave-active,
297
+ .q-text-fade-left-enter-active,
298
+ .q-text-fade-left-leave-active,
299
+ .q-text-fade-right-enter-active,
300
+ .q-text-fade-right-leave-active,
301
+ .q-text-zoom-enter-active,
302
+ .q-text-zoom-leave-active,
303
+ .q-text-blur-enter-active,
304
+ .q-text-blur-leave-active,
305
+ .q-text-slide-up-enter-active,
306
+ .q-text-slide-up-leave-active,
307
+ .q-text-slide-down-enter-active,
308
+ .q-text-slide-down-leave-active,
309
+ .q-text-slide-left-enter-active,
310
+ .q-text-slide-left-leave-active,
311
+ .q-text-slide-right-enter-active,
312
+ .q-text-slide-right-leave-active {
313
+ transition: none;
314
+ }
315
+ }
316
+ </style>
@@ -0,0 +1,122 @@
1
+ <script setup lang="ts">
2
+ // QTextCaption — légende superposée à une image (QImg) ou tout conteneur positionné.
3
+ // <q-text-caption position="bottom" absolute title="…" subtitle="…" />
4
+ // Dégradé sombre par défaut ; position bottom|top|left|right ; absolute = ancré au bord.
5
+ import { computed } from "vue"
6
+ import { cn } from "../lib/utils"
7
+
8
+ export type QTextCaptionPosition = "bottom" | "top" | "left" | "right"
9
+
10
+ interface Props {
11
+ /** Bord du conteneur où la légende est ancrée */
12
+ position?: QTextCaptionPosition
13
+ /** Position absolute (sort du flux, ancré au bord du conteneur positionné) */
14
+ absolute?: boolean
15
+ /** Prend toute la largeur du conteneur */
16
+ fit?: boolean
17
+ /** Dégradé sombre derrière le texte (défaut : true) */
18
+ gradient?: boolean
19
+ /** Titre en gras */
20
+ title?: string
21
+ /** Sous-titre */
22
+ subtitle?: string
23
+ /** Respiration douce du titre/sous-titre : mots qui s'estompent en boucle */
24
+ breathing?: boolean
25
+ /** Apparition « générée » : mots révélés en séquence (blur + fondu) */
26
+ generate?: boolean
27
+ /** Sous-chaîne du titre à surligner (marqueur qui se dessine) */
28
+ highlight?: string
29
+ }
30
+
31
+ const props = withDefaults(defineProps<Props>(), {
32
+ position: "bottom",
33
+ absolute: false,
34
+ fit: false,
35
+ gradient: true,
36
+ title: "",
37
+ subtitle: "",
38
+ breathing: false,
39
+ generate: false,
40
+ highlight: "",
41
+ })
42
+
43
+ const classes = computed(() =>
44
+ cn(
45
+ "q-text-caption",
46
+ `q-text-caption--${props.position}`,
47
+ props.absolute && "q-text-caption--absolute",
48
+ props.fit && "q-text-caption--fit",
49
+ !props.gradient && "q-text-caption--flat",
50
+ ),
51
+ )
52
+
53
+ /** Mots d'une chaîne + délai de respiration (cascade) */
54
+ const words = (s: string) => s.split(" ").filter(Boolean)
55
+ const wordDelay = (i: number) => ({ animationDelay: `${i * 0.12}s` })
56
+
57
+ /** Parties du titre autour du mot surligné : [avant, mot, après] ou null */
58
+ const hlParts = computed<[string, string, string] | null>(() => {
59
+ if (!props.highlight || !props.title) return null
60
+ const idx = props.title.indexOf(props.highlight)
61
+ if (idx === -1) return null
62
+ return [
63
+ props.title.slice(0, idx),
64
+ props.highlight,
65
+ props.title.slice(idx + props.highlight.length),
66
+ ]
67
+ })
68
+ </script>
69
+
70
+ <template>
71
+ <div :class="classes">
72
+ <slot>
73
+ <span v-if="title" class="q-text-caption__title">
74
+ <template v-if="hlParts">
75
+ <template v-for="(part, i) in hlParts" :key="i">
76
+ <span v-if="i === 1" class="q-text-caption__highlight">{{ part }}</span>
77
+ <template v-else>{{ part }}</template>
78
+ </template>
79
+ </template>
80
+ <template v-else-if="generate">
81
+ <span
82
+ v-for="(w, i) in words(title)"
83
+ :key="i"
84
+ class="q-text-caption__gen-word"
85
+ :style="{ animationDelay: `${i * 0.08}s` }"
86
+ >{{ w }}</span>
87
+ </template>
88
+ <template v-else-if="breathing">
89
+ <span
90
+ v-for="(w, i) in words(title)"
91
+ :key="i"
92
+ class="q-text-caption__word"
93
+ :style="wordDelay(i)"
94
+ >{{ w }}</span>
95
+ </template>
96
+ <template v-else>{{ title }}</template>
97
+ </span>
98
+
99
+ <template v-if="generate && subtitle">
100
+ <span class="q-text-caption__subtitle">
101
+ <span
102
+ v-for="(w, i) in words(subtitle)"
103
+ :key="i"
104
+ class="q-text-caption__gen-word"
105
+ :style="{ animationDelay: `${i * 0.08}s` }"
106
+ >{{ w }}</span>
107
+ </span>
108
+ </template>
109
+ <template v-else-if="breathing && subtitle">
110
+ <span class="q-text-caption__subtitle q-text-caption__breathe">
111
+ <span
112
+ v-for="(w, i) in words(subtitle)"
113
+ :key="i"
114
+ class="q-text-caption__word"
115
+ :style="wordDelay(i)"
116
+ >{{ w }}</span>
117
+ </span>
118
+ </template>
119
+ <span v-else-if="subtitle" class="q-text-caption__subtitle">{{ subtitle }}</span>
120
+ </slot>
121
+ </div>
122
+ </template>
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ // QToolbar — API Quasar : <q-toolbar inset shrink>
3
+ // Barre d'actions avec padding horizontal par défaut (0 12px), min-height 50px.
4
+ // Styles : packages/ui/styles/main.css (classe .q-toolbar)
5
+ interface Props {
6
+ /** Padding gauche supplémentaire (16px) — cas : toolbar sous un drawer */
7
+ inset?: boolean
8
+ /** Réduit la toolbar à la largeur de son contenu (pas de stretch) */
9
+ shrink?: boolean
10
+ }
11
+
12
+ const props = withDefaults(defineProps<Props>(), {
13
+ inset: false,
14
+ shrink: false,
15
+ })
16
+ </script>
17
+
18
+ <template>
19
+ <div
20
+ class="q-toolbar"
21
+ :class="{
22
+ 'q-toolbar--inset': props.inset,
23
+ 'q-toolbar--shrink': props.shrink,
24
+ }"
25
+ >
26
+ <slot />
27
+ </div>
28
+ </template>