@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,177 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// QBottomSheet — bottom sheet type Drawer shadcn-vue (vaul), style Quasar :
|
|
3
|
+
// <q-bottom-sheet v-model="open"> + slot trigger / QBottomSheetHeader / QBottomSheetFooter
|
|
4
|
+
// Drag vers le bas pour fermer, backdrop, Échap, safe-area bottom.
|
|
5
|
+
import type { InjectionKey, Ref } from "vue"
|
|
6
|
+
|
|
7
|
+
export interface BottomSheetContext {
|
|
8
|
+
open: Readonly<Ref<boolean>>
|
|
9
|
+
setOpen: (v: boolean) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const qBottomSheetKey: InjectionKey<BottomSheetContext> = Symbol("q-bottom-sheet")
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from "vue"
|
|
17
|
+
import { cn } from "../lib/utils"
|
|
18
|
+
import { useOverlayBack } from "../lib/overlayBack"
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
/** Ouvert (v-model) */
|
|
22
|
+
modelValue?: boolean
|
|
23
|
+
/** Ne se ferme ni au backdrop ni à Échap */
|
|
24
|
+
persistent?: boolean
|
|
25
|
+
/** Largeur max du panneau */
|
|
26
|
+
width?: string
|
|
27
|
+
/** Hauteur du panneau (sinon max-height 90vh) */
|
|
28
|
+
height?: string
|
|
29
|
+
/** Arrondi des coins hauts : true | false | valeur CSS */
|
|
30
|
+
rounded?: boolean | string
|
|
31
|
+
dark?: boolean
|
|
32
|
+
/** Fond translucide (frosted glass) : true = 70%, ou valeur % */
|
|
33
|
+
translucent?: boolean | number
|
|
34
|
+
/** Seuil de drag (px) au-delà duquel on ferme */
|
|
35
|
+
dragThreshold?: number
|
|
36
|
+
/** Style du panneau (variables de thème pour les contenus téléportés) */
|
|
37
|
+
contentStyle?: Record<string, string>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
41
|
+
modelValue: false,
|
|
42
|
+
persistent: false,
|
|
43
|
+
width: "640px",
|
|
44
|
+
rounded: true,
|
|
45
|
+
dark: false,
|
|
46
|
+
dragThreshold: 80,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const emit = defineEmits<{ "update:modelValue": [value: boolean] }>()
|
|
50
|
+
|
|
51
|
+
const open = computed({
|
|
52
|
+
get: () => props.modelValue,
|
|
53
|
+
set: (v) => emit("update:modelValue", v),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
provide<BottomSheetContext>(qBottomSheetKey, {
|
|
57
|
+
open,
|
|
58
|
+
setOpen: (v) => {
|
|
59
|
+
open.value = v
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// « Retour » navigateur → ferme le bottom sheet au lieu de naviguer
|
|
64
|
+
useOverlayBack(open, () => { open.value = false }, "QBottomSheet")
|
|
65
|
+
|
|
66
|
+
// Échap + verrouillage du scroll du body
|
|
67
|
+
const onDocKeydown = (e: KeyboardEvent) => {
|
|
68
|
+
if (e.key === "Escape" && open.value && !props.persistent) open.value = false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
watch(open, (v) => {
|
|
72
|
+
if (typeof document !== "undefined") document.body.style.overflow = v ? "hidden" : ""
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
onMounted(() => {
|
|
76
|
+
if (typeof document !== "undefined") document.addEventListener("keydown", onDocKeydown)
|
|
77
|
+
})
|
|
78
|
+
onBeforeUnmount(() => {
|
|
79
|
+
if (typeof document !== "undefined") {
|
|
80
|
+
document.removeEventListener("keydown", onDocKeydown)
|
|
81
|
+
document.body.style.overflow = ""
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// — Drag to dismiss (pattern vaul) —
|
|
86
|
+
const panelRef = ref<HTMLElement | null>(null)
|
|
87
|
+
const dragging = ref(false)
|
|
88
|
+
let startY = 0
|
|
89
|
+
let currentDy = 0
|
|
90
|
+
|
|
91
|
+
const onHandleDown = (e: PointerEvent) => {
|
|
92
|
+
;(e.target as HTMLElement).setPointerCapture(e.pointerId)
|
|
93
|
+
startY = e.clientY
|
|
94
|
+
currentDy = 0
|
|
95
|
+
dragging.value = true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const onHandleMove = (e: PointerEvent) => {
|
|
99
|
+
if (!dragging.value) return
|
|
100
|
+
currentDy = Math.max(0, e.clientY - startY)
|
|
101
|
+
if (panelRef.value) panelRef.value.style.transform = `translateY(${currentDy}px)`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const onHandleUp = () => {
|
|
105
|
+
if (!dragging.value) return
|
|
106
|
+
dragging.value = false
|
|
107
|
+
if (currentDy > props.dragThreshold) open.value = false
|
|
108
|
+
else if (panelRef.value) panelRef.value.style.transform = ""
|
|
109
|
+
currentDy = 0
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const panelStyle = computed<Record<string, string>>(() => ({
|
|
113
|
+
maxWidth: props.width,
|
|
114
|
+
...(props.height ? { height: props.height } : {}),
|
|
115
|
+
}))
|
|
116
|
+
|
|
117
|
+
const radiusStyle = computed<Record<string, string> | undefined>(() => {
|
|
118
|
+
if (props.rounded === false) return { borderRadius: "0" }
|
|
119
|
+
if (typeof props.rounded === "string") return { borderRadius: props.rounded }
|
|
120
|
+
return undefined
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const panelClasses = computed(() =>
|
|
124
|
+
cn(
|
|
125
|
+
"q-bottom-sheet__panel",
|
|
126
|
+
props.rounded === false && "q-bottom-sheet__panel--square",
|
|
127
|
+
props.dark && "q-bottom-sheet__panel--dark",
|
|
128
|
+
(props.translucent === true || typeof props.translucent === "number") && "q-bottom-sheet__panel--translucent",
|
|
129
|
+
dragging.value && "q-bottom-sheet__panel--dragging",
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
const translucentStyle = computed<Record<string, string> | undefined>(() =>
|
|
134
|
+
props.translucent === true || typeof props.translucent === "number"
|
|
135
|
+
? { "--q-translucent-opacity": `${typeof props.translucent === "number" ? props.translucent : 70}%` }
|
|
136
|
+
: undefined,
|
|
137
|
+
)
|
|
138
|
+
</script>
|
|
139
|
+
|
|
140
|
+
<template>
|
|
141
|
+
<span v-if="$slots.trigger" class="q-bottom-sheet__trigger" @click="open = true">
|
|
142
|
+
<slot name="trigger" />
|
|
143
|
+
</span>
|
|
144
|
+
|
|
145
|
+
<Teleport to="body">
|
|
146
|
+
<Transition name="q-bs">
|
|
147
|
+
<div
|
|
148
|
+
v-if="open"
|
|
149
|
+
class="q-bottom-sheet__overlay"
|
|
150
|
+
@click="!persistent && (open = false)"
|
|
151
|
+
>
|
|
152
|
+
<div
|
|
153
|
+
ref="panelRef"
|
|
154
|
+
class="q-bottom-sheet__panel"
|
|
155
|
+
:class="panelClasses"
|
|
156
|
+
:style="[panelStyle, radiusStyle, translucentStyle, props.contentStyle]"
|
|
157
|
+
role="dialog"
|
|
158
|
+
aria-modal="true"
|
|
159
|
+
@click.stop
|
|
160
|
+
>
|
|
161
|
+
<div
|
|
162
|
+
class="q-bottom-sheet__handle"
|
|
163
|
+
@pointerdown="onHandleDown"
|
|
164
|
+
@pointermove="onHandleMove"
|
|
165
|
+
@pointerup="onHandleUp"
|
|
166
|
+
@pointercancel="onHandleUp"
|
|
167
|
+
>
|
|
168
|
+
<span class="q-bottom-sheet__handle-bar" />
|
|
169
|
+
</div>
|
|
170
|
+
<div class="q-bottom-sheet__body">
|
|
171
|
+
<slot />
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</Transition>
|
|
176
|
+
</Teleport>
|
|
177
|
+
</template>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QBottomSheetHeader — header du panneau : titre + description + bouton fermer (équivalent DrawerHeader/DrawerTitle).
|
|
3
|
+
import { inject } from "vue"
|
|
4
|
+
import { Icon } from "@iconify/vue"
|
|
5
|
+
import { icons } from "../lib/icons"
|
|
6
|
+
import { qBottomSheetKey } from "./QBottomSheet.vue"
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
title?: string
|
|
10
|
+
description?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const props = defineProps<Props>()
|
|
14
|
+
const sheet = inject(qBottomSheetKey, null)
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<div class="q-bottom-sheet__header">
|
|
19
|
+
<div class="q-bottom-sheet__header-text">
|
|
20
|
+
<h2 v-if="title" class="q-bottom-sheet__title">{{ title }}</h2>
|
|
21
|
+
<slot name="title" />
|
|
22
|
+
<p v-if="description" class="q-bottom-sheet__description">{{ description }}</p>
|
|
23
|
+
<slot name="description" />
|
|
24
|
+
</div>
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
class="q-bottom-sheet__close"
|
|
28
|
+
aria-label="Fermer"
|
|
29
|
+
@click="sheet?.setOpen(false)"
|
|
30
|
+
>
|
|
31
|
+
<Icon :icon="icons.x" aria-hidden="true" />
|
|
32
|
+
</button>
|
|
33
|
+
<slot />
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QBottomSheetProvider — rend la pile de bottom sheets programmatiques ($q.bottomSheet).
|
|
3
|
+
// Intégré dans QConfigProvider (rendu automatiquement par le plus externe),
|
|
4
|
+
// utilisable aussi en autonome : <q-bottom-sheet-provider><slot /></q-bottom-sheet-provider>.
|
|
5
|
+
import { computed, inject } from "vue"
|
|
6
|
+
import { bottomSheetStack, closeBottomSheet } from "../lib/q"
|
|
7
|
+
import type { BottomSheetController } from "../lib/q"
|
|
8
|
+
import { qConfigKey } from "../lib/config"
|
|
9
|
+
import { themeVarsStyle } from "../lib/themeVars"
|
|
10
|
+
|
|
11
|
+
const sheets = bottomSheetStack
|
|
12
|
+
|
|
13
|
+
const config = inject(qConfigKey, null)
|
|
14
|
+
|
|
15
|
+
// Téléporté au body : on repose les tokens du thème sur le panneau
|
|
16
|
+
const sheetThemeStyle = themeVarsStyle(computed(() => config?.theme.value ?? {}))
|
|
17
|
+
|
|
18
|
+
const handleOk = (ctrl: BottomSheetController, data?: unknown) => {
|
|
19
|
+
ctrl._resolvers.ok?.(data)
|
|
20
|
+
closeBottomSheet(ctrl)
|
|
21
|
+
}
|
|
22
|
+
const handleCancel = (ctrl: BottomSheetController) => {
|
|
23
|
+
ctrl._resolvers.cancel?.()
|
|
24
|
+
closeBottomSheet(ctrl)
|
|
25
|
+
}
|
|
26
|
+
const handleDismiss = (ctrl: BottomSheetController) => {
|
|
27
|
+
ctrl._resolvers.dismiss?.()
|
|
28
|
+
closeBottomSheet(ctrl)
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<slot />
|
|
34
|
+
<q-bottom-sheet
|
|
35
|
+
v-for="(sheet, idx) in sheets"
|
|
36
|
+
:key="idx"
|
|
37
|
+
:model-value="true"
|
|
38
|
+
:persistent="sheet._opts.persistent"
|
|
39
|
+
:width="sheet._opts.width"
|
|
40
|
+
:height="sheet._opts.height"
|
|
41
|
+
:rounded="sheet._opts.rounded"
|
|
42
|
+
:dark="sheet._opts.dark"
|
|
43
|
+
:translucent="sheet._opts.translucent"
|
|
44
|
+
:drag-threshold="sheet._opts.dragThreshold"
|
|
45
|
+
:content-style="sheetThemeStyle"
|
|
46
|
+
@update:model-value="(v: boolean) => !v && handleDismiss(sheet)"
|
|
47
|
+
>
|
|
48
|
+
<q-bottom-sheet-header
|
|
49
|
+
v-if="sheet._opts.title || sheet._opts.description"
|
|
50
|
+
:title="sheet._opts.title"
|
|
51
|
+
:description="sheet._opts.description"
|
|
52
|
+
/>
|
|
53
|
+
<component
|
|
54
|
+
:is="sheet._opts.component"
|
|
55
|
+
v-bind="sheet._opts.componentProps"
|
|
56
|
+
@ok="(data?: unknown) => handleOk(sheet, data)"
|
|
57
|
+
@cancel="handleCancel(sheet)"
|
|
58
|
+
@dismiss="handleDismiss(sheet)"
|
|
59
|
+
@close="handleDismiss(sheet)"
|
|
60
|
+
/>
|
|
61
|
+
</q-bottom-sheet>
|
|
62
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QBottomSheetTrigger — bouton d'ouverture (équivalent DrawerTrigger), alternative au slot #trigger.
|
|
3
|
+
import { inject } from "vue"
|
|
4
|
+
import { qBottomSheetKey } from "./QBottomSheet.vue"
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
label?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const props = defineProps<Props>()
|
|
11
|
+
const sheet = inject(qBottomSheetKey, null)
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<button type="button" class="q-bottom-sheet__trigger-btn" @click="sheet?.setOpen(true)">
|
|
16
|
+
<slot>{{ label }}</slot>
|
|
17
|
+
</button>
|
|
18
|
+
</template>
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QBtn — API Quasar : <q-btn flat dense outline rounded no-caps label="…" icon="…" />
|
|
3
|
+
// Implémentation shadcn-vue : cva (variants) + cn() ; styles dans styles/main.css
|
|
4
|
+
import { computed } from "vue"
|
|
5
|
+
import { Icon } from "@iconify/vue"
|
|
6
|
+
import { cva } from "class-variance-authority"
|
|
7
|
+
import { cn } from "../lib/utils"
|
|
8
|
+
import { radiusStyle, useRadius } from "../lib/useComponentProps"
|
|
9
|
+
import type { RadiusProp } from "../lib/useComponentProps"
|
|
10
|
+
|
|
11
|
+
const btnVariants = cva("q-btn", {
|
|
12
|
+
variants: {
|
|
13
|
+
size: {
|
|
14
|
+
sm: "q-btn--sm",
|
|
15
|
+
md: "q-btn--md",
|
|
16
|
+
lg: "q-btn--lg",
|
|
17
|
+
xl: "q-btn--xl",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: { size: "md" },
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const TOKEN_COLORS = ["primary", "secondary", "accent", "dark", "positive", "negative", "info", "warning"] as const
|
|
24
|
+
const SIZE_TOKENS = ["sm", "md", "lg", "xl"] as const
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
/** Texte du bouton (sinon contenu du slot) */
|
|
28
|
+
label?: string
|
|
29
|
+
/** Icône Iconify à gauche du label (ex. : "lucide:download") */
|
|
30
|
+
icon?: string
|
|
31
|
+
/** Icône Iconify à droite du label */
|
|
32
|
+
iconRight?: string
|
|
33
|
+
/** Couleur : token (primary, secondary, negative…) ou hex (#1976d2) */
|
|
34
|
+
color?: string
|
|
35
|
+
/** Couleur du texte : token ou hex */
|
|
36
|
+
textColor?: string
|
|
37
|
+
/** Taille : token (sm|md|lg|xl) ou valeur CSS ("2rem") */
|
|
38
|
+
size?: string
|
|
39
|
+
type?: "button" | "submit" | "reset"
|
|
40
|
+
/** Rend un lien natif <a> */
|
|
41
|
+
href?: string
|
|
42
|
+
/** Fond transparent, texte coloré */
|
|
43
|
+
flat?: boolean
|
|
44
|
+
/** Fond transparent + bordure */
|
|
45
|
+
outline?: boolean
|
|
46
|
+
/** Supprime l'ombre portée */
|
|
47
|
+
unelevated?: boolean
|
|
48
|
+
/** Hauteur réduite */
|
|
49
|
+
dense?: boolean
|
|
50
|
+
/** Coins arrondis : true = pilule, ou échelle xs|sm|md|lg (none = carré) */
|
|
51
|
+
radius?: RadiusProp
|
|
52
|
+
/** Cercle parfait (bouton icône seule) */
|
|
53
|
+
round?: boolean
|
|
54
|
+
/** Coins droits */
|
|
55
|
+
square?: boolean
|
|
56
|
+
/** Désactive la mise en majuscules */
|
|
57
|
+
noCaps?: boolean
|
|
58
|
+
/** Largeur 100% */
|
|
59
|
+
stretch?: boolean
|
|
60
|
+
/** Affiche un spinner et bloque les clics */
|
|
61
|
+
loading?: boolean
|
|
62
|
+
/** Désactive le bouton */
|
|
63
|
+
disable?: boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
67
|
+
type: "button",
|
|
68
|
+
size: "md",
|
|
69
|
+
color: "primary",
|
|
70
|
+
flat: false,
|
|
71
|
+
outline: false,
|
|
72
|
+
unelevated: false,
|
|
73
|
+
dense: false,
|
|
74
|
+
round: false,
|
|
75
|
+
square: false,
|
|
76
|
+
noCaps: false,
|
|
77
|
+
stretch: false,
|
|
78
|
+
loading: false,
|
|
79
|
+
disable: false,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const isTokenColor = (c?: string) => !!c && (TOKEN_COLORS as readonly string[]).includes(c)
|
|
83
|
+
const isSizeToken = (s?: string) => !!s && (SIZE_TOKENS as readonly string[]).includes(s)
|
|
84
|
+
|
|
85
|
+
const sizeVariant = computed(() =>
|
|
86
|
+
isSizeToken(props.size) ? (props.size as "sm" | "md" | "lg" | "xl") : "md",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
/** Taille CSS libre : passée en variable CSS --q-btn-h */
|
|
90
|
+
const sizeStyle = computed<Record<string, string>>((): Record<string, string> =>
|
|
91
|
+
props.size && !isSizeToken(props.size) ? { "--q-btn-h": props.size } : {},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
/** Couleurs hex libres : passées en variables CSS */
|
|
95
|
+
const colorStyle = computed<Record<string, string>>(() => {
|
|
96
|
+
const style: Record<string, string> = {}
|
|
97
|
+
if (props.color && !isTokenColor(props.color)) style["--q-btn-bg"] = props.color
|
|
98
|
+
if (props.textColor && !isTokenColor(props.textColor)) style["--q-btn-fg"] = props.textColor
|
|
99
|
+
return style
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const isDisabled = computed(() => props.disable || props.loading)
|
|
103
|
+
|
|
104
|
+
// radius : prop explicite > composantProps.QBtn.radius ; échelle → --q-radius
|
|
105
|
+
const effectiveRadius = useRadius("QBtn", () => props.radius)
|
|
106
|
+
const roundedStyle = computed(() => radiusStyle(effectiveRadius.value))
|
|
107
|
+
|
|
108
|
+
const btnClasses = computed(() =>
|
|
109
|
+
cn(
|
|
110
|
+
btnVariants({ size: sizeVariant.value }),
|
|
111
|
+
isTokenColor(props.color) && `q-btn--${props.color}`,
|
|
112
|
+
isTokenColor(props.textColor) && `q-btn--text-${props.textColor}`,
|
|
113
|
+
props.flat && "q-btn--flat",
|
|
114
|
+
props.outline && "q-btn--outline",
|
|
115
|
+
props.unelevated && "q-btn--unelevated",
|
|
116
|
+
props.dense && "q-btn--dense",
|
|
117
|
+
effectiveRadius.value === true && "q-btn--rounded",
|
|
118
|
+
props.round && "q-btn--round",
|
|
119
|
+
props.square && "q-btn--square",
|
|
120
|
+
props.noCaps && "q-btn--no-caps",
|
|
121
|
+
props.stretch && "q-btn--stretch",
|
|
122
|
+
props.loading && "q-btn--loading",
|
|
123
|
+
isDisabled.value && "q-btn--disabled",
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<template>
|
|
129
|
+
<component
|
|
130
|
+
:is="href ? 'a' : 'button'"
|
|
131
|
+
:href="href || undefined"
|
|
132
|
+
:type="href ? undefined : type"
|
|
133
|
+
:disabled="href ? undefined : isDisabled"
|
|
134
|
+
:aria-disabled="isDisabled || undefined"
|
|
135
|
+
:class="btnClasses"
|
|
136
|
+
:style="[sizeStyle, colorStyle, roundedStyle]"
|
|
137
|
+
>
|
|
138
|
+
<span v-if="loading" class="q-btn__spinner" aria-hidden="true" />
|
|
139
|
+
<Icon v-else-if="icon" :icon="icon" class="q-btn__icon" aria-hidden="true" />
|
|
140
|
+
<span v-if="label" class="q-btn__label">{{ label }}</span>
|
|
141
|
+
<slot />
|
|
142
|
+
<Icon v-if="iconRight && !loading" :icon="iconRight" class="q-btn__icon" aria-hidden="true" />
|
|
143
|
+
</component>
|
|
144
|
+
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QBtnGroup — API Quasar : <q-btn-group unelevated outline stretch>
|
|
3
|
+
// Groupe de QBtn soudés (bords fusionnés, rayons sur les extrémités).
|
|
4
|
+
interface Props {
|
|
5
|
+
/** Boutons sans ombre */
|
|
6
|
+
unelevated?: boolean
|
|
7
|
+
/** Contour de groupe */
|
|
8
|
+
outline?: boolean
|
|
9
|
+
/** Les boutons s'étendent également */
|
|
10
|
+
stretch?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
14
|
+
unelevated: false,
|
|
15
|
+
outline: false,
|
|
16
|
+
stretch: false,
|
|
17
|
+
})
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<div
|
|
22
|
+
class="q-btn-group"
|
|
23
|
+
:class="{
|
|
24
|
+
'q-btn-group--unelevated': props.unelevated,
|
|
25
|
+
'q-btn-group--outline': props.outline,
|
|
26
|
+
'q-btn-group--stretch': props.stretch,
|
|
27
|
+
}"
|
|
28
|
+
>
|
|
29
|
+
<slot />
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QBubble — bulle de conversation type Bubble shadcn-vue :
|
|
3
|
+
// variants default | secondary | muted | tinted | outline | ghost | destructive, align start|end.
|
|
4
|
+
import { computed } from "vue"
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
import { QPrimitive } from "../lib/primitive"
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/** Traitement visuel */
|
|
10
|
+
variant?: "default" | "secondary" | "muted" | "tinted" | "outline" | "ghost" | "destructive"
|
|
11
|
+
/** Alignement dans la conversation */
|
|
12
|
+
align?: "start" | "end"
|
|
13
|
+
/** Élément/composant rendu */
|
|
14
|
+
as?: string
|
|
15
|
+
/** Fusion sur l'enfant du slot (bouton, lien…) */
|
|
16
|
+
asChild?: boolean
|
|
17
|
+
class?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
21
|
+
variant: "default",
|
|
22
|
+
align: "start",
|
|
23
|
+
as: "div",
|
|
24
|
+
asChild: false,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const classes = computed(() =>
|
|
28
|
+
cn("q-bubble", `q-bubble--${props.variant}`, `q-bubble--align-${props.align}`, props.class),
|
|
29
|
+
)
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<QPrimitive :as="as" :as-child="asChild" :class="classes">
|
|
34
|
+
<slot />
|
|
35
|
+
</QPrimitive>
|
|
36
|
+
</template>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QBubbleContent — contenu de la bulle (équivalent BubbleContent).
|
|
3
|
+
// as-child : le conteneur (bouton/lien) reçoit le style de la bulle.
|
|
4
|
+
import { computed } from "vue"
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
import { QPrimitive } from "../lib/primitive"
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/** Élément/composant rendu */
|
|
10
|
+
as?: string
|
|
11
|
+
/** Fusion sur l'enfant du slot */
|
|
12
|
+
asChild?: boolean
|
|
13
|
+
class?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
17
|
+
as: "div",
|
|
18
|
+
asChild: false,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const classes = computed(() => cn("q-bubble__content", props.class))
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<QPrimitive :as="as" :as-child="asChild" :class="classes">
|
|
26
|
+
<slot />
|
|
27
|
+
</QPrimitive>
|
|
28
|
+
</template>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QBubbleGroup — groupe de bulles consécutives du même expéditeur (équivalent BubbleGroup).
|
|
3
|
+
import { computed } from "vue"
|
|
4
|
+
import { cn } from "../lib/utils"
|
|
5
|
+
import { QPrimitive } from "../lib/primitive"
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
as?: string
|
|
9
|
+
asChild?: boolean
|
|
10
|
+
class?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
14
|
+
as: "div",
|
|
15
|
+
asChild: false,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const classes = computed(() => cn("q-bubble-group", props.class))
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<QPrimitive :as="as" :as-child="asChild" :class="classes">
|
|
23
|
+
<slot />
|
|
24
|
+
</QPrimitive>
|
|
25
|
+
</template>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QBubbleReactions — réactions ancrées au bord de la bulle (équivalent BubbleReactions).
|
|
3
|
+
// side : top | bottom (chevauché) ; align : start | end.
|
|
4
|
+
import { computed } from "vue"
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
import { QPrimitive } from "../lib/primitive"
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/** Côté de la bulle où ancrer les réactions */
|
|
10
|
+
side?: "top" | "bottom"
|
|
11
|
+
/** Alignement inline */
|
|
12
|
+
align?: "start" | "end"
|
|
13
|
+
/** Élément/composant rendu */
|
|
14
|
+
as?: string
|
|
15
|
+
asChild?: boolean
|
|
16
|
+
class?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
20
|
+
side: "bottom",
|
|
21
|
+
align: "end",
|
|
22
|
+
as: "div",
|
|
23
|
+
asChild: false,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const classes = computed(() =>
|
|
27
|
+
cn(
|
|
28
|
+
"q-bubble__reactions",
|
|
29
|
+
props.align === "start" && "q-bubble__reactions--start",
|
|
30
|
+
props.side === "top" && "q-bubble__reactions--top",
|
|
31
|
+
props.class,
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<QPrimitive :as="as" :as-child="asChild" :class="classes">
|
|
38
|
+
<slot />
|
|
39
|
+
</QPrimitive>
|
|
40
|
+
</template>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QCard — API Quasar : <q-card elevated bordered dark square rounded="lg">
|
|
3
|
+
// Pas d'ombre par défaut ; elevated l'ajoute, flat la retire explicitement.
|
|
4
|
+
// Slots : default, media (zone image en haut), actions (zone actions en bas)
|
|
5
|
+
import { computed } from "vue"
|
|
6
|
+
import { radiusStyle, useRadius } from "../lib/useComponentProps"
|
|
7
|
+
import type { RadiusProp } from "../lib/useComponentProps"
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
/** Ajoute l'ombre portée (défaut : aucune) */
|
|
11
|
+
elevated?: boolean
|
|
12
|
+
/** Retire explicitement l'ombre */
|
|
13
|
+
flat?: boolean
|
|
14
|
+
/** Thème sombre */
|
|
15
|
+
dark?: boolean
|
|
16
|
+
/** Bordure fine */
|
|
17
|
+
bordered?: boolean
|
|
18
|
+
/** Coins droits */
|
|
19
|
+
square?: boolean
|
|
20
|
+
/** Coins arrondis : true = md, ou échelle xs|sm|md|lg (none = carré) */
|
|
21
|
+
radius?: RadiusProp
|
|
22
|
+
/** Glassmorphism : fond translucide + flou d'arrière-plan (backdrop-filter) */
|
|
23
|
+
glass?: boolean
|
|
24
|
+
/** Animation au survol : élévation + ombre renforcée */
|
|
25
|
+
hover?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
29
|
+
elevated: false,
|
|
30
|
+
flat: false,
|
|
31
|
+
dark: false,
|
|
32
|
+
bordered: false,
|
|
33
|
+
square: false,
|
|
34
|
+
glass: false,
|
|
35
|
+
hover: false,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// radius : prop explicite > composantProps.QCard.radius ; échelle → --q-radius
|
|
39
|
+
const effectiveRadius = useRadius("QCard", () => props.radius)
|
|
40
|
+
const roundedStyle = computed(() => radiusStyle(effectiveRadius.value))
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<div
|
|
45
|
+
class="q-card"
|
|
46
|
+
:class="{
|
|
47
|
+
'q-card--elevated': props.elevated,
|
|
48
|
+
'q-card--flat': props.flat,
|
|
49
|
+
'q-card--dark': props.dark,
|
|
50
|
+
'q-card--bordered': props.bordered,
|
|
51
|
+
'q-card--square': props.square,
|
|
52
|
+
'q-card--glass': props.glass,
|
|
53
|
+
'q-card--hover': props.hover,
|
|
54
|
+
}"
|
|
55
|
+
:style="roundedStyle"
|
|
56
|
+
>
|
|
57
|
+
<slot name="media" />
|
|
58
|
+
<slot />
|
|
59
|
+
<div v-if="$slots.actions" class="q-card__actions-slot">
|
|
60
|
+
<slot name="actions" />
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</template>
|