codevdesign 1.0.51 → 1.0.53

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,58 +1,79 @@
1
1
  <template>
2
2
  <v-date-input
3
- v-model="dateValeur"
3
+ v-model="dateInterne"
4
4
  v-bind="$attrs"
5
5
  input-format="yyyy-MM-dd"
6
6
  variant="outlined"
7
7
  prepend-icon=""
8
8
  prepend-inner-icon="$calendar"
9
9
  density="comfortable"
10
- @update:model-value="formatDate"
11
- ></v-date-input>
10
+ @update:model-value="choixUtilisateur"
11
+ />
12
12
  </template>
13
13
 
14
14
  <script setup lang="ts">
15
- import { ref, watch } from 'vue'
15
+ import { computed } from 'vue'
16
16
  import { VDateInput } from 'vuetify/labs/VDateInput'
17
17
 
18
- const props = withDefaults(
19
- defineProps<{
20
- modelValue: string | Date | null
21
- }>(),
22
- {},
23
- )
24
-
25
- const emit = defineEmits(['update:modelValue'])
26
- const dateValeur = ref<Date | string | null>(props.modelValue)
27
-
28
- const formatDate = (date: Date | string | null) => {
29
- if (date == null) {
30
- dateValeur.value = null
31
- emit('update:modelValue', null)
32
- return
33
- }
18
+ type Model = string | Date | null
19
+
20
+ const props = defineProps<{ modelValue: Model }>()
21
+
22
+ const emit = defineEmits<{
23
+ (e: 'update:model-value', v: string | null): void
24
+ (e: 'change', v: string | null): void
25
+ }>()
26
+
27
+ // Normalisation, TOUJOURS "YYYY-MM-DD" ou null
28
+ const normalizeToYmd = (v: Date | string | null | undefined): string | null => {
29
+ if (v == null || v === '') return null
34
30
 
35
- if (typeof date === 'string') {
36
- dateValeur.value = date
37
- emit('update:modelValue', date)
38
- return
31
+ if (v instanceof Date) {
32
+ if (isNaN(v.getTime())) return null
33
+ const yyyy = v.getFullYear()
34
+ const mm = String(v.getMonth() + 1).padStart(2, '0')
35
+ const dd = String(v.getDate()).padStart(2, '0')
36
+ return `${yyyy}-${mm}-${dd}`
39
37
  }
40
38
 
41
- const d = new Date(date)
39
+ const s = String(v).trim()
40
+
41
+ // si ça commence par YYYY-MM-DD, on prend les 10 premiers chars
42
+ const m = s.match(/^(\d{4}-\d{2}-\d{2})/)
43
+ if (m) return m[1]
44
+
45
+ // Sinon, on tente un fallback
46
+ const d = new Date(s)
47
+ if (isNaN(d.getTime())) return null
42
48
  const yyyy = d.getFullYear()
43
49
  const mm = String(d.getMonth() + 1).padStart(2, '0')
44
50
  const dd = String(d.getDate()).padStart(2, '0')
45
- const formatte = `${yyyy}-${mm}-${dd}`
46
-
47
- dateValeur.value = formatte
48
- emit('update:modelValue', formatte)
51
+ return `${yyyy}-${mm}-${dd}`
49
52
  }
50
53
 
51
- // Synchronise les props externes avec l'état interne
52
- watch(
53
- () => props.modelValue,
54
- nouvelle => {
55
- dateValeur.value = nouvelle
54
+ // UI: VDateInput stable avec Date|null
55
+ const dateInterne = computed<Date | null>({
56
+ get() {
57
+ const ymd = normalizeToYmd(props.modelValue as any)
58
+ if (!ymd) return null
59
+
60
+ // Construire une Date locale sans décalage timezone (évite UTC shift)
61
+ const [y, m, d] = ymd.split('-').map(Number)
62
+ return new Date(y, m - 1, d)
63
+ },
64
+ set() {
65
+ // rien: on émet seulement dans choixUtilisateur
56
66
  },
57
- )
67
+ })
68
+
69
+ const choixUtilisateur = (v: Date | string | null) => {
70
+ const next = normalizeToYmd(v)
71
+ const current = normalizeToYmd(props.modelValue as any)
72
+
73
+ // Bloque la 2e émission au blur (même si type diffère Date/string)
74
+ if (next === current) return
75
+
76
+ emit('update:model-value', next)
77
+ emit('change', next)
78
+ }
58
79
  </script>
@@ -1,193 +1,220 @@
1
- <template>
2
- <v-row
3
- dense
4
- class="align-center"
5
- >
6
- <!-- Texte + détails -->
7
- <v-col
8
- cols="10"
9
- xl="11"
10
- class="py-0"
11
- >
12
- <component
13
- :is="labelCliquable ? 'label' : 'div'"
14
- :for="labelCliquable ? switchId : undefined"
15
- class="labelSwitchSiSwitchApres"
16
- :class="{ 'label-cliquable': labelCliquable && !desactiver }"
17
- >
18
- <slot name="label">
19
- {{ texte }}
20
- </slot>
21
- </component>
22
-
23
- <div
24
- v-if="afficherDetails"
25
- class="details"
26
- >
27
- <slot name="details">
28
- <span v-html="texteDetaille"></span>
29
- </slot>
30
- </div>
31
- </v-col>
32
-
33
- <!-- Switch -->
34
- <v-col
35
- cols="2"
36
- xl="1"
37
- class="d-flex align-center justify-end py-0"
38
- >
39
- <span class="d-inline-flex">
40
- <v-switch
41
- :id="switchId"
42
- class="switch-compact switch-tristate"
43
- :class="{
44
- 'is-null': maValeur === null,
45
- 'is-true': maValeur === true,
46
- 'is-false': maValeur === false,
47
- }"
48
- :disabled="desactiver"
49
- hide-details
50
- v-bind="$attrs"
51
- :model-value="switchChecked"
52
- :indeterminate="isIndeterminate"
53
- indeterminate-icon="mdi-minus"
54
- @click.prevent="onToggle"
55
- @keydown.enter.prevent="onToggle"
56
- @keydown.space.prevent="onToggle"
57
- />
58
-
59
- <v-tooltip
60
- v-if="maValeur === null"
61
- activator="parent"
62
- >
63
- {{ $t("csqc.csqcOptionSwitch.indeterminee") }}
64
- </v-tooltip>
65
- </span>
66
- </v-col>
67
- </v-row>
68
- </template>
69
-
70
- <script setup lang="ts">
71
- import { computed, useSlots } from 'vue'
72
-
73
- type TriBool = boolean | null
74
-
75
- const props = defineProps({
76
- valeurInverse: { type: Boolean, default: false },
77
- desactiver: { type: Boolean, default: false },
78
- modelValue: { type: [Boolean, null] as unknown as () => TriBool, default: false },
79
- autoriserNull: { type: Boolean, default: false },
80
- texte: { type: String, required: true },
81
- texteDetaille: { type: String, default: '' },
82
- labelCliquable: { type: Boolean, default: true },
83
- })
84
-
85
- const emit = defineEmits<{
86
- (e: 'update:modelValue', v: TriBool): void
87
- }>()
88
- const isIndeterminate = computed(() => maValeur.value === null)
89
-
90
-
91
- const slots = useSlots()
92
-
93
- /**
94
- * Valeur "logique" du composant (après inversion).
95
- * Peut être true/false/null si allowNull=true.
96
- */
97
- const maValeur = computed<TriBool>({
98
- get: () => (props.valeurInverse ? inverseTri(props.modelValue) : props.modelValue),
99
- set: v => emit('update:modelValue', props.valeurInverse ? inverseTri(v) : v),
100
- })
101
-
102
- function inverseTri(v: TriBool): TriBool {
103
- if (v === null) return null
104
- return !v
105
- }
106
-
107
- /**
108
- * v-switch attend un bool pour afficher ON/OFF.
109
- * - null => on affiche OFF (false) visuellement
110
- */
111
- const switchChecked = computed(() => maValeur.value === true)
112
-
113
- function onToggle() {
114
- if (props.desactiver) return
115
-
116
- // Mode tri-state
117
- if (props.autoriserNull) {
118
- // cycle: null -> true -> false -> null
119
- const cur = maValeur.value
120
- const next: TriBool = cur === null ? true : cur === true ? false : null
121
- maValeur.value = next
122
- return
123
- }
124
-
125
- // Mode classique (ne change rien vs avant)
126
- maValeur.value = !switchChecked.value
127
- }
128
-
129
- const afficherDetails = computed(() => {
130
- const slotExiste = !!slots.details?.().length
131
- return slotExiste || props.texteDetaille.trim().length > 0
132
- })
133
-
134
- const switchId = `sw_${Math.random().toString(36).slice(2)}`
135
- </script>
136
-
137
- <style scoped>
138
- /* OFF (false) */
139
- :deep(.switch-tristate.is-false .v-switch__track) {
140
- opacity: 0.22 !important;
141
- }
142
- :deep(.switch-tristate.is-false .v-switch__thumb) {
143
- opacity: 0.65 !important;
144
- }
145
-
146
- /* NULL (centre + très distinct) */
147
- :deep(.switch-tristate.is-null .v-switch__track) {
148
- opacity: 1 !important;
149
- background:
150
- repeating-linear-gradient(
151
- 45deg,
152
- rgba(255, 255, 255, 0.22),
153
- rgba(255, 255, 255, 0.22) 6px,
154
- rgba(255, 255, 255, 0.05) 6px,
155
- rgba(255, 255, 255, 0.05) 12px
156
- ),
157
- rgb(var(--v-theme-warning)) !important;
158
- }
159
-
160
- :deep(.switch-tristate.is-null .v-switch__thumb) {
161
- transform: translateX(calc((100% - 20px) / 2)) !important;
162
- background: white !important;
163
- opacity: 1 !important;
164
- box-shadow:
165
- 0 0 0 2px rgba(var(--v-theme-warning), 0.55),
166
- 0 2px 6px rgba(0, 0, 0, 0.18) !important;
167
- }
168
-
169
- /* Icône minus visible sur fond blanc */
170
- :deep(.switch-tristate.is-null .v-selection-control__input-icon) {
171
- color: rgb(var(--v-theme-warning)) !important;
172
- }
173
-
174
- /* Transition */
175
- :deep(.switch-tristate .v-switch__thumb) {
176
- transition:
177
- transform 0.18s ease,
178
- background 0.18s ease,
179
- box-shadow 0.18s ease;
180
- }
181
- .labelSwitchSiSwitchApres {
182
- font-weight: bold;
183
- line-height: 1.2;
184
- }
185
-
186
- .label-cliquable {
187
- cursor: pointer;
188
- }
189
-
190
- .details {
191
- margin-top: 4px;
192
- }
193
- </style>
1
+ <template>
2
+ <v-row
3
+ dense
4
+ class="align-center"
5
+ >
6
+ <!-- Texte + détails -->
7
+ <v-col
8
+ cols="10"
9
+ xl="11"
10
+ class="py-0"
11
+ >
12
+ <component
13
+ :is="labelCliquable ? 'label' : 'div'"
14
+ :for="labelCliquable ? switchId : undefined"
15
+ class="labelSwitchSiSwitchApres"
16
+ :class="{ 'label-cliquable': labelCliquable && !desactiver }"
17
+ >
18
+ <div class="label-ligne">
19
+ <div class="label-gauche">
20
+ <slot name="label">
21
+ {{ texte }}
22
+ </slot>
23
+ </div>
24
+
25
+ <div
26
+ v-if="$slots.valeurDroite"
27
+ class="label-droite"
28
+ >
29
+ <slot name="valeurDroite" />
30
+ </div>
31
+ </div>
32
+ </component>
33
+
34
+ <div
35
+ v-if="afficherDetails"
36
+ class="details"
37
+ >
38
+ <slot name="details">
39
+ <span v-html="texteDetaille"></span>
40
+ </slot>
41
+ </div>
42
+ </v-col>
43
+
44
+ <!-- Switch -->
45
+ <v-col
46
+ cols="2"
47
+ xl="1"
48
+ class="d-flex align-center justify-end py-0"
49
+ >
50
+ <span class="d-inline-flex">
51
+ <v-switch
52
+ :id="switchId"
53
+ class="switch-compact switch-tristate"
54
+ :class="{
55
+ 'is-null': maValeur === null,
56
+ 'is-true': maValeur === true,
57
+ 'is-false': maValeur === false,
58
+ }"
59
+ :disabled="desactiver"
60
+ hide-details
61
+ v-bind="$attrs"
62
+ :model-value="switchChecked"
63
+ :indeterminate="isIndeterminate"
64
+ indeterminate-icon="mdi-minus"
65
+ @click.prevent="onToggle"
66
+ @keydown.enter.prevent="onToggle"
67
+ @keydown.space.prevent="onToggle"
68
+ />
69
+
70
+ <v-tooltip
71
+ v-if="maValeur === null"
72
+ activator="parent"
73
+ >
74
+ {{ $t('csqc.csqcOptionSwitch.indeterminee') }}
75
+ </v-tooltip>
76
+ </span>
77
+ </v-col>
78
+ </v-row>
79
+ </template>
80
+
81
+ <script setup lang="ts">
82
+ import { computed, useSlots } from 'vue'
83
+
84
+ type TriBool = boolean | null
85
+
86
+ const props = defineProps({
87
+ valeurInverse: { type: Boolean, default: false },
88
+ desactiver: { type: Boolean, default: false },
89
+ modelValue: { type: [Boolean, null] as unknown as () => TriBool, default: false },
90
+ autoriserNull: { type: Boolean, default: false },
91
+ texte: { type: String, required: true },
92
+ texteDetaille: { type: String, default: '' },
93
+ labelCliquable: { type: Boolean, default: true },
94
+ })
95
+
96
+ const emit = defineEmits<{
97
+ (e: 'update:modelValue', v: TriBool): void
98
+ }>()
99
+ const isIndeterminate = computed(() => maValeur.value === null)
100
+
101
+ const slots = useSlots()
102
+
103
+ /**
104
+ * Valeur "logique" du composant (après inversion).
105
+ * Peut être true/false/null si allowNull=true.
106
+ */
107
+ const maValeur = computed<TriBool>({
108
+ get: () => (props.valeurInverse ? inverseTri(props.modelValue) : props.modelValue),
109
+ set: v => emit('update:modelValue', props.valeurInverse ? inverseTri(v) : v),
110
+ })
111
+
112
+ function inverseTri(v: TriBool): TriBool {
113
+ if (v === null) return null
114
+ return !v
115
+ }
116
+
117
+ /**
118
+ * v-switch attend un bool pour afficher ON/OFF.
119
+ * - null => on affiche OFF (false) visuellement
120
+ */
121
+ const switchChecked = computed(() => maValeur.value === true)
122
+
123
+ function onToggle() {
124
+ if (props.desactiver) return
125
+
126
+ // Mode tri-state
127
+ if (props.autoriserNull) {
128
+ // cycle: null -> true -> false -> null
129
+ const cur = maValeur.value
130
+ const next: TriBool = cur === null ? true : cur === true ? false : null
131
+ maValeur.value = next
132
+ return
133
+ }
134
+
135
+ // Mode classique (ne change rien vs avant)
136
+ maValeur.value = !switchChecked.value
137
+ }
138
+
139
+ const afficherDetails = computed(() => {
140
+ const slotExiste = !!slots.details?.().length
141
+ return slotExiste || props.texteDetaille.trim().length > 0
142
+ })
143
+
144
+ const switchId = `sw_${Math.random().toString(36).slice(2)}`
145
+ </script>
146
+
147
+ <style scoped>
148
+ /* OFF (false) */
149
+ :deep(.switch-tristate.is-false .v-switch__track) {
150
+ opacity: 0.22 !important;
151
+ }
152
+ :deep(.switch-tristate.is-false .v-switch__thumb) {
153
+ opacity: 0.65 !important;
154
+ }
155
+
156
+ /* NULL (centre + très distinct) */
157
+ :deep(.switch-tristate.is-null .v-switch__track) {
158
+ opacity: 1 !important;
159
+ background:
160
+ repeating-linear-gradient(
161
+ 45deg,
162
+ rgba(255, 255, 255, 0.22),
163
+ rgba(255, 255, 255, 0.22) 6px,
164
+ rgba(255, 255, 255, 0.05) 6px,
165
+ rgba(255, 255, 255, 0.05) 12px
166
+ ),
167
+ rgb(var(--v-theme-warning)) !important;
168
+ }
169
+
170
+ :deep(.switch-tristate.is-null .v-switch__thumb) {
171
+ transform: translateX(calc((100% - 20px) / 2)) !important;
172
+ background: white !important;
173
+ opacity: 1 !important;
174
+ box-shadow:
175
+ 0 0 0 2px rgba(var(--v-theme-warning), 0.55),
176
+ 0 2px 6px rgba(0, 0, 0, 0.18) !important;
177
+ }
178
+
179
+ /* Icône minus visible sur fond blanc */
180
+ :deep(.switch-tristate.is-null .v-selection-control__input-icon) {
181
+ color: rgb(var(--v-theme-warning)) !important;
182
+ }
183
+
184
+ /* Transition */
185
+ :deep(.switch-tristate .v-switch__thumb) {
186
+ transition:
187
+ transform 0.18s ease,
188
+ background 0.18s ease,
189
+ box-shadow 0.18s ease;
190
+ }
191
+ .labelSwitchSiSwitchApres {
192
+ font-weight: 700;
193
+ line-height: 1.2;
194
+ }
195
+
196
+ .label-ligne {
197
+ display: flex;
198
+ align-items: baseline;
199
+ justify-content: space-between;
200
+ gap: 12px;
201
+ }
202
+
203
+ .label-gauche {
204
+ min-width: 0;
205
+ }
206
+
207
+ .label-droite {
208
+ font-weight: 400; /* pas en gras */
209
+ opacity: 0.75;
210
+ white-space: nowrap; /* reste sur la même ligne */
211
+ }
212
+
213
+ .label-cliquable {
214
+ cursor: pointer;
215
+ }
216
+
217
+ .details {
218
+ margin-top: 4px;
219
+ }
220
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codevdesign",
3
- "version": "1.0.51",
3
+ "version": "1.0.53",
4
4
  "description": "Composants Vuetify 3 pour les projets Codev",
5
5
  "files": [
6
6
  "./**/*.vue",