@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,172 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QSpinner — indicateur de chargement : <q-spinner size="30px" color="primary" :thickness="4" type="tail" />
|
|
3
|
+
// Variantes SVG (type) : tail (défaut), oval, bars, dots, grid, rings, puff, audio.
|
|
4
|
+
import { computed } from "vue"
|
|
5
|
+
import { colorValue } from "../lib/colors"
|
|
6
|
+
|
|
7
|
+
type SpinnerType =
|
|
8
|
+
| "tail"
|
|
9
|
+
| "oval"
|
|
10
|
+
| "bars"
|
|
11
|
+
| "dots"
|
|
12
|
+
| "grid"
|
|
13
|
+
| "rings"
|
|
14
|
+
| "puff"
|
|
15
|
+
| "audio"
|
|
16
|
+
| "ios"
|
|
17
|
+
| "radio"
|
|
18
|
+
| "gear"
|
|
19
|
+
| "gears"
|
|
20
|
+
| "cube"
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
/** Variante : tail (défaut) | oval | bars | dots | grid | rings | puff | audio | ios | radio | gear | gears | cube */
|
|
24
|
+
type?: SpinnerType
|
|
25
|
+
/** Taille (px ou chaîne CSS) — défaut 32px */
|
|
26
|
+
size?: string | number
|
|
27
|
+
/** Couleur (token ou hex) — défaut primary */
|
|
28
|
+
color?: string
|
|
29
|
+
/** Épaisseur du trait (SVG stroke-width) — défaut 4 */
|
|
30
|
+
thickness?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
34
|
+
type: "tail",
|
|
35
|
+
size: 32,
|
|
36
|
+
color: "primary",
|
|
37
|
+
thickness: 4,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const rootStyle = computed<Record<string, string>>(() => ({
|
|
41
|
+
width: typeof props.size === "number" ? props.size + "px" : props.size,
|
|
42
|
+
height: typeof props.size === "number" ? props.size + "px" : props.size,
|
|
43
|
+
color: colorValue(props.color),
|
|
44
|
+
}))
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<span class="q-spinner" :class="`q-spinner--${type}`" :style="rootStyle" role="status" aria-label="Loading">
|
|
49
|
+
<!-- Tail : piste + arc qui tourne -->
|
|
50
|
+
<svg v-if="type === 'tail'" viewBox="0 0 44 44" class="q-spinner__svg">
|
|
51
|
+
<circle cx="22" cy="22" r="17" fill="none" stroke="currentColor" :stroke-width="thickness" opacity="0.25" />
|
|
52
|
+
<path
|
|
53
|
+
d="M22 5 a17 17 0 0 1 17 17"
|
|
54
|
+
fill="none"
|
|
55
|
+
stroke="currentColor"
|
|
56
|
+
:stroke-width="thickness"
|
|
57
|
+
stroke-linecap="round"
|
|
58
|
+
class="q-spinner__arc"
|
|
59
|
+
/>
|
|
60
|
+
</svg>
|
|
61
|
+
|
|
62
|
+
<!-- Oval : cercle hachuré qui tourne -->
|
|
63
|
+
<svg v-else-if="type === 'oval'" viewBox="0 0 44 44" class="q-spinner__svg">
|
|
64
|
+
<circle
|
|
65
|
+
cx="22"
|
|
66
|
+
cy="22"
|
|
67
|
+
r="16"
|
|
68
|
+
fill="none"
|
|
69
|
+
stroke="currentColor"
|
|
70
|
+
:stroke-width="thickness"
|
|
71
|
+
stroke-linecap="round"
|
|
72
|
+
stroke-dasharray="55 120"
|
|
73
|
+
class="q-spinner__oval"
|
|
74
|
+
/>
|
|
75
|
+
</svg>
|
|
76
|
+
|
|
77
|
+
<!-- Bars : 5 barres qui ondulent -->
|
|
78
|
+
<svg v-else-if="type === 'bars'" viewBox="0 0 44 44" class="q-spinner__svg q-spinner__svg--wide">
|
|
79
|
+
<rect v-for="(y, i) in [18, 12, 6, 12, 18]" :key="i" :x="2 + i * 9" :y="y" width="5" :height="44 - y * 2" rx="2.5" fill="currentColor" class="q-spinner__bar" :class="`q-spinner__bar--${i + 1}`" />
|
|
80
|
+
</svg>
|
|
81
|
+
|
|
82
|
+
<!-- Dots : 3 points qui rebondissent -->
|
|
83
|
+
<svg v-else-if="type === 'dots'" viewBox="0 0 80 24" class="q-spinner__svg q-spinner__svg--wide">
|
|
84
|
+
<circle v-for="i in 3" :key="i" :cx="10 + (i - 1) * 30" cy="12" r="6" fill="currentColor" class="q-spinner__dot" :class="`q-spinner__dot--${i}`" />
|
|
85
|
+
</svg>
|
|
86
|
+
|
|
87
|
+
<!-- Grid : 3×3 points pulsants -->
|
|
88
|
+
<svg v-else-if="type === 'grid'" viewBox="0 0 44 44" class="q-spinner__svg">
|
|
89
|
+
<circle
|
|
90
|
+
v-for="i in 9"
|
|
91
|
+
:key="i"
|
|
92
|
+
:cx="8 + ((i - 1) % 3) * 14"
|
|
93
|
+
:cy="8 + Math.floor((i - 1) / 3) * 14"
|
|
94
|
+
r="4.5"
|
|
95
|
+
fill="currentColor"
|
|
96
|
+
class="q-spinner__cell"
|
|
97
|
+
:class="`q-spinner__cell--${i}`"
|
|
98
|
+
/>
|
|
99
|
+
</svg>
|
|
100
|
+
|
|
101
|
+
<!-- Rings : deux anneaux contrarotatifs -->
|
|
102
|
+
<svg v-else-if="type === 'rings'" viewBox="0 0 44 44" class="q-spinner__svg">
|
|
103
|
+
<circle cx="22" cy="22" r="17" fill="none" stroke="currentColor" :stroke-width="thickness" opacity="0.35" class="q-spinner__ring" />
|
|
104
|
+
<circle cx="22" cy="22" r="10" fill="none" stroke="currentColor" :stroke-width="thickness" stroke-linecap="round" class="q-spinner__ring q-spinner__ring--2" />
|
|
105
|
+
</svg>
|
|
106
|
+
|
|
107
|
+
<!-- Puff : point qui pulse -->
|
|
108
|
+
<svg v-else-if="type === 'puff'" viewBox="0 0 44 44" class="q-spinner__svg">
|
|
109
|
+
<circle cx="22" cy="22" r="14" fill="currentColor" class="q-spinner__puff" />
|
|
110
|
+
</svg>
|
|
111
|
+
|
|
112
|
+
<!-- Audio : égaliseur -->
|
|
113
|
+
<svg v-else-if="type === 'audio'" viewBox="0 0 28 44" class="q-spinner__svg q-spinner__svg--wide">
|
|
114
|
+
<rect v-for="(y, i) in [14, 6, 10]" :key="i" :x="2 + i * 9" :y="y" width="5" :height="44 - y * 2" rx="2.5" fill="currentColor" class="q-spinner__eq" :class="`q-spinner__eq--${i + 1}`" />
|
|
115
|
+
</svg>
|
|
116
|
+
|
|
117
|
+
<!-- Ios : 12 segments qui s'estompent en séquence -->
|
|
118
|
+
<svg v-else-if="type === 'ios'" viewBox="0 0 44 44" class="q-spinner__svg">
|
|
119
|
+
<g v-for="i in 12" :key="i" :transform="`rotate(${(i - 1) * 30} 22 22)`">
|
|
120
|
+
<line x1="22" y1="7" x2="22" y2="11" stroke="currentColor" :stroke-width="2.5" stroke-linecap="round" class="q-spinner__seg" :class="`q-spinner__seg--${i}`" />
|
|
121
|
+
</g>
|
|
122
|
+
</svg>
|
|
123
|
+
|
|
124
|
+
<!-- Radio : point + ondes qui se propagent -->
|
|
125
|
+
<svg v-else-if="type === 'radio'" viewBox="0 0 44 44" class="q-spinner__svg">
|
|
126
|
+
<circle cx="22" cy="22" r="4" fill="currentColor" class="q-spinner__radio-dot" />
|
|
127
|
+
<circle cx="22" cy="22" r="4" fill="none" stroke="currentColor" :stroke-width="2.5" class="q-spinner__radio-ring" />
|
|
128
|
+
<circle cx="22" cy="22" r="4" fill="none" stroke="currentColor" :stroke-width="2.5" class="q-spinner__radio-ring q-spinner__radio-ring--2" />
|
|
129
|
+
</svg>
|
|
130
|
+
|
|
131
|
+
<!-- Gear : un seul engrenage -->
|
|
132
|
+
<svg v-else-if="type === 'gear'" viewBox="0 0 44 44" class="q-spinner__svg">
|
|
133
|
+
<g transform="translate(22 22)">
|
|
134
|
+
<g class="q-spinner__cog">
|
|
135
|
+
<circle r="9.5" fill="none" stroke="currentColor" :stroke-width="9" />
|
|
136
|
+
<g v-for="i in 8" :key="i" :transform="`rotate(${(i - 1) * 45})`">
|
|
137
|
+
<rect x="-1.5" y="-15" width="3" height="8" rx="1.2" fill="currentColor" />
|
|
138
|
+
</g>
|
|
139
|
+
</g>
|
|
140
|
+
</g>
|
|
141
|
+
</svg>
|
|
142
|
+
|
|
143
|
+
<!-- Gears : deux engrenages contrarotatifs -->
|
|
144
|
+
<svg v-else-if="type === 'gears'" viewBox="0 0 44 44" class="q-spinner__svg">
|
|
145
|
+
<g transform="translate(15 22)">
|
|
146
|
+
<g class="q-spinner__cog">
|
|
147
|
+
<circle r="9.5" fill="none" stroke="currentColor" :stroke-width="9" />
|
|
148
|
+
<g v-for="i in 8" :key="i" :transform="`rotate(${(i - 1) * 45})`">
|
|
149
|
+
<rect x="-1.5" y="-15" width="3" height="8" rx="1.2" fill="currentColor" />
|
|
150
|
+
</g>
|
|
151
|
+
</g>
|
|
152
|
+
</g>
|
|
153
|
+
<g transform="translate(31 15) scale(0.55)">
|
|
154
|
+
<g class="q-spinner__cog q-spinner__cog--2">
|
|
155
|
+
<circle r="9.5" fill="none" stroke="currentColor" :stroke-width="9" />
|
|
156
|
+
<g v-for="i in 8" :key="i" :transform="`rotate(${(i - 1) * 45})`">
|
|
157
|
+
<rect x="-1.5" y="-15" width="3" height="8" rx="1.2" fill="currentColor" />
|
|
158
|
+
</g>
|
|
159
|
+
</g>
|
|
160
|
+
</g>
|
|
161
|
+
</svg>
|
|
162
|
+
|
|
163
|
+
<!-- Cube : cube isométrique qui tourne -->
|
|
164
|
+
<svg v-else viewBox="0 0 44 44" class="q-spinner__svg">
|
|
165
|
+
<g class="q-spinner__cube">
|
|
166
|
+
<path d="M22 4 L40 14 L22 24 L4 14 Z" fill="currentColor" opacity="0.9" />
|
|
167
|
+
<path d="M4 14 L22 24 L22 42 L4 32 Z" fill="currentColor" opacity="0.55" />
|
|
168
|
+
<path d="M40 14 L22 24 L22 42 L40 32 Z" fill="currentColor" opacity="0.3" />
|
|
169
|
+
</g>
|
|
170
|
+
</svg>
|
|
171
|
+
</span>
|
|
172
|
+
</template>
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QSplitter — API Quasar : <q-splitter v-model="ratio" horizontal :limits="[20, 80]" unit="%">
|
|
3
|
+
// Équivalent du Resizable shadcn-vue : panneaux flex + handle draggable (pointer) + clavier.
|
|
4
|
+
import { computed, ref } from "vue"
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Ratio de division : % (0-100) ou px selon unit */
|
|
9
|
+
modelValue?: number
|
|
10
|
+
/** Division horizontale (sinon verticale) */
|
|
11
|
+
horizontal?: boolean
|
|
12
|
+
/** Limites [min, max] du panneau avant, en % (défaut [0, 50]) */
|
|
13
|
+
limits?: [number, number]
|
|
14
|
+
/** Inverse les deux panneaux */
|
|
15
|
+
reverse?: boolean
|
|
16
|
+
/** Unité de modelValue */
|
|
17
|
+
unit?: "%" | "px"
|
|
18
|
+
disable?: boolean
|
|
19
|
+
separatorClass?: string
|
|
20
|
+
separatorStyle?: string
|
|
21
|
+
beforeClass?: string
|
|
22
|
+
beforeStyle?: string
|
|
23
|
+
afterClass?: string
|
|
24
|
+
afterStyle?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
28
|
+
modelValue: 50,
|
|
29
|
+
horizontal: false,
|
|
30
|
+
limits: () => [0, 50],
|
|
31
|
+
reverse: false,
|
|
32
|
+
unit: "%",
|
|
33
|
+
disable: false,
|
|
34
|
+
separatorClass: "",
|
|
35
|
+
separatorStyle: "",
|
|
36
|
+
beforeClass: "",
|
|
37
|
+
beforeStyle: "",
|
|
38
|
+
afterClass: "",
|
|
39
|
+
afterStyle: "",
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const emit = defineEmits<{ "update:modelValue": [value: number] }>()
|
|
43
|
+
|
|
44
|
+
const containerRef = ref<HTMLElement | null>(null)
|
|
45
|
+
const dragging = ref(false)
|
|
46
|
+
|
|
47
|
+
// — Drag du handle —
|
|
48
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
49
|
+
if (props.disable) return
|
|
50
|
+
dragging.value = true
|
|
51
|
+
;(e.target as HTMLElement).setPointerCapture(e.pointerId)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const onPointerMove = (e: PointerEvent) => {
|
|
55
|
+
if (!dragging.value || props.disable) return
|
|
56
|
+
const rect = containerRef.value?.getBoundingClientRect()
|
|
57
|
+
if (!rect) return
|
|
58
|
+
const total = props.horizontal ? rect.height : rect.width
|
|
59
|
+
if (total <= 0) return
|
|
60
|
+
const pos = props.horizontal ? e.clientY - rect.top : e.clientX - rect.left
|
|
61
|
+
const pct = Math.max(props.limits[0], Math.min(props.limits[1], (pos / total) * 100))
|
|
62
|
+
emit("update:modelValue", props.unit === "%" ? pct : (pct / 100) * total)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const onPointerUp = () => {
|
|
66
|
+
dragging.value = false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// — Clavier sur le handle (ARIA) —
|
|
70
|
+
const onKeydown = (e: KeyboardEvent) => {
|
|
71
|
+
if (props.disable) return
|
|
72
|
+
const dir =
|
|
73
|
+
props.horizontal
|
|
74
|
+
? (e.key === "ArrowUp" ? 1 : e.key === "ArrowDown" ? -1 : 0)
|
|
75
|
+
: (e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0)
|
|
76
|
+
if (dir === 0) return
|
|
77
|
+
e.preventDefault()
|
|
78
|
+
const step = e.shiftKey ? 10 : 1
|
|
79
|
+
const current = props.modelValue ?? 50
|
|
80
|
+
const rect = containerRef.value?.getBoundingClientRect()
|
|
81
|
+
const total = rect ? (props.horizontal ? rect.height : rect.width) : 100
|
|
82
|
+
const currentPct = props.unit === "%" ? current : (current / total) * 100
|
|
83
|
+
const nextPct = Math.max(props.limits[0], Math.min(props.limits[1], currentPct + dir * step))
|
|
84
|
+
emit("update:modelValue", props.unit === "%" ? nextPct : (nextPct / 100) * total)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const beforeStyle = computed<Record<string, string>>(() => {
|
|
88
|
+
const v = props.modelValue ?? 50
|
|
89
|
+
return { flex: `0 0 ${v}${props.unit}` }
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const rootClasses = computed(() =>
|
|
93
|
+
cn(
|
|
94
|
+
"q-splitter",
|
|
95
|
+
props.horizontal && "q-splitter--horizontal",
|
|
96
|
+
props.reverse && "q-splitter--reverse",
|
|
97
|
+
props.disable && "q-splitter--disabled",
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const separatorClasses = computed(() =>
|
|
102
|
+
cn(
|
|
103
|
+
"q-splitter__separator",
|
|
104
|
+
dragging.value && "q-splitter__separator--dragging",
|
|
105
|
+
props.separatorClass,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<template>
|
|
111
|
+
<div ref="containerRef" class="q-splitter" :class="rootClasses">
|
|
112
|
+
<div
|
|
113
|
+
class="q-splitter__before"
|
|
114
|
+
:class="beforeClass"
|
|
115
|
+
:style="[beforeStyle, props.beforeStyle]"
|
|
116
|
+
>
|
|
117
|
+
<slot name="before" />
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div
|
|
121
|
+
class="q-splitter__separator"
|
|
122
|
+
:class="separatorClasses"
|
|
123
|
+
:style="separatorStyle"
|
|
124
|
+
role="separator"
|
|
125
|
+
:aria-orientation="horizontal ? 'horizontal' : 'vertical'"
|
|
126
|
+
:aria-valuenow="Math.round(modelValue ?? 50)"
|
|
127
|
+
:aria-valuemin="limits[0]"
|
|
128
|
+
:aria-valuemax="limits[1]"
|
|
129
|
+
tabindex="0"
|
|
130
|
+
@pointerdown="onPointerDown"
|
|
131
|
+
@pointermove="onPointerMove"
|
|
132
|
+
@pointerup="onPointerUp"
|
|
133
|
+
@pointercancel="onPointerUp"
|
|
134
|
+
@keydown="onKeydown"
|
|
135
|
+
>
|
|
136
|
+
<slot name="separator">
|
|
137
|
+
<span class="q-splitter__handle" />
|
|
138
|
+
</slot>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div class="q-splitter__after" :class="afterClass" :style="afterStyle">
|
|
142
|
+
<slot name="after" />
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</template>
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QSticky — colle du contenu au haut (ou bas) du viewport pendant le scroll :
|
|
3
|
+
// <q-sticky :offset-top="16"><q-btn … /></q-sticky>.
|
|
4
|
+
// Le composant garde sa place dans le flux (placeholder de même taille) et passe
|
|
5
|
+
// en position:fixed quand il atteint l'offset ; avec `container`, il reste
|
|
6
|
+
// à l'intérieur de ce conteneur (transform borné au bord bas/haut).
|
|
7
|
+
// Events : @scroll="{ scrollTop, isFixed }" et @change="isFixed".
|
|
8
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue"
|
|
9
|
+
|
|
10
|
+
type QStickyPosition = "top" | "bottom"
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
/** Offset depuis le haut du viewport quand le composant est collé, en px (défaut 0) */
|
|
14
|
+
offsetTop?: number
|
|
15
|
+
/** Offset depuis le bas du viewport quand le composant est collé, en px (défaut 0) */
|
|
16
|
+
offsetBottom?: number
|
|
17
|
+
/** Position de collage : "top" | "bottom" (défaut top) */
|
|
18
|
+
position?: QStickyPosition
|
|
19
|
+
/** Conteneur optionnel : le composant reste à l'intérieur (s'arrête au bord du conteneur) */
|
|
20
|
+
container?: Element | null
|
|
21
|
+
/** z-index quand collé (défaut 99) */
|
|
22
|
+
zIndex?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
26
|
+
offsetTop: 0,
|
|
27
|
+
offsetBottom: 0,
|
|
28
|
+
position: "top",
|
|
29
|
+
container: null,
|
|
30
|
+
zIndex: 99,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const emit = defineEmits<{
|
|
34
|
+
scroll: [payload: { scrollTop: number; isFixed: boolean }]
|
|
35
|
+
change: [isFixed: boolean]
|
|
36
|
+
}>()
|
|
37
|
+
|
|
38
|
+
const root = ref<HTMLElement | null>(null)
|
|
39
|
+
|
|
40
|
+
const state = reactive({
|
|
41
|
+
fixed: false,
|
|
42
|
+
width: 0, // largeur mesurée
|
|
43
|
+
height: 0, // hauteur mesurée
|
|
44
|
+
transform: 0, // correction pour rester dans le conteneur
|
|
45
|
+
})
|
|
46
|
+
const isReset = ref(false)
|
|
47
|
+
|
|
48
|
+
const offset = computed(() =>
|
|
49
|
+
props.position === "top" ? props.offsetTop : props.offsetBottom,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const isHidden = () => !root.value || root.value.offsetParent === null
|
|
53
|
+
|
|
54
|
+
/** Conteneur de scroll réel le plus proche (overflow scrollable), sinon window */
|
|
55
|
+
const findScrollParent = (el: HTMLElement | null): HTMLElement | Window => {
|
|
56
|
+
let node = el?.parentElement ?? null
|
|
57
|
+
while (node) {
|
|
58
|
+
const overflowY = getComputedStyle(node).overflowY
|
|
59
|
+
if (
|
|
60
|
+
/(auto|scroll|overlay)/.test(overflowY) &&
|
|
61
|
+
node.scrollHeight > node.clientHeight
|
|
62
|
+
) {
|
|
63
|
+
return node
|
|
64
|
+
}
|
|
65
|
+
node = node.parentElement
|
|
66
|
+
}
|
|
67
|
+
return window
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let scrollParent: HTMLElement | Window = window
|
|
71
|
+
|
|
72
|
+
const scrollTop = () =>
|
|
73
|
+
scrollParent === window
|
|
74
|
+
? window.scrollY || document.documentElement.scrollTop
|
|
75
|
+
: (scrollParent as HTMLElement).scrollTop
|
|
76
|
+
|
|
77
|
+
const onScroll = () => {
|
|
78
|
+
if (!root.value || isHidden()) return
|
|
79
|
+
|
|
80
|
+
const { container, position } = props
|
|
81
|
+
const rootRect = root.value.getBoundingClientRect()
|
|
82
|
+
|
|
83
|
+
state.width = rootRect.width
|
|
84
|
+
state.height = rootRect.height
|
|
85
|
+
|
|
86
|
+
if (position === "top") {
|
|
87
|
+
if (container) {
|
|
88
|
+
const containerRect = container.getBoundingClientRect()
|
|
89
|
+
const difference = containerRect.bottom - offset.value - state.height
|
|
90
|
+
state.fixed = offset.value > rootRect.top && containerRect.bottom > 0
|
|
91
|
+
state.transform = difference < 0 ? difference : 0
|
|
92
|
+
} else {
|
|
93
|
+
state.fixed = offset.value > rootRect.top
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
const clientHeight = document.documentElement.clientHeight
|
|
97
|
+
if (container) {
|
|
98
|
+
const containerRect = container.getBoundingClientRect()
|
|
99
|
+
const difference =
|
|
100
|
+
clientHeight - containerRect.top - offset.value - state.height
|
|
101
|
+
state.fixed =
|
|
102
|
+
clientHeight - offset.value < rootRect.bottom &&
|
|
103
|
+
clientHeight > containerRect.top
|
|
104
|
+
state.transform = difference < 0 ? -difference : 0
|
|
105
|
+
} else {
|
|
106
|
+
state.fixed = clientHeight - offset.value < rootRect.bottom
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
emit("scroll", { scrollTop: scrollTop(), isFixed: state.fixed })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
watch(
|
|
114
|
+
() => state.fixed,
|
|
115
|
+
(value) => emit("change", value),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
/** Re-mesure après un resize quand le composant est collé */
|
|
119
|
+
const onResize = () => {
|
|
120
|
+
if (!root.value || isHidden() || !state.fixed) return
|
|
121
|
+
isReset.value = true
|
|
122
|
+
nextTick(() => {
|
|
123
|
+
if (!root.value) return
|
|
124
|
+
const r = root.value.getBoundingClientRect()
|
|
125
|
+
state.width = r.width
|
|
126
|
+
state.height = r.height
|
|
127
|
+
isReset.value = false
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
onMounted(() => {
|
|
132
|
+
if (root.value) {
|
|
133
|
+
scrollParent = findScrollParent(root.value)
|
|
134
|
+
scrollParent.addEventListener("scroll", onScroll, { passive: true })
|
|
135
|
+
window.addEventListener("resize", onResize)
|
|
136
|
+
onScroll()
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
onBeforeUnmount(() => {
|
|
141
|
+
scrollParent.removeEventListener("scroll", onScroll)
|
|
142
|
+
window.removeEventListener("resize", onResize)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Placeholder : conserve la place du contenu dans le flux quand il est collé
|
|
146
|
+
const rootStyle = computed(() => {
|
|
147
|
+
if (isReset.value || !state.fixed) return undefined
|
|
148
|
+
return { width: `${state.width}px`, height: `${state.height}px` }
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const stickyStyle = computed(() => {
|
|
152
|
+
if (!state.fixed || isReset.value) return undefined
|
|
153
|
+
const style: Record<string, string> = {
|
|
154
|
+
width: `${state.width}px`,
|
|
155
|
+
height: `${state.height}px`,
|
|
156
|
+
[props.position]: `${offset.value}px`,
|
|
157
|
+
"--q-sticky-z-index": String(props.zIndex),
|
|
158
|
+
}
|
|
159
|
+
if (state.transform) style.transform = `translate3d(0, ${state.transform}px, 0)`
|
|
160
|
+
return style
|
|
161
|
+
})
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
<template>
|
|
165
|
+
<div ref="root" class="q-sticky" :style="rootStyle">
|
|
166
|
+
<div
|
|
167
|
+
class="q-sticky__content"
|
|
168
|
+
:class="{ 'q-sticky__content--fixed': state.fixed && !isReset }"
|
|
169
|
+
:style="stickyStyle"
|
|
170
|
+
>
|
|
171
|
+
<slot />
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</template>
|
|
175
|
+
|
|
176
|
+
<style scoped>
|
|
177
|
+
.q-sticky__content--fixed {
|
|
178
|
+
position: fixed;
|
|
179
|
+
z-index: var(--q-sticky-z-index, 99);
|
|
180
|
+
}
|
|
181
|
+
</style>
|