@dnax/ui 0.0.48 → 0.0.49
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/components/QBottomSheet.vue +172 -6
- package/components/QImagePicker.vue +28 -1
- package/components/QInputChat.vue +859 -0
- package/components/QInputCurrency.vue +319 -0
- package/components/QItemLabel.vue +99 -0
- package/components/QMap.vue +410 -0
- package/components/QNumericKeyboard.vue +304 -0
- package/components/QPage.vue +26 -6
- package/components/QPageContainer.vue +32 -0
- package/components/QPopupProxy.vue +378 -0
- package/components/QQrcode.vue +132 -0
- package/components/QSpreadsheet.vue +395 -69
- package/components/QTime.vue +845 -0
- package/components/QTimeline.vue +111 -0
- package/components/QTimelineEntry.vue +271 -0
- package/index.ts +11 -1
- package/lib/bottomSheet.test.ts +121 -0
- package/lib/bottomSheet.ts +115 -0
- package/lib/color.ts +13 -0
- package/lib/currency.test.ts +193 -0
- package/lib/currency.ts +301 -0
- package/lib/fixedLayout.ts +12 -11
- package/lib/icons.ts +3 -20
- package/lib/map.test.ts +365 -0
- package/lib/map.ts +438 -0
- package/lib/mapEngine.ts +158 -0
- package/lib/mapLeaflet.ts +142 -0
- package/lib/mapMaplibre.ts +106 -0
- package/lib/mapMaptiler.ts +93 -0
- package/lib/numericKeyboard.test.ts +179 -0
- package/lib/numericKeyboard.ts +166 -0
- package/lib/pagePadding.test.ts +37 -0
- package/lib/pagePadding.ts +32 -0
- package/lib/qrcode.test.ts +103 -0
- package/lib/qrcode.ts +125 -0
- package/lib/spreadsheetChanges.test.ts +230 -0
- package/lib/spreadsheetChanges.ts +331 -0
- package/package.json +5 -17
- package/shims.d.ts +19 -0
- package/styles/main.css +689 -884
- package/components/QTiptap.vue +0 -996
- package/components/internal/QTiptapTableView.vue +0 -123
- package/components/internal/QTiptapTaskItemView.vue +0 -49
- package/lib/tiptap-table.ts +0 -19
- package/lib/tiptap-task-list.ts +0 -16
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// QInputChat — composer de chat : textarea multiligne auto-extensible, bouton d'envoi
|
|
3
|
+
// intégré, pastilles d'options activables (v-model:options), bouton « + » à menu
|
|
4
|
+
// (actions + pièce jointe) et fichiers joints (v-model:files).
|
|
5
|
+
//
|
|
6
|
+
// Les interfaces `ChatOption` / `ChatAction` vivent dans ce bloc `<script lang="ts">`
|
|
7
|
+
// « normal » (comme `QTimeline`) : elles sont exportées pour les démos / la doc.
|
|
8
|
+
|
|
9
|
+
/** Option activable d'un composer (pastille) — sélection pilotée par `v-model:options` */
|
|
10
|
+
export interface ChatOption {
|
|
11
|
+
/** Libellé de la pastille */
|
|
12
|
+
label: string
|
|
13
|
+
/** Icône Iconify à gauche (ex. "lucide:globe") */
|
|
14
|
+
icon?: string
|
|
15
|
+
/** Valeur métier de l'option */
|
|
16
|
+
value?: any
|
|
17
|
+
/** Identifiant stable (recommandé) — sert à retrouver l'option au clic ; repli sur l'index */
|
|
18
|
+
_id?: string
|
|
19
|
+
/** Sélectionnée (mis à jour par le composant et renvoyé via `v-model:options`) */
|
|
20
|
+
active?: boolean
|
|
21
|
+
/** Désactivée */
|
|
22
|
+
disable?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Entrée du menu du bouton « + ». Les champs correspondent à ceux de `BtnAction`
|
|
27
|
+
* de `QBtnActions` : l'objet peut être mappé directement.
|
|
28
|
+
*/
|
|
29
|
+
export interface ChatAction {
|
|
30
|
+
/** Libellé de l'entrée de menu */
|
|
31
|
+
label?: string
|
|
32
|
+
/** Icône Iconify à gauche (ex. "lucide:paperclip") */
|
|
33
|
+
icon?: string
|
|
34
|
+
/** Icône Iconify à droite */
|
|
35
|
+
iconRight?: string
|
|
36
|
+
/** Valeur émise par `@action` ; à défaut, l'action elle-même est émise */
|
|
37
|
+
value?: any
|
|
38
|
+
/** Entrée désactivée */
|
|
39
|
+
disable?: boolean
|
|
40
|
+
/** Ligne de séparation affichée au-dessus de l'entrée */
|
|
41
|
+
separator?: boolean
|
|
42
|
+
/** Info-bulle */
|
|
43
|
+
title?: string
|
|
44
|
+
/** Rappel local au clic (en plus de l'événement `action`) */
|
|
45
|
+
onClick?: (action: ChatAction) => void
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<script setup lang="ts">
|
|
50
|
+
import { computed, nextTick, onMounted, ref, useSlots, watch } from "vue"
|
|
51
|
+
import { Icon } from "@iconify/vue"
|
|
52
|
+
import { cn } from "../lib/utils"
|
|
53
|
+
import { icons } from "../lib/icons"
|
|
54
|
+
import { radiusStyle, useRadius } from "../lib/useComponentProps"
|
|
55
|
+
import type { RadiusProp } from "../lib/useComponentProps"
|
|
56
|
+
import QBtn from "./QBtn.vue"
|
|
57
|
+
import QBtnActions, { type BtnAction } from "./QBtnActions.vue"
|
|
58
|
+
|
|
59
|
+
/** Entrée de menu interne : la `BtnAction` porte une référence à son `ChatAction`. */
|
|
60
|
+
interface MenuEntry extends BtnAction {
|
|
61
|
+
__chatAction?: ChatAction
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Valeur sentinelle de l'entrée « joindre un fichier » du menu du « + ». */
|
|
65
|
+
const ATTACH_VALUE = "__dnax-input-chat-attach__"
|
|
66
|
+
|
|
67
|
+
interface Props {
|
|
68
|
+
/** Texte saisi (v-model:message) */
|
|
69
|
+
message?: string
|
|
70
|
+
/** Texte d'invite affiché quand le champ est vide */
|
|
71
|
+
placeholder?: string
|
|
72
|
+
/** Label affiché au-dessus du champ */
|
|
73
|
+
label?: string
|
|
74
|
+
/** Aide affichée sous le champ */
|
|
75
|
+
hint?: string
|
|
76
|
+
/** État d'erreur */
|
|
77
|
+
error?: boolean
|
|
78
|
+
/** Message d'erreur (affiché quand error) */
|
|
79
|
+
errorMessage?: string
|
|
80
|
+
/** Affiche le compteur (nécessite maxlength) */
|
|
81
|
+
counter?: boolean
|
|
82
|
+
/** Longueur maximale du texte */
|
|
83
|
+
maxlength?: number
|
|
84
|
+
/** Hauteur minimale du textarea, en lignes */
|
|
85
|
+
rows?: number
|
|
86
|
+
/** Plafond d'auto-extension, en lignes (0 = pas de plafond) */
|
|
87
|
+
maxRows?: number
|
|
88
|
+
/** Le textarea grandit avec le contenu jusqu'à maxRows, puis défile */
|
|
89
|
+
autogrow?: boolean
|
|
90
|
+
/** Icône Iconify du bouton d'envoi */
|
|
91
|
+
sendIcon?: string
|
|
92
|
+
/** Libellé accessible (aria-label) du bouton d'envoi */
|
|
93
|
+
sendLabel?: string
|
|
94
|
+
/** Couleur du bouton d'envoi : token (primary, secondary…) ou hex */
|
|
95
|
+
sendColor?: string
|
|
96
|
+
/** Entrée envoie le message (Maj+Entrée insère un saut de ligne) */
|
|
97
|
+
sendOnEnter?: boolean
|
|
98
|
+
/** Vide le champ après l'envoi */
|
|
99
|
+
clearOnSend?: boolean
|
|
100
|
+
/** Désactive le bouton d'envoi quand le texte est vide */
|
|
101
|
+
disableSendWhenEmpty?: boolean
|
|
102
|
+
/** Affiche un spinner dans le bouton d'envoi et bloque l'envoi */
|
|
103
|
+
loading?: boolean
|
|
104
|
+
/** Pastilles activables (v-model:options) — la sélection est gérée par le composant */
|
|
105
|
+
options?: ChatOption[]
|
|
106
|
+
/** Entrées du menu du bouton « + » */
|
|
107
|
+
actions?: ChatAction[]
|
|
108
|
+
/** Icône Iconify du bouton « + » */
|
|
109
|
+
actionsIcon?: string
|
|
110
|
+
/** Libellé accessible (aria-label) du bouton « + » */
|
|
111
|
+
actionsLabel?: string
|
|
112
|
+
/** Fichiers joints (v-model:files) */
|
|
113
|
+
files?: File[]
|
|
114
|
+
/** Types de fichiers acceptés (attribut `accept` du sélecteur) */
|
|
115
|
+
accept?: string
|
|
116
|
+
/** Autorise plusieurs fichiers à la fois */
|
|
117
|
+
multiple?: boolean
|
|
118
|
+
/**
|
|
119
|
+
* Entrée « joindre un fichier » dans le menu du « + » : `false` la masque, une chaîne
|
|
120
|
+
* remplace son libellé. Défaut effectif : activée si `accept` est fourni, sinon masquée.
|
|
121
|
+
*/
|
|
122
|
+
attach?: boolean | string
|
|
123
|
+
/** Affiche les fichiers choisis en pastilles retirables au-dessus du textarea */
|
|
124
|
+
showFiles?: boolean
|
|
125
|
+
/** Padding du contrôle (valeur CSS) — pour l'aspect aéré des maquettes */
|
|
126
|
+
padding?: string
|
|
127
|
+
/** Padding horizontal (gauche/droite) de la barre d'options (valeur CSS) */
|
|
128
|
+
paddingOptions?: string
|
|
129
|
+
/** Bordure visible */
|
|
130
|
+
outlined?: boolean
|
|
131
|
+
/** Fond gris, soulignement */
|
|
132
|
+
filled?: boolean
|
|
133
|
+
/** Aucune bordure */
|
|
134
|
+
borderless?: boolean
|
|
135
|
+
/** Hauteur réduite */
|
|
136
|
+
dense?: boolean
|
|
137
|
+
/** Coins arrondis : true = pilule, ou échelle xs|sm|md|lg (none = carré) */
|
|
138
|
+
radius?: RadiusProp
|
|
139
|
+
/** Force l'habillage sombre (sinon hérite de l'ancêtre .dark) */
|
|
140
|
+
dark?: boolean
|
|
141
|
+
/** Désactive le champ et le bouton d'envoi */
|
|
142
|
+
disable?: boolean
|
|
143
|
+
/** Champ en lecture seule (le bouton d'envoi est désactivé) */
|
|
144
|
+
readonly?: boolean
|
|
145
|
+
/** Applique la safe-area basse (composer plaqué au bas de l'écran) */
|
|
146
|
+
safeArea?: boolean
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
150
|
+
error: false,
|
|
151
|
+
counter: false,
|
|
152
|
+
rows: 1,
|
|
153
|
+
maxRows: 6,
|
|
154
|
+
autogrow: true,
|
|
155
|
+
sendIcon: "lucide:send",
|
|
156
|
+
sendLabel: "Send",
|
|
157
|
+
sendColor: "primary",
|
|
158
|
+
sendOnEnter: true,
|
|
159
|
+
clearOnSend: true,
|
|
160
|
+
disableSendWhenEmpty: true,
|
|
161
|
+
loading: false,
|
|
162
|
+
options: () => [],
|
|
163
|
+
actions: () => [],
|
|
164
|
+
actionsIcon: "lucide:plus",
|
|
165
|
+
actionsLabel: "More actions",
|
|
166
|
+
files: () => [],
|
|
167
|
+
multiple: true,
|
|
168
|
+
showFiles: true,
|
|
169
|
+
outlined: false,
|
|
170
|
+
filled: false,
|
|
171
|
+
borderless: false,
|
|
172
|
+
dense: false,
|
|
173
|
+
dark: false,
|
|
174
|
+
disable: false,
|
|
175
|
+
readonly: false,
|
|
176
|
+
safeArea: true,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
const emit = defineEmits<{
|
|
180
|
+
"update:message": [value: string]
|
|
181
|
+
/** Pastilles mises à jour — **nouveau tableau** (le composant bascule `active`) */
|
|
182
|
+
"update:options": [options: ChatOption[]]
|
|
183
|
+
/** Fichiers joints mis à jour — nouveau tableau */
|
|
184
|
+
"update:files": [files: File[]]
|
|
185
|
+
/** Option basculée (en plus de `update:options`) */
|
|
186
|
+
option: [{ option: ChatOption; active: boolean }]
|
|
187
|
+
send: [value: string]
|
|
188
|
+
clear: []
|
|
189
|
+
/** Entrée du menu du « + » choisie */
|
|
190
|
+
action: [{ action: ChatAction; value: any }]
|
|
191
|
+
focus: [event: FocusEvent]
|
|
192
|
+
blur: [event: FocusEvent]
|
|
193
|
+
}>()
|
|
194
|
+
|
|
195
|
+
const slots = useSlots()
|
|
196
|
+
|
|
197
|
+
const nativeEl = ref<HTMLTextAreaElement | null>(null)
|
|
198
|
+
const fileInput = ref<HTMLInputElement | null>(null)
|
|
199
|
+
// Composition IME en cours (clavier chinois/japonais…) : Entrée ne doit PAS envoyer
|
|
200
|
+
// tant que la composition n'est pas terminée.
|
|
201
|
+
const composing = ref(false)
|
|
202
|
+
|
|
203
|
+
const value = computed(() => props.message ?? "")
|
|
204
|
+
|
|
205
|
+
// — Sélection des options : état interne synchronisé sur la prop (`watch`) — le
|
|
206
|
+
// composant fonctionne avec OU sans `v-model:options`.
|
|
207
|
+
const localOptions = ref<ChatOption[]>([...props.options])
|
|
208
|
+
watch(
|
|
209
|
+
() => props.options,
|
|
210
|
+
(next) => {
|
|
211
|
+
localOptions.value = [...next]
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
// — Fichiers joints : même principe (état interne synchronisé sur la prop).
|
|
216
|
+
const localFiles = ref<File[]>([...props.files])
|
|
217
|
+
watch(
|
|
218
|
+
() => props.files,
|
|
219
|
+
(next) => {
|
|
220
|
+
localFiles.value = [...next]
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
// radius : prop explicite > composantProps.QInputChat.radius ; échelle → --q-radius
|
|
225
|
+
const effectiveRadius = useRadius("QInputChat", () => props.radius)
|
|
226
|
+
const roundedStyle = computed(() => radiusStyle(effectiveRadius.value))
|
|
227
|
+
|
|
228
|
+
const fieldClasses = computed(() =>
|
|
229
|
+
cn(
|
|
230
|
+
"q-input-chat",
|
|
231
|
+
props.outlined && "q-field--outlined",
|
|
232
|
+
props.filled && "q-field--filled",
|
|
233
|
+
props.borderless && "q-field--borderless",
|
|
234
|
+
effectiveRadius.value === true && "q-field--rounded",
|
|
235
|
+
props.dense && "q-field--dense",
|
|
236
|
+
props.error && "q-field--error",
|
|
237
|
+
props.disable && "q-field--disabled",
|
|
238
|
+
props.dark && "q-input-chat--dark",
|
|
239
|
+
props.safeArea && "q-input-chat--safe-area",
|
|
240
|
+
// `padding` fourni → la hauteur du champ est réglée par le contrôle (plein
|
|
241
|
+
// composeur) : le textarea garde la hauteur de champ standard.
|
|
242
|
+
props.padding && "q-input-chat--custom-padding",
|
|
243
|
+
),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
// `padding` : valeur CSS posée en variable, lue par la règle du contrôle.
|
|
247
|
+
const controlStyle = computed<Record<string, string> | undefined>(() =>
|
|
248
|
+
props.padding ? { "--q-input-chat-padding": props.padding } : undefined,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
// `padding-options` : valeur CSS posée en variable, lue en padding horizontal de la barre.
|
|
252
|
+
const optionsStyle = computed<Record<string, string> | undefined>(() =>
|
|
253
|
+
props.paddingOptions ? { "--q-input-chat-options-padding": props.paddingOptions } : undefined,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
// — Pièce jointe : activée si `accept` fourni (ou `attach` explicite) —
|
|
257
|
+
const attachEnabled = computed(() =>
|
|
258
|
+
props.attach === undefined ? !!props.accept : props.attach !== false,
|
|
259
|
+
)
|
|
260
|
+
const attachLabel = computed(() => (typeof props.attach === "string" ? props.attach : "Add file"))
|
|
261
|
+
|
|
262
|
+
// — Menu du « + » : entrée attach (si activée) + actions mappées en `BtnAction` —
|
|
263
|
+
const menuActions = computed<MenuEntry[]>(() => {
|
|
264
|
+
const list: MenuEntry[] = []
|
|
265
|
+
if (attachEnabled.value) {
|
|
266
|
+
list.push({ label: attachLabel.value, icon: "lucide:paperclip", value: ATTACH_VALUE })
|
|
267
|
+
}
|
|
268
|
+
for (const action of props.actions) list.push({ ...action, __chatAction: action })
|
|
269
|
+
return list
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
const showToolbar = computed(
|
|
273
|
+
() =>
|
|
274
|
+
localOptions.value.length > 0 ||
|
|
275
|
+
!!slots.options ||
|
|
276
|
+
props.actions.length > 0 ||
|
|
277
|
+
!!slots.actions ||
|
|
278
|
+
!!slots.tools ||
|
|
279
|
+
attachEnabled.value,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
// Séparateur « | » entre les pastilles d'options et le bouton « + » (rendu seulement
|
|
283
|
+
// quand les deux zones sont présentes, sinon la barre ne montrerait qu'un trait isolé).
|
|
284
|
+
const showOptionsSeparator = computed(
|
|
285
|
+
() =>
|
|
286
|
+
(localOptions.value.length > 0 || !!slots.options) &&
|
|
287
|
+
(menuActions.value.length > 0 || !!slots.actions),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
// — Sélection d'une option : bascule dans l'état interne, émet le NOUVEAU tableau —
|
|
291
|
+
const toggleOption = (option: ChatOption, index: number) => {
|
|
292
|
+
if (option.disable) return
|
|
293
|
+
const same = (o: ChatOption, i: number) =>
|
|
294
|
+
option._id !== undefined ? o._id === option._id : i === index
|
|
295
|
+
const next = localOptions.value.map((o, i) => (same(o, i) ? { ...o, active: !o.active } : o))
|
|
296
|
+
localOptions.value = next
|
|
297
|
+
const changed = next.find((o, i) => same(o, i))
|
|
298
|
+
emit("update:options", next)
|
|
299
|
+
if (changed) emit("option", { option: changed, active: !!changed.active })
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// — Pièce jointe : sélecteur de fichiers —
|
|
303
|
+
const openFilePicker = () => fileInput.value?.click()
|
|
304
|
+
|
|
305
|
+
const fileKey = (f: File) => `${f.name}:${f.size}:${f.lastModified}`
|
|
306
|
+
|
|
307
|
+
const onFilesChange = (e: Event) => {
|
|
308
|
+
const input = e.target as HTMLInputElement
|
|
309
|
+
const picked = Array.from(input.files ?? [])
|
|
310
|
+
// Remet à zéro pour pouvoir re-choisir le même fichier.
|
|
311
|
+
input.value = ""
|
|
312
|
+
if (!picked.length) return
|
|
313
|
+
const seen = new Set(localFiles.value.map(fileKey))
|
|
314
|
+
const next = [...localFiles.value]
|
|
315
|
+
for (const f of picked) {
|
|
316
|
+
const key = fileKey(f)
|
|
317
|
+
if (seen.has(key)) continue
|
|
318
|
+
seen.add(key)
|
|
319
|
+
next.push(f)
|
|
320
|
+
}
|
|
321
|
+
localFiles.value = next
|
|
322
|
+
emit("update:files", next)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const removeFile = (index: number) => {
|
|
326
|
+
const next = localFiles.value.filter((_, i) => i !== index)
|
|
327
|
+
localFiles.value = next
|
|
328
|
+
emit("update:files", next)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const formatSize = (bytes: number): string => {
|
|
332
|
+
if (bytes < 1024) return `${bytes} B`
|
|
333
|
+
if (bytes < 1024 * 1024) {
|
|
334
|
+
const ko = bytes / 1024
|
|
335
|
+
return `${ko < 10 ? ko.toFixed(1) : Math.round(ko)} Ko`
|
|
336
|
+
}
|
|
337
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} Mo`
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// — Entrée du menu du « + » choisie —
|
|
341
|
+
const onSelectAction = (value: unknown) => {
|
|
342
|
+
if (value === ATTACH_VALUE) {
|
|
343
|
+
openFilePicker()
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
// QBtnActions renvoie `value` de l'entrée, sinon l'entrée elle-même (qui porte la ref).
|
|
347
|
+
const entry: MenuEntry | undefined =
|
|
348
|
+
value && typeof value === "object" && "__chatAction" in value
|
|
349
|
+
? (value as MenuEntry)
|
|
350
|
+
: menuActions.value.find((e) => e.value !== undefined && e.value === value)
|
|
351
|
+
const action = entry?.__chatAction
|
|
352
|
+
if (!action) return
|
|
353
|
+
action.onClick?.(action)
|
|
354
|
+
emit("action", { action, value: action.value ?? action })
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// — Auto-extension : mesure scrollHeight (hauteur remise à `auto` avant) plafonnée à
|
|
358
|
+
// `maxRows` lignes (line-height réelle mesurée), puis overflow-y auto. Même mécanique
|
|
359
|
+
// que `resizeTextarea` de QInput.vue.
|
|
360
|
+
const resizeTextarea = async () => {
|
|
361
|
+
const el = nativeEl.value
|
|
362
|
+
if (!el || !props.autogrow) return
|
|
363
|
+
await nextTick()
|
|
364
|
+
el.style.height = "auto"
|
|
365
|
+
const cs = getComputedStyle(el)
|
|
366
|
+
const fontSize = Number.parseFloat(cs.fontSize) || 14
|
|
367
|
+
const lineHeight = Number.parseFloat(cs.lineHeight) || fontSize * 1.4
|
|
368
|
+
const padding = (Number.parseFloat(cs.paddingTop) || 0) + (Number.parseFloat(cs.paddingBottom) || 0)
|
|
369
|
+
const cap = props.maxRows > 0 ? lineHeight * props.maxRows + padding : Number.POSITIVE_INFINITY
|
|
370
|
+
const full = el.scrollHeight
|
|
371
|
+
el.style.height = `${Math.min(full, cap)}px`
|
|
372
|
+
el.style.overflowY = full > cap ? "auto" : "hidden"
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
watch([value, () => props.autogrow, () => props.maxRows], resizeTextarea)
|
|
376
|
+
onMounted(resizeTextarea)
|
|
377
|
+
|
|
378
|
+
// — Envoi —
|
|
379
|
+
const canSend = computed(() => !props.disable && !props.readonly && !props.loading)
|
|
380
|
+
const sendDisabled = computed(
|
|
381
|
+
() => !canSend.value || (props.disableSendWhenEmpty && value.value.trim() === ""),
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
const focus = () => nativeEl.value?.focus()
|
|
385
|
+
const blur = () => nativeEl.value?.blur()
|
|
386
|
+
|
|
387
|
+
const send = () => {
|
|
388
|
+
if (!canSend.value) return
|
|
389
|
+
// Valeur vide (après trim) : rien n'est émis. Le texte émis est **trimé**.
|
|
390
|
+
const text = value.value.trim()
|
|
391
|
+
if (!text) return
|
|
392
|
+
emit("send", text)
|
|
393
|
+
if (props.clearOnSend) {
|
|
394
|
+
emit("update:message", "")
|
|
395
|
+
emit("clear")
|
|
396
|
+
}
|
|
397
|
+
// Textarea remis à sa hauteur de base (contenu vidé) + focus rendu à la saisie.
|
|
398
|
+
nextTick(() => {
|
|
399
|
+
resizeTextarea()
|
|
400
|
+
focus()
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const clear = () => {
|
|
405
|
+
emit("update:message", "")
|
|
406
|
+
emit("clear")
|
|
407
|
+
nextTick(() => {
|
|
408
|
+
resizeTextarea()
|
|
409
|
+
focus()
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// — Saisie & clavier —
|
|
414
|
+
const onInput = (e: Event) => {
|
|
415
|
+
emit("update:message", (e.target as HTMLTextAreaElement).value)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const onKeydown = (e: KeyboardEvent) => {
|
|
419
|
+
if (e.key !== "Enter" || !props.sendOnEnter) return
|
|
420
|
+
// Maj/Alt/Ctrl/Meta + Entrée → comportement natif (saut de ligne).
|
|
421
|
+
if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) return
|
|
422
|
+
// Jamais d'envoi pendant une composition IME (`isComposing` en complément du flag).
|
|
423
|
+
if (e.isComposing || composing.value) return
|
|
424
|
+
e.preventDefault()
|
|
425
|
+
send()
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const onCompositionStart = () => {
|
|
429
|
+
composing.value = true
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const onCompositionEnd = () => {
|
|
433
|
+
composing.value = false
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const onFocus = (e: FocusEvent) => emit("focus", e)
|
|
437
|
+
|
|
438
|
+
const onBlur = (e: FocusEvent) => {
|
|
439
|
+
composing.value = false
|
|
440
|
+
emit("blur", e)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const inputAttrs = computed(() => ({
|
|
444
|
+
maxlength: props.maxlength,
|
|
445
|
+
placeholder: props.placeholder,
|
|
446
|
+
disabled: props.disable || undefined,
|
|
447
|
+
readonly: props.readonly || undefined,
|
|
448
|
+
"aria-label": props.label ?? props.placeholder,
|
|
449
|
+
}))
|
|
450
|
+
|
|
451
|
+
// Forme explicite `clé: valeur` : l'analyse statique des docs détecte les méthodes
|
|
452
|
+
// exposées en lisant les clés de `defineExpose` (cf. `scripts/component-parse.ts`).
|
|
453
|
+
defineExpose({ focus: focus, blur: blur, clear: clear, send: send })
|
|
454
|
+
</script>
|
|
455
|
+
|
|
456
|
+
<template>
|
|
457
|
+
<div class="q-input-chat" :class="fieldClasses" :style="roundedStyle" v-bind="$attrs">
|
|
458
|
+
<label v-if="label" class="q-field__label-stack">{{ label }}</label>
|
|
459
|
+
|
|
460
|
+
<!-- Fichiers joints : pastilles retirables au-dessus du textarea -->
|
|
461
|
+
<div v-if="showFiles && localFiles.length" class="q-input-chat__files">
|
|
462
|
+
<slot name="files" :files="localFiles" :remove="removeFile">
|
|
463
|
+
<span v-for="(f, i) in localFiles" :key="`${f.name}-${i}`" class="q-input-chat__file">
|
|
464
|
+
<Icon :icon="icons.file" class="q-input-chat__file-icon" aria-hidden="true" />
|
|
465
|
+
<span class="q-input-chat__file-name">{{ f.name }}</span>
|
|
466
|
+
<span class="q-input-chat__file-size">{{ formatSize(f.size) }}</span>
|
|
467
|
+
<button
|
|
468
|
+
type="button"
|
|
469
|
+
class="q-input-chat__file-remove"
|
|
470
|
+
:aria-label="`Remove ${f.name}`"
|
|
471
|
+
@click="removeFile(i)"
|
|
472
|
+
>
|
|
473
|
+
<Icon :icon="icons.x" aria-hidden="true" />
|
|
474
|
+
</button>
|
|
475
|
+
</span>
|
|
476
|
+
</slot>
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
<div class="q-field__control q-input-chat__control" :style="controlStyle">
|
|
480
|
+
<slot name="prepend" />
|
|
481
|
+
<div class="q-input-chat__textarea-wrap">
|
|
482
|
+
<textarea
|
|
483
|
+
ref="nativeEl"
|
|
484
|
+
class="q-field__native q-input-chat__native"
|
|
485
|
+
:value="value"
|
|
486
|
+
:rows="rows"
|
|
487
|
+
v-bind="inputAttrs"
|
|
488
|
+
@input="onInput"
|
|
489
|
+
@keydown="onKeydown"
|
|
490
|
+
@compositionstart="onCompositionStart"
|
|
491
|
+
@compositionend="onCompositionEnd"
|
|
492
|
+
@focus="onFocus"
|
|
493
|
+
@blur="onBlur"
|
|
494
|
+
/>
|
|
495
|
+
</div>
|
|
496
|
+
<slot name="append" />
|
|
497
|
+
<!-- Envoyer « inline » quand il n'y a pas de barre (une seule ligne) -->
|
|
498
|
+
<slot v-if="!showToolbar" name="send" :send="send" :disabled="sendDisabled" :loading="loading">
|
|
499
|
+
<q-btn
|
|
500
|
+
class="q-input-chat__send"
|
|
501
|
+
type="button"
|
|
502
|
+
round
|
|
503
|
+
:icon="sendIcon"
|
|
504
|
+
:color="sendColor"
|
|
505
|
+
:loading="loading"
|
|
506
|
+
:disable="sendDisabled"
|
|
507
|
+
:aria-label="sendLabel"
|
|
508
|
+
@click="send"
|
|
509
|
+
/>
|
|
510
|
+
</slot>
|
|
511
|
+
</div>
|
|
512
|
+
|
|
513
|
+
<div v-if="showToolbar" class="q-input-chat__toolbar" :style="optionsStyle">
|
|
514
|
+
<div class="q-input-chat__options">
|
|
515
|
+
<slot name="options" :options="localOptions" :toggle="toggleOption">
|
|
516
|
+
<q-btn
|
|
517
|
+
v-for="(o, i) in localOptions"
|
|
518
|
+
:key="o._id ?? i"
|
|
519
|
+
class="q-input-chat__option"
|
|
520
|
+
:class="{ 'q-input-chat__option--active': o.active }"
|
|
521
|
+
outline
|
|
522
|
+
dense
|
|
523
|
+
no-caps
|
|
524
|
+
radius
|
|
525
|
+
:icon="o.icon"
|
|
526
|
+
:label="o.label"
|
|
527
|
+
:disable="o.disable"
|
|
528
|
+
@click="toggleOption(o, i)"
|
|
529
|
+
/>
|
|
530
|
+
</slot>
|
|
531
|
+
</div>
|
|
532
|
+
|
|
533
|
+
<span v-if="showOptionsSeparator" class="q-input-chat__toolbar-separator" aria-hidden="true" />
|
|
534
|
+
|
|
535
|
+
<div v-if="$slots.actions || menuActions.length" class="q-input-chat__actions">
|
|
536
|
+
<slot name="actions">
|
|
537
|
+
<q-btn-actions
|
|
538
|
+
class="q-input-chat__add"
|
|
539
|
+
flat
|
|
540
|
+
dense
|
|
541
|
+
round
|
|
542
|
+
no-caps
|
|
543
|
+
:icon="actionsIcon"
|
|
544
|
+
:aria-label="actionsLabel"
|
|
545
|
+
:actions="menuActions"
|
|
546
|
+
dropdown-icon="lucide:chevron-down"
|
|
547
|
+
@select-action="onSelectAction"
|
|
548
|
+
/>
|
|
549
|
+
</slot>
|
|
550
|
+
</div>
|
|
551
|
+
|
|
552
|
+
<div class="q-input-chat__spacer" />
|
|
553
|
+
<div class="q-input-chat__tools">
|
|
554
|
+
<slot name="tools" />
|
|
555
|
+
<slot name="send" :send="send" :disabled="sendDisabled" :loading="loading">
|
|
556
|
+
<q-btn
|
|
557
|
+
class="q-input-chat__send"
|
|
558
|
+
type="button"
|
|
559
|
+
round
|
|
560
|
+
:icon="sendIcon"
|
|
561
|
+
:color="sendColor"
|
|
562
|
+
:loading="loading"
|
|
563
|
+
:disable="sendDisabled"
|
|
564
|
+
:aria-label="sendLabel"
|
|
565
|
+
@click="send"
|
|
566
|
+
/>
|
|
567
|
+
</slot>
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
<input
|
|
572
|
+
ref="fileInput"
|
|
573
|
+
type="file"
|
|
574
|
+
class="q-input-chat__file-input"
|
|
575
|
+
:accept="accept"
|
|
576
|
+
:multiple="multiple"
|
|
577
|
+
hidden
|
|
578
|
+
@change="onFilesChange"
|
|
579
|
+
/>
|
|
580
|
+
|
|
581
|
+
<div class="q-field__bottom">
|
|
582
|
+
<div v-if="error" class="q-field__error">
|
|
583
|
+
<slot name="error">{{ errorMessage }}</slot>
|
|
584
|
+
</div>
|
|
585
|
+
<div v-else-if="hint || $slots.hint" class="q-field__hint">
|
|
586
|
+
<slot name="hint">{{ hint }}</slot>
|
|
587
|
+
</div>
|
|
588
|
+
<div v-if="counter && maxlength" class="q-field__counter">
|
|
589
|
+
{{ value.length }}/{{ maxlength }}
|
|
590
|
+
</div>
|
|
591
|
+
</div>
|
|
592
|
+
</div>
|
|
593
|
+
</template>
|
|
594
|
+
|
|
595
|
+
<style scoped>
|
|
596
|
+
/* Racine = colonne, comme `.q-input` (les règles globales de champ sont scopées à
|
|
597
|
+
`.q-input`, jamais à cette racine). */
|
|
598
|
+
.q-input-chat {
|
|
599
|
+
display: flex;
|
|
600
|
+
flex-direction: column;
|
|
601
|
+
min-width: 0;
|
|
602
|
+
font-size: 14px;
|
|
603
|
+
|
|
604
|
+
/* Hauteur d'une ligne du textarea. Un composer de chat s'assied un peu plus haut
|
|
605
|
+
qu'un champ simple (40px) ; surchargée par les modifiers ci-dessous. */
|
|
606
|
+
--q-input-chat-field-height: 44px;
|
|
607
|
+
--q-input-chat-field-padding: 12px;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/* `padding` custom (plein composeur) : le contrôle porte la hauteur, on rend au
|
|
611
|
+
textarea la hauteur de champ standard pour ne pas décaler la maquette. */
|
|
612
|
+
.q-input-chat.q-input-chat--custom-padding {
|
|
613
|
+
--q-input-chat-field-height: 38px;
|
|
614
|
+
--q-input-chat-field-padding: 9px;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/* Variante dense : une ligne resserrée (accorde le contrôle à 32px). */
|
|
618
|
+
.q-input-chat.q-field--dense {
|
|
619
|
+
--q-input-chat-field-height: 32px;
|
|
620
|
+
--q-input-chat-field-padding: 6px;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/* Padding du contrôle : valeur par défaut, surchargée par la prop `padding`
|
|
624
|
+
(`--q-input-chat-padding`). `align-items: center` centre `#prepend`/`#append` dans
|
|
625
|
+
la ligne (le textarea la remplit, l'envoi inline s'ancre en haut via `align-self`).
|
|
626
|
+
Spécificité montée (2 classes) pour battre `.q-field__control`. */
|
|
627
|
+
.q-field__control.q-input-chat__control {
|
|
628
|
+
align-items: center;
|
|
629
|
+
padding: var(--q-input-chat-padding, 0 12px);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/* Enveloppe du textarea : prend la largeur restante (prépare les piliers gauche/droite). */
|
|
633
|
+
.q-input-chat__textarea-wrap {
|
|
634
|
+
display: flex;
|
|
635
|
+
flex: 1;
|
|
636
|
+
min-width: 0;
|
|
637
|
+
align-items: flex-end;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/* Champ natif — recopie du nécessaire de `.q-input .q-field__native`. */
|
|
641
|
+
.q-input-chat__native {
|
|
642
|
+
flex: 1;
|
|
643
|
+
min-width: 0;
|
|
644
|
+
height: auto;
|
|
645
|
+
min-height: var(--q-input-chat-field-height);
|
|
646
|
+
padding: var(--q-input-chat-field-padding) 0;
|
|
647
|
+
border: none;
|
|
648
|
+
outline: none;
|
|
649
|
+
background: transparent;
|
|
650
|
+
color: var(--foreground);
|
|
651
|
+
font: inherit;
|
|
652
|
+
line-height: 20px;
|
|
653
|
+
resize: none;
|
|
654
|
+
overflow-y: auto;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.q-input-chat__native::placeholder {
|
|
658
|
+
color: var(--muted-foreground);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/* Envoi : 30px dans les deux cas (même échelle que les pastilles d'options et les
|
|
662
|
+
outils de la barre), sinon le `round` de 36px par défaut écrase la rangée. Sélecteurs
|
|
663
|
+
à 2 classes pour battre `--q-btn-h: 36px` / `font-size: 14px` déclarés sur l'élément
|
|
664
|
+
par `.q-btn` / `.q-btn--md`. */
|
|
665
|
+
.q-input-chat__control .q-input-chat__send,
|
|
666
|
+
.q-input-chat__tools .q-input-chat__send {
|
|
667
|
+
flex: none;
|
|
668
|
+
--q-btn-h: 30px;
|
|
669
|
+
font-size: 12px;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/* Envoi « inline » (pas de barre) : ancré **en haut** — le `margin-top` vaut la
|
|
673
|
+
demi-différence avec la ligne (hauteur de champ − bouton 30px − 2px de bordures),
|
|
674
|
+
donc centré sur la première ligne et **en haut à droite** quand le textarea grandit
|
|
675
|
+
(autogrow). Dans la barre, le bouton est centré par `.q-input-chat__tools`
|
|
676
|
+
(`align-items: center`), sans marge. */
|
|
677
|
+
.q-input-chat__control .q-input-chat__send {
|
|
678
|
+
align-self: flex-start;
|
|
679
|
+
margin-top: calc((var(--q-input-chat-field-height) - 32px) / 2);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/* ===== Fichiers joints ===== */
|
|
683
|
+
.q-input-chat__files {
|
|
684
|
+
display: flex;
|
|
685
|
+
flex-wrap: wrap;
|
|
686
|
+
gap: 6px;
|
|
687
|
+
padding: 0 12px 6px;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.q-input-chat__file {
|
|
691
|
+
display: inline-flex;
|
|
692
|
+
align-items: center;
|
|
693
|
+
gap: 6px;
|
|
694
|
+
max-width: 100%;
|
|
695
|
+
padding: 4px 6px 4px 8px;
|
|
696
|
+
border: 1px solid var(--border);
|
|
697
|
+
border-radius: 999px;
|
|
698
|
+
background-color: color-mix(in srgb, var(--foreground) 4%, transparent);
|
|
699
|
+
font-size: 12px;
|
|
700
|
+
color: var(--muted-foreground);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.q-input-chat__file-icon {
|
|
704
|
+
width: 14px;
|
|
705
|
+
height: 14px;
|
|
706
|
+
flex-shrink: 0;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
.q-input-chat__file-name {
|
|
710
|
+
max-width: 220px;
|
|
711
|
+
overflow: hidden;
|
|
712
|
+
white-space: nowrap;
|
|
713
|
+
text-overflow: ellipsis;
|
|
714
|
+
color: var(--foreground);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.q-input-chat__file-size {
|
|
718
|
+
flex-shrink: 0;
|
|
719
|
+
opacity: 0.8;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.q-input-chat__file-remove {
|
|
723
|
+
display: inline-flex;
|
|
724
|
+
align-items: center;
|
|
725
|
+
justify-content: center;
|
|
726
|
+
width: 16px;
|
|
727
|
+
height: 16px;
|
|
728
|
+
padding: 0;
|
|
729
|
+
border: none;
|
|
730
|
+
border-radius: 50%;
|
|
731
|
+
background: transparent;
|
|
732
|
+
color: inherit;
|
|
733
|
+
cursor: pointer;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.q-input-chat__file-remove:hover {
|
|
737
|
+
background-color: color-mix(in srgb, var(--foreground) 10%, transparent);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
.q-input-chat__file-remove svg {
|
|
741
|
+
width: 12px;
|
|
742
|
+
height: 12px;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/* ===== Barre (options + « + » à gauche, outils + envoi à droite) ===== */
|
|
746
|
+
.q-input-chat__toolbar {
|
|
747
|
+
display: flex;
|
|
748
|
+
flex-wrap: wrap;
|
|
749
|
+
align-items: center;
|
|
750
|
+
gap: 0;
|
|
751
|
+
padding-top: 6px;
|
|
752
|
+
padding-bottom: 10px;
|
|
753
|
+
/* Padding horizontal (gauche/droite) piloté par la prop `padding-options`. */
|
|
754
|
+
padding-inline: var(--q-input-chat-options-padding, 0);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.q-input-chat__options,
|
|
758
|
+
.q-input-chat__actions,
|
|
759
|
+
.q-input-chat__tools {
|
|
760
|
+
display: flex;
|
|
761
|
+
flex-wrap: wrap;
|
|
762
|
+
align-items: center;
|
|
763
|
+
gap: 6px;
|
|
764
|
+
min-width: 0;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/* Séparateur « | » entre les pastilles et le bouton « + ». */
|
|
768
|
+
.q-input-chat__toolbar-separator {
|
|
769
|
+
align-self: center;
|
|
770
|
+
width: 1px;
|
|
771
|
+
height: 16px;
|
|
772
|
+
margin: 0 8px;
|
|
773
|
+
background-color: var(--border);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.q-input-chat__spacer {
|
|
777
|
+
flex: 1 1 auto;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.q-input-chat__tools {
|
|
781
|
+
margin-left: auto;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/* Pastille d'option : bordure 1px, rayon pilule, libellé ~13px. */
|
|
785
|
+
.q-input-chat__option {
|
|
786
|
+
--q-btn-bg: var(--muted-foreground);
|
|
787
|
+
--q-btn-h: 30px;
|
|
788
|
+
padding: 0 10px;
|
|
789
|
+
gap: 6px;
|
|
790
|
+
border-color: var(--border);
|
|
791
|
+
color: var(--muted-foreground);
|
|
792
|
+
font-size: 13px;
|
|
793
|
+
font-weight: 500;
|
|
794
|
+
text-transform: none;
|
|
795
|
+
letter-spacing: 0;
|
|
796
|
+
box-shadow: none;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.q-input-chat__option:hover {
|
|
800
|
+
border-color: var(--muted-foreground);
|
|
801
|
+
background-color: color-mix(in srgb, var(--foreground) 6%, transparent);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/* Option active : bordure + texte primaires, léger fond primaire. */
|
|
805
|
+
.q-input-chat__option.q-input-chat__option--active {
|
|
806
|
+
--q-btn-bg: var(--primary);
|
|
807
|
+
border-color: var(--primary);
|
|
808
|
+
color: var(--primary);
|
|
809
|
+
background-color: color-mix(in srgb, var(--primary) 12%, transparent);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
.q-input-chat__option.q-input-chat__option--active:hover {
|
|
813
|
+
background-color: color-mix(in srgb, var(--primary) 18%, transparent);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/* Bouton « + » (trigger interne de QBtnActions) : icône grise, rond. */
|
|
817
|
+
.q-input-chat__add :deep(.q-btn-actions__trigger) {
|
|
818
|
+
--q-btn-bg: var(--muted-foreground);
|
|
819
|
+
--q-btn-h: 30px;
|
|
820
|
+
color: var(--muted-foreground);
|
|
821
|
+
box-shadow: none;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/* ===== Mode sombre ===== */
|
|
825
|
+
/* Champ : ancêtre `.dark` (héritage) ou prop `dark` (classe `--dark`). */
|
|
826
|
+
.dark .q-input-chat__native,
|
|
827
|
+
.q-input-chat--dark .q-input-chat__native {
|
|
828
|
+
color: var(--foreground);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
.dark .q-input-chat__native::placeholder,
|
|
832
|
+
.q-input-chat--dark .q-input-chat__native::placeholder {
|
|
833
|
+
color: var(--muted-foreground);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
.dark .q-input-chat__option:hover,
|
|
837
|
+
.q-input-chat--dark .q-input-chat__option:hover {
|
|
838
|
+
background-color: rgb(255 255 255 / 0.06);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.dark .q-input-chat__add :deep(.q-btn-actions__trigger:hover),
|
|
842
|
+
.q-input-chat--dark .q-input-chat__add :deep(.q-btn-actions__trigger:hover) {
|
|
843
|
+
background-color: rgb(255 255 255 / 0.08);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
.dark .q-input-chat__file,
|
|
847
|
+
.q-input-chat--dark .q-input-chat__file {
|
|
848
|
+
border-color: var(--border);
|
|
849
|
+
background-color: rgb(255 255 255 / 0.04);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/* Safe-area basse (règle projet obligatoire) : un composer de chat est plaqué au bas
|
|
853
|
+
de l'écran. Chaîne de fallback en 3 niveaux, dans cet ordre. */
|
|
854
|
+
.q-input-chat--safe-area {
|
|
855
|
+
padding-bottom: 0; /* fallback vieux navigateurs */
|
|
856
|
+
padding-bottom: constant(safe-area-inset-bottom); /* iOS 11.0 – 11.2 */
|
|
857
|
+
padding-bottom: env(safe-area-inset-bottom); /* iOS 11.2+ */
|
|
858
|
+
}
|
|
859
|
+
</style>
|