@dnax/ui 0.0.64 → 0.0.66
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/components/QDatePicker.vue +12 -5
- package/components/QSpreadsheet.vue +51 -6
- package/components/QTable.vue +20 -6
- package/components/QTabs.vue +11 -2
- package/components/internal/QDateCalendar.vue +25 -23
- package/index.ts +16 -0
- package/lib/dateI18n.test.ts +91 -0
- package/lib/dateI18n.ts +85 -0
- package/lib/formula.test.ts +186 -1
- package/lib/formula.ts +442 -0
- package/lib/network.test.ts +199 -0
- package/lib/network.ts +147 -0
- package/lib/q.ts +6 -1
- package/lib/styleTokens.test.ts +46 -0
- package/lib/wakeLock.test.ts +219 -0
- package/lib/wakeLock.ts +202 -0
- package/package.json +1 -1
- package/runtime.ts +7 -0
- package/styles/main.css +119 -23
|
@@ -9,6 +9,8 @@ import { icons } from "../lib/icons"
|
|
|
9
9
|
import { cn } from "../lib/utils"
|
|
10
10
|
import { useOverlayBack } from "../lib/overlayBack"
|
|
11
11
|
import { placePopover, type PopoverPlacement } from "../lib/datePicker"
|
|
12
|
+
import { useConfigLang } from "../lib/useComponentProps"
|
|
13
|
+
import { dateLabels, dateLocale } from "../lib/dateI18n"
|
|
12
14
|
import QDateCalendar from "./internal/QDateCalendar.vue"
|
|
13
15
|
|
|
14
16
|
interface Props {
|
|
@@ -43,7 +45,7 @@ interface Props {
|
|
|
43
45
|
width?: string
|
|
44
46
|
/** Titre du panneau (modes à voile : sheet/modal/dialog) */
|
|
45
47
|
title?: string
|
|
46
|
-
/** Raccourci «
|
|
48
|
+
/** Raccourci « aujourd'hui » sous le calendrier (tous les modes) */
|
|
47
49
|
todayBtn?: boolean
|
|
48
50
|
/** En-tête : libellé cliquable ouvrant le choix du mois (tous les modes) */
|
|
49
51
|
monthDropdown?: boolean
|
|
@@ -75,6 +77,11 @@ const emit = defineEmits<{
|
|
|
75
77
|
clear: []
|
|
76
78
|
}>()
|
|
77
79
|
|
|
80
|
+
// Langue de l'app (`QConfigProvider lang`) : format d'affichage et libellés.
|
|
81
|
+
const configLang = useConfigLang()
|
|
82
|
+
const locale = computed(() => dateLocale(configLang.value))
|
|
83
|
+
const labels = computed(() => dateLabels(configLang.value))
|
|
84
|
+
|
|
78
85
|
/** Champ déclencheur (tout sauf `inline`) */
|
|
79
86
|
const isPanelMode = computed(() => props.mode !== "inline")
|
|
80
87
|
/** Panneau ancré au champ : ni voile, ni blocage du scroll, ni retour navigateur */
|
|
@@ -98,7 +105,7 @@ const formatFn = computed(
|
|
|
98
105
|
() =>
|
|
99
106
|
props.format ??
|
|
100
107
|
((d: Date) =>
|
|
101
|
-
new Intl.DateTimeFormat(
|
|
108
|
+
new Intl.DateTimeFormat(locale.value, { day: "2-digit", month: "short", year: "numeric" }).format(d)),
|
|
102
109
|
)
|
|
103
110
|
|
|
104
111
|
const displayText = computed(() => (props.modelValue ? formatFn.value(props.modelValue) : ""))
|
|
@@ -139,7 +146,7 @@ const closePopup = () => {
|
|
|
139
146
|
open.value = false
|
|
140
147
|
}
|
|
141
148
|
|
|
142
|
-
const sheetTitle = computed(() => props.title || props.label ||
|
|
149
|
+
const sheetTitle = computed(() => props.title || props.label || labels.value.selectDate)
|
|
143
150
|
|
|
144
151
|
const sheetStyle = computed<Record<string, string> | undefined>(() =>
|
|
145
152
|
props.width ? { width: props.width, maxWidth: props.width } : undefined,
|
|
@@ -263,7 +270,7 @@ onBeforeUnmount(() => {
|
|
|
263
270
|
v-if="clearable && modelValue && !disable && !readonly"
|
|
264
271
|
class="q-field__clear"
|
|
265
272
|
type="button"
|
|
266
|
-
aria-label="
|
|
273
|
+
:aria-label="labels.clear"
|
|
267
274
|
@click.stop="onClear"
|
|
268
275
|
>
|
|
269
276
|
<Icon :icon="icons.x" aria-hidden="true" />
|
|
@@ -304,7 +311,7 @@ onBeforeUnmount(() => {
|
|
|
304
311
|
<button
|
|
305
312
|
type="button"
|
|
306
313
|
class="q-date-picker__sheet-close"
|
|
307
|
-
:aria-label="
|
|
314
|
+
:aria-label="`${labels.close} ${sheetTitle}`"
|
|
308
315
|
@click="closePopup"
|
|
309
316
|
>
|
|
310
317
|
<Icon :icon="icons.x" aria-hidden="true" />
|
|
@@ -1984,12 +1984,30 @@ const onFxKeydown = (e: KeyboardEvent) => {
|
|
|
1984
1984
|
|
|
1985
1985
|
// ─── Autocomplétion des fonctions (barre fx + éditeur formule) ───
|
|
1986
1986
|
const FORMULA_FNS: { name: string; sig: string }[] = [
|
|
1987
|
+
// Agrégats & critères
|
|
1987
1988
|
{ name: "SUM", sig: "a, b, …" },
|
|
1989
|
+
{ name: "SUMIF", sig: "range, criteria, [sum_range]" },
|
|
1990
|
+
{ name: "SUMIFS", sig: "sum_range, range1, crit1…" },
|
|
1988
1991
|
{ name: "AVERAGE", sig: "a, b, …" },
|
|
1989
|
-
{ name: "
|
|
1990
|
-
{ name: "
|
|
1992
|
+
{ name: "AVERAGEIF", sig: "range, criteria, [avg_range]" },
|
|
1993
|
+
{ name: "AVERAGEIFS", sig: "avg_range, range1, crit1…" },
|
|
1991
1994
|
{ name: "COUNT", sig: "a, b, …" },
|
|
1992
1995
|
{ name: "COUNTA", sig: "a, b, …" },
|
|
1996
|
+
{ name: "COUNTIF", sig: "range, criteria" },
|
|
1997
|
+
{ name: "COUNTIFS", sig: "range1, crit1…" },
|
|
1998
|
+
{ name: "SUMPRODUCT", sig: "range1, range2…" },
|
|
1999
|
+
{ name: "MIN", sig: "a, b, …" },
|
|
2000
|
+
{ name: "MAX", sig: "a, b, …" },
|
|
2001
|
+
{ name: "MEDIAN", sig: "a, b, …" },
|
|
2002
|
+
// Statistiques
|
|
2003
|
+
{ name: "LARGE", sig: "range, k" },
|
|
2004
|
+
{ name: "SMALL", sig: "range, k" },
|
|
2005
|
+
{ name: "RANK", sig: "value, range, [order]" },
|
|
2006
|
+
{ name: "STDEV", sig: "a, b, …" },
|
|
2007
|
+
{ name: "STDEVP", sig: "a, b, …" },
|
|
2008
|
+
{ name: "VAR", sig: "a, b, …" },
|
|
2009
|
+
{ name: "VARP", sig: "a, b, …" },
|
|
2010
|
+
// Logique
|
|
1993
2011
|
{ name: "IF", sig: "cond, yes, no" },
|
|
1994
2012
|
{ name: "IFS", sig: "cond1, v1, cond2, v2…" },
|
|
1995
2013
|
{ name: "IFERROR", sig: "value, fallback" },
|
|
@@ -1998,7 +2016,22 @@ const FORMULA_FNS: { name: string; sig: string }[] = [
|
|
|
1998
2016
|
{ name: "AND", sig: "a, b…" },
|
|
1999
2017
|
{ name: "OR", sig: "a, b…" },
|
|
2000
2018
|
{ name: "NOT", sig: "v" },
|
|
2001
|
-
{ name: "
|
|
2019
|
+
{ name: "TRUE", sig: "" },
|
|
2020
|
+
{ name: "FALSE", sig: "" },
|
|
2021
|
+
// Info / erreurs
|
|
2022
|
+
{ name: "ISBLANK", sig: "v" },
|
|
2023
|
+
{ name: "ISNUMBER", sig: "v" },
|
|
2024
|
+
{ name: "ISTEXT", sig: "v" },
|
|
2025
|
+
{ name: "ISERROR", sig: "v" },
|
|
2026
|
+
{ name: "ISERR", sig: "v" },
|
|
2027
|
+
{ name: "ISNA", sig: "v" },
|
|
2028
|
+
{ name: "NA", sig: "" },
|
|
2029
|
+
// Recherche
|
|
2030
|
+
{ name: "VLOOKUP", sig: "key, range, col" },
|
|
2031
|
+
{ name: "XLOOKUP", sig: "key, table, return_range, [not_found]" },
|
|
2032
|
+
{ name: "INDEX", sig: "range, row, [col]" },
|
|
2033
|
+
{ name: "MATCH", sig: "key, range, [type]" },
|
|
2034
|
+
// Maths
|
|
2002
2035
|
{ name: "ABS", sig: "n" },
|
|
2003
2036
|
{ name: "ROUND", sig: "n, [digits]" },
|
|
2004
2037
|
{ name: "ROUNDUP", sig: "n, [digits]" },
|
|
@@ -2011,23 +2044,26 @@ const FORMULA_FNS: { name: string; sig: string }[] = [
|
|
|
2011
2044
|
{ name: "POWER", sig: "n, exp" },
|
|
2012
2045
|
{ name: "PI", sig: "" },
|
|
2013
2046
|
{ name: "RAND", sig: "" },
|
|
2047
|
+
// Texte
|
|
2014
2048
|
{ name: "CONCAT", sig: "a, b…" },
|
|
2015
2049
|
{ name: "CONCATENATE", sig: "a, b…" },
|
|
2050
|
+
{ name: "TEXTJOIN", sig: "delim, ignore_empty, a, b…" },
|
|
2016
2051
|
{ name: "UPPER", sig: "text" },
|
|
2017
2052
|
{ name: "LOWER", sig: "text" },
|
|
2053
|
+
{ name: "PROPER", sig: "text" },
|
|
2018
2054
|
{ name: "TRIM", sig: "text" },
|
|
2019
2055
|
{ name: "LEN", sig: "text" },
|
|
2020
2056
|
{ name: "LEFT", sig: "text, [n]" },
|
|
2021
2057
|
{ name: "RIGHT", sig: "text, [n]" },
|
|
2022
2058
|
{ name: "MID", sig: "text, start, n" },
|
|
2059
|
+
{ name: "SEARCH", sig: "find, text, [start]" },
|
|
2023
2060
|
{ name: "FIND", sig: "find, text, [start]" },
|
|
2024
2061
|
{ name: "SUBSTITUTE", sig: "text, old, new, [n]" },
|
|
2025
2062
|
{ name: "REPLACE", sig: "text, start, n, new" },
|
|
2063
|
+
{ name: "EXACT", sig: "a, b" },
|
|
2026
2064
|
{ name: "VALUE", sig: "text" },
|
|
2027
2065
|
{ name: "N", sig: "v" },
|
|
2028
|
-
|
|
2029
|
-
{ name: "ISNUMBER", sig: "v" },
|
|
2030
|
-
{ name: "ISTEXT", sig: "v" },
|
|
2066
|
+
// Dates
|
|
2031
2067
|
{ name: "TODAY", sig: "" },
|
|
2032
2068
|
{ name: "NOW", sig: "" },
|
|
2033
2069
|
{ name: "DATE", sig: "y, m, d" },
|
|
@@ -2035,6 +2071,15 @@ const FORMULA_FNS: { name: string; sig: string }[] = [
|
|
|
2035
2071
|
{ name: "MONTH", sig: "date" },
|
|
2036
2072
|
{ name: "DAY", sig: "date" },
|
|
2037
2073
|
{ name: "EDATE", sig: "date, months" },
|
|
2074
|
+
{ name: "EOMONTH", sig: "date, months" },
|
|
2075
|
+
{ name: "DATEDIF", sig: "start, end, unit" },
|
|
2076
|
+
{ name: "DAYS", sig: "end, start" },
|
|
2077
|
+
{ name: "WEEKDAY", sig: "date, [type]" },
|
|
2078
|
+
{ name: "WEEKNUM", sig: "date, [type]" },
|
|
2079
|
+
{ name: "ISOWEEKNUM", sig: "date" },
|
|
2080
|
+
{ name: "WORKDAY", sig: "start, days, [holidays]" },
|
|
2081
|
+
{ name: "NETWORKDAYS", sig: "start, end, [holidays]" },
|
|
2082
|
+
{ name: "TEXT", sig: "value, format" },
|
|
2038
2083
|
]
|
|
2039
2084
|
// La source alimente des méthodes de chaîne (`.trim()`, `.startsWith()`) :
|
|
2040
2085
|
// la valeur brute d'une cellule peut être un nombre / booléen → on coerce.
|
package/components/QTable.vue
CHANGED
|
@@ -35,6 +35,7 @@ import { icons } from "../lib/icons"
|
|
|
35
35
|
import { cn } from "../lib/utils"
|
|
36
36
|
import { radiusStyle, useRadius } from "../lib/useComponentProps"
|
|
37
37
|
import type { RadiusProp } from "../lib/useComponentProps"
|
|
38
|
+
import QSelect from "./QSelect.vue"
|
|
38
39
|
|
|
39
40
|
interface Props {
|
|
40
41
|
/** Lignes de données */
|
|
@@ -164,6 +165,13 @@ const rowsPerPage = computed({
|
|
|
164
165
|
get: () => pagination.value.rowsPerPage,
|
|
165
166
|
set: (v) => setPagination({ page: 1, rowsPerPage: v }),
|
|
166
167
|
})
|
|
168
|
+
|
|
169
|
+
/** Libellé d'une option « lignes par page » (0 = tout afficher).
|
|
170
|
+
* Reçoit l'option normalisée par QSelect (`{ value, label }`) ou la valeur brute. */
|
|
171
|
+
const rowsPerPageOptionLabel = (opt: any): string => {
|
|
172
|
+
const value = opt !== null && typeof opt === "object" ? opt.value : opt
|
|
173
|
+
return value === 0 ? "All" : String(value)
|
|
174
|
+
}
|
|
167
175
|
const sortBy = computed(() => pagination.value.sortBy)
|
|
168
176
|
const descending = computed(() => pagination.value.descending)
|
|
169
177
|
|
|
@@ -729,12 +737,18 @@ const virtual = computed(() => {
|
|
|
729
737
|
<slot name="bottom" />
|
|
730
738
|
<div class="q-table__pagination">
|
|
731
739
|
<span class="q-table__range">{{ rangeLabel }}</span>
|
|
732
|
-
<
|
|
733
|
-
Rows per page
|
|
734
|
-
<select
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
740
|
+
<div class="q-table__rows-select">
|
|
741
|
+
<span>Rows per page</span>
|
|
742
|
+
<q-select
|
|
743
|
+
v-model="rowsPerPage"
|
|
744
|
+
class="q-table__rows-field"
|
|
745
|
+
:options="rowsPerPageOptions"
|
|
746
|
+
:option-label="rowsPerPageOptionLabel"
|
|
747
|
+
emit-value
|
|
748
|
+
dense
|
|
749
|
+
aria-label="Rows per page"
|
|
750
|
+
/>
|
|
751
|
+
</div>
|
|
738
752
|
<div class="q-table__page-nav">
|
|
739
753
|
<button
|
|
740
754
|
type="button"
|
package/components/QTabs.vue
CHANGED
|
@@ -183,6 +183,9 @@ const scrollArrows = (dir: -1 | 1) => {
|
|
|
183
183
|
el.scrollBy(props.vertical ? { top: delta, behavior: "smooth" } : { left: delta, behavior: "smooth" })
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
/** Débordement réel → les deux flèches existent (celle du côté bloqué est **désactivée**). */
|
|
187
|
+
const scrollable = computed(() => canScrollStart.value || canScrollEnd.value)
|
|
188
|
+
|
|
186
189
|
// Recalcule la position de l'indicateur (et les flèches) après chaque changement de DOM
|
|
187
190
|
const measure = async () => {
|
|
188
191
|
await nextTick()
|
|
@@ -285,6 +288,10 @@ const tabsClasses = computed(() =>
|
|
|
285
288
|
props.animated && `q-tabs--anim-${props.transition}`,
|
|
286
289
|
props.collapseInactive && "q-tabs--collapse-inactive",
|
|
287
290
|
props.showArrows && "q-tabs--arrows",
|
|
291
|
+
// Fondu des bords : uniquement du côté où il reste du contenu à révéler (le CSS
|
|
292
|
+
// n'utilise qu'un masque alpha → rendu identique en thème clair et sombre).
|
|
293
|
+
props.showArrows && canScrollStart.value && "q-tabs--scroll-start",
|
|
294
|
+
props.showArrows && canScrollEnd.value && "q-tabs--scroll-end",
|
|
288
295
|
),
|
|
289
296
|
)
|
|
290
297
|
</script>
|
|
@@ -299,9 +306,10 @@ const tabsClasses = computed(() =>
|
|
|
299
306
|
:aria-orientation="vertical ? 'vertical' : 'horizontal'"
|
|
300
307
|
>
|
|
301
308
|
<button
|
|
302
|
-
v-if="showArrows &&
|
|
309
|
+
v-if="showArrows && scrollable"
|
|
303
310
|
type="button"
|
|
304
311
|
class="q-tabs__arrow q-tabs__arrow--start"
|
|
312
|
+
:disabled="!canScrollStart"
|
|
305
313
|
:aria-label="vertical ? 'Defiler les onglets vers le haut' : 'Defiler les onglets vers la gauche'"
|
|
306
314
|
@click="scrollArrows(-1)"
|
|
307
315
|
>
|
|
@@ -317,9 +325,10 @@ const tabsClasses = computed(() =>
|
|
|
317
325
|
<div class="q-tabs__indicator" :style="indicatorStyle" aria-hidden="true" />
|
|
318
326
|
</div>
|
|
319
327
|
<button
|
|
320
|
-
v-if="showArrows &&
|
|
328
|
+
v-if="showArrows && scrollable"
|
|
321
329
|
type="button"
|
|
322
330
|
class="q-tabs__arrow q-tabs__arrow--end"
|
|
331
|
+
:disabled="!canScrollEnd"
|
|
323
332
|
:aria-label="vertical ? 'Defiler les onglets vers le bas' : 'Defiler les onglets vers la droite'"
|
|
324
333
|
@click="scrollArrows(1)"
|
|
325
334
|
>
|
|
@@ -4,6 +4,14 @@ import { computed, ref, watch } from "vue"
|
|
|
4
4
|
import { Icon } from "@iconify/vue"
|
|
5
5
|
import { icons } from "../../lib/icons"
|
|
6
6
|
import { cn } from "../../lib/utils"
|
|
7
|
+
import { useConfigLang } from "../../lib/useComponentProps"
|
|
8
|
+
import {
|
|
9
|
+
dateLabels,
|
|
10
|
+
dateLocale,
|
|
11
|
+
monthShortNames,
|
|
12
|
+
monthYearLabel,
|
|
13
|
+
weekdayShortNames,
|
|
14
|
+
} from "../../lib/dateI18n"
|
|
7
15
|
|
|
8
16
|
interface Props {
|
|
9
17
|
modelValue?: Date | null
|
|
@@ -15,7 +23,7 @@ interface Props {
|
|
|
15
23
|
disabledDates?: (d: Date) => boolean
|
|
16
24
|
/** Premier jour de la semaine : 0 = dimanche, 1 = lundi (défaut) */
|
|
17
25
|
firstDayOfWeek?: number
|
|
18
|
-
/**
|
|
26
|
+
/** Libellé du raccourci « aujourd'hui » (texte selon `lang`) */
|
|
19
27
|
todayBtn?: boolean
|
|
20
28
|
/** En-tête : libellé cliquable ouvrant le choix du mois (et de l'année) */
|
|
21
29
|
monthDropdown?: boolean
|
|
@@ -30,6 +38,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
30
38
|
|
|
31
39
|
const emit = defineEmits<{ select: [date: Date] }>()
|
|
32
40
|
|
|
41
|
+
// Langue de l'app (`QConfigProvider lang`) : mois, jours et libellés suivent.
|
|
42
|
+
const configLang = useConfigLang()
|
|
43
|
+
const locale = computed(() => dateLocale(configLang.value))
|
|
44
|
+
const labels = computed(() => dateLabels(configLang.value))
|
|
45
|
+
|
|
33
46
|
const today = new Date()
|
|
34
47
|
const viewYear = ref(props.modelValue?.getFullYear() ?? today.getFullYear())
|
|
35
48
|
const viewMonth = ref(props.modelValue?.getMonth() ?? today.getMonth())
|
|
@@ -51,10 +64,7 @@ const startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDat
|
|
|
51
64
|
const sameDay = (a: Date, b: Date) =>
|
|
52
65
|
a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
|
|
53
66
|
|
|
54
|
-
const weekdayLabels = computed(() =>
|
|
55
|
-
const names = ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"] as const
|
|
56
|
-
return Array.from({ length: 7 }, (_, i) => names[(props.firstDayOfWeek + i) % 7])
|
|
57
|
-
})
|
|
67
|
+
const weekdayLabels = computed(() => weekdayShortNames(locale.value, props.firstDayOfWeek))
|
|
58
68
|
|
|
59
69
|
const weeks = computed<(Date | null)[][]>(() => {
|
|
60
70
|
const y = viewYear.value
|
|
@@ -70,18 +80,10 @@ const weeks = computed<(Date | null)[][]>(() => {
|
|
|
70
80
|
return result
|
|
71
81
|
})
|
|
72
82
|
|
|
73
|
-
const monthLabel = computed(() =>
|
|
74
|
-
new Intl.DateTimeFormat(undefined, { month: "long", year: "numeric" }).format(
|
|
75
|
-
new Date(viewYear.value, viewMonth.value, 1),
|
|
76
|
-
),
|
|
77
|
-
)
|
|
83
|
+
const monthLabel = computed(() => monthYearLabel(locale.value, viewYear.value, viewMonth.value))
|
|
78
84
|
|
|
79
|
-
/** Noms courts des 12 mois, dans la langue
|
|
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
|
+
/** Noms courts des 12 mois, dans la langue de l'app (comme `monthLabel`) */
|
|
86
|
+
const monthNames = computed(() => monthShortNames(locale.value))
|
|
85
87
|
|
|
86
88
|
const prevMonth = () => {
|
|
87
89
|
if (viewMonth.value === 0) {
|
|
@@ -126,7 +128,7 @@ const onSelectDate = (d: Date) => {
|
|
|
126
128
|
emit("select", d)
|
|
127
129
|
}
|
|
128
130
|
|
|
129
|
-
/** Raccourci «
|
|
131
|
+
/** Raccourci « aujourd'hui » : désactivé quand aujourd'hui est hors bornes / désactivé */
|
|
130
132
|
const todaySelectable = computed(() => !isDisabled(startOfDay(new Date())))
|
|
131
133
|
|
|
132
134
|
const selectToday = () => {
|
|
@@ -141,7 +143,7 @@ const selectToday = () => {
|
|
|
141
143
|
<button
|
|
142
144
|
type="button"
|
|
143
145
|
class="q-date-calendar__nav-btn"
|
|
144
|
-
aria-label="
|
|
146
|
+
:aria-label="labels.prevMonth"
|
|
145
147
|
@click="prevMonth"
|
|
146
148
|
>
|
|
147
149
|
<Icon :icon="icons.chevronLeft" aria-hidden="true" />
|
|
@@ -152,7 +154,7 @@ const selectToday = () => {
|
|
|
152
154
|
class="q-date-calendar__nav-label q-date-calendar__nav-label--button"
|
|
153
155
|
:class="{ 'q-date-calendar__nav-label--open': monthPickerOpen }"
|
|
154
156
|
:aria-expanded="monthPickerOpen"
|
|
155
|
-
aria-label="
|
|
157
|
+
:aria-label="labels.chooseMonth"
|
|
156
158
|
@click="monthPickerOpen = !monthPickerOpen"
|
|
157
159
|
>
|
|
158
160
|
<span class="q-date-calendar__nav-text">{{ monthLabel }}</span>
|
|
@@ -162,7 +164,7 @@ const selectToday = () => {
|
|
|
162
164
|
<button
|
|
163
165
|
type="button"
|
|
164
166
|
class="q-date-calendar__nav-btn"
|
|
165
|
-
aria-label="
|
|
167
|
+
:aria-label="labels.nextMonth"
|
|
166
168
|
@click="nextMonth"
|
|
167
169
|
>
|
|
168
170
|
<Icon :icon="icons.chevronRight" aria-hidden="true" />
|
|
@@ -175,7 +177,7 @@ const selectToday = () => {
|
|
|
175
177
|
<button
|
|
176
178
|
type="button"
|
|
177
179
|
class="q-date-calendar__nav-btn"
|
|
178
|
-
aria-label="
|
|
180
|
+
:aria-label="labels.prevYear"
|
|
179
181
|
@click="prevYear"
|
|
180
182
|
>
|
|
181
183
|
<Icon :icon="icons.chevronLeft" aria-hidden="true" />
|
|
@@ -184,7 +186,7 @@ const selectToday = () => {
|
|
|
184
186
|
<button
|
|
185
187
|
type="button"
|
|
186
188
|
class="q-date-calendar__nav-btn"
|
|
187
|
-
aria-label="
|
|
189
|
+
:aria-label="labels.nextYear"
|
|
188
190
|
@click="nextYear"
|
|
189
191
|
>
|
|
190
192
|
<Icon :icon="icons.chevronRight" aria-hidden="true" />
|
|
@@ -239,7 +241,7 @@ const selectToday = () => {
|
|
|
239
241
|
:disabled="!todaySelectable"
|
|
240
242
|
@click="selectToday"
|
|
241
243
|
>
|
|
242
|
-
|
|
244
|
+
{{ labels.today }}
|
|
243
245
|
</button>
|
|
244
246
|
</div>
|
|
245
247
|
</div>
|
package/index.ts
CHANGED
|
@@ -164,6 +164,22 @@ export { screen } from "./lib/screen"
|
|
|
164
164
|
export type { QScreen } from "./lib/screen"
|
|
165
165
|
export { loading } from "./lib/loading"
|
|
166
166
|
export type { LoadingOptions, QLoadingPlugin } from "./lib/loading"
|
|
167
|
+
export { network, createNetwork, resolveOnline, onlineAfterEvent } from "./lib/network"
|
|
168
|
+
export type { QNetwork, NetworkEvent, NetworkOptions, NetworkEventTarget, NetworkNavigator } from "./lib/network"
|
|
169
|
+
export {
|
|
170
|
+
wakeLock,
|
|
171
|
+
createWakeLock,
|
|
172
|
+
isWakeLockCapable,
|
|
173
|
+
isDocumentVisible,
|
|
174
|
+
shouldReacquire,
|
|
175
|
+
} from "./lib/wakeLock"
|
|
176
|
+
export type {
|
|
177
|
+
QWakeLock,
|
|
178
|
+
WakeLockOptions,
|
|
179
|
+
WakeLockApiLike,
|
|
180
|
+
WakeLockSentinelLike,
|
|
181
|
+
WakeLockVisibilityLike,
|
|
182
|
+
} from "./lib/wakeLock"
|
|
167
183
|
export { localStorage, sessionStorage } from "./lib/storage"
|
|
168
184
|
export type { QWebStorage } from "./lib/storage"
|
|
169
185
|
export {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Libellés / locale des composants de date — pilotés par le `lang` du provider.
|
|
2
|
+
import { describe, expect, it } from "bun:test"
|
|
3
|
+
import {
|
|
4
|
+
dateLabels,
|
|
5
|
+
dateLocale,
|
|
6
|
+
monthShortNames,
|
|
7
|
+
monthYearLabel,
|
|
8
|
+
weekdayShortNames,
|
|
9
|
+
} from "./dateI18n"
|
|
10
|
+
|
|
11
|
+
describe("dateLocale", () => {
|
|
12
|
+
it("mappe la langue de l'app sur une locale BCP 47 déterministe", () => {
|
|
13
|
+
expect(dateLocale("fr")).toBe("fr-FR")
|
|
14
|
+
expect(dateLocale("en")).toBe("en-US")
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe("dateLabels", () => {
|
|
19
|
+
it("« Today » devient « Aujourd'hui » en français", () => {
|
|
20
|
+
expect(dateLabels("fr").today).toBe("Aujourd'hui")
|
|
21
|
+
expect(dateLabels("en").today).toBe("Today")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("couvre les libellés du panneau et de la navigation", () => {
|
|
25
|
+
expect(dateLabels("fr").selectDate).toBe("Sélectionner une date")
|
|
26
|
+
expect(dateLabels("fr").clear).toBe("Effacer")
|
|
27
|
+
expect(dateLabels("fr").close).toBe("Fermer")
|
|
28
|
+
expect(dateLabels("fr").prevMonth).toBe("Mois précédent")
|
|
29
|
+
expect(dateLabels("fr").nextMonth).toBe("Mois suivant")
|
|
30
|
+
expect(dateLabels("en").selectDate).toBe("Select a date")
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("retombe sur l'anglais pour une langue inconnue (JS non typé)", () => {
|
|
34
|
+
expect(dateLabels("de" as never).today).toBe("Today")
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe("weekdayShortNames", () => {
|
|
39
|
+
it("suit la locale demandée, pas celle du navigateur", () => {
|
|
40
|
+
expect(weekdayShortNames("fr-FR", 1)[0]).toMatch(/^lun/i)
|
|
41
|
+
expect(weekdayShortNames("en-US", 1)[0]).toMatch(/^mon/i)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("commence par le jour demandé (0 = dimanche)", () => {
|
|
45
|
+
const monday = weekdayShortNames("en-US", 1)
|
|
46
|
+
const sunday = weekdayShortNames("en-US", 0)
|
|
47
|
+
expect(sunday[0]).toMatch(/^sun/i)
|
|
48
|
+
expect(sunday[0]).toBe(monday[6])
|
|
49
|
+
expect(sunday[6]).toBe(monday[5])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("rend 7 jours sans point final ni minuscule initiale", () => {
|
|
53
|
+
const names = weekdayShortNames("fr-FR")
|
|
54
|
+
expect(names).toHaveLength(7)
|
|
55
|
+
for (const name of names) {
|
|
56
|
+
expect(name.endsWith(".")).toBe(false)
|
|
57
|
+
expect(name[0]).toBe(name[0]!.toUpperCase())
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe("monthShortNames", () => {
|
|
63
|
+
it("rend les 12 mois dans la langue demandée", () => {
|
|
64
|
+
const fr = monthShortNames("fr-FR")
|
|
65
|
+
const en = monthShortNames("en-US")
|
|
66
|
+
expect(fr).toHaveLength(12)
|
|
67
|
+
expect(en).toEqual([
|
|
68
|
+
"Jan",
|
|
69
|
+
"Feb",
|
|
70
|
+
"Mar",
|
|
71
|
+
"Apr",
|
|
72
|
+
"May",
|
|
73
|
+
"Jun",
|
|
74
|
+
"Jul",
|
|
75
|
+
"Aug",
|
|
76
|
+
"Sep",
|
|
77
|
+
"Oct",
|
|
78
|
+
"Nov",
|
|
79
|
+
"Dec",
|
|
80
|
+
])
|
|
81
|
+
expect(fr[0]).toMatch(/^janv/i)
|
|
82
|
+
expect(fr[11]).toMatch(/^déc/i)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe("monthYearLabel", () => {
|
|
87
|
+
it("en-tête long dans la langue demandée", () => {
|
|
88
|
+
expect(monthYearLabel("en-US", 2026, 0)).toBe("January 2026")
|
|
89
|
+
expect(monthYearLabel("fr-FR", 2026, 0)).toContain("janvier 2026")
|
|
90
|
+
})
|
|
91
|
+
})
|
package/lib/dateI18n.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Libellés et locale des composants de date (QDatePicker / QDateCalendar).
|
|
2
|
+
//
|
|
3
|
+
// La langue vient du `QConfigProvider` (`lang`, défaut "en") et arrive ici via
|
|
4
|
+
// `useConfigLang()`. On ne s'appuie **pas** sur la locale du navigateur : le rendu
|
|
5
|
+
// doit être déterministe (serveur ↔ client) et suivre la langue choisie dans l'app.
|
|
6
|
+
import type { QAppLang } from "./config"
|
|
7
|
+
|
|
8
|
+
/** Clés d'interface des composants de date. */
|
|
9
|
+
export type DateUiKey =
|
|
10
|
+
| "today"
|
|
11
|
+
| "prevMonth"
|
|
12
|
+
| "nextMonth"
|
|
13
|
+
| "chooseMonth"
|
|
14
|
+
| "prevYear"
|
|
15
|
+
| "nextYear"
|
|
16
|
+
| "selectDate"
|
|
17
|
+
| "clear"
|
|
18
|
+
| "close"
|
|
19
|
+
|
|
20
|
+
const DATE_UI: Record<QAppLang, Record<DateUiKey, string>> = {
|
|
21
|
+
en: {
|
|
22
|
+
today: "Today",
|
|
23
|
+
prevMonth: "Previous month",
|
|
24
|
+
nextMonth: "Next month",
|
|
25
|
+
chooseMonth: "Choose month",
|
|
26
|
+
prevYear: "Previous year",
|
|
27
|
+
nextYear: "Next year",
|
|
28
|
+
selectDate: "Select a date",
|
|
29
|
+
clear: "Clear",
|
|
30
|
+
close: "Close",
|
|
31
|
+
},
|
|
32
|
+
fr: {
|
|
33
|
+
today: "Aujourd'hui",
|
|
34
|
+
prevMonth: "Mois précédent",
|
|
35
|
+
nextMonth: "Mois suivant",
|
|
36
|
+
chooseMonth: "Choisir le mois",
|
|
37
|
+
prevYear: "Année précédente",
|
|
38
|
+
nextYear: "Année suivante",
|
|
39
|
+
selectDate: "Sélectionner une date",
|
|
40
|
+
clear: "Effacer",
|
|
41
|
+
close: "Fermer",
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Locale BCP 47 associée à la langue de l'app (déterministe, contrairement à `undefined`). */
|
|
46
|
+
export function dateLocale(lang: QAppLang): string {
|
|
47
|
+
return lang === "fr" ? "fr-FR" : "en-US"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Libellés d'interface des composants de date. */
|
|
51
|
+
export function dateLabels(lang: QAppLang): Record<DateUiKey, string> {
|
|
52
|
+
return DATE_UI[lang] ?? DATE_UI.en
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Abréviation : initiale en majuscule, sans point final (`dim.` → `Dim`). */
|
|
56
|
+
function shortName(value: string): string {
|
|
57
|
+
const clean = value.replace(/[.,]+$/, "").trim()
|
|
58
|
+
return clean.charAt(0).toUpperCase() + clean.slice(1)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Jours de la semaine abrégés, en commençant par `firstDayOfWeek`
|
|
63
|
+
* (0 = dimanche, 1 = lundi).
|
|
64
|
+
*/
|
|
65
|
+
export function weekdayShortNames(locale: string, firstDayOfWeek = 1): string[] {
|
|
66
|
+
const fmt = new Intl.DateTimeFormat(locale, { weekday: "short" })
|
|
67
|
+
// 2024-01-07 est un dimanche — référence stable, indépendante de la date du jour.
|
|
68
|
+
const names = Array.from({ length: 7 }, (_, i) =>
|
|
69
|
+
shortName(fmt.format(new Date(2024, 0, 7 + i))),
|
|
70
|
+
)
|
|
71
|
+
const start = ((firstDayOfWeek % 7) + 7) % 7
|
|
72
|
+
return Array.from({ length: 7 }, (_, i) => names[(start + i) % 7]!)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Mois abrégés, dans l'ordre de l'année (`Janv.` / `Jan`). */
|
|
76
|
+
export function monthShortNames(locale: string): string[] {
|
|
77
|
+
const fmt = new Intl.DateTimeFormat(locale, { month: "short" })
|
|
78
|
+
return Array.from({ length: 12 }, (_, i) => shortName(fmt.format(new Date(2024, i, 1))))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** En-tête du calendrier : « janvier 2026 » / « January 2026 ». */
|
|
82
|
+
export function monthYearLabel(locale: string, year: number, month: number): string {
|
|
83
|
+
return new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" })
|
|
84
|
+
.format(new Date(year, month, 1))
|
|
85
|
+
}
|