@dnax/ui 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/components/QAccordion.vue +68 -0
- package/components/QAccordionContent.vue +62 -0
- package/components/QAccordionItem.vue +24 -0
- package/components/QAccordionTrigger.vue +51 -0
- package/components/QActionSheet.vue +159 -0
- package/components/QApp.vue +8 -0
- package/components/QAutocomplete.vue +316 -0
- package/components/QAvatar.vue +92 -0
- package/components/QBackHeader.vue +69 -0
- package/components/QBackTop.vue +125 -0
- package/components/QBadge.vue +55 -0
- package/components/QBottomSheet.vue +177 -0
- package/components/QBottomSheetFooter.vue +9 -0
- package/components/QBottomSheetHeader.vue +35 -0
- package/components/QBottomSheetProvider.vue +62 -0
- package/components/QBottomSheetTrigger.vue +18 -0
- package/components/QBtn.vue +144 -0
- package/components/QBtnGroup.vue +31 -0
- package/components/QBubble.vue +36 -0
- package/components/QBubbleContent.vue +28 -0
- package/components/QBubbleGroup.vue +25 -0
- package/components/QBubbleReactions.vue +40 -0
- package/components/QCard.vue +63 -0
- package/components/QCardActions.vue +27 -0
- package/components/QCardContent.vue +29 -0
- package/components/QCarousel.vue +115 -0
- package/components/QCarouselContent.vue +23 -0
- package/components/QCarouselItem.vue +20 -0
- package/components/QCarouselNav.vue +51 -0
- package/components/QCheckbox.vue +146 -0
- package/components/QChip.vue +91 -0
- package/components/QCircularProgress.vue +122 -0
- package/components/QCol.vue +52 -0
- package/components/QCollapse.vue +141 -0
- package/components/QConfigProvider.vue +164 -0
- package/components/QContainer.vue +201 -0
- package/components/QCountDown.vue +164 -0
- package/components/QCountryPicker.vue +593 -0
- package/components/QDataGrid.vue +491 -0
- package/components/QDatePicker.vue +250 -0
- package/components/QDialog.vue +163 -0
- package/components/QDialogFooter.vue +9 -0
- package/components/QDialogHeader.vue +41 -0
- package/components/QDialogProvider.vue +65 -0
- package/components/QDialogTrigger.vue +18 -0
- package/components/QFab.vue +80 -0
- package/components/QFabAction.vue +48 -0
- package/components/QFilePicker.vue +294 -0
- package/components/QFooter.vue +72 -0
- package/components/QGallery.vue +280 -0
- package/components/QGrid.vue +54 -0
- package/components/QHeader.vue +38 -0
- package/components/QIcon.vue +53 -0
- package/components/QImagePicker.vue +248 -0
- package/components/QImagePreview.vue +225 -0
- package/components/QImagePreviewProvider.vue +35 -0
- package/components/QImg.vue +142 -0
- package/components/QInfiniteScroll.vue +109 -0
- package/components/QInnerLoading.vue +67 -0
- package/components/QInput.vue +289 -0
- package/components/QInputOtp.vue +238 -0
- package/components/QInputPassword.vue +67 -0
- package/components/QInputTag.vue +281 -0
- package/components/QIntersection.vue +119 -0
- package/components/QItem.vue +60 -0
- package/components/QItemSection.vue +47 -0
- package/components/QLinearProgress.vue +94 -0
- package/components/QList.vue +39 -0
- package/components/QLoading.vue +77 -0
- package/components/QLoadingProvider.vue +41 -0
- package/components/QMarquee.vue +56 -0
- package/components/QMessageScroller.vue +10 -0
- package/components/QMessageScrollerButton.vue +48 -0
- package/components/QMessageScrollerContent.vue +31 -0
- package/components/QMessageScrollerItem.vue +49 -0
- package/components/QMessageScrollerProvider.vue +198 -0
- package/components/QMessageScrollerViewport.vue +109 -0
- package/components/QNavMenu.vue +75 -0
- package/components/QNavMenuContent.vue +10 -0
- package/components/QNavMenuItem.vue +60 -0
- package/components/QNavMenuTrigger.vue +96 -0
- package/components/QNotifyProvider.vue +33 -0
- package/components/QNotifyToast.vue +60 -0
- package/components/QPage.vue +56 -0
- package/components/QPagination.vue +195 -0
- package/components/QParallax.vue +124 -0
- package/components/QPullToRefresh.vue +138 -0
- package/components/QRadio.vue +99 -0
- package/components/QRating.vue +105 -0
- package/components/QReorder.vue +191 -0
- package/components/QRollingText.vue +225 -0
- package/components/QRow.vue +23 -0
- package/components/QSafeArea.vue +39 -0
- package/components/QScrollArea.vue +118 -0
- package/components/QSelect.vue +698 -0
- package/components/QSeparator.vue +39 -0
- package/components/QSidebar.vue +153 -0
- package/components/QSidebarContent.vue +14 -0
- package/components/QSidebarFooter.vue +14 -0
- package/components/QSidebarHeader.vue +14 -0
- package/components/QSidebarMenu.vue +9 -0
- package/components/QSidebarMenuButton.vue +59 -0
- package/components/QSidebarMenuItem.vue +9 -0
- package/components/QSidebarTrigger.vue +32 -0
- package/components/QSkeleton.vue +95 -0
- package/components/QSlider.vue +205 -0
- package/components/QSpace.vue +10 -0
- package/components/QSpinner.vue +172 -0
- package/components/QSplitter.vue +145 -0
- package/components/QSticky.vue +181 -0
- package/components/QSwipeCell.vue +217 -0
- package/components/QSwiper.vue +102 -0
- package/components/QSwiperSlide.vue +19 -0
- package/components/QSyntax.vue +171 -0
- package/components/QTab.vue +175 -0
- package/components/QTabPanel.vue +50 -0
- package/components/QTabPanels.vue +106 -0
- package/components/QTable.vue +575 -0
- package/components/QTabs.vue +188 -0
- package/components/QText.vue +316 -0
- package/components/QTextCaption.vue +122 -0
- package/components/QToolbar.vue +28 -0
- package/components/QTooltip.vue +313 -0
- package/components/QUploader.vue +473 -0
- package/components/QVideo.vue +289 -0
- package/components/QVirtualScroll.vue +190 -0
- package/components/internal/QDateCalendar.vue +153 -0
- package/index.ts +151 -0
- package/lib/breakpoints.ts +44 -0
- package/lib/colors.ts +24 -0
- package/lib/config.ts +34 -0
- package/lib/countries.ts +261 -0
- package/lib/formula.ts +630 -0
- package/lib/icons.ts +38 -0
- package/lib/loading.ts +75 -0
- package/lib/overlayBack.ts +124 -0
- package/lib/platform.ts +146 -0
- package/lib/primitive.ts +25 -0
- package/lib/q.ts +287 -0
- package/lib/screen.ts +105 -0
- package/lib/search.ts +35 -0
- package/lib/themeVars.ts +22 -0
- package/lib/useComponentProps.ts +54 -0
- package/lib/utils.ts +7 -0
- package/module.ts +89 -0
- package/package.json +41 -0
- package/scripts/generate-exports.ts +56 -0
- package/styles/main.css +7627 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,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>
|