@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,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QSeparator — API Quasar : <q-separator vertical inset spaced dark>
|
|
3
|
+
// Équivalent du Separator shadcn-vue (reka-ui) : div role="separator".
|
|
4
|
+
interface Props {
|
|
5
|
+
/** Séparateur vertical (sinon horizontal) */
|
|
6
|
+
vertical?: boolean
|
|
7
|
+
/** Marge intérieure (16px) */
|
|
8
|
+
inset?: boolean
|
|
9
|
+
/** Espacement (8px) autour */
|
|
10
|
+
spaced?: boolean
|
|
11
|
+
/** Thème sombre */
|
|
12
|
+
dark?: boolean
|
|
13
|
+
/** Décoration pure : pas de rôle ARIA (role="none") */
|
|
14
|
+
decorative?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
18
|
+
vertical: false,
|
|
19
|
+
inset: false,
|
|
20
|
+
spaced: false,
|
|
21
|
+
dark: false,
|
|
22
|
+
decorative: false,
|
|
23
|
+
})
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<div
|
|
28
|
+
class="q-separator"
|
|
29
|
+
:class="{
|
|
30
|
+
'q-separator--horizontal': !props.vertical,
|
|
31
|
+
'q-separator--vertical': props.vertical,
|
|
32
|
+
'q-separator--inset': props.inset,
|
|
33
|
+
'q-separator--spaced': props.spaced,
|
|
34
|
+
'q-separator--dark': props.dark,
|
|
35
|
+
}"
|
|
36
|
+
:role="props.decorative ? 'none' : 'separator'"
|
|
37
|
+
:aria-orientation="props.vertical ? 'vertical' : 'horizontal'"
|
|
38
|
+
/>
|
|
39
|
+
</template>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// QSidebar — panneau latéral type Sidebar shadcn-vue, API QDrawer Quasar :
|
|
3
|
+
// <q-sidebar v-model="open" side="left" width="260px" bordered show-if-above breakpoint="1023">
|
|
4
|
+
// Fournit le contexte (toggle) aux QSidebarTrigger enfants.
|
|
5
|
+
import type { InjectionKey, Ref } from "vue"
|
|
6
|
+
|
|
7
|
+
export interface SidebarContext {
|
|
8
|
+
open: Readonly<Ref<boolean>>
|
|
9
|
+
setOpen: (v: boolean) => void
|
|
10
|
+
toggle: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const qSidebarKey: InjectionKey<SidebarContext> = Symbol("q-sidebar")
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import { computed, onBeforeUnmount, onMounted, provide, ref } from "vue"
|
|
18
|
+
import type { StyleValue } from "vue"
|
|
19
|
+
import { cn } from "../lib/utils"
|
|
20
|
+
import { useOverlayBack } from "../lib/overlayBack"
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
/** Ouvert (v-model) — mode offcanvas ; ignoré en mode statique */
|
|
24
|
+
modelValue?: boolean
|
|
25
|
+
/** Côté : left | right */
|
|
26
|
+
side?: "left" | "right"
|
|
27
|
+
/** Largeur (défaut 260px) */
|
|
28
|
+
width?: string
|
|
29
|
+
/** Hauteur du panneau (ex. "calc(100vh - 64px)", "80vh") — sinon pleine hauteur */
|
|
30
|
+
height?: string
|
|
31
|
+
/** Hauteur max — le contenu scrolle (ex. "70vh") */
|
|
32
|
+
maxHeight?: string
|
|
33
|
+
/** Bordure côté intérieur */
|
|
34
|
+
bordered?: boolean
|
|
35
|
+
/** Ombre portée */
|
|
36
|
+
elevated?: boolean
|
|
37
|
+
/** Thème sombre */
|
|
38
|
+
dark?: boolean
|
|
39
|
+
/** Toujours visible au-delà du breakpoint (mode statique) */
|
|
40
|
+
showIfAbove?: boolean
|
|
41
|
+
/** Mode statique : reste collé en haut pendant le scroll de la page (position: sticky) */
|
|
42
|
+
sticky?: boolean
|
|
43
|
+
/** Breakpoint de bascule statique/offcanvas (px) */
|
|
44
|
+
breakpoint?: number
|
|
45
|
+
/** Styles CSS additionnels appliqués au panneau */
|
|
46
|
+
style?: StyleValue
|
|
47
|
+
/** Classe(s) additionnelle(s) appliquée(s) au panneau */
|
|
48
|
+
class?: string
|
|
49
|
+
disable?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
53
|
+
modelValue: true,
|
|
54
|
+
side: "left",
|
|
55
|
+
width: "260px",
|
|
56
|
+
height: "",
|
|
57
|
+
maxHeight: "",
|
|
58
|
+
bordered: false,
|
|
59
|
+
elevated: false,
|
|
60
|
+
dark: false,
|
|
61
|
+
showIfAbove: false,
|
|
62
|
+
sticky: false,
|
|
63
|
+
breakpoint: 1023,
|
|
64
|
+
style: undefined,
|
|
65
|
+
class: "",
|
|
66
|
+
disable: false,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const emit = defineEmits<{ "update:modelValue": [value: boolean] }>()
|
|
70
|
+
|
|
71
|
+
// — Mode statique (viewport >= breakpoint && show-if-above) —
|
|
72
|
+
const isAbove = ref(false)
|
|
73
|
+
let mql: MediaQueryList | null = null
|
|
74
|
+
|
|
75
|
+
const updateMql = () => {
|
|
76
|
+
isAbove.value = mql?.matches ?? false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
onMounted(() => {
|
|
80
|
+
if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
|
|
81
|
+
mql = window.matchMedia(`(min-width: ${props.breakpoint}px)`)
|
|
82
|
+
updateMql()
|
|
83
|
+
mql.addEventListener("change", updateMql)
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
onBeforeUnmount(() => {
|
|
87
|
+
mql?.removeEventListener("change", updateMql)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const isStatic = computed(() => props.showIfAbove && isAbove.value && !props.disable)
|
|
91
|
+
const open = computed(() => (isStatic.value ? true : (props.modelValue ?? true)))
|
|
92
|
+
|
|
93
|
+
const setOpen = (v: boolean) => {
|
|
94
|
+
if (!isStatic.value) emit("update:modelValue", v)
|
|
95
|
+
}
|
|
96
|
+
const toggle = () => setOpen(!open.value)
|
|
97
|
+
|
|
98
|
+
provide<SidebarContext>(qSidebarKey, { open, setOpen, toggle })
|
|
99
|
+
|
|
100
|
+
// « Retour » navigateur → ferme la sidebar (mode offcanvas uniquement)
|
|
101
|
+
const overlayOpen = computed(() => !isStatic.value && open.value)
|
|
102
|
+
useOverlayBack(overlayOpen, () => setOpen(false), "QSidebar")
|
|
103
|
+
|
|
104
|
+
// Échap ferme en offcanvas
|
|
105
|
+
const onDocKeydown = (e: KeyboardEvent) => {
|
|
106
|
+
if (e.key === "Escape" && !isStatic.value && open.value) setOpen(false)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
onMounted(() => {
|
|
110
|
+
if (typeof document !== "undefined") document.addEventListener("keydown", onDocKeydown)
|
|
111
|
+
})
|
|
112
|
+
onBeforeUnmount(() => {
|
|
113
|
+
if (typeof document !== "undefined") document.removeEventListener("keydown", onDocKeydown)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const rootClasses = computed(() =>
|
|
117
|
+
cn(
|
|
118
|
+
"q-sidebar",
|
|
119
|
+
isStatic.value ? "q-sidebar--static" : "q-sidebar--offcanvas",
|
|
120
|
+
!isStatic.value && open.value && "q-sidebar--open",
|
|
121
|
+
props.side === "right" && "q-sidebar--right",
|
|
122
|
+
props.sticky && "q-sidebar--sticky",
|
|
123
|
+
props.bordered && "q-sidebar--bordered",
|
|
124
|
+
props.elevated && "q-sidebar--elevated",
|
|
125
|
+
props.dark && "q-sidebar--dark",
|
|
126
|
+
props.class,
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const rootStyle = computed<Record<string, string>>(() => {
|
|
131
|
+
const style: Record<string, string> = { "--q-sidebar-w": props.width }
|
|
132
|
+
if (props.height) style.height = props.height
|
|
133
|
+
if (props.maxHeight) style.maxHeight = props.maxHeight
|
|
134
|
+
return style
|
|
135
|
+
})
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<template>
|
|
139
|
+
<aside
|
|
140
|
+
class="q-sidebar"
|
|
141
|
+
:class="rootClasses"
|
|
142
|
+
:style="[rootStyle, props.style]"
|
|
143
|
+
:aria-hidden="!isStatic && !open ? 'true' : undefined"
|
|
144
|
+
>
|
|
145
|
+
<slot />
|
|
146
|
+
</aside>
|
|
147
|
+
<div
|
|
148
|
+
v-if="!isStatic && open && !disable"
|
|
149
|
+
class="q-sidebar__backdrop"
|
|
150
|
+
aria-hidden="true"
|
|
151
|
+
@click="setOpen(false)"
|
|
152
|
+
/>
|
|
153
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QSidebarContent — zone centrale scrollable du QSidebar (équivalent SidebarContent).
|
|
3
|
+
interface Props {
|
|
4
|
+
class?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const props = defineProps<Props>()
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<div class="q-sidebar__content" :class="props.class">
|
|
12
|
+
<slot />
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QSidebarFooter — zone basse du QSidebar (équivalent SidebarFooter).
|
|
3
|
+
interface Props {
|
|
4
|
+
class?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const props = defineProps<Props>()
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<div class="q-sidebar__footer" :class="props.class">
|
|
12
|
+
<slot />
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QSidebarHeader — zone haute du QSidebar (équivalent SidebarHeader).
|
|
3
|
+
interface Props {
|
|
4
|
+
class?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const props = defineProps<Props>()
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<div class="q-sidebar__header" :class="props.class">
|
|
12
|
+
<slot />
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QSidebarMenuButton — bouton/lien de menu (équivalent SidebarMenuButton).
|
|
3
|
+
import { computed } from "vue"
|
|
4
|
+
import { Icon } from "@iconify/vue"
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
label?: string
|
|
9
|
+
/** Icône Iconify (ex. : "lucide:settings") */
|
|
10
|
+
icon?: string
|
|
11
|
+
/** Lien natif (<a>) ; sinon bouton */
|
|
12
|
+
href?: string
|
|
13
|
+
/** État actif */
|
|
14
|
+
active?: boolean
|
|
15
|
+
activeClass?: string
|
|
16
|
+
disable?: boolean
|
|
17
|
+
/** Badge affiché à droite */
|
|
18
|
+
badge?: string | number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
22
|
+
active: false,
|
|
23
|
+
activeClass: "",
|
|
24
|
+
disable: false,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const emit = defineEmits<{ click: [event: MouseEvent] }>()
|
|
28
|
+
|
|
29
|
+
const buttonClasses = computed(() =>
|
|
30
|
+
cn(
|
|
31
|
+
"q-sidebar__menu-button",
|
|
32
|
+
props.active && "q-sidebar__menu-button--active",
|
|
33
|
+
props.active && props.activeClass,
|
|
34
|
+
props.disable && "q-sidebar__menu-button--disabled",
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const onClick = (e: MouseEvent) => {
|
|
39
|
+
if (props.disable) return
|
|
40
|
+
emit("click", e)
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<component
|
|
46
|
+
:is="href ? 'a' : 'button'"
|
|
47
|
+
:href="href || undefined"
|
|
48
|
+
:disabled="href ? undefined : disable"
|
|
49
|
+
class="q-sidebar__menu-button"
|
|
50
|
+
:class="buttonClasses"
|
|
51
|
+
:aria-current="active ? 'page' : undefined"
|
|
52
|
+
@click="onClick"
|
|
53
|
+
>
|
|
54
|
+
<Icon :icon="icon" v-if="icon" class="q-sidebar__menu-button-icon" aria-hidden="true" />
|
|
55
|
+
<span v-if="label" class="q-sidebar__menu-button-label">{{ label }}</span>
|
|
56
|
+
<slot v-else />
|
|
57
|
+
<span v-if="badge !== undefined" class="q-sidebar__menu-badge">{{ badge }}</span>
|
|
58
|
+
</component>
|
|
59
|
+
</template>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QSidebarTrigger — bouton bascule du QSidebar (équivalent SidebarTrigger).
|
|
3
|
+
// À placer n'importe où dans le sous-arbre d'un QSidebar.
|
|
4
|
+
import { inject } from "vue"
|
|
5
|
+
import { Icon } from "@iconify/vue"
|
|
6
|
+
import { icons } from "../lib/icons"
|
|
7
|
+
import { qSidebarKey } from "./QSidebar.vue"
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
/** Libellé accessible */
|
|
11
|
+
label?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
15
|
+
label: "Ouvrir/fermer la barre latérale",
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const sidebar = inject(qSidebarKey, null)
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
class="q-sidebar__trigger"
|
|
25
|
+
:aria-label="label"
|
|
26
|
+
@click="sidebar?.toggle()"
|
|
27
|
+
>
|
|
28
|
+
<slot>
|
|
29
|
+
<Icon :icon="icons.menu" aria-hidden="true" />
|
|
30
|
+
</slot>
|
|
31
|
+
</button>
|
|
32
|
+
</template>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QSkeleton — API Quasar : <q-skeleton type="QBtn" animation="pulse" square dark size width height>
|
|
3
|
+
// Équivalent shadcn-vue (div animée) + types composants rendus en formes CSS.
|
|
4
|
+
import { computed } from "vue"
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
|
|
7
|
+
type SkeletonType =
|
|
8
|
+
| "text"
|
|
9
|
+
| "rect"
|
|
10
|
+
| "circle"
|
|
11
|
+
| "QBtn"
|
|
12
|
+
| "QAvatar"
|
|
13
|
+
| "QBadge"
|
|
14
|
+
| "QChip"
|
|
15
|
+
| "QInput"
|
|
16
|
+
| "QList"
|
|
17
|
+
| "QToolbar"
|
|
18
|
+
| "QToggle"
|
|
19
|
+
| "QCheckbox"
|
|
20
|
+
| "QRadio"
|
|
21
|
+
| "QSlider"
|
|
22
|
+
|
|
23
|
+
type SkeletonAnimation = "wave" | "pulse" | "pulse-x" | "pulse-y" | "fade" | "blink" | "none"
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
/** Forme : text, rect, circle ou un type de composant (QBtn, QInput…) */
|
|
27
|
+
type?: SkeletonType
|
|
28
|
+
animation?: SkeletonAnimation
|
|
29
|
+
/** Coins droits */
|
|
30
|
+
square?: boolean
|
|
31
|
+
/** Thème sombre */
|
|
32
|
+
dark?: boolean
|
|
33
|
+
/** Taille : circle → diamètre, text → font-size */
|
|
34
|
+
size?: string
|
|
35
|
+
width?: string
|
|
36
|
+
height?: string
|
|
37
|
+
style?: string
|
|
38
|
+
class?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
42
|
+
type: "text",
|
|
43
|
+
animation: "wave",
|
|
44
|
+
square: false,
|
|
45
|
+
dark: false,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const isSimple = computed(
|
|
49
|
+
() => props.type === "text" || props.type === "rect" || props.type === "circle",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const skeletonStyle = computed<Record<string, string>>(() => {
|
|
53
|
+
const style: Record<string, string> = {}
|
|
54
|
+
if (props.width) style.width = props.width
|
|
55
|
+
if (props.height) style.height = props.height
|
|
56
|
+
if (props.type === "circle" && props.size) {
|
|
57
|
+
style.width = props.size
|
|
58
|
+
style.height = props.size
|
|
59
|
+
}
|
|
60
|
+
else if (props.type === "text" && props.size) {
|
|
61
|
+
style.fontSize = props.size
|
|
62
|
+
}
|
|
63
|
+
return style
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const skeletonClasses = computed(() =>
|
|
67
|
+
cn(
|
|
68
|
+
"q-skeleton",
|
|
69
|
+
`q-skeleton--${props.type}`,
|
|
70
|
+
`q-skeleton--${props.animation}`,
|
|
71
|
+
props.square && "q-skeleton--square",
|
|
72
|
+
props.dark && "q-skeleton--dark",
|
|
73
|
+
props.class,
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<template>
|
|
79
|
+
<div :class="skeletonClasses" :style="[skeletonStyle, props.style]">
|
|
80
|
+
<template v-if="type === 'QBtn'">
|
|
81
|
+
<span class="q-skeleton__bar" style="width: 60%" />
|
|
82
|
+
</template>
|
|
83
|
+
<template v-else-if="type === 'QList'">
|
|
84
|
+
<span class="q-skeleton__line" />
|
|
85
|
+
<span class="q-skeleton__line" style="width: 80%" />
|
|
86
|
+
<span class="q-skeleton__line" style="width: 55%" />
|
|
87
|
+
</template>
|
|
88
|
+
<template v-else-if="type === 'QToolbar'">
|
|
89
|
+
<span class="q-skeleton__dot" />
|
|
90
|
+
<span class="q-skeleton__bar" style="width: 40%" />
|
|
91
|
+
<span class="q-skeleton__bar" style="width: 25%" />
|
|
92
|
+
</template>
|
|
93
|
+
<slot />
|
|
94
|
+
</div>
|
|
95
|
+
</template>
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QSlider — curseur : <q-slider v-model="val" :min="0" :max="100" step="5" label markers />
|
|
3
|
+
// Drag pointer (track + thumb), clavier (flèches/Home/End), snap au step, bulle de valeur.
|
|
4
|
+
import { computed, ref } from "vue"
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
import { colorValue } from "../lib/colors"
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
modelValue?: number
|
|
10
|
+
min?: number
|
|
11
|
+
max?: number
|
|
12
|
+
/** Pas de progression (défaut 1) */
|
|
13
|
+
step?: number
|
|
14
|
+
/** Le curseur saute sur les steps pendant le drag (sinon au relâchement) */
|
|
15
|
+
snap?: boolean
|
|
16
|
+
/** Bulle de valeur pendant le drag */
|
|
17
|
+
label?: boolean
|
|
18
|
+
/** Bulle toujours visible */
|
|
19
|
+
labelAlways?: boolean
|
|
20
|
+
/** Points sur les steps */
|
|
21
|
+
markers?: boolean
|
|
22
|
+
vertical?: boolean
|
|
23
|
+
reverse?: boolean
|
|
24
|
+
disable?: boolean
|
|
25
|
+
readonly?: boolean
|
|
26
|
+
color?: string
|
|
27
|
+
trackColor?: string
|
|
28
|
+
thumbColor?: string
|
|
29
|
+
dense?: boolean
|
|
30
|
+
dark?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
34
|
+
modelValue: 0,
|
|
35
|
+
min: 0,
|
|
36
|
+
max: 100,
|
|
37
|
+
step: 1,
|
|
38
|
+
snap: false,
|
|
39
|
+
label: false,
|
|
40
|
+
labelAlways: false,
|
|
41
|
+
markers: false,
|
|
42
|
+
vertical: false,
|
|
43
|
+
reverse: false,
|
|
44
|
+
disable: false,
|
|
45
|
+
readonly: false,
|
|
46
|
+
color: "primary",
|
|
47
|
+
trackColor: "",
|
|
48
|
+
thumbColor: "",
|
|
49
|
+
dense: false,
|
|
50
|
+
dark: false,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const emit = defineEmits<{ "update:modelValue": [value: number]; change: [value: number] }>()
|
|
54
|
+
|
|
55
|
+
const trackEl = ref<HTMLElement | null>(null)
|
|
56
|
+
const dragging = ref(false)
|
|
57
|
+
const dragValue = ref<number | null>(null)
|
|
58
|
+
|
|
59
|
+
const snapped = (v: number): number => {
|
|
60
|
+
const steps = Math.round((v - props.min) / props.step)
|
|
61
|
+
return Math.min(props.max, Math.max(props.min, props.min + steps * props.step))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const displayValue = computed(() => {
|
|
65
|
+
if (dragging.value && !props.snap && dragValue.value !== null) return dragValue.value
|
|
66
|
+
return snapped(props.modelValue ?? 0)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const pct = computed(() => {
|
|
70
|
+
const span = props.max - props.min
|
|
71
|
+
if (span <= 0) return 0
|
|
72
|
+
return ((displayValue.value - props.min) / span) * 100
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const valueFromPos = (clientX: number, clientY: number): number => {
|
|
76
|
+
const el = trackEl.value
|
|
77
|
+
if (!el) return props.modelValue ?? 0
|
|
78
|
+
const r = el.getBoundingClientRect()
|
|
79
|
+
let ratio = props.vertical
|
|
80
|
+
? 1 - (clientY - r.top) / Math.max(1, r.height)
|
|
81
|
+
: (clientX - r.left) / Math.max(1, r.width)
|
|
82
|
+
if (props.reverse) ratio = 1 - ratio
|
|
83
|
+
ratio = Math.min(1, Math.max(0, ratio))
|
|
84
|
+
return props.min + ratio * (props.max - props.min)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
88
|
+
if (props.disable || props.readonly) return
|
|
89
|
+
dragging.value = true
|
|
90
|
+
;(e.target as HTMLElement).setPointerCapture(e.pointerId)
|
|
91
|
+
const v = valueFromPos(e.clientX, e.clientY)
|
|
92
|
+
if (props.snap) emit("update:modelValue", snapped(v))
|
|
93
|
+
else dragValue.value = v
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const onPointerMove = (e: PointerEvent) => {
|
|
97
|
+
if (!dragging.value) return
|
|
98
|
+
const v = valueFromPos(e.clientX, e.clientY)
|
|
99
|
+
if (props.snap) emit("update:modelValue", snapped(v))
|
|
100
|
+
else dragValue.value = v
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const onPointerUp = () => {
|
|
104
|
+
if (!dragging.value) return
|
|
105
|
+
dragging.value = false
|
|
106
|
+
if (dragValue.value !== null) {
|
|
107
|
+
const v = snapped(dragValue.value)
|
|
108
|
+
dragValue.value = null
|
|
109
|
+
emit("update:modelValue", v)
|
|
110
|
+
emit("change", v)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const onKeydown = (e: KeyboardEvent) => {
|
|
115
|
+
if (props.disable || props.readonly) return
|
|
116
|
+
let next: number | null = null
|
|
117
|
+
if (e.key === "ArrowRight" || e.key === "ArrowUp") next = (props.modelValue ?? 0) + props.step
|
|
118
|
+
else if (e.key === "ArrowLeft" || e.key === "ArrowDown") next = (props.modelValue ?? 0) - props.step
|
|
119
|
+
else if (e.key === "Home") next = props.min
|
|
120
|
+
else if (e.key === "End") next = props.max
|
|
121
|
+
if (next === null) return
|
|
122
|
+
e.preventDefault()
|
|
123
|
+
const v = snapped(next)
|
|
124
|
+
emit("update:modelValue", v)
|
|
125
|
+
emit("change", v)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// — styles calculés —
|
|
129
|
+
const THUMB = 20
|
|
130
|
+
|
|
131
|
+
const trackStyle = computed<Record<string, string>>(() => {
|
|
132
|
+
const style: Record<string, string> = {}
|
|
133
|
+
if (props.trackColor) style.backgroundColor = colorValue(props.trackColor)
|
|
134
|
+
return style
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const fillStyle = computed<Record<string, string>>(() => ({
|
|
138
|
+
backgroundColor: colorValue(props.color),
|
|
139
|
+
...(props.vertical ? { height: `${pct.value}%` } : { width: `${pct.value}%` }),
|
|
140
|
+
}))
|
|
141
|
+
|
|
142
|
+
const thumbStyle = computed<Record<string, string>>(() => ({
|
|
143
|
+
borderColor: colorValue(props.thumbColor || props.color),
|
|
144
|
+
...(props.vertical
|
|
145
|
+
? { top: `calc(${pct.value}% - ${THUMB / 2}px)` }
|
|
146
|
+
: { left: `calc(${pct.value}% - ${THUMB / 2}px)` }),
|
|
147
|
+
}))
|
|
148
|
+
|
|
149
|
+
const markerPositions = computed<number[]>(() => {
|
|
150
|
+
const list: number[] = []
|
|
151
|
+
const span = props.max - props.min
|
|
152
|
+
if (span <= 0 || props.step <= 0) return list
|
|
153
|
+
for (let v = props.min; v <= props.max + 1e-9; v += props.step) {
|
|
154
|
+
list.push(((v - props.min) / span) * 100)
|
|
155
|
+
}
|
|
156
|
+
return list
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const rootClasses = computed(() =>
|
|
160
|
+
cn(
|
|
161
|
+
"q-slider",
|
|
162
|
+
props.vertical && "q-slider--vertical",
|
|
163
|
+
props.dense && "q-slider--dense",
|
|
164
|
+
props.dark && "q-slider--dark",
|
|
165
|
+
(props.disable || props.readonly) && "q-slider--disabled",
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
</script>
|
|
169
|
+
|
|
170
|
+
<template>
|
|
171
|
+
<div
|
|
172
|
+
ref="trackEl"
|
|
173
|
+
class="q-slider"
|
|
174
|
+
:class="rootClasses"
|
|
175
|
+
role="slider"
|
|
176
|
+
:aria-valuemin="min"
|
|
177
|
+
:aria-valuemax="max"
|
|
178
|
+
:aria-valuenow="Math.round(displayValue)"
|
|
179
|
+
:tabindex="disable || readonly ? -1 : 0"
|
|
180
|
+
@pointerdown="onPointerDown"
|
|
181
|
+
@pointermove="onPointerMove"
|
|
182
|
+
@pointerup="onPointerUp"
|
|
183
|
+
@pointercancel="onPointerUp"
|
|
184
|
+
@keydown="onKeydown"
|
|
185
|
+
>
|
|
186
|
+
<div class="q-slider__track" :style="trackStyle">
|
|
187
|
+
<div class="q-slider__track-fill" :style="fillStyle" />
|
|
188
|
+
<div class="q-slider__thumb" :class="{ 'q-slider__thumb--dragging': dragging }" :style="thumbStyle">
|
|
189
|
+
<span
|
|
190
|
+
v-if="label && (labelAlways || dragging)"
|
|
191
|
+
class="q-slider__label"
|
|
192
|
+
:style="{ backgroundColor: colorValue(color) }"
|
|
193
|
+
>
|
|
194
|
+
{{ Math.round(displayValue) }}
|
|
195
|
+
</span>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
<span
|
|
199
|
+
v-for="(m, i) in markerPositions"
|
|
200
|
+
:key="i"
|
|
201
|
+
class="q-slider__marker"
|
|
202
|
+
:style="vertical ? { top: `${m}%` } : { left: `${m}%` }"
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
</template>
|