@dnax/ui 0.0.43 → 0.0.44

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.
@@ -1,19 +1,21 @@
1
1
  <script setup lang="ts">
2
2
  // QDatePicker — sélecteur de date type DatePicker shadcn-vue, API Quasar :
3
3
  // <q-date-picker v-model="date" mode="sheet" label="Échéance" clearable outlined />
4
- // Modes : inline (calendrier en place) | modal (centré) | sheet (bottom sheet) | dialog (plein écran).
5
- import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"
4
+ // Modes : inline (calendrier en place) | popover (panneau ancré sous le champ) |
5
+ // modal (centré) | sheet (bottom sheet) | dialog (plein écran).
6
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
6
7
  import { Icon } from "@iconify/vue"
7
8
  import { icons } from "../lib/icons"
8
9
  import { cn } from "../lib/utils"
9
10
  import { useOverlayBack } from "../lib/overlayBack"
11
+ import { placePopover, type PopoverPlacement } from "../lib/datePicker"
10
12
  import QDateCalendar from "./internal/QDateCalendar.vue"
11
13
 
12
14
  interface Props {
13
15
  /** Date sélectionnée */
14
16
  modelValue?: Date | null
15
17
  /** Mode d'affichage */
16
- mode?: "inline" | "sheet" | "modal" | "dialog"
18
+ mode?: "inline" | "popover" | "sheet" | "modal" | "dialog"
17
19
  label?: string
18
20
  stackLabel?: boolean
19
21
  hint?: string
@@ -37,10 +39,14 @@ interface Props {
37
39
  disabledDates?: (d: Date) => boolean
38
40
  /** Premier jour de la semaine : 0 = dimanche, 1 = lundi (défaut) */
39
41
  firstDayOfWeek?: number
40
- /** Largeur du panneau (modal/sheet) */
42
+ /** Largeur du panneau (popover/sheet/modal) */
41
43
  width?: string
42
- /** Titre du panneau */
44
+ /** Titre du panneau (modes à voile : sheet/modal/dialog) */
43
45
  title?: string
46
+ /** Raccourci « Today » sous le calendrier (tous les modes) */
47
+ todayBtn?: boolean
48
+ /** En-tête : libellé cliquable ouvrant le choix du mois (tous les modes) */
49
+ monthDropdown?: boolean
44
50
  }
45
51
 
46
52
  const props = withDefaults(defineProps<Props>(), {
@@ -58,6 +64,8 @@ const props = withDefaults(defineProps<Props>(), {
58
64
  firstDayOfWeek: 1,
59
65
  width: "",
60
66
  title: "",
67
+ todayBtn: false,
68
+ monthDropdown: false,
61
69
  })
62
70
 
63
71
  const emit = defineEmits<{
@@ -67,14 +75,22 @@ const emit = defineEmits<{
67
75
  clear: []
68
76
  }>()
69
77
 
78
+ /** Champ déclencheur (tout sauf `inline`) */
70
79
  const isPanelMode = computed(() => props.mode !== "inline")
80
+ /** Panneau ancré au champ : ni voile, ni blocage du scroll, ni retour navigateur */
81
+ const isPopover = computed(() => props.mode === "popover")
82
+ /** Modes à voile sombre (sheet/modal/dialog) */
83
+ const isOverlayMode = computed(
84
+ () => props.mode === "sheet" || props.mode === "modal" || props.mode === "dialog",
85
+ )
71
86
 
72
87
  const open = ref(false)
73
88
  const rootEl = ref<HTMLElement | null>(null)
74
89
  const sheetRef = ref<HTMLElement | null>(null)
75
90
 
76
- // « Retour » navigateur → ferme le panneau (sheet/modal/dialog) au lieu de naviguer
77
- const panelOpen = computed(() => isPanelMode.value && open.value)
91
+ // « Retour » navigateur → ferme le panneau au voile (sheet/modal/dialog) au lieu de
92
+ // naviguer. Le popover, lui, reste un menu : clic extérieur / Échap suffisent.
93
+ const panelOpen = computed(() => isOverlayMode.value && open.value)
78
94
  useOverlayBack(panelOpen, () => closePopup(), "QDatePicker")
79
95
 
80
96
  // — Affichage du champ —
@@ -129,6 +145,51 @@ const sheetStyle = computed<Record<string, string> | undefined>(() =>
129
145
  props.width ? { width: props.width, maxWidth: props.width } : undefined,
130
146
  )
131
147
 
148
+ // — Popover : panneau ancré au champ, position calculée en `fixed` (téléporté) —
149
+ // Le calcul vit dans un helper pur (`lib/datePicker.ts`) : ici on ne fait que
150
+ // mesurer les rectangles et sérialiser le résultat.
151
+ const popoverPlacement = ref<PopoverPlacement | null>(null)
152
+ // Conserve le dernier emplacement à la fermeture : l'animation de sortie en dépend.
153
+ const popoverStyle = computed<Record<string, string>>(() => {
154
+ const p = popoverPlacement.value
155
+ if (!p) return { visibility: "hidden" }
156
+ return {
157
+ top: p.top === null ? "auto" : `${p.top}px`,
158
+ bottom: p.bottom === null ? "auto" : `${p.bottom}px`,
159
+ left: `${p.left}px`,
160
+ "--q-date-picker-caret": `${p.caret}px`,
161
+ }
162
+ })
163
+
164
+ /** Le panneau borne sa hauteur à l'espace visible ; la liste scrolle au-delà. */
165
+ const popoverPanelStyle = computed<Record<string, string | undefined>>(() => ({
166
+ ...sheetStyle.value,
167
+ maxHeight: popoverPlacement.value ? `${popoverPlacement.value.maxHeight}px` : undefined,
168
+ }))
169
+
170
+ const positionPopover = () => {
171
+ const el = rootEl.value
172
+ if (!el || typeof window === "undefined") return
173
+
174
+ // Ancre = bas du CHAMP : `.q-field__bottom` réserve ~24px même vide, ce qui
175
+ // éloignerait le panneau. Quand un hint / une erreur est affiché, on prend la
176
+ // racine pour ne pas recouvrir le texte (même règle que le popup de QSelect).
177
+ const control = el.querySelector<HTMLElement>(".q-field__control")
178
+ const hasBottomText = !!el.querySelector(".q-field__hint, .q-field__error")
179
+ const anchor = (!hasBottomText && control ? control : el).getBoundingClientRect()
180
+
181
+ popoverPlacement.value = placePopover({
182
+ anchor: { top: anchor.top, bottom: anchor.bottom, left: anchor.left, width: anchor.width },
183
+ viewport: { width: window.innerWidth, height: window.innerHeight },
184
+ // Largeur mesurée après rendu (le champ `width` peut être en %, vw…)
185
+ panelWidth: sheetRef.value?.getBoundingClientRect().width,
186
+ })
187
+ }
188
+
189
+ const onViewportChange = () => {
190
+ if (open.value && isPopover.value) positionPopover()
191
+ }
192
+
132
193
  // — Fermetures : clic extérieur (hors panneau téléporté), Échap, scroll lock —
133
194
  const onDocMousedown = (e: MouseEvent) => {
134
195
  const target = e.target as Node
@@ -141,10 +202,15 @@ const onDocKeydown = (e: KeyboardEvent) => {
141
202
  if (e.key === "Escape" && open.value) closePopup()
142
203
  }
143
204
 
144
- watch(open, (v) => {
145
- if (typeof document !== "undefined" && isPanelMode.value) {
205
+ watch(open, async (v) => {
206
+ // Le voile bloque le scroll de la page ; le popover le laisse vivre.
207
+ if (typeof document !== "undefined" && isOverlayMode.value) {
146
208
  document.body.style.overflow = v ? "hidden" : ""
147
209
  }
210
+ if (v && isPopover.value) {
211
+ await nextTick()
212
+ positionPopover()
213
+ }
148
214
  })
149
215
 
150
216
  onMounted(() => {
@@ -153,12 +219,17 @@ onMounted(() => {
153
219
  // sinon l'événement avant `document` → calendrier jamais fermé au clic extérieur.
154
220
  document.addEventListener("mousedown", onDocMousedown, true)
155
221
  document.addEventListener("keydown", onDocKeydown)
222
+ // Le champ peut bouger (scroll) ou la fenêtre changer de taille : on repositionne
223
+ window.addEventListener("scroll", onViewportChange, true)
224
+ window.addEventListener("resize", onViewportChange)
156
225
  }
157
226
  })
158
227
  onBeforeUnmount(() => {
159
228
  if (typeof document !== "undefined") {
160
229
  document.removeEventListener("mousedown", onDocMousedown, true)
161
230
  document.removeEventListener("keydown", onDocKeydown)
231
+ window.removeEventListener("scroll", onViewportChange, true)
232
+ window.removeEventListener("resize", onViewportChange)
162
233
  document.body.style.overflow = ""
163
234
  }
164
235
  })
@@ -174,6 +245,8 @@ onBeforeUnmount(() => {
174
245
  :max-date="maxDate"
175
246
  :disabled-dates="disabledDates"
176
247
  :first-day-of-week="firstDayOfWeek"
248
+ :today-btn="todayBtn"
249
+ :month-dropdown="monthDropdown"
177
250
  @select="onSelect"
178
251
  />
179
252
  </div>
@@ -203,11 +276,15 @@ onBeforeUnmount(() => {
203
276
  </div>
204
277
 
205
278
  <Teleport to="body">
206
- <Transition name="q-date-modal">
279
+ <Transition :name="isPopover ? 'q-date-popover' : 'q-date-modal'">
207
280
  <div
208
281
  v-if="open"
209
282
  class="q-date-picker__overlay"
210
- :class="`q-date-picker__overlay--${mode}`"
283
+ :class="[
284
+ `q-date-picker__overlay--${mode}`,
285
+ isPopover && popoverPlacement?.direction === 'up' && 'q-date-picker__overlay--popover-up',
286
+ ]"
287
+ :style="isPopover ? popoverStyle : undefined"
211
288
  role="presentation"
212
289
  @mousedown.self="closePopup"
213
290
  >
@@ -215,12 +292,14 @@ onBeforeUnmount(() => {
215
292
  ref="sheetRef"
216
293
  class="q-date-picker__sheet"
217
294
  :class="`q-date-picker__sheet--${mode}`"
218
- :style="sheetStyle"
295
+ :style="isPopover ? popoverPanelStyle : sheetStyle"
219
296
  role="dialog"
220
- aria-modal="true"
297
+ :aria-modal="isOverlayMode ? 'true' : undefined"
221
298
  :aria-label="sheetTitle"
222
299
  >
223
- <div class="q-date-picker__sheet-header">
300
+ <!-- En-tête (titre + fermeture) réservé aux modes à voile : un popover
301
+ n'a ni titre ni bouton de fermeture, il se ferme au clic extérieur. -->
302
+ <div v-if="isOverlayMode" class="q-date-picker__sheet-header">
224
303
  <span class="q-date-picker__sheet-title">{{ sheetTitle }}</span>
225
304
  <button
226
305
  type="button"
@@ -238,6 +317,8 @@ onBeforeUnmount(() => {
238
317
  :max-date="maxDate"
239
318
  :disabled-dates="disabledDates"
240
319
  :first-day-of-week="firstDayOfWeek"
320
+ :today-btn="todayBtn"
321
+ :month-dropdown="monthDropdown"
241
322
  @select="onSelect"
242
323
  />
243
324
  </div>
@@ -15,11 +15,17 @@ interface Props {
15
15
  disabledDates?: (d: Date) => boolean
16
16
  /** Premier jour de la semaine : 0 = dimanche, 1 = lundi (défaut) */
17
17
  firstDayOfWeek?: number
18
+ /** Raccourci « Today » sous la grille */
19
+ todayBtn?: boolean
20
+ /** En-tête : libellé cliquable ouvrant le choix du mois (et de l'année) */
21
+ monthDropdown?: boolean
18
22
  }
19
23
 
20
24
  const props = withDefaults(defineProps<Props>(), {
21
25
  modelValue: null,
22
26
  firstDayOfWeek: 1,
27
+ todayBtn: false,
28
+ monthDropdown: false,
23
29
  })
24
30
 
25
31
  const emit = defineEmits<{ select: [date: Date] }>()
@@ -27,6 +33,8 @@ const emit = defineEmits<{ select: [date: Date] }>()
27
33
  const today = new Date()
28
34
  const viewYear = ref(props.modelValue?.getFullYear() ?? today.getFullYear())
29
35
  const viewMonth = ref(props.modelValue?.getMonth() ?? today.getMonth())
36
+ /** Choix du mois ouvert (en-tête) — remplace la grille des jours */
37
+ const monthPickerOpen = ref(false)
30
38
 
31
39
  watch(
32
40
  () => props.modelValue,
@@ -68,6 +76,13 @@ const monthLabel = computed(() =>
68
76
  ),
69
77
  )
70
78
 
79
+ /** Noms courts des 12 mois, dans la langue du navigateur (comme `monthLabel`) */
80
+ const monthNames = computed(() =>
81
+ Array.from({ length: 12 }, (_, i) =>
82
+ new Intl.DateTimeFormat(undefined, { month: "short" }).format(new Date(2000, i, 1)),
83
+ ),
84
+ )
85
+
71
86
  const prevMonth = () => {
72
87
  if (viewMonth.value === 0) {
73
88
  viewMonth.value = 11
@@ -88,6 +103,15 @@ const nextMonth = () => {
88
103
  }
89
104
  }
90
105
 
106
+ const prevYear = () => viewYear.value--
107
+ const nextYear = () => viewYear.value++
108
+
109
+ /** Choix d'un mois dans l'en-tête : on saute au mois, la grille revient */
110
+ const onPickMonth = (month: number) => {
111
+ viewMonth.value = month
112
+ monthPickerOpen.value = false
113
+ }
114
+
91
115
  const isSelected = (d: Date) => !!props.modelValue && sameDay(props.modelValue, d)
92
116
  const isToday = (d: Date) => sameDay(d, new Date())
93
117
 
@@ -101,6 +125,14 @@ const onSelectDate = (d: Date) => {
101
125
  if (isDisabled(d)) return
102
126
  emit("select", d)
103
127
  }
128
+
129
+ /** Raccourci « Today » : désactivé quand aujourd'hui est hors bornes / désactivé */
130
+ const todaySelectable = computed(() => !isDisabled(startOfDay(new Date())))
131
+
132
+ const selectToday = () => {
133
+ if (!todaySelectable.value) return
134
+ emit("select", startOfDay(new Date()))
135
+ }
104
136
  </script>
105
137
 
106
138
  <template>
@@ -114,7 +146,19 @@ const onSelectDate = (d: Date) => {
114
146
  >
115
147
  <Icon :icon="icons.chevronLeft" aria-hidden="true" />
116
148
  </button>
117
- <span class="q-date-calendar__nav-label">{{ monthLabel }}</span>
149
+ <button
150
+ v-if="monthDropdown"
151
+ type="button"
152
+ class="q-date-calendar__nav-label q-date-calendar__nav-label--button"
153
+ :class="{ 'q-date-calendar__nav-label--open': monthPickerOpen }"
154
+ :aria-expanded="monthPickerOpen"
155
+ aria-label="Choisir le mois"
156
+ @click="monthPickerOpen = !monthPickerOpen"
157
+ >
158
+ <span class="q-date-calendar__nav-text">{{ monthLabel }}</span>
159
+ <Icon :icon="icons.chevronDown" class="q-date-calendar__nav-caret" aria-hidden="true" />
160
+ </button>
161
+ <span v-else class="q-date-calendar__nav-label">{{ monthLabel }}</span>
118
162
  <button
119
163
  type="button"
120
164
  class="q-date-calendar__nav-btn"
@@ -125,29 +169,78 @@ const onSelectDate = (d: Date) => {
125
169
  </button>
126
170
  </div>
127
171
 
128
- <div class="q-date-calendar__weekdays">
129
- <span v-for="(d, i) in weekdayLabels" :key="i" class="q-date-calendar__weekday">{{ d }}</span>
130
- </div>
131
-
132
- <div class="q-date-calendar__weeks">
133
- <div v-for="(week, wi) in weeks" :key="wi" class="q-date-calendar__week">
172
+ <!-- Choix du mois / de l'année (en-tête cliquable) -->
173
+ <div v-if="monthDropdown && monthPickerOpen" class="q-date-calendar__picker">
174
+ <div class="q-date-calendar__picker-year">
175
+ <button
176
+ type="button"
177
+ class="q-date-calendar__nav-btn"
178
+ aria-label="Année précédente"
179
+ @click="prevYear"
180
+ >
181
+ <Icon :icon="icons.chevronLeft" aria-hidden="true" />
182
+ </button>
183
+ <span class="q-date-calendar__picker-year-label">{{ viewYear }}</span>
134
184
  <button
135
- v-for="(d, di) in week"
136
- :key="di"
137
185
  type="button"
138
- class="q-date-calendar__day"
139
- :class="cn(
140
- !d && 'q-date-calendar__day--empty',
141
- !!d && isToday(d) && 'q-date-calendar__day--today',
142
- !!d && isSelected(d) && 'q-date-calendar__day--selected',
143
- )"
144
- :disabled="!d || isDisabled(d)"
145
- :aria-selected="!!d && isSelected(d) ? 'true' : 'false'"
146
- @click="d && onSelectDate(d)"
186
+ class="q-date-calendar__nav-btn"
187
+ aria-label="Année suivante"
188
+ @click="nextYear"
147
189
  >
148
- {{ d ? d.getDate() : "" }}
190
+ <Icon :icon="icons.chevronRight" aria-hidden="true" />
149
191
  </button>
150
192
  </div>
193
+ <div class="q-date-calendar__picker-months">
194
+ <button
195
+ v-for="(name, index) in monthNames"
196
+ :key="name"
197
+ type="button"
198
+ class="q-date-calendar__picker-month"
199
+ :class="{ 'q-date-calendar__picker-month--current': index === viewMonth }"
200
+ :aria-current="index === viewMonth ? 'true' : undefined"
201
+ @click="onPickMonth(index)"
202
+ >
203
+ {{ name }}
204
+ </button>
205
+ </div>
206
+ </div>
207
+
208
+ <template v-else>
209
+ <div class="q-date-calendar__weekdays">
210
+ <span v-for="(d, i) in weekdayLabels" :key="i" class="q-date-calendar__weekday">{{ d }}</span>
211
+ </div>
212
+
213
+ <div class="q-date-calendar__weeks">
214
+ <div v-for="(week, wi) in weeks" :key="wi" class="q-date-calendar__week">
215
+ <button
216
+ v-for="(d, di) in week"
217
+ :key="di"
218
+ type="button"
219
+ class="q-date-calendar__day"
220
+ :class="cn(
221
+ !d && 'q-date-calendar__day--empty',
222
+ !!d && isToday(d) && 'q-date-calendar__day--today',
223
+ !!d && isSelected(d) && 'q-date-calendar__day--selected',
224
+ )"
225
+ :disabled="!d || isDisabled(d)"
226
+ :aria-selected="!!d && isSelected(d) ? 'true' : 'false'"
227
+ @click="d && onSelectDate(d)"
228
+ >
229
+ {{ d ? d.getDate() : "" }}
230
+ </button>
231
+ </div>
232
+ </div>
233
+ </template>
234
+
235
+ <div v-if="todayBtn" class="q-date-calendar__footer">
236
+ <button
237
+ type="button"
238
+ class="q-date-calendar__today"
239
+ :disabled="!todaySelectable"
240
+ @click="selectToday"
241
+ >
242
+ Today
243
+ </button>
151
244
  </div>
152
245
  </div>
153
246
  </template>
package/index.ts CHANGED
@@ -33,6 +33,7 @@ export { default as QCarousel } from "./components/QCarousel.vue"
33
33
  export { default as QCarouselContent } from "./components/QCarouselContent.vue"
34
34
  export { default as QCarouselItem } from "./components/QCarouselItem.vue"
35
35
  export { default as QCarouselNav } from "./components/QCarouselNav.vue"
36
+ export { default as QChart } from "./components/QChart.vue"
36
37
  export { default as QCheckbox } from "./components/QCheckbox.vue"
37
38
  export { default as QChip } from "./components/QChip.vue"
38
39
  export { default as QCircularProgress } from "./components/QCircularProgress.vue"
@@ -50,7 +51,6 @@ export { default as QDialogFooter } from "./components/QDialogFooter.vue"
50
51
  export { default as QDialogHeader } from "./components/QDialogHeader.vue"
51
52
  export { default as QDialogProvider } from "./components/QDialogProvider.vue"
52
53
  export { default as QDialogTrigger } from "./components/QDialogTrigger.vue"
53
- export { default as QEditorJs } from "./components/QEditorJs.vue"
54
54
  export { default as QFab } from "./components/QFab.vue"
55
55
  export { default as QFabAction } from "./components/QFabAction.vue"
56
56
  export { default as QFilePicker } from "./components/QFilePicker.vue"
@@ -166,8 +166,28 @@ export {
166
166
  } from "./lib/overlayBack"
167
167
  export type { OverlayHandle } from "./lib/overlayBack"
168
168
  export { markOverlayClose, closeParentOverlay, vClose } from "./lib/closeOverlay"
169
+ export { placePopover } from "./lib/datePicker"
170
+ export type {
171
+ PlacePopoverOptions,
172
+ PopoverAnchor,
173
+ PopoverPlacement,
174
+ PopoverViewport,
175
+ } from "./lib/datePicker"
169
176
  export { vRipple } from "./lib/ripple"
170
177
  export type { RippleOptions, RippleValue } from "./lib/ripple"
178
+ export { chartToECharts, channelValue, pickFromEvent, tableModel, textValue } from "./lib/chart"
179
+ export type {
180
+ QChartAxis,
181
+ QChartChannel,
182
+ QChartColumn,
183
+ QChartConfig,
184
+ QChartLegend,
185
+ QChartMark,
186
+ QChartMarkType,
187
+ QChartOrientation,
188
+ QChartTable,
189
+ QChartPick,
190
+ } from "./lib/chart"
171
191
  export { vTouchPan } from "./lib/touchPan"
172
192
  export type { PanDetails, TouchPanHandler, PanDirection } from "./lib/touchPan"
173
193
  export { vTouchHold } from "./lib/touchHold"