@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,217 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QSwipeCell — cellule que l'on glisse horizontalement pour révéler des actions
|
|
3
|
+
// (API Vant SwipeCell) : <q-swipe-cell :right-width="160"><template #right>…</template>…</q-swipe-cell>.
|
|
4
|
+
// Drag vers la gauche → panneau droit, vers la droite → panneau gauche (snap à la moitié).
|
|
5
|
+
// Props : left-width, right-width, disabled, name, before-close.
|
|
6
|
+
// Events : open / close / click ({ name, position }). Méthodes : open, close.
|
|
7
|
+
import { onBeforeUnmount, ref, watch } from "vue"
|
|
8
|
+
|
|
9
|
+
type SwipePosition = "left" | "right"
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** Largeur du panneau d'actions de gauche, en px (0 = aucun) */
|
|
13
|
+
leftWidth?: number
|
|
14
|
+
/** Largeur du panneau d'actions de droite, en px (0 = aucun) */
|
|
15
|
+
rightWidth?: number
|
|
16
|
+
/** Désactive le glissement */
|
|
17
|
+
disabled?: boolean
|
|
18
|
+
/** Bloque le glissement quand les actions sont affichées (on ferme par clic) */
|
|
19
|
+
lockOnOpen?: boolean
|
|
20
|
+
/** Identifiant (retourné dans les events, pour la coordination) */
|
|
21
|
+
name?: string | number
|
|
22
|
+
/** Hook avant fermeture : retourner false (ou Promise false) garde la cellule ouverte */
|
|
23
|
+
beforeClose?: (args: { name?: string | number; position: SwipePosition | "" }) => boolean | Promise<boolean>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
27
|
+
leftWidth: 0,
|
|
28
|
+
rightWidth: 0,
|
|
29
|
+
disabled: false,
|
|
30
|
+
lockOnOpen: false,
|
|
31
|
+
name: "",
|
|
32
|
+
beforeClose: undefined,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const emit = defineEmits<{
|
|
36
|
+
open: [payload: { name?: string | number; position: SwipePosition }]
|
|
37
|
+
close: [payload: { name?: string | number; position: SwipePosition }]
|
|
38
|
+
click: [payload: { name?: string | number; position: SwipePosition | "" }]
|
|
39
|
+
}>()
|
|
40
|
+
|
|
41
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
42
|
+
const wrapperEl = ref<HTMLElement | null>(null)
|
|
43
|
+
|
|
44
|
+
const offset = ref(0)
|
|
45
|
+
const opened = ref<SwipePosition | "">("")
|
|
46
|
+
let dragging = false
|
|
47
|
+
let moved = false
|
|
48
|
+
let startX = 0
|
|
49
|
+
let startOffset = 0
|
|
50
|
+
|
|
51
|
+
const maxLeft = () => props.leftWidth || 0
|
|
52
|
+
const maxRight = () => props.rightWidth || 0
|
|
53
|
+
|
|
54
|
+
/** Applique la translation au wrapper (avec transition) */
|
|
55
|
+
const applyOffset = (v: number, transition = true) => {
|
|
56
|
+
offset.value = v
|
|
57
|
+
const el = wrapperEl.value
|
|
58
|
+
if (!el) return
|
|
59
|
+
if (transition) el.style.transition = "transform 0.3s ease"
|
|
60
|
+
el.style.transform = `translateX(${v}px)`
|
|
61
|
+
if (transition) {
|
|
62
|
+
setTimeout(() => el.style.removeProperty("transition"), 320)
|
|
63
|
+
}
|
|
64
|
+
const position: SwipePosition | "" = v < 0 ? "right" : v > 0 ? "left" : ""
|
|
65
|
+
if (position !== opened.value) {
|
|
66
|
+
const from = opened.value
|
|
67
|
+
opened.value = position
|
|
68
|
+
if (from) emit("close", { name: props.name, position: from })
|
|
69
|
+
if (position) emit("open", { name: props.name, position })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
74
|
+
if (props.disabled) return
|
|
75
|
+
// lock-on-open : quand les actions sont affichées, le glissement est bloqué
|
|
76
|
+
if (props.lockOnOpen && opened.value) return
|
|
77
|
+
dragging = true
|
|
78
|
+
moved = false
|
|
79
|
+
startX = e.clientX
|
|
80
|
+
startOffset = offset.value
|
|
81
|
+
;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
|
|
82
|
+
if (wrapperEl.value) wrapperEl.value.style.removeProperty("transition")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const onPointerMove = (e: PointerEvent) => {
|
|
86
|
+
if (!dragging) return
|
|
87
|
+
const dx = e.clientX - startX
|
|
88
|
+
if (!moved && Math.abs(dx) > 4) moved = true
|
|
89
|
+
if (!moved) return
|
|
90
|
+
const next = Math.min(maxLeft(), Math.max(-maxRight(), startOffset + dx))
|
|
91
|
+
offset.value = next // <— indispensable : le snap (pointerup) se base dessus
|
|
92
|
+
if (wrapperEl.value) wrapperEl.value.style.transform = `translateX(${next}px)`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const onPointerUp = () => {
|
|
96
|
+
if (!dragging) return
|
|
97
|
+
dragging = false
|
|
98
|
+
if (moved) {
|
|
99
|
+
// snap : ouvre le côté si on a dépassé la moitié, sinon revient
|
|
100
|
+
const target =
|
|
101
|
+
offset.value <= -maxRight() / 2
|
|
102
|
+
? -maxRight()
|
|
103
|
+
: offset.value >= maxLeft() / 2
|
|
104
|
+
? maxLeft()
|
|
105
|
+
: 0
|
|
106
|
+
applyOffset(target)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const close = async () => {
|
|
111
|
+
if (offset.value === 0) return
|
|
112
|
+
if (props.beforeClose) {
|
|
113
|
+
const ok = await props.beforeClose({ name: props.name, position: opened.value })
|
|
114
|
+
if (ok === false) return
|
|
115
|
+
}
|
|
116
|
+
applyOffset(0)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Fermeture forcée, sans passer par before-close (clic sur la cellule) */
|
|
120
|
+
const forceClose = () => applyOffset(0)
|
|
121
|
+
|
|
122
|
+
const open = (position: SwipePosition) => {
|
|
123
|
+
const w = position === "left" ? maxLeft() : maxRight()
|
|
124
|
+
if (!w) return
|
|
125
|
+
applyOffset(position === "left" ? w : -w)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const onClickCell = () => {
|
|
129
|
+
if (opened.value) {
|
|
130
|
+
// Le clic sur la cellule referme TOUJOURS (before-close ne s'applique qu'aux actions)
|
|
131
|
+
forceClose()
|
|
132
|
+
} else {
|
|
133
|
+
emit("click", { name: props.name, position: "" })
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const onClickAction = (position: SwipePosition) => {
|
|
138
|
+
emit("click", { name: props.name, position })
|
|
139
|
+
close()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
defineExpose({ open, close })
|
|
143
|
+
|
|
144
|
+
// — Clic extérieur : referme la cellule ouverte (clic en dehors des actions) —
|
|
145
|
+
const onDocPointerDown = (e: PointerEvent) => {
|
|
146
|
+
if (!opened.value) return
|
|
147
|
+
if (rootEl.value && !rootEl.value.contains(e.target as Node)) {
|
|
148
|
+
forceClose()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
watch(opened, (v) => {
|
|
153
|
+
if (v) document.addEventListener("pointerdown", onDocPointerDown)
|
|
154
|
+
else document.removeEventListener("pointerdown", onDocPointerDown)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
onBeforeUnmount(() => {
|
|
158
|
+
document.removeEventListener("pointerdown", onDocPointerDown)
|
|
159
|
+
if (wrapperEl.value) wrapperEl.value.style.removeProperty("transition")
|
|
160
|
+
})
|
|
161
|
+
</script>
|
|
162
|
+
|
|
163
|
+
<template>
|
|
164
|
+
<div ref="rootEl" class="q-swipe-cell">
|
|
165
|
+
<div class="q-swipe-cell__left" :style="{ width: `${leftWidth}px` }" @click="onClickAction('left')">
|
|
166
|
+
<slot name="left" />
|
|
167
|
+
</div>
|
|
168
|
+
<div class="q-swipe-cell__right" :style="{ width: `${rightWidth}px` }" @click="onClickAction('right')">
|
|
169
|
+
<slot name="right" />
|
|
170
|
+
</div>
|
|
171
|
+
<div
|
|
172
|
+
ref="wrapperEl"
|
|
173
|
+
class="q-swipe-cell__wrapper"
|
|
174
|
+
@pointerdown="onPointerDown"
|
|
175
|
+
@pointermove="onPointerMove"
|
|
176
|
+
@pointerup="onPointerUp"
|
|
177
|
+
@pointercancel="onPointerUp"
|
|
178
|
+
@click="onClickCell"
|
|
179
|
+
>
|
|
180
|
+
<slot />
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</template>
|
|
184
|
+
|
|
185
|
+
<style scoped>
|
|
186
|
+
.q-swipe-cell {
|
|
187
|
+
position: relative;
|
|
188
|
+
overflow: hidden;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* panneaux d'actions : à leur place DANS le conteneur, derrière le wrapper
|
|
192
|
+
(qui les couvre) — le glissement du wrapper les révèle */
|
|
193
|
+
.q-swipe-cell__left,
|
|
194
|
+
.q-swipe-cell__right {
|
|
195
|
+
position: absolute;
|
|
196
|
+
top: 0;
|
|
197
|
+
height: 100%;
|
|
198
|
+
display: flex;
|
|
199
|
+
align-items: stretch;
|
|
200
|
+
z-index: 0;
|
|
201
|
+
}
|
|
202
|
+
.q-swipe-cell__left {
|
|
203
|
+
left: 0;
|
|
204
|
+
}
|
|
205
|
+
.q-swipe-cell__right {
|
|
206
|
+
right: 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* contenu de la cellule : glisse horizontalement AU-DESSUS des panneaux */
|
|
210
|
+
.q-swipe-cell__wrapper {
|
|
211
|
+
position: relative;
|
|
212
|
+
z-index: 1;
|
|
213
|
+
transition: transform 0.3s ease;
|
|
214
|
+
touch-action: pan-y; /* scroll vertical OK, glissement horizontal capté */
|
|
215
|
+
cursor: grab;
|
|
216
|
+
}
|
|
217
|
+
</style>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QSwiper — carrousel type Swiper (API swiperjs/vue) :
|
|
3
|
+
// <q-swiper :slides-per-view="2" :space-between="16" loop navigation :autoplay="{ delay: 2000 }" effect="fade">
|
|
4
|
+
// Wrapper autour de swiper/vue : TOUS les paramètres Swiper passent en props/attrs
|
|
5
|
+
// (slides-per-view, space-between, loop, centered-slides, navigation, pagination,
|
|
6
|
+
// autoplay, effect, virtual, thumbs…), les événements via @slide-change, @swiper…
|
|
7
|
+
// Tous les modules courants sont pré-enregistrés — remplaçables via :modules.
|
|
8
|
+
import { Swiper } from "swiper/vue"
|
|
9
|
+
import {
|
|
10
|
+
A11y,
|
|
11
|
+
Autoplay,
|
|
12
|
+
Controller,
|
|
13
|
+
EffectCards,
|
|
14
|
+
EffectCoverflow,
|
|
15
|
+
EffectCreative,
|
|
16
|
+
EffectCube,
|
|
17
|
+
EffectFade,
|
|
18
|
+
EffectFlip,
|
|
19
|
+
FreeMode,
|
|
20
|
+
Grid,
|
|
21
|
+
HashNavigation,
|
|
22
|
+
History,
|
|
23
|
+
Keyboard,
|
|
24
|
+
Manipulation,
|
|
25
|
+
Mousewheel,
|
|
26
|
+
Navigation,
|
|
27
|
+
Pagination,
|
|
28
|
+
Parallax,
|
|
29
|
+
Scrollbar,
|
|
30
|
+
Thumbs,
|
|
31
|
+
Virtual,
|
|
32
|
+
Zoom,
|
|
33
|
+
} from "swiper/modules"
|
|
34
|
+
|
|
35
|
+
// Styles Swiper (core + modules)
|
|
36
|
+
import "swiper/css"
|
|
37
|
+
import "swiper/css/a11y"
|
|
38
|
+
import "swiper/css/autoplay"
|
|
39
|
+
import "swiper/css/controller"
|
|
40
|
+
import "swiper/css/effect-cards"
|
|
41
|
+
import "swiper/css/effect-coverflow"
|
|
42
|
+
import "swiper/css/effect-creative"
|
|
43
|
+
import "swiper/css/effect-cube"
|
|
44
|
+
import "swiper/css/effect-fade"
|
|
45
|
+
import "swiper/css/effect-flip"
|
|
46
|
+
import "swiper/css/free-mode"
|
|
47
|
+
import "swiper/css/grid"
|
|
48
|
+
import "swiper/css/hash-navigation"
|
|
49
|
+
import "swiper/css/history"
|
|
50
|
+
import "swiper/css/keyboard"
|
|
51
|
+
import "swiper/css/manipulation"
|
|
52
|
+
import "swiper/css/mousewheel"
|
|
53
|
+
import "swiper/css/navigation"
|
|
54
|
+
import "swiper/css/pagination"
|
|
55
|
+
import "swiper/css/parallax"
|
|
56
|
+
import "swiper/css/scrollbar"
|
|
57
|
+
import "swiper/css/thumbs"
|
|
58
|
+
import "swiper/css/virtual"
|
|
59
|
+
import "swiper/css/zoom"
|
|
60
|
+
|
|
61
|
+
interface Props {
|
|
62
|
+
/** Modules Swiper — défaut : tous les modules courants (navigation, pagination,
|
|
63
|
+
* autoplay, effets, virtual, thumbs…). Passez un tableau custom pour réduire le bundle. */
|
|
64
|
+
modules?: unknown[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
68
|
+
modules: () => [
|
|
69
|
+
A11y,
|
|
70
|
+
Autoplay,
|
|
71
|
+
Controller,
|
|
72
|
+
EffectCards,
|
|
73
|
+
EffectCoverflow,
|
|
74
|
+
EffectCreative,
|
|
75
|
+
EffectCube,
|
|
76
|
+
EffectFade,
|
|
77
|
+
EffectFlip,
|
|
78
|
+
FreeMode,
|
|
79
|
+
Grid,
|
|
80
|
+
HashNavigation,
|
|
81
|
+
History,
|
|
82
|
+
Keyboard,
|
|
83
|
+
Manipulation,
|
|
84
|
+
Mousewheel,
|
|
85
|
+
Navigation,
|
|
86
|
+
Pagination,
|
|
87
|
+
Parallax,
|
|
88
|
+
Scrollbar,
|
|
89
|
+
Thumbs,
|
|
90
|
+
Virtual,
|
|
91
|
+
Zoom,
|
|
92
|
+
],
|
|
93
|
+
})
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<template>
|
|
97
|
+
<div class="q-swiper">
|
|
98
|
+
<Swiper v-bind="$attrs" :modules="modules">
|
|
99
|
+
<slot />
|
|
100
|
+
</Swiper>
|
|
101
|
+
</div>
|
|
102
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QSwiperSlide — slide de QSwiper (wrapper de SwiperSlide) :
|
|
3
|
+
// <q-swiper-slide v-for="(s, i) in slides" :key="i" :virtual-index="i">
|
|
4
|
+
// Reçoit les props de SwiperSlide (tag, zoom, virtualIndex) + swiperRef/swiperSlideIndex
|
|
5
|
+
// injectés automatiquement par le Swiper parent. Le slot expose isActive/isVisible/isPrev/isNext.
|
|
6
|
+
import { SwiperSlide } from "swiper/vue"
|
|
7
|
+
|
|
8
|
+
// swiper/vue identifie les slides par type.name === "SwiperSlide" — le wrapper
|
|
9
|
+
// doit conserver ce nom pour que le parent les récupère.
|
|
10
|
+
defineOptions({ name: "SwiperSlide" })
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<SwiperSlide v-bind="$attrs">
|
|
15
|
+
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
|
|
16
|
+
<slot :name="name" v-bind="slotProps" />
|
|
17
|
+
</template>
|
|
18
|
+
</SwiperSlide>
|
|
19
|
+
</template>
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QSyntax — bloc de code avec coloration syntaxique Shiki (https://shiki.style).
|
|
3
|
+
// <q-syntax code="const a = 1" lang="ts" theme="github-dark" filename="app.ts" copy />
|
|
4
|
+
// code peut venir de la prop `code` ou du slot par défaut.
|
|
5
|
+
// Rendu côté client : le code brut s'affiche en fallback, puis est surligné.
|
|
6
|
+
import { computed, onBeforeUnmount, onMounted, ref, useSlots, watch } from "vue"
|
|
7
|
+
import { createHighlighter, type Highlighter } from "shiki"
|
|
8
|
+
import { createJavaScriptRegexEngine } from "shiki/engine/javascript"
|
|
9
|
+
import { Icon } from "@iconify/vue"
|
|
10
|
+
import { icons } from "../lib/icons"
|
|
11
|
+
import { radiusStyle, useRadius } from "../lib/useComponentProps"
|
|
12
|
+
import type { RadiusProp } from "../lib/useComponentProps"
|
|
13
|
+
|
|
14
|
+
const DEFAULT_THEME = "github-dark-default"
|
|
15
|
+
|
|
16
|
+
// Langages chargés par défaut (grammaires chargées à la demande par Shiki).
|
|
17
|
+
// Les autres langages sont chargés via loadLanguage ; repli sur "text" si inconnu.
|
|
18
|
+
const DEFAULT_LANGS = [
|
|
19
|
+
"vue", "ts", "js", "tsx", "jsx", "html", "css", "scss", "json", "jsonc",
|
|
20
|
+
"bash", "markdown", "yaml", "python", "go", "rust", "sql",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
/** Code à afficher (sinon contenu du slot par défaut) */
|
|
25
|
+
code?: string
|
|
26
|
+
/** Langage Shiki : vue, ts, js, html, css, json, bash, markdown, yaml… */
|
|
27
|
+
lang?: string
|
|
28
|
+
/** Thème Shiki (défaut : github-dark-default) */
|
|
29
|
+
theme?: string
|
|
30
|
+
/** Nom de fichier affiché dans la barre de titre */
|
|
31
|
+
filename?: string
|
|
32
|
+
/** Bouton copier */
|
|
33
|
+
copy?: boolean
|
|
34
|
+
/** Coins arrondis : true = md, ou échelle xs|sm|md|lg (none = carré) — défaut : composantProps */
|
|
35
|
+
radius?: RadiusProp
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
39
|
+
lang: "text",
|
|
40
|
+
theme: DEFAULT_THEME,
|
|
41
|
+
filename: "",
|
|
42
|
+
copy: false,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// radius : prop explicite > composantProps.QSyntax.radius > composantProps.default.radius
|
|
46
|
+
const effectiveRadius = useRadius("QSyntax", () => props.radius)
|
|
47
|
+
const roundedStyle = computed(() => radiusStyle(effectiveRadius.value))
|
|
48
|
+
|
|
49
|
+
const slots = useSlots()
|
|
50
|
+
|
|
51
|
+
// Texte du slot par défaut (si pas de prop code)
|
|
52
|
+
const slotCode = computed(() => {
|
|
53
|
+
const extract = (children: unknown): string => {
|
|
54
|
+
if (typeof children === "string") return children
|
|
55
|
+
if (Array.isArray(children)) return children.map(extract).join("")
|
|
56
|
+
if (children && typeof children === "object" && "children" in children) {
|
|
57
|
+
return extract((children as { children: unknown }).children)
|
|
58
|
+
}
|
|
59
|
+
return ""
|
|
60
|
+
}
|
|
61
|
+
return (slots.default?.() ?? []).map((v) => extract(v.children)).join("")
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const source = computed(() => props.code ?? slotCode.value)
|
|
65
|
+
|
|
66
|
+
// Highlighter singleton : une seule instance pour toute l'application
|
|
67
|
+
let highlighterPromise: Promise<Highlighter> | null = null
|
|
68
|
+
const getHighlighter = () => {
|
|
69
|
+
highlighterPromise ??= createHighlighter({
|
|
70
|
+
themes: [DEFAULT_THEME],
|
|
71
|
+
langs: DEFAULT_LANGS,
|
|
72
|
+
engine: createJavaScriptRegexEngine(),
|
|
73
|
+
})
|
|
74
|
+
return highlighterPromise
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const html = ref("")
|
|
78
|
+
const highlighted = ref(false)
|
|
79
|
+
const error = ref(false)
|
|
80
|
+
|
|
81
|
+
const highlight = async () => {
|
|
82
|
+
const code = source.value
|
|
83
|
+
if (!code) {
|
|
84
|
+
html.value = ""
|
|
85
|
+
highlighted.value = false
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const highlighter = await getHighlighter()
|
|
90
|
+
|
|
91
|
+
// Thème custom chargé à la demande
|
|
92
|
+
let theme = props.theme
|
|
93
|
+
if (theme !== DEFAULT_THEME) {
|
|
94
|
+
try {
|
|
95
|
+
await highlighter.loadTheme(theme as never)
|
|
96
|
+
} catch {
|
|
97
|
+
theme = DEFAULT_THEME
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Langage custom chargé à la demande ; repli sur "text"
|
|
102
|
+
let lang = props.lang
|
|
103
|
+
if (lang !== "text" && !highlighter.getLoadedLanguages().includes(lang)) {
|
|
104
|
+
try {
|
|
105
|
+
await highlighter.loadLanguage(lang as never)
|
|
106
|
+
} catch {
|
|
107
|
+
lang = "text"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
html.value = highlighter.codeToHtml(code, { lang, theme })
|
|
113
|
+
} catch {
|
|
114
|
+
html.value = highlighter.codeToHtml(code, { lang, theme: DEFAULT_THEME })
|
|
115
|
+
}
|
|
116
|
+
highlighted.value = true
|
|
117
|
+
error.value = false
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
console.error("[QSyntax]", e)
|
|
121
|
+
error.value = true
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
watch(() => [source.value, props.lang, props.theme], () => {
|
|
126
|
+
highlighted.value = false
|
|
127
|
+
highlight()
|
|
128
|
+
})
|
|
129
|
+
onMounted(highlight)
|
|
130
|
+
|
|
131
|
+
// — Copier —
|
|
132
|
+
const copied = ref(false)
|
|
133
|
+
let copyTimer: ReturnType<typeof setTimeout> | undefined
|
|
134
|
+
const onCopy = async () => {
|
|
135
|
+
try {
|
|
136
|
+
await navigator.clipboard.writeText(source.value)
|
|
137
|
+
copied.value = true
|
|
138
|
+
clearTimeout(copyTimer)
|
|
139
|
+
copyTimer = setTimeout(() => (copied.value = false), 1600)
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
/* presse-papiers indisponible */
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
onBeforeUnmount(() => clearTimeout(copyTimer))
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<template>
|
|
149
|
+
<div class="q-syntax" :class="{ 'q-syntax--error': error }" :style="roundedStyle">
|
|
150
|
+
<div v-if="filename || copy" class="q-syntax__bar">
|
|
151
|
+
<span v-if="filename" class="q-syntax__filename">
|
|
152
|
+
<Icon :icon="icons.fileCode" aria-hidden="true" />
|
|
153
|
+
{{ filename }}
|
|
154
|
+
</span>
|
|
155
|
+
<button
|
|
156
|
+
v-if="copy"
|
|
157
|
+
type="button"
|
|
158
|
+
class="q-syntax__copy"
|
|
159
|
+
:class="{ 'q-syntax__copy--done': copied }"
|
|
160
|
+
:aria-label="copied ? 'Copied' : 'Copy code'"
|
|
161
|
+
@click="onCopy"
|
|
162
|
+
>
|
|
163
|
+
<Icon :icon="copied ? icons.check : icons.copy" aria-hidden="true" />
|
|
164
|
+
{{ copied ? "Copied" : "Copy" }}
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div v-if="highlighted" class="q-syntax__body" v-html="html" />
|
|
169
|
+
<pre v-else class="q-syntax__plain"><code>{{ source }}</code></pre>
|
|
170
|
+
</div>
|
|
171
|
+
</template>
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QTab — API Quasar (source officielle) :
|
|
3
|
+
// <q-tab name="a" icon="…" label="Accueil" alert="negative" alert-icon="…" no-caps />
|
|
4
|
+
// alert : true = point de notification ; string = couleur du point (token ou hex).
|
|
5
|
+
// name : auto-généré (t_1, t_2…) si absent, comme chez Quasar.
|
|
6
|
+
import { computed, inject, onBeforeUnmount, onMounted, ref } from "vue"
|
|
7
|
+
import { Icon } from "@iconify/vue"
|
|
8
|
+
import { cn } from "../lib/utils"
|
|
9
|
+
import { qTabsKey } from "./QTabs.vue"
|
|
10
|
+
|
|
11
|
+
const TOKEN_COLORS = ["primary", "secondary", "accent", "dark", "positive", "negative", "info", "warning"] as const
|
|
12
|
+
|
|
13
|
+
/** Token → var(--token), sinon valeur directe (hex, rgb(), nom CSS) */
|
|
14
|
+
const colorValue = (c: string) =>
|
|
15
|
+
(TOKEN_COLORS as readonly string[]).includes(c) ? `var(--${c})` : c
|
|
16
|
+
|
|
17
|
+
let tabSeq = 0
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
/** Identifiant du tab (comparé au v-model du QTabs) */
|
|
21
|
+
name?: string | number
|
|
22
|
+
/** Icône Iconify (ex. : "lucide:home") */
|
|
23
|
+
icon?: string
|
|
24
|
+
/** Texte ou nombre */
|
|
25
|
+
label?: string | number
|
|
26
|
+
/** true = point de notification ; string = couleur du point */
|
|
27
|
+
alert?: boolean | string
|
|
28
|
+
/** Icône Iconify remplaçant le point de notification */
|
|
29
|
+
alertIcon?: string
|
|
30
|
+
/** Nombre de notifications — badge numérique (masqué si 0 ou absent) */
|
|
31
|
+
count?: number
|
|
32
|
+
/** Couleur du badge count — token ou hex (défaut : couleur de alert, sinon negative) */
|
|
33
|
+
countColor?: string
|
|
34
|
+
/** Seuil d'affichage : au-delà, « max+ » (défaut : 99) */
|
|
35
|
+
countMax?: number
|
|
36
|
+
/** Pas de mise en majuscules */
|
|
37
|
+
noCaps?: boolean
|
|
38
|
+
tabindex?: string | number
|
|
39
|
+
/** Désactive le tab */
|
|
40
|
+
disable?: boolean
|
|
41
|
+
/** Classe du contenu interne */
|
|
42
|
+
contentClass?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
46
|
+
alert: false,
|
|
47
|
+
noCaps: false,
|
|
48
|
+
disable: false,
|
|
49
|
+
countMax: 99,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const emit = defineEmits<{ click: [event: MouseEvent]; keydown: [event: KeyboardEvent] }>()
|
|
53
|
+
|
|
54
|
+
const tabs = inject(qTabsKey, null)
|
|
55
|
+
|
|
56
|
+
const resolvedName = props.name ?? `t_${tabSeq++}`
|
|
57
|
+
|
|
58
|
+
const el = ref<HTMLElement | null>(null)
|
|
59
|
+
|
|
60
|
+
onMounted(() => tabs?.register({ name: resolvedName, el }))
|
|
61
|
+
onBeforeUnmount(() => tabs?.unregister(resolvedName))
|
|
62
|
+
|
|
63
|
+
const isActive = computed(() => tabs?.activeName.value === resolvedName)
|
|
64
|
+
|
|
65
|
+
// Roving tabindex (WAI-ARIA) : seul le tab actif est dans le tab order
|
|
66
|
+
const tabIndex = computed(() =>
|
|
67
|
+
props.disable ? -1 : (props.tabindex ?? (isActive.value ? 0 : -1)),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const alertColor = computed(() => (typeof props.alert === "string" ? props.alert : undefined))
|
|
71
|
+
const alertStyle = computed<Record<string, string>>((): Record<string, string> =>
|
|
72
|
+
alertColor.value ? { backgroundColor: colorValue(alertColor.value) } : {},
|
|
73
|
+
)
|
|
74
|
+
const alertIconStyle = computed<Record<string, string>>((): Record<string, string> =>
|
|
75
|
+
alertColor.value ? { color: colorValue(alertColor.value) } : {},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
// Badge numérique (notifications non lues…)
|
|
79
|
+
const hasCount = computed(() => typeof props.count === "number" && props.count > 0)
|
|
80
|
+
const countDisplay = computed(() => {
|
|
81
|
+
if (!hasCount.value) return ""
|
|
82
|
+
return props.count! > props.countMax ? props.countMax + "+" : String(props.count)
|
|
83
|
+
})
|
|
84
|
+
const countColor = computed(() =>
|
|
85
|
+
props.countColor ?? (typeof props.alert === "string" ? props.alert : "negative"),
|
|
86
|
+
)
|
|
87
|
+
const countStyle = computed<Record<string, string>>(() => ({
|
|
88
|
+
backgroundColor: colorValue(countColor.value),
|
|
89
|
+
}))
|
|
90
|
+
|
|
91
|
+
const tabClasses = computed(() =>
|
|
92
|
+
cn(
|
|
93
|
+
"q-tab",
|
|
94
|
+
isActive.value && "q-tab--active",
|
|
95
|
+
isActive.value && tabs?.activeClass.value,
|
|
96
|
+
(props.noCaps || tabs?.noCaps.value) && "q-tab--no-caps",
|
|
97
|
+
props.disable && "q-tab--disabled",
|
|
98
|
+
tabs?.inlineLabel.value && "q-tab--inline-label",
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const activate = () => {
|
|
103
|
+
if (!props.disable) tabs?.setActive(resolvedName)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const onClick = (e: MouseEvent) => {
|
|
107
|
+
activate()
|
|
108
|
+
emit("click", e)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Clavier (WAI-ARIA) : Entrée/Espace active, flèches/Home/End déplacent le focus
|
|
112
|
+
const onKeydown = (e: KeyboardEvent) => {
|
|
113
|
+
emit("keydown", e)
|
|
114
|
+
if (e.defaultPrevented || props.disable) return
|
|
115
|
+
|
|
116
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
117
|
+
e.preventDefault()
|
|
118
|
+
activate()
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const root = el.value?.parentElement
|
|
123
|
+
if (!root) return
|
|
124
|
+
const list = Array.from(root.querySelectorAll(".q-tab")) as HTMLElement[]
|
|
125
|
+
const index = list.indexOf(el.value as HTMLElement)
|
|
126
|
+
let target: HTMLElement | undefined
|
|
127
|
+
if (e.key === "ArrowRight") target = list[(index + 1) % list.length]
|
|
128
|
+
else if (e.key === "ArrowLeft") target = list[(index - 1 + list.length) % list.length]
|
|
129
|
+
else if (e.key === "Home") target = list[0]
|
|
130
|
+
else if (e.key === "End") target = list[list.length - 1]
|
|
131
|
+
if (target) {
|
|
132
|
+
e.preventDefault()
|
|
133
|
+
target.focus()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<template>
|
|
139
|
+
<div
|
|
140
|
+
ref="el"
|
|
141
|
+
role="tab"
|
|
142
|
+
:aria-selected="isActive ? 'true' : 'false'"
|
|
143
|
+
:aria-disabled="disable || undefined"
|
|
144
|
+
:tabindex="tabIndex"
|
|
145
|
+
class="q-tab"
|
|
146
|
+
:class="tabClasses"
|
|
147
|
+
@click="onClick"
|
|
148
|
+
@keydown="onKeydown"
|
|
149
|
+
>
|
|
150
|
+
<div class="q-tab__content" :class="contentClass">
|
|
151
|
+
<Icon :icon="icon" v-if="icon" class="q-tab__icon" aria-hidden="true" />
|
|
152
|
+
<span v-if="label !== undefined" class="q-tab__label">{{ label }}</span>
|
|
153
|
+
<slot v-else />
|
|
154
|
+
<span
|
|
155
|
+
v-if="alert && !alertIcon && !hasCount"
|
|
156
|
+
class="q-tab__alert"
|
|
157
|
+
:style="alertStyle"
|
|
158
|
+
aria-label="Notification"
|
|
159
|
+
/>
|
|
160
|
+
<span
|
|
161
|
+
v-if="hasCount && !alertIcon"
|
|
162
|
+
class="q-tab__count"
|
|
163
|
+
:style="countStyle"
|
|
164
|
+
:aria-label="countDisplay + ' notifications'"
|
|
165
|
+
>{{ countDisplay }}</span>
|
|
166
|
+
<Icon
|
|
167
|
+
:icon="alertIcon"
|
|
168
|
+
v-if="alertIcon"
|
|
169
|
+
class="q-tab__alert-icon"
|
|
170
|
+
:style="alertIconStyle"
|
|
171
|
+
aria-hidden="true"
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</template>
|