@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.
Files changed (150) hide show
  1. package/README.md +15 -0
  2. package/components/QAccordion.vue +68 -0
  3. package/components/QAccordionContent.vue +62 -0
  4. package/components/QAccordionItem.vue +24 -0
  5. package/components/QAccordionTrigger.vue +51 -0
  6. package/components/QActionSheet.vue +159 -0
  7. package/components/QApp.vue +8 -0
  8. package/components/QAutocomplete.vue +316 -0
  9. package/components/QAvatar.vue +92 -0
  10. package/components/QBackHeader.vue +69 -0
  11. package/components/QBackTop.vue +125 -0
  12. package/components/QBadge.vue +55 -0
  13. package/components/QBottomSheet.vue +177 -0
  14. package/components/QBottomSheetFooter.vue +9 -0
  15. package/components/QBottomSheetHeader.vue +35 -0
  16. package/components/QBottomSheetProvider.vue +62 -0
  17. package/components/QBottomSheetTrigger.vue +18 -0
  18. package/components/QBtn.vue +144 -0
  19. package/components/QBtnGroup.vue +31 -0
  20. package/components/QBubble.vue +36 -0
  21. package/components/QBubbleContent.vue +28 -0
  22. package/components/QBubbleGroup.vue +25 -0
  23. package/components/QBubbleReactions.vue +40 -0
  24. package/components/QCard.vue +63 -0
  25. package/components/QCardActions.vue +27 -0
  26. package/components/QCardContent.vue +29 -0
  27. package/components/QCarousel.vue +115 -0
  28. package/components/QCarouselContent.vue +23 -0
  29. package/components/QCarouselItem.vue +20 -0
  30. package/components/QCarouselNav.vue +51 -0
  31. package/components/QCheckbox.vue +146 -0
  32. package/components/QChip.vue +91 -0
  33. package/components/QCircularProgress.vue +122 -0
  34. package/components/QCol.vue +52 -0
  35. package/components/QCollapse.vue +141 -0
  36. package/components/QConfigProvider.vue +164 -0
  37. package/components/QContainer.vue +201 -0
  38. package/components/QCountDown.vue +164 -0
  39. package/components/QCountryPicker.vue +593 -0
  40. package/components/QDataGrid.vue +491 -0
  41. package/components/QDatePicker.vue +250 -0
  42. package/components/QDialog.vue +163 -0
  43. package/components/QDialogFooter.vue +9 -0
  44. package/components/QDialogHeader.vue +41 -0
  45. package/components/QDialogProvider.vue +65 -0
  46. package/components/QDialogTrigger.vue +18 -0
  47. package/components/QFab.vue +80 -0
  48. package/components/QFabAction.vue +48 -0
  49. package/components/QFilePicker.vue +294 -0
  50. package/components/QFooter.vue +72 -0
  51. package/components/QGallery.vue +280 -0
  52. package/components/QGrid.vue +54 -0
  53. package/components/QHeader.vue +38 -0
  54. package/components/QIcon.vue +53 -0
  55. package/components/QImagePicker.vue +248 -0
  56. package/components/QImagePreview.vue +225 -0
  57. package/components/QImagePreviewProvider.vue +35 -0
  58. package/components/QImg.vue +142 -0
  59. package/components/QInfiniteScroll.vue +109 -0
  60. package/components/QInnerLoading.vue +67 -0
  61. package/components/QInput.vue +289 -0
  62. package/components/QInputOtp.vue +238 -0
  63. package/components/QInputPassword.vue +67 -0
  64. package/components/QInputTag.vue +281 -0
  65. package/components/QIntersection.vue +119 -0
  66. package/components/QItem.vue +60 -0
  67. package/components/QItemSection.vue +47 -0
  68. package/components/QLinearProgress.vue +94 -0
  69. package/components/QList.vue +39 -0
  70. package/components/QLoading.vue +77 -0
  71. package/components/QLoadingProvider.vue +41 -0
  72. package/components/QMarquee.vue +56 -0
  73. package/components/QMessageScroller.vue +10 -0
  74. package/components/QMessageScrollerButton.vue +48 -0
  75. package/components/QMessageScrollerContent.vue +31 -0
  76. package/components/QMessageScrollerItem.vue +49 -0
  77. package/components/QMessageScrollerProvider.vue +198 -0
  78. package/components/QMessageScrollerViewport.vue +109 -0
  79. package/components/QNavMenu.vue +75 -0
  80. package/components/QNavMenuContent.vue +10 -0
  81. package/components/QNavMenuItem.vue +60 -0
  82. package/components/QNavMenuTrigger.vue +96 -0
  83. package/components/QNotifyProvider.vue +33 -0
  84. package/components/QNotifyToast.vue +60 -0
  85. package/components/QPage.vue +56 -0
  86. package/components/QPagination.vue +195 -0
  87. package/components/QParallax.vue +124 -0
  88. package/components/QPullToRefresh.vue +138 -0
  89. package/components/QRadio.vue +99 -0
  90. package/components/QRating.vue +105 -0
  91. package/components/QReorder.vue +191 -0
  92. package/components/QRollingText.vue +225 -0
  93. package/components/QRow.vue +23 -0
  94. package/components/QSafeArea.vue +39 -0
  95. package/components/QScrollArea.vue +118 -0
  96. package/components/QSelect.vue +698 -0
  97. package/components/QSeparator.vue +39 -0
  98. package/components/QSidebar.vue +153 -0
  99. package/components/QSidebarContent.vue +14 -0
  100. package/components/QSidebarFooter.vue +14 -0
  101. package/components/QSidebarHeader.vue +14 -0
  102. package/components/QSidebarMenu.vue +9 -0
  103. package/components/QSidebarMenuButton.vue +59 -0
  104. package/components/QSidebarMenuItem.vue +9 -0
  105. package/components/QSidebarTrigger.vue +32 -0
  106. package/components/QSkeleton.vue +95 -0
  107. package/components/QSlider.vue +205 -0
  108. package/components/QSpace.vue +10 -0
  109. package/components/QSpinner.vue +172 -0
  110. package/components/QSplitter.vue +145 -0
  111. package/components/QSticky.vue +181 -0
  112. package/components/QSwipeCell.vue +217 -0
  113. package/components/QSwiper.vue +102 -0
  114. package/components/QSwiperSlide.vue +19 -0
  115. package/components/QSyntax.vue +171 -0
  116. package/components/QTab.vue +175 -0
  117. package/components/QTabPanel.vue +50 -0
  118. package/components/QTabPanels.vue +106 -0
  119. package/components/QTable.vue +575 -0
  120. package/components/QTabs.vue +188 -0
  121. package/components/QText.vue +316 -0
  122. package/components/QTextCaption.vue +122 -0
  123. package/components/QToolbar.vue +28 -0
  124. package/components/QTooltip.vue +313 -0
  125. package/components/QUploader.vue +473 -0
  126. package/components/QVideo.vue +289 -0
  127. package/components/QVirtualScroll.vue +190 -0
  128. package/components/internal/QDateCalendar.vue +153 -0
  129. package/index.ts +151 -0
  130. package/lib/breakpoints.ts +44 -0
  131. package/lib/colors.ts +24 -0
  132. package/lib/config.ts +34 -0
  133. package/lib/countries.ts +261 -0
  134. package/lib/formula.ts +630 -0
  135. package/lib/icons.ts +38 -0
  136. package/lib/loading.ts +75 -0
  137. package/lib/overlayBack.ts +124 -0
  138. package/lib/platform.ts +146 -0
  139. package/lib/primitive.ts +25 -0
  140. package/lib/q.ts +287 -0
  141. package/lib/screen.ts +105 -0
  142. package/lib/search.ts +35 -0
  143. package/lib/themeVars.ts +22 -0
  144. package/lib/useComponentProps.ts +54 -0
  145. package/lib/utils.ts +7 -0
  146. package/module.ts +89 -0
  147. package/package.json +41 -0
  148. package/scripts/generate-exports.ts +56 -0
  149. package/styles/main.css +7627 -0
  150. 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,9 @@
1
+ <script setup lang="ts">
2
+ // QSidebarMenu — liste de navigation (équivalent SidebarMenu).
3
+ </script>
4
+
5
+ <template>
6
+ <ul class="q-sidebar__menu">
7
+ <slot />
8
+ </ul>
9
+ </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,9 @@
1
+ <script setup lang="ts">
2
+ // QSidebarMenuItem — item de menu (équivalent SidebarMenuItem).
3
+ </script>
4
+
5
+ <template>
6
+ <li class="q-sidebar__menu-item">
7
+ <slot />
8
+ </li>
9
+ </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>
@@ -0,0 +1,10 @@
1
+ <script setup lang="ts">
2
+ // QSpace — API Quasar : <q-space />
3
+ // Remplit l'espace restant dans un conteneur flex (push les éléments à gauche/droite).
4
+ </script>
5
+
6
+ <template>
7
+ <div class="q-space" aria-hidden="true">
8
+ <slot />
9
+ </div>
10
+ </template>