@dnax/ui 0.0.35 → 0.0.36
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/QBtnActions.vue +7 -1
- package/components/QBtnDropdown.vue +1 -0
- package/index.ts +2 -0
- package/lib/ripple.ts +322 -0
- package/module.ts +8 -2
- package/package.json +1 -1
- package/scripts/generate-exports.ts +2 -0
- package/styles/main.css +60 -0
|
@@ -85,6 +85,7 @@ interface Props {
|
|
|
85
85
|
round?: boolean
|
|
86
86
|
square?: boolean
|
|
87
87
|
noCaps?: boolean
|
|
88
|
+
/** Pleine largeur : la racine `.q-btn-actions` s'étend sur 100 % (le déclencheur suit) */
|
|
88
89
|
stretch?: boolean
|
|
89
90
|
loading?: boolean
|
|
90
91
|
disable?: boolean
|
|
@@ -311,7 +312,12 @@ onBeforeUnmount(() => {
|
|
|
311
312
|
</script>
|
|
312
313
|
|
|
313
314
|
<template>
|
|
314
|
-
<div
|
|
315
|
+
<div
|
|
316
|
+
ref="rootEl"
|
|
317
|
+
class="q-btn-actions"
|
|
318
|
+
:class="[dark && 'dark', stretch && 'q-btn-actions--stretch']"
|
|
319
|
+
@keydown="onKeydown"
|
|
320
|
+
>
|
|
315
321
|
<QBtn
|
|
316
322
|
class="q-btn-actions__trigger"
|
|
317
323
|
:class="triggerClasses"
|
package/index.ts
CHANGED
|
@@ -166,6 +166,8 @@ export {
|
|
|
166
166
|
} from "./lib/overlayBack"
|
|
167
167
|
export type { OverlayHandle } from "./lib/overlayBack"
|
|
168
168
|
export { markOverlayClose, closeParentOverlay, vClose } from "./lib/closeOverlay"
|
|
169
|
+
export { vRipple } from "./lib/ripple"
|
|
170
|
+
export type { RippleOptions, RippleValue } from "./lib/ripple"
|
|
169
171
|
export { vTouchPan } from "./lib/touchPan"
|
|
170
172
|
export type { PanDetails, TouchPanHandler, PanDirection } from "./lib/touchPan"
|
|
171
173
|
export { vTouchHold } from "./lib/touchHold"
|
package/lib/ripple.ts
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// v-ripple — directive : onde « material ripple » au clic et au clavier,
|
|
2
|
+
// équivalent Quasar https://quasar.dev/vue-directives/material-ripple
|
|
3
|
+
//
|
|
4
|
+
// Usage :
|
|
5
|
+
// <div v-ripple /> → onde depuis le point cliqué
|
|
6
|
+
// <div v-ripple.center /> → onde depuis le centre
|
|
7
|
+
// <div v-ripple:primary /> → couleur (token ou couleur CSS)
|
|
8
|
+
// <div v-ripple="{ early: true, stop: true }" /> → déclenchement dès pointerdown
|
|
9
|
+
// <div v-ripple="false" /> → directive désactivée
|
|
10
|
+
// <div v-ripple="{ keyCodes: [13, 32], color: '#f00' }" />
|
|
11
|
+
// modifiers : center | early | stop
|
|
12
|
+
//
|
|
13
|
+
// Le conteneur `.q-ripple` (position absolute, 100 %×100 %, overflow hidden) est
|
|
14
|
+
// injecté dans l'élément : pas besoin de lui imposer `overflow: hidden`. En
|
|
15
|
+
// revanche il faut un bloc conteneur → si `position` calculée vaut `static`,
|
|
16
|
+
// la directive pose `position: relative` (et restaure la valeur au démontage).
|
|
17
|
+
|
|
18
|
+
import { colorValue } from "./colors"
|
|
19
|
+
|
|
20
|
+
/** Options de `v-ripple` (les modifiers `.center` / `.early` / `.stop` ont le même effet) */
|
|
21
|
+
export interface RippleOptions {
|
|
22
|
+
/** Déclenche l'onde dès `pointerdown` (au lieu du `click`), annulée si le geste devient un scroll/pan */
|
|
23
|
+
early?: boolean
|
|
24
|
+
/** Stoppe la propagation de l'événement déclencheur */
|
|
25
|
+
stop?: boolean
|
|
26
|
+
/** L'onde démarre au centre de l'élément au lieu du point d'interaction */
|
|
27
|
+
center?: boolean
|
|
28
|
+
/** Couleur de l'onde : token dnax.ui (`primary`, `negative`…) ou couleur CSS (`#f00`, `rgb()`, nom CSS) */
|
|
29
|
+
color?: string
|
|
30
|
+
/** Codes clavier déclenchant l'onde (défaut `[13, 32]` — Entrée, Espace) */
|
|
31
|
+
keyCodes?: number | number[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** `false` désactive la directive ; un objet configure les options */
|
|
35
|
+
export type RippleValue = boolean | RippleOptions | undefined
|
|
36
|
+
|
|
37
|
+
/** Attente avant peinture (tactile : le temps que le navigateur tranche tap vs scroll) */
|
|
38
|
+
const ENTER_DELAY = 50
|
|
39
|
+
const TOUCH_ENTER_DELAY = 100
|
|
40
|
+
/** Onde pleine puis extinction */
|
|
41
|
+
const HOLD_MS = 250
|
|
42
|
+
const LEAVE_MS = 275
|
|
43
|
+
/** Anti-rebond du déclenchement clavier (répétition de touche) */
|
|
44
|
+
const KEY_THROTTLE = 300
|
|
45
|
+
/** Entrée / Espace */
|
|
46
|
+
const DEFAULT_KEY_CODES = [13, 32]
|
|
47
|
+
|
|
48
|
+
/** Éléments que le navigateur active nativement au clavier (le `click` suffit : pas de doublon) */
|
|
49
|
+
const NATIVE_ACTIVATION = "button, a[href], input[type='button'], input[type='submit'], input[type='reset']"
|
|
50
|
+
|
|
51
|
+
interface RippleHandle {
|
|
52
|
+
/** Identifiant du pointeur tant que le geste peut encore devenir un clic (mode `early`) */
|
|
53
|
+
pointerId: number | null
|
|
54
|
+
/** Retire immédiatement l'onde (démontage) */
|
|
55
|
+
abort: () => void
|
|
56
|
+
/** Éteint l'onde en douceur (geste devenu scroll/pan) */
|
|
57
|
+
cancel: () => void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface RippleContext {
|
|
61
|
+
enabled: boolean
|
|
62
|
+
early: boolean
|
|
63
|
+
stop: boolean
|
|
64
|
+
center: boolean
|
|
65
|
+
color?: string
|
|
66
|
+
keyCodes: number[]
|
|
67
|
+
keyTime: number
|
|
68
|
+
/** 0 = aucun listener, 1 = mode clic, 2 = mode early */
|
|
69
|
+
bound: 0 | 1 | 2
|
|
70
|
+
ripples: RippleHandle[]
|
|
71
|
+
/** Position inline d'origine (null = jamais touchée par la directive) */
|
|
72
|
+
prevPosition: string | null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface RippleInstance {
|
|
76
|
+
ctx: RippleContext
|
|
77
|
+
/** (Re)branche les listeners selon `enabled` / `early` */
|
|
78
|
+
bind: () => void
|
|
79
|
+
/** Débranche tout et restaure les styles touchés */
|
|
80
|
+
dispose: () => void
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface RippleBinding {
|
|
84
|
+
value?: unknown
|
|
85
|
+
arg?: string
|
|
86
|
+
modifiers?: Record<string, boolean>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type QElement = HTMLElement & { __qRipple?: RippleInstance }
|
|
90
|
+
type EventFlags = Event & { qSkipRipple?: boolean; qKeyEvent?: boolean }
|
|
91
|
+
|
|
92
|
+
/** Point d'interaction d'un événement souris / tactile / clavier */
|
|
93
|
+
const eventPoint = (evt: Event): { left: number; top: number } => {
|
|
94
|
+
const e = evt as MouseEvent & { touches?: TouchList }
|
|
95
|
+
const touch = e.touches?.[0]
|
|
96
|
+
if (touch) return { left: touch.clientX, top: touch.clientY }
|
|
97
|
+
if (typeof e.clientX === "number") return { left: e.clientX, top: e.clientY }
|
|
98
|
+
return { left: 0, top: 0 }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const parseKeyCodes = (value: RippleOptions["keyCodes"]): number[] => {
|
|
102
|
+
if (value === undefined) return [...DEFAULT_KEY_CODES]
|
|
103
|
+
const list = (Array.isArray(value) ? value.flat() : [value]).map(Number)
|
|
104
|
+
return list.filter((n) => Number.isFinite(n))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Un élément activable nativement produit déjà un `click` au clavier */
|
|
108
|
+
const hasNativeActivation = (el: HTMLElement): boolean => {
|
|
109
|
+
try {
|
|
110
|
+
return el.matches(NATIVE_ACTIVATION)
|
|
111
|
+
} catch {
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Options effectives = valeur objet + modifiers + argument (couleur) */
|
|
117
|
+
const applyOptions = (ctx: RippleContext, binding: RippleBinding) => {
|
|
118
|
+
const opts: RippleOptions = typeof binding.value === "object" && binding.value !== null
|
|
119
|
+
? (binding.value as RippleOptions)
|
|
120
|
+
: {}
|
|
121
|
+
const m = binding.modifiers ?? {}
|
|
122
|
+
|
|
123
|
+
ctx.early = opts.early === true || m.early === true
|
|
124
|
+
ctx.stop = opts.stop === true || m.stop === true
|
|
125
|
+
ctx.center = opts.center === true || m.center === true
|
|
126
|
+
ctx.color = opts.color ?? binding.arg
|
|
127
|
+
ctx.keyCodes = parseKeyCodes(opts.keyCodes)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Crée et lance l'onde (conteneur `.q-ripple` + pastille `.q-ripple__inner`) */
|
|
131
|
+
const spawnRipple = (el: HTMLElement, ctx: RippleContext, evt: Event, forceCenter: boolean) => {
|
|
132
|
+
if (ctx.stop) evt.stopPropagation()
|
|
133
|
+
|
|
134
|
+
const rect = el.getBoundingClientRect()
|
|
135
|
+
const diameter = Math.hypot(rect.width, rect.height)
|
|
136
|
+
if (!diameter) return
|
|
137
|
+
|
|
138
|
+
const radius = diameter / 2
|
|
139
|
+
const point = eventPoint(evt)
|
|
140
|
+
const centerX = (rect.width - diameter) / 2
|
|
141
|
+
const centerY = (rect.height - diameter) / 2
|
|
142
|
+
const fromCenter = ctx.center || forceCenter
|
|
143
|
+
const x = fromCenter ? centerX : point.left - rect.left - radius
|
|
144
|
+
const y = fromCenter ? centerY : point.top - rect.top - radius
|
|
145
|
+
|
|
146
|
+
const node = document.createElement("span")
|
|
147
|
+
node.className = "q-ripple"
|
|
148
|
+
node.setAttribute("dir", "ltr")
|
|
149
|
+
if (ctx.color) node.style.color = colorValue(ctx.color)
|
|
150
|
+
|
|
151
|
+
const inner = document.createElement("span")
|
|
152
|
+
inner.className = "q-ripple__inner"
|
|
153
|
+
inner.style.width = `${diameter}px`
|
|
154
|
+
inner.style.height = `${diameter}px`
|
|
155
|
+
inner.style.transform = `translate(${x}px, ${y}px) scale(0.2)`
|
|
156
|
+
inner.style.opacity = "0"
|
|
157
|
+
|
|
158
|
+
node.append(inner)
|
|
159
|
+
el.append(node)
|
|
160
|
+
|
|
161
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
162
|
+
// 0 = en attente, 1 = apparition, 2 = extinction
|
|
163
|
+
let phase: 0 | 1 | 2 = 0
|
|
164
|
+
|
|
165
|
+
const remove = () => {
|
|
166
|
+
clearTimeout(timer)
|
|
167
|
+
node.remove()
|
|
168
|
+
const index = ctx.ripples.indexOf(handle)
|
|
169
|
+
if (index !== -1) ctx.ripples.splice(index, 1)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const leave = () => {
|
|
173
|
+
phase = 2
|
|
174
|
+
inner.classList.remove("q-ripple__inner--enter")
|
|
175
|
+
inner.classList.add("q-ripple__inner--leave")
|
|
176
|
+
inner.style.opacity = "0"
|
|
177
|
+
timer = setTimeout(remove, LEAVE_MS)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const enter = () => {
|
|
181
|
+
phase = 1
|
|
182
|
+
inner.classList.add("q-ripple__inner--enter")
|
|
183
|
+
inner.style.transform = `translate(${centerX}px, ${centerY}px) scale(1)`
|
|
184
|
+
inner.style.opacity = "0.2"
|
|
185
|
+
timer = setTimeout(leave, HOLD_MS)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const handle: RippleHandle = {
|
|
189
|
+
pointerId: evt.type === "pointerdown" ? (evt as PointerEvent).pointerId : null,
|
|
190
|
+
abort() {
|
|
191
|
+
clearTimeout(timer)
|
|
192
|
+
node.remove()
|
|
193
|
+
},
|
|
194
|
+
cancel() {
|
|
195
|
+
handle.pointerId = null
|
|
196
|
+
if (phase === 2) return
|
|
197
|
+
clearTimeout(timer)
|
|
198
|
+
if (phase === 0) remove()
|
|
199
|
+
else leave()
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
ctx.ripples.push(handle)
|
|
204
|
+
|
|
205
|
+
const isTouch = evt.type === "pointerdown" && (evt as PointerEvent).pointerType === "touch"
|
|
206
|
+
timer = setTimeout(enter, isTouch ? TOUCH_ENTER_DELAY : ENTER_DELAY)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function setup(el: HTMLElement, binding: RippleBinding) {
|
|
210
|
+
const ctx: RippleContext = {
|
|
211
|
+
enabled: binding.value !== false,
|
|
212
|
+
early: false,
|
|
213
|
+
stop: false,
|
|
214
|
+
center: false,
|
|
215
|
+
keyCodes: [...DEFAULT_KEY_CODES],
|
|
216
|
+
keyTime: 0,
|
|
217
|
+
bound: 0,
|
|
218
|
+
ripples: [],
|
|
219
|
+
prevPosition: null,
|
|
220
|
+
}
|
|
221
|
+
applyOptions(ctx, binding)
|
|
222
|
+
|
|
223
|
+
// Le conteneur d'onde est en `position: absolute` → il lui faut un bloc conteneur.
|
|
224
|
+
const ensurePosition = () => {
|
|
225
|
+
if (!ctx.enabled) return
|
|
226
|
+
if (typeof getComputedStyle !== "function") return
|
|
227
|
+
if (getComputedStyle(el).position !== "static") return
|
|
228
|
+
if (ctx.prevPosition === null) ctx.prevPosition = el.style.position
|
|
229
|
+
el.style.position = "relative"
|
|
230
|
+
}
|
|
231
|
+
ensurePosition()
|
|
232
|
+
|
|
233
|
+
const onStart = (evt: Event) => {
|
|
234
|
+
const e = evt as EventFlags & MouseEvent
|
|
235
|
+
if (e.qSkipRipple) return
|
|
236
|
+
// `detail === 0` : clic synthétisé au clavier (pas de coordonnées fiables) → onde centrée
|
|
237
|
+
spawnRipple(el, ctx, evt, e.qKeyEvent === true || (evt.type === "click" && e.detail === 0))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const onKey = (evt: Event) => {
|
|
241
|
+
const e = evt as EventFlags & KeyboardEvent
|
|
242
|
+
if (e.qSkipRipple) return
|
|
243
|
+
if (!ctx.keyCodes.includes(e.keyCode)) return
|
|
244
|
+
// Sur un <button>/<a href>, le navigateur émet déjà un `click` (centré) : ne pas doubler
|
|
245
|
+
if (hasNativeActivation(el)) return
|
|
246
|
+
const now = Date.now()
|
|
247
|
+
if (now - ctx.keyTime < KEY_THROTTLE) return
|
|
248
|
+
ctx.keyTime = now
|
|
249
|
+
spawnRipple(el, ctx, evt, true)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Le geste ne peut plus devenir un clic (scroll/pan, ou pointeur sorti)
|
|
253
|
+
const onCancel = (evt: Event) => {
|
|
254
|
+
const e = evt as PointerEvent
|
|
255
|
+
if (e.type === "pointerleave" && e.buttons === 0) return
|
|
256
|
+
for (let i = ctx.ripples.length - 1; i >= 0; i--) {
|
|
257
|
+
const ripple = ctx.ripples[i]
|
|
258
|
+
if (ripple.pointerId === e.pointerId) ripple.cancel()
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const bind = () => {
|
|
263
|
+
const mode: 0 | 1 | 2 = ctx.enabled ? (ctx.early ? 2 : 1) : 0
|
|
264
|
+
if (mode === ctx.bound) return
|
|
265
|
+
|
|
266
|
+
if (ctx.bound === 1) {
|
|
267
|
+
el.removeEventListener("click", onStart)
|
|
268
|
+
el.removeEventListener("keyup", onKey)
|
|
269
|
+
} else if (ctx.bound === 2) {
|
|
270
|
+
el.removeEventListener("pointerdown", onStart)
|
|
271
|
+
el.removeEventListener("pointercancel", onCancel)
|
|
272
|
+
el.removeEventListener("pointerleave", onCancel)
|
|
273
|
+
el.removeEventListener("keydown", onKey)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (mode === 1) {
|
|
277
|
+
el.addEventListener("click", onStart)
|
|
278
|
+
el.addEventListener("keyup", onKey)
|
|
279
|
+
} else if (mode === 2) {
|
|
280
|
+
el.addEventListener("pointerdown", onStart)
|
|
281
|
+
el.addEventListener("pointercancel", onCancel)
|
|
282
|
+
el.addEventListener("pointerleave", onCancel)
|
|
283
|
+
el.addEventListener("keydown", onKey)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
ctx.bound = mode
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
bind()
|
|
290
|
+
|
|
291
|
+
;(el as QElement).__qRipple = {
|
|
292
|
+
ctx,
|
|
293
|
+
bind,
|
|
294
|
+
dispose() {
|
|
295
|
+
ctx.ripples.forEach((ripple) => ripple.abort())
|
|
296
|
+
ctx.ripples.length = 0
|
|
297
|
+
ctx.enabled = false
|
|
298
|
+
bind()
|
|
299
|
+
if (ctx.prevPosition !== null) el.style.position = ctx.prevPosition
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Directive `v-ripple` : <div v-ripple.center:primary /> */
|
|
305
|
+
export const vRipple = {
|
|
306
|
+
mounted(el: HTMLElement, binding: RippleBinding) {
|
|
307
|
+
setup(el, binding)
|
|
308
|
+
},
|
|
309
|
+
updated(el: HTMLElement, binding: RippleBinding) {
|
|
310
|
+
const instance = (el as QElement).__qRipple
|
|
311
|
+
if (!instance) return
|
|
312
|
+
instance.ctx.enabled = binding.value !== false
|
|
313
|
+
applyOptions(instance.ctx, binding)
|
|
314
|
+
instance.bind()
|
|
315
|
+
},
|
|
316
|
+
unmounted(el: HTMLElement) {
|
|
317
|
+
const instance = (el as QElement).__qRipple
|
|
318
|
+
if (!instance) return
|
|
319
|
+
instance.dispose()
|
|
320
|
+
delete (el as QElement).__qRipple
|
|
321
|
+
},
|
|
322
|
+
}
|
package/module.ts
CHANGED
|
@@ -86,13 +86,18 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|
|
86
86
|
})
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
// Directive globale v-close
|
|
89
|
+
// Directive globale v-close / v-ripple / v-touch-* / v-intersection :
|
|
90
|
+
// <q-btn v-close>, <div v-ripple.center>…
|
|
91
|
+
// Plugin UNIVERSEL (pas "client") : sans enregistrement côté serveur, tout
|
|
92
|
+
// rendu SSR/SSG d'une page utilisant une directive crash (Vue SSR lit
|
|
93
|
+
// `dir.getSSRProps` sur une directive non résolue → `undefined`).
|
|
94
|
+
// Les directives n'ont pas de getSSRProps : en SSR elles rendent `{}` (no-op).
|
|
90
95
|
addPluginTemplate({
|
|
91
96
|
filename: "dnax-ui-directives.mjs",
|
|
92
|
-
mode: "client",
|
|
93
97
|
getContents: () => `
|
|
94
98
|
import { defineNuxtPlugin } from "#imports"
|
|
95
99
|
import { vClose } from "@dnax/ui/runtime"
|
|
100
|
+
import { vRipple } from "@dnax/ui/runtime"
|
|
96
101
|
import { vTouchPan } from "@dnax/ui/runtime"
|
|
97
102
|
import { vTouchHold } from "@dnax/ui/runtime"
|
|
98
103
|
import { vTouchSwipe } from "@dnax/ui/runtime"
|
|
@@ -101,6 +106,7 @@ import { vIntersection } from "@dnax/ui/runtime"
|
|
|
101
106
|
|
|
102
107
|
export default defineNuxtPlugin((nuxtApp) => {
|
|
103
108
|
nuxtApp.vueApp.directive("close", vClose)
|
|
109
|
+
nuxtApp.vueApp.directive("ripple", vRipple)
|
|
104
110
|
nuxtApp.vueApp.directive("touch-pan", vTouchPan)
|
|
105
111
|
nuxtApp.vueApp.directive("touch-hold", vTouchHold)
|
|
106
112
|
nuxtApp.vueApp.directive("touch-swipe", vTouchSwipe)
|
package/package.json
CHANGED
|
@@ -54,6 +54,8 @@ export {
|
|
|
54
54
|
} from "./lib/overlayBack"
|
|
55
55
|
export type { OverlayHandle } from "./lib/overlayBack"
|
|
56
56
|
export { markOverlayClose, closeParentOverlay, vClose } from "./lib/closeOverlay"
|
|
57
|
+
export { vRipple } from "./lib/ripple"
|
|
58
|
+
export type { RippleOptions, RippleValue } from "./lib/ripple"
|
|
57
59
|
export { vTouchPan } from "./lib/touchPan"
|
|
58
60
|
export type { PanDetails, TouchPanHandler, PanDirection } from "./lib/touchPan"
|
|
59
61
|
export { vTouchHold } from "./lib/touchHold"
|
package/styles/main.css
CHANGED
|
@@ -2440,6 +2440,15 @@ html.dark body {
|
|
|
2440
2440
|
margin-left: -1px;
|
|
2441
2441
|
}
|
|
2442
2442
|
|
|
2443
|
+
.q-btn-group--stretch {
|
|
2444
|
+
/* stretch : la racine doit faire 100 % de large. Sinon `display: inline-flex`
|
|
2445
|
+
réduit le groupe à la taille de son contenu et `flex: 1 1 auto` sur les
|
|
2446
|
+
boutons (règle ci-dessous) n'a aucun effet. Même correctif que QTabs
|
|
2447
|
+
(`.q-tabs--stretch`) — un simple `class="w-full"` côté consommateur
|
|
2448
|
+
contournait le problème. */
|
|
2449
|
+
width: 100%;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2443
2452
|
.q-btn-group--stretch > .q-btn {
|
|
2444
2453
|
flex: 1 1 auto;
|
|
2445
2454
|
}
|
|
@@ -2451,6 +2460,14 @@ html.dark body {
|
|
|
2451
2460
|
vertical-align: middle;
|
|
2452
2461
|
}
|
|
2453
2462
|
|
|
2463
|
+
.q-btn-actions--stretch {
|
|
2464
|
+
/* stretch : la racine doit elle-même faire 100 % de large, sinon son parent
|
|
2465
|
+
`inline-flex` « shrink-to-fit » l'épouse au contenu et le `width: 100%` du
|
|
2466
|
+
QBtn interne (`.q-btn--stretch`) n'a pas de référentiel → rien ne s'étire.
|
|
2467
|
+
Même correctif que QTabs (`.q-tabs--stretch`). */
|
|
2468
|
+
width: 100%;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2454
2471
|
.q-btn-actions__caret {
|
|
2455
2472
|
width: 1em;
|
|
2456
2473
|
height: 1em;
|
|
@@ -10767,3 +10784,46 @@ a.q-breadcrumbs__el:hover {
|
|
|
10767
10784
|
.q-board--dense .q-board__cell-label {
|
|
10768
10785
|
font-size: 9px;
|
|
10769
10786
|
}
|
|
10787
|
+
|
|
10788
|
+
/* ===== v-ripple — onde « material ripple » =====
|
|
10789
|
+
Conteneur injecté par la directive dans l'élément hôte (position absolute,
|
|
10790
|
+
100 %×100 %, overflow hidden + border-radius: inherit → l'onde est clippée à
|
|
10791
|
+
la forme de l'hôte, sans imposer `overflow: hidden` au consommateur).
|
|
10792
|
+
La pastille utilise `currentColor` par défaut ; `v-ripple:primary` ou
|
|
10793
|
+
`{ color }` posent la couleur en inline (tokens → var(--token)). */
|
|
10794
|
+
.q-ripple {
|
|
10795
|
+
position: absolute;
|
|
10796
|
+
top: 0;
|
|
10797
|
+
left: 0;
|
|
10798
|
+
width: 100%;
|
|
10799
|
+
height: 100%;
|
|
10800
|
+
z-index: 0;
|
|
10801
|
+
contain: strict;
|
|
10802
|
+
overflow: hidden;
|
|
10803
|
+
border-radius: inherit;
|
|
10804
|
+
color: inherit;
|
|
10805
|
+
pointer-events: none;
|
|
10806
|
+
}
|
|
10807
|
+
|
|
10808
|
+
.q-ripple__inner {
|
|
10809
|
+
position: absolute;
|
|
10810
|
+
top: 0;
|
|
10811
|
+
left: 0;
|
|
10812
|
+
border-radius: 50%;
|
|
10813
|
+
background: currentColor;
|
|
10814
|
+
opacity: 0;
|
|
10815
|
+
pointer-events: none;
|
|
10816
|
+
will-change: transform, opacity;
|
|
10817
|
+
}
|
|
10818
|
+
|
|
10819
|
+
.q-ripple__inner--enter {
|
|
10820
|
+
transition:
|
|
10821
|
+
transform 0.225s cubic-bezier(0.4, 0, 0.2, 1),
|
|
10822
|
+
opacity 0.1s cubic-bezier(0.4, 0, 0.2, 1);
|
|
10823
|
+
}
|
|
10824
|
+
|
|
10825
|
+
.q-ripple__inner--leave {
|
|
10826
|
+
transition:
|
|
10827
|
+
transform 0.225s cubic-bezier(0.4, 0, 0.2, 1),
|
|
10828
|
+
opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
10829
|
+
}
|