@dnax/ui 0.0.44 → 0.0.46
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/QApp.vue +13 -6
- package/components/QFooter.vue +16 -2
- package/components/QHeader.vue +16 -2
- package/components/QLayout.vue +138 -0
- package/components/QSelect.vue +91 -56
- package/components/QSidebar.vue +28 -3
- package/components/QTiptap.vue +1 -0
- package/index.ts +3 -1
- package/lib/layout.test.ts +125 -0
- package/lib/layout.ts +181 -0
- package/lib/useComponentProps.test.ts +36 -0
- package/lib/useComponentProps.ts +7 -1
- package/module.ts +0 -18
- package/package.json +1 -2
- package/scripts/generate-exports.ts +9 -0
- package/styles/main.css +112 -20
- package/components/QVideo.vue +0 -305
package/components/QVideo.vue
DELETED
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
// QVideo — lecteur vidéo basé sur le framework HTML de Video.js v10 (@videojs/html) :
|
|
3
|
-
// <q-video src="movie.mp4" poster="…" ratio="16/9" />
|
|
4
|
-
// Custom elements composables : <video-player> > <video-skin> > media element.
|
|
5
|
-
// Le média est choisi selon la source : <video> (mp4/webm/ogg), <youtube-video>
|
|
6
|
-
// (YouTube), <hlsjs-video> (.m3u8) — mux-video est aussi enregistré (prêt à l'emploi).
|
|
7
|
-
// Les modules sont chargés uniquement côté client (SSR safe — customElements).
|
|
8
|
-
// Events : @ready (media element), @play, @pause, @ended, @timeupdate, @loadedmetadata, @volumechange.
|
|
9
|
-
// Méthodes exposées : play / pause / togglePlay / seek / getCurrentTime / getDuration / setVolume / setMuted / isPlaying.
|
|
10
|
-
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"
|
|
11
|
-
// Importé en texte brut + injecté côté client : passé dans le pipeline CSS normal, ce
|
|
12
|
-
// fichier tiers (build Tailwind de @videojs/html) contient du `@layer`/`@scope` natif qui
|
|
13
|
-
// casse le build des apps consommatrices en Tailwind v3 (PostCSS exige le `@tailwind`
|
|
14
|
-
// correspondant). L'injection runtime l'exclut de PostCSS sans perdre les styles globaux.
|
|
15
|
-
import skinCss from "@videojs/html/video/skin.css?raw"
|
|
16
|
-
|
|
17
|
-
interface Props {
|
|
18
|
-
/** URL de la vidéo (mp4, webm, ogg, HLS .m3u8, YouTube…) */
|
|
19
|
-
src?: string
|
|
20
|
-
/** Image d'affiche avant lecture */
|
|
21
|
-
poster?: string
|
|
22
|
-
/** Image de remplacement affichée tant que la vidéo n'est pas chargée (ou slot #placeholder) */
|
|
23
|
-
placeholder?: string
|
|
24
|
-
/** Texte alternatif de l'image placeholder (défaut "Video placeholder") */
|
|
25
|
-
placeholderAlt?: string
|
|
26
|
-
/** Affiche les contrôles (défaut true). Le skin par défaut gère ses contrôles
|
|
27
|
-
* (état visible/masqué pendant la lecture). */
|
|
28
|
-
controls?: boolean
|
|
29
|
-
/** Démarre la lecture automatiquement (souvent combiné avec muted) */
|
|
30
|
-
autoplay?: boolean
|
|
31
|
-
/** Boucle à la fin */
|
|
32
|
-
loop?: boolean
|
|
33
|
-
/** Muet */
|
|
34
|
-
muted?: boolean
|
|
35
|
-
/** Chargement : "auto" | "metadata" (défaut) | "none" */
|
|
36
|
-
preload?: string
|
|
37
|
-
/** Ratio d'affichage : "16/9" (défaut), "4/3", "1/1", "none" (hauteur auto) */
|
|
38
|
-
ratio?: string
|
|
39
|
-
/** Largeur max du lecteur (défaut 100%) */
|
|
40
|
-
width?: string
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
44
|
-
src: "",
|
|
45
|
-
poster: "",
|
|
46
|
-
placeholder: "",
|
|
47
|
-
placeholderAlt: "Video placeholder",
|
|
48
|
-
controls: true,
|
|
49
|
-
autoplay: false,
|
|
50
|
-
loop: false,
|
|
51
|
-
muted: false,
|
|
52
|
-
preload: "metadata",
|
|
53
|
-
ratio: "16/9",
|
|
54
|
-
width: "",
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
const emit = defineEmits<{
|
|
58
|
-
ready: [media: any]
|
|
59
|
-
play: []
|
|
60
|
-
pause: []
|
|
61
|
-
ended: []
|
|
62
|
-
timeupdate: [payload: { currentTime: number; duration: number }]
|
|
63
|
-
loadedmetadata: [payload: { duration: number }]
|
|
64
|
-
volumechange: [payload: { muted: boolean; volume: number }]
|
|
65
|
-
}>()
|
|
66
|
-
|
|
67
|
-
/** URL YouTube (watch, shorts, embed, youtu.be) */
|
|
68
|
-
const isYoutube = computed(() =>
|
|
69
|
-
/(youtube\.com\/(watch\?v=|shorts\/|embed\/)|youtu\.be\/)/.test(props.src),
|
|
70
|
-
)
|
|
71
|
-
/** Flux HLS (.m3u8) → media element hlsjs-video */
|
|
72
|
-
const isHls = computed(() => /\.m3u8(\?|$)/.test(props.src))
|
|
73
|
-
|
|
74
|
-
const mediaEl = ref<HTMLElement | null>(null)
|
|
75
|
-
const playing = ref(false)
|
|
76
|
-
/** true dès que le média a chargé ses métadonnées (ou qu'il joue) → masque le placeholder */
|
|
77
|
-
const metaLoaded = ref(false)
|
|
78
|
-
|
|
79
|
-
const media = (): any => mediaEl.value
|
|
80
|
-
|
|
81
|
-
const onPlay = () => {
|
|
82
|
-
metaLoaded.value = true
|
|
83
|
-
playing.value = true
|
|
84
|
-
emit("play")
|
|
85
|
-
}
|
|
86
|
-
const onPause = () => {
|
|
87
|
-
playing.value = false
|
|
88
|
-
emit("pause")
|
|
89
|
-
}
|
|
90
|
-
const onEnded = () => emit("ended")
|
|
91
|
-
const onTimeupdate = () =>
|
|
92
|
-
emit("timeupdate", { currentTime: media()?.currentTime ?? 0, duration: media()?.duration ?? 0 })
|
|
93
|
-
const onLoadedmetadata = () => {
|
|
94
|
-
metaLoaded.value = true
|
|
95
|
-
emit("loadedmetadata", { duration: media()?.duration ?? 0 })
|
|
96
|
-
}
|
|
97
|
-
const onVolumechange = () =>
|
|
98
|
-
emit("volumechange", { muted: media()?.muted ?? false, volume: media()?.volume ?? 1 })
|
|
99
|
-
|
|
100
|
-
const listeners: Array<[string, EventListener]> = [
|
|
101
|
-
["play", onPlay],
|
|
102
|
-
["pause", onPause],
|
|
103
|
-
["ended", onEnded],
|
|
104
|
-
["timeupdate", onTimeupdate],
|
|
105
|
-
["loadedmetadata", onLoadedmetadata],
|
|
106
|
-
["volumechange", onVolumechange],
|
|
107
|
-
]
|
|
108
|
-
|
|
109
|
-
const attach = (el: HTMLElement) => {
|
|
110
|
-
for (const [type, fn] of listeners) el.addEventListener(type, fn)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const detach = (el: HTMLElement | null) => {
|
|
114
|
-
if (!el) return
|
|
115
|
-
for (const [type, fn] of listeners) el.removeEventListener(type, fn)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Attache les listeners dès que le media element existe (et à chaque changement de
|
|
119
|
-
// type de média) — sinon loadedmetadata est émis avant l'init et le placeholder
|
|
120
|
-
// resterait affiché, bloquant les contrôles.
|
|
121
|
-
watch(mediaEl, (el, old) => {
|
|
122
|
-
detach(old ?? null)
|
|
123
|
-
if (el) attach(el)
|
|
124
|
-
}, { immediate: true })
|
|
125
|
-
|
|
126
|
-
// Clic sur le placeholder : masque-le et lance la lecture
|
|
127
|
-
const onPlaceholderClick = () => {
|
|
128
|
-
metaLoaded.value = true
|
|
129
|
-
play()
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
let skinInjected = false
|
|
133
|
-
const injectSkin = () => {
|
|
134
|
-
if (skinInjected || typeof document === "undefined") return
|
|
135
|
-
skinInjected = true
|
|
136
|
-
if (document.getElementById("q-video-skin")) return
|
|
137
|
-
const style = document.createElement("style")
|
|
138
|
-
style.id = "q-video-skin"
|
|
139
|
-
style.textContent = skinCss
|
|
140
|
-
document.head.appendChild(style)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
onMounted(async () => {
|
|
144
|
-
injectSkin()
|
|
145
|
-
// Charge le framework HTML (custom elements) — uniquement côté client
|
|
146
|
-
await Promise.all([
|
|
147
|
-
import("@videojs/html/video/player" as string),
|
|
148
|
-
import("@videojs/html/video/skin" as string),
|
|
149
|
-
import("@videojs/html/media/mux-video" as string),
|
|
150
|
-
import("@videojs/html/media/youtube-video" as string),
|
|
151
|
-
import("@videojs/html/media/hlsjs-video" as string),
|
|
152
|
-
])
|
|
153
|
-
if (typeof customElements !== "undefined") {
|
|
154
|
-
await customElements.whenDefined("video-player")
|
|
155
|
-
}
|
|
156
|
-
emit("ready", mediaEl.value)
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
onBeforeUnmount(() => detach(mediaEl.value ?? null))
|
|
160
|
-
|
|
161
|
-
// — Méthodes exposées —
|
|
162
|
-
const play = () => media()?.play()
|
|
163
|
-
const pause = () => media()?.pause()
|
|
164
|
-
const togglePlay = () => {
|
|
165
|
-
const el = media()
|
|
166
|
-
if (!el) return
|
|
167
|
-
if (el.paused) el.play()
|
|
168
|
-
else el.pause()
|
|
169
|
-
}
|
|
170
|
-
const seek = (t: number) => {
|
|
171
|
-
if (media()) media().currentTime = t
|
|
172
|
-
}
|
|
173
|
-
const getCurrentTime = () => media()?.currentTime ?? 0
|
|
174
|
-
const getDuration = () => media()?.duration ?? 0
|
|
175
|
-
const setVolume = (v: number) => {
|
|
176
|
-
if (media()) media().volume = v
|
|
177
|
-
}
|
|
178
|
-
const setMuted = (m: boolean) => {
|
|
179
|
-
if (media()) media().muted = m
|
|
180
|
-
}
|
|
181
|
-
const isPlaying = () => playing.value
|
|
182
|
-
|
|
183
|
-
defineExpose({
|
|
184
|
-
play,
|
|
185
|
-
pause,
|
|
186
|
-
togglePlay,
|
|
187
|
-
seek,
|
|
188
|
-
getCurrentTime,
|
|
189
|
-
getDuration,
|
|
190
|
-
setVolume,
|
|
191
|
-
setMuted,
|
|
192
|
-
isPlaying,
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
const wrapperStyle = (): Record<string, string> => {
|
|
196
|
-
const style: Record<string, string> = {}
|
|
197
|
-
if (props.width) style.maxWidth = props.width
|
|
198
|
-
if (props.ratio !== "none") style.aspectRatio = props.ratio.replace("/", " / ")
|
|
199
|
-
else style.aspectRatio = "auto"
|
|
200
|
-
return style
|
|
201
|
-
}
|
|
202
|
-
</script>
|
|
203
|
-
|
|
204
|
-
<template>
|
|
205
|
-
<div class="q-video" :style="wrapperStyle()">
|
|
206
|
-
<!-- Custom elements @videojs/html : player > skin > media -->
|
|
207
|
-
<video-player class="q-video__player">
|
|
208
|
-
<video-skin>
|
|
209
|
-
<video
|
|
210
|
-
v-if="!isYoutube && !isHls"
|
|
211
|
-
ref="mediaEl"
|
|
212
|
-
:src="src"
|
|
213
|
-
:poster="poster"
|
|
214
|
-
:autoplay="autoplay"
|
|
215
|
-
:loop="loop"
|
|
216
|
-
:muted="muted"
|
|
217
|
-
:preload="preload"
|
|
218
|
-
playsinline
|
|
219
|
-
/>
|
|
220
|
-
<youtube-video
|
|
221
|
-
v-else-if="isYoutube"
|
|
222
|
-
ref="mediaEl"
|
|
223
|
-
:src="src"
|
|
224
|
-
:poster="poster"
|
|
225
|
-
:autoplay="autoplay"
|
|
226
|
-
:loop="loop"
|
|
227
|
-
:muted="muted"
|
|
228
|
-
playsinline
|
|
229
|
-
/>
|
|
230
|
-
<hlsjs-video
|
|
231
|
-
v-else
|
|
232
|
-
ref="mediaEl"
|
|
233
|
-
:src="src"
|
|
234
|
-
:poster="poster"
|
|
235
|
-
:autoplay="autoplay"
|
|
236
|
-
:loop="loop"
|
|
237
|
-
:muted="muted"
|
|
238
|
-
playsinline
|
|
239
|
-
/>
|
|
240
|
-
</video-skin>
|
|
241
|
-
</video-player>
|
|
242
|
-
|
|
243
|
-
<!-- Placeholder tant que la vidéo n'est pas chargée (clic = lecture) -->
|
|
244
|
-
<div
|
|
245
|
-
v-if="!metaLoaded && (placeholder || $slots.placeholder)"
|
|
246
|
-
class="q-video__placeholder"
|
|
247
|
-
role="button"
|
|
248
|
-
:aria-label="placeholderAlt"
|
|
249
|
-
@click="onPlaceholderClick"
|
|
250
|
-
>
|
|
251
|
-
<img v-if="placeholder" :src="placeholder" :alt="placeholderAlt" />
|
|
252
|
-
<slot name="placeholder" />
|
|
253
|
-
</div>
|
|
254
|
-
</div>
|
|
255
|
-
</template>
|
|
256
|
-
|
|
257
|
-
<style scoped>
|
|
258
|
-
.q-video {
|
|
259
|
-
position: relative;
|
|
260
|
-
width: 100%;
|
|
261
|
-
overflow: hidden;
|
|
262
|
-
border-radius: var(--q-radius, 12px);
|
|
263
|
-
background: #000;
|
|
264
|
-
}
|
|
265
|
-
/* Avant l'upgrade des custom elements : display block + remplir le cadre */
|
|
266
|
-
.q-video video-player,
|
|
267
|
-
.q-video video-skin,
|
|
268
|
-
.q-video video,
|
|
269
|
-
.q-video youtube-video,
|
|
270
|
-
.q-video hlsjs-video {
|
|
271
|
-
display: block;
|
|
272
|
-
}
|
|
273
|
-
.q-video video-player,
|
|
274
|
-
.q-video video-skin {
|
|
275
|
-
position: absolute;
|
|
276
|
-
inset: 0;
|
|
277
|
-
width: 100% !important;
|
|
278
|
-
height: 100% !important;
|
|
279
|
-
}
|
|
280
|
-
.q-video video,
|
|
281
|
-
.q-video youtube-video,
|
|
282
|
-
.q-video hlsjs-video {
|
|
283
|
-
width: 100%;
|
|
284
|
-
height: 100%;
|
|
285
|
-
object-fit: contain;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/* Placeholder de chargement : image ou contenu, cliquable pour lancer la lecture */
|
|
289
|
-
.q-video__placeholder {
|
|
290
|
-
position: absolute;
|
|
291
|
-
inset: 0;
|
|
292
|
-
z-index: 1;
|
|
293
|
-
display: flex;
|
|
294
|
-
align-items: center;
|
|
295
|
-
justify-content: center;
|
|
296
|
-
overflow: hidden;
|
|
297
|
-
cursor: pointer;
|
|
298
|
-
background: #000;
|
|
299
|
-
}
|
|
300
|
-
.q-video__placeholder img {
|
|
301
|
-
width: 100%;
|
|
302
|
-
height: 100%;
|
|
303
|
-
object-fit: cover;
|
|
304
|
-
}
|
|
305
|
-
</style>
|