@dnax/ui 0.0.72 → 0.0.75
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/QBtnDropdown.vue +8 -1
- package/components/QCard.vue +3 -3
- package/components/QDialog.vue +2 -2
- package/components/QGridItem.vue +1 -1
- package/components/QImg.vue +3 -1
- package/components/QInput.vue +60 -21
- package/components/QInputOtp.vue +2 -0
- package/components/QInputTextarea.vue +211 -0
- package/components/QSyntax.vue +2 -2
- package/components/QTable.vue +2 -2
- package/index.ts +9 -0
- package/lib/iconSet.test.ts +61 -0
- package/lib/iconSet.ts +91 -0
- package/lib/iconSetScrape.ts +139 -0
- package/lib/icons.ts +16 -0
- package/lib/registerIcons.ts +37 -0
- package/lib/textarea.test.ts +80 -0
- package/lib/textarea.ts +68 -0
- package/lib/useComponentProps.test.ts +55 -1
- package/lib/useComponentProps.ts +15 -0
- package/lib/useTextareaAutosize.ts +83 -0
- package/module.ts +124 -6
- package/package.json +4 -2
- package/runtime.ts +5 -0
- package/scripts/gen-icon-set.ts +110 -0
- package/scripts/generate-exports.ts +46 -69
- package/styles/main.css +68 -0
|
@@ -31,6 +31,7 @@ export interface DropdownItem {
|
|
|
31
31
|
import { computed } from "vue"
|
|
32
32
|
import type { StyleValue } from "vue"
|
|
33
33
|
import QBtnActions, { type DropdownPosition } from "./QBtnActions.vue"
|
|
34
|
+
import { useRadius } from "../lib/useComponentProps"
|
|
34
35
|
import type { RadiusProp } from "../lib/useComponentProps"
|
|
35
36
|
|
|
36
37
|
interface Props {
|
|
@@ -115,6 +116,12 @@ const mappedActions = computed(() =>
|
|
|
115
116
|
)
|
|
116
117
|
|
|
117
118
|
const onSelectAction = (value: any) => emit("select", value)
|
|
119
|
+
|
|
120
|
+
// radius : prop explicite > composantProps.QBtnDropdown > composantProps.default.
|
|
121
|
+
// On transmet la valeur **résolue** (et non la prop brute) pour que
|
|
122
|
+
// `componentProps.QBtnDropdown.radius` ne soit pas ignoré au profit de celui du
|
|
123
|
+
// moteur QBtnActions.
|
|
124
|
+
const effectiveRadius = useRadius("QBtnDropdown", () => props.radius)
|
|
118
125
|
</script>
|
|
119
126
|
|
|
120
127
|
<template>
|
|
@@ -134,7 +141,7 @@ const onSelectAction = (value: any) => emit("select", value)
|
|
|
134
141
|
:color="color"
|
|
135
142
|
:text-color="textColor"
|
|
136
143
|
:size="size"
|
|
137
|
-
:radius="
|
|
144
|
+
:radius="effectiveRadius"
|
|
138
145
|
:flat="flat"
|
|
139
146
|
:outline="outline"
|
|
140
147
|
:unelevated="unelevated"
|
package/components/QCard.vue
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Pas d'ombre par défaut ; elevated l'ajoute, flat la retire explicitement.
|
|
4
4
|
// Slots : default, media (zone image en haut), actions (zone actions en bas)
|
|
5
5
|
import { computed } from "vue"
|
|
6
|
-
import {
|
|
6
|
+
import { radiusStyleNative, useRadius } from "../lib/useComponentProps"
|
|
7
7
|
import type { RadiusProp } from "../lib/useComponentProps"
|
|
8
8
|
|
|
9
9
|
interface Props {
|
|
@@ -35,9 +35,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
35
35
|
hover: false,
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
-
// radius : prop explicite > composantProps.QCard.radius ; échelle → --q-radius
|
|
38
|
+
// radius : prop explicite > composantProps.QCard.radius ; échelle → --q-radius (true = md)
|
|
39
39
|
const effectiveRadius = useRadius("QCard", () => props.radius)
|
|
40
|
-
const roundedStyle = computed(() =>
|
|
40
|
+
const roundedStyle = computed(() => radiusStyleNative(effectiveRadius.value))
|
|
41
41
|
</script>
|
|
42
42
|
|
|
43
43
|
<template>
|
package/components/QDialog.vue
CHANGED
|
@@ -14,7 +14,7 @@ export const qDialogKey: InjectionKey<DialogContext> = Symbol("q-dialog")
|
|
|
14
14
|
import { computed, onBeforeUnmount, onMounted, provide, watch } from "vue"
|
|
15
15
|
import type { StyleValue } from "vue"
|
|
16
16
|
import { cn } from "../lib/utils"
|
|
17
|
-
import {
|
|
17
|
+
import { radiusStyleNative, useRadius } from "../lib/useComponentProps"
|
|
18
18
|
import type { RadiusProp } from "../lib/useComponentProps"
|
|
19
19
|
import { useOverlayBack } from "../lib/overlayBack"
|
|
20
20
|
import { markOverlayClose } from "../lib/closeOverlay"
|
|
@@ -150,7 +150,7 @@ const overlayClasses = computed(() => cn("q-dialog__overlay", `q-dialog__overlay
|
|
|
150
150
|
|
|
151
151
|
// radius : prop explicite > composantProps.QDialog.radius ; échelle → --q-radius
|
|
152
152
|
const effectiveRadius = useRadius("QDialog", () => props.radius)
|
|
153
|
-
const roundedStyle = computed(() =>
|
|
153
|
+
const roundedStyle = computed(() => radiusStyleNative(effectiveRadius.value))
|
|
154
154
|
|
|
155
155
|
const contentClasses = computed(() =>
|
|
156
156
|
cn(
|
package/components/QGridItem.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// QGridItem — cellule de la grille style Vant : <q-grid-item icon="lucide:
|
|
2
|
+
// QGridItem — cellule de la grille style Vant : <q-grid-item icon="lucide:image" text="Photos" badge="3" />
|
|
3
3
|
// À utiliser dans <q-grid :column-num="…">. Affiche une icône (ou un slot) + un
|
|
4
4
|
// texte, avec badge / point de notification, effet cliquable et navigation (href).
|
|
5
5
|
import { computed } from "vue"
|
package/components/QImg.vue
CHANGED
|
@@ -77,8 +77,10 @@ const imageClasses = computed(() =>
|
|
|
77
77
|
)
|
|
78
78
|
|
|
79
79
|
// radius : prop explicite > composantProps.QImg > composantProps.default → --q-radius
|
|
80
|
+
// `true` = pilule (forme native documentée) → classe dédiée, pas un `--q-radius`
|
|
80
81
|
const effectiveRadius = useRadius("QImg", () => props.radius)
|
|
81
82
|
const roundedStyle = computed(() => radiusStyle(effectiveRadius.value))
|
|
83
|
+
const roundedClass = computed(() => (effectiveRadius.value === true ? "q-img--rounded" : undefined))
|
|
82
84
|
|
|
83
85
|
const imageStyle = computed(() => `${props.imgStyle}; object-position: ${props.position}`)
|
|
84
86
|
|
|
@@ -101,7 +103,7 @@ const showLoading = computed(() => props.loading || imgLoading.value)
|
|
|
101
103
|
</script>
|
|
102
104
|
|
|
103
105
|
<template>
|
|
104
|
-
<div class="q-img" :style="[containerStyle, roundedStyle]">
|
|
106
|
+
<div class="q-img" :class="roundedClass" :style="[containerStyle, roundedStyle]">
|
|
105
107
|
<!-- Placeholder basse résolution pendant le chargement -->
|
|
106
108
|
<img
|
|
107
109
|
v-if="placeholderSrc && imgLoading && !imgError"
|
package/components/QInput.vue
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// QInput — API Quasar : <q-input v-model="…" label="Email" outlined dense clearable counter maxlength="60" hint error error-message />
|
|
3
3
|
// Variants : outlined (défaut) | filled | borderless ; le label est toujours affiché au-dessus du champ.
|
|
4
|
-
|
|
4
|
+
// Multiligne : `autogrow` (auto-extension plafonnée à `max-rows`), `rows` (lignes visibles)
|
|
5
|
+
// ou `resize` — pour un champ multiligne complet, préférer `<q-input-textarea>`.
|
|
6
|
+
import { computed, ref } from "vue"
|
|
5
7
|
import { Icon } from "@iconify/vue"
|
|
6
8
|
import { icons } from "../lib/icons"
|
|
7
9
|
import { cn } from "../lib/utils"
|
|
8
10
|
import { radiusStyle, useRadius } from "../lib/useComponentProps"
|
|
9
11
|
import type { RadiusProp } from "../lib/useComponentProps"
|
|
12
|
+
import { useTextareaAutosize } from "../lib/useTextareaAutosize"
|
|
10
13
|
|
|
11
14
|
interface Props {
|
|
12
15
|
modelValue?: string | number | null
|
|
@@ -33,7 +36,7 @@ interface Props {
|
|
|
33
36
|
iconLeft?: string
|
|
34
37
|
/** Icône Iconify à droite (remplacée par le slot #append s'il est fourni) */
|
|
35
38
|
iconRight?: string
|
|
36
|
-
/** Masque de saisie : # = chiffre, A = lettre, N = alphanumérique, X = quelconque (ex. "##-##-##") */
|
|
39
|
+
/** Masque de saisie : # = chiffre, A = lettre, N = alphanumérique, X = quelconque (ex. "##-##-##") — ignoré en mode textarea */
|
|
37
40
|
mask?: string
|
|
38
41
|
/** Remplit les emplacements vides par "_" (défaut : la saisie s'arrête au dernier caractère) */
|
|
39
42
|
fillMask?: boolean
|
|
@@ -51,8 +54,16 @@ interface Props {
|
|
|
51
54
|
radius?: RadiusProp
|
|
52
55
|
/** Hauteur réduite */
|
|
53
56
|
dense?: boolean
|
|
54
|
-
/** Textarea à hauteur auto */
|
|
57
|
+
/** Textarea à hauteur auto, plafonné à `maxRows` lignes */
|
|
55
58
|
autogrow?: boolean
|
|
59
|
+
/** Nombre de lignes visibles (textarea) — `> 1` bascule le champ en multiligne */
|
|
60
|
+
rows?: number
|
|
61
|
+
/** Plafond d'auto-extension en lignes, 0 = illimité : au-delà, le champ défile */
|
|
62
|
+
maxRows?: number
|
|
63
|
+
/** Redimensionnement manuel du textarea (sans effet en `autogrow`) */
|
|
64
|
+
resize?: "none" | "vertical" | "both"
|
|
65
|
+
/** Attribut autocomplete transmis au champ natif */
|
|
66
|
+
autocomplete?: string
|
|
56
67
|
disable?: boolean
|
|
57
68
|
readonly?: boolean
|
|
58
69
|
placeholder?: string
|
|
@@ -69,6 +80,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
69
80
|
borderless: false,
|
|
70
81
|
dense: false,
|
|
71
82
|
autogrow: false,
|
|
83
|
+
rows: 1,
|
|
84
|
+
maxRows: 0,
|
|
85
|
+
resize: "none",
|
|
72
86
|
disable: false,
|
|
73
87
|
readonly: false,
|
|
74
88
|
fillMask: false,
|
|
@@ -93,6 +107,17 @@ const roundedStyle = computed(() => radiusStyle(effectiveRadius.value))
|
|
|
93
107
|
|
|
94
108
|
const value = computed(() => props.modelValue ?? "")
|
|
95
109
|
|
|
110
|
+
// Le champ est multiligne dès qu'on demande plusieurs lignes, une auto-extension ou
|
|
111
|
+
// un redimensionnement manuel — un `<input>` n'a ni hauteur de plusieurs lignes ni
|
|
112
|
+
// poignée de redimensionnement.
|
|
113
|
+
const isTextarea = computed(
|
|
114
|
+
() => props.autogrow || props.rows > 1 || props.resize !== "none",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
// Un masque de saisie n'a pas de sens sur du multiligne (il supprime les retours à
|
|
118
|
+
// la ligne) : il est ignoré en mode textarea.
|
|
119
|
+
const maskEnabled = computed(() => Boolean(props.mask) && !isTextarea.value)
|
|
120
|
+
|
|
96
121
|
// ═════════ Masque de saisie : # chiffre, A lettre, N alphanumérique, X quelconque ═════════
|
|
97
122
|
const isToken = (c: string): boolean => c === "#" || c === "A" || c === "N" || c === "X"
|
|
98
123
|
|
|
@@ -107,7 +132,7 @@ const matchesToken = (tok: string, c: string): boolean =>
|
|
|
107
132
|
|
|
108
133
|
/** Extrait les caractères réels (emplacements tokens) d'une valeur affichée masquée. */
|
|
109
134
|
const unmask = (v: string, fill: boolean): string => {
|
|
110
|
-
if (!props.mask) return v
|
|
135
|
+
if (!maskEnabled.value || !props.mask) return v
|
|
111
136
|
let out = ""
|
|
112
137
|
let vi = 0
|
|
113
138
|
for (const ch of props.mask) {
|
|
@@ -126,7 +151,7 @@ const unmask = (v: string, fill: boolean): string => {
|
|
|
126
151
|
|
|
127
152
|
/** Applique le masque à une chaîne de caractères bruts (littéraux insérés, caractères invalides sautés). */
|
|
128
153
|
const mask = (raw: string, fill: boolean): string => {
|
|
129
|
-
if (!props.mask) return raw
|
|
154
|
+
if (!maskEnabled.value || !props.mask) return raw
|
|
130
155
|
let out = ""
|
|
131
156
|
let ri = 0
|
|
132
157
|
for (const ch of props.mask) {
|
|
@@ -151,7 +176,7 @@ const mask = (raw: string, fill: boolean): string => {
|
|
|
151
176
|
/** Valeur affichée dans le champ : applique le masque au modelValue (brut si unmaskedValue). */
|
|
152
177
|
const displayValue = computed(() => {
|
|
153
178
|
const v = String(props.modelValue ?? "")
|
|
154
|
-
if (!
|
|
179
|
+
if (!maskEnabled.value) return v
|
|
155
180
|
const raw = props.unmaskedValue ? v : unmask(v, props.fillMask)
|
|
156
181
|
return mask(raw, props.fillMask)
|
|
157
182
|
})
|
|
@@ -169,24 +194,33 @@ const fieldClasses = computed(() =>
|
|
|
169
194
|
props.error && "q-field--error",
|
|
170
195
|
props.disable && "q-field--disabled",
|
|
171
196
|
floatActive.value && "q-field--float",
|
|
197
|
+
isTextarea.value && "q-input--textarea",
|
|
172
198
|
),
|
|
173
199
|
)
|
|
174
200
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
201
|
+
// Auto-extension : la mesure et le plafond `maxRows` sont délégués au composable
|
|
202
|
+
// (calcul pur dans `lib/textarea.ts`). `observeWidth: false` — en mode `<input>`
|
|
203
|
+
// l'élément observé serait remplacé au changement de mode.
|
|
204
|
+
const { resize } = useTextareaAutosize(nativeEl, {
|
|
205
|
+
enabled: () => props.autogrow,
|
|
206
|
+
maxRows: () => props.maxRows,
|
|
207
|
+
sources: () => [value.value, props.rows, props.maxRows, props.autogrow],
|
|
208
|
+
observeWidth: false,
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// `resize` exposé (onglet Methods de la doc) : re-mesure forcée après un
|
|
212
|
+
// changement de contenu fait hors du cycle normal (ex. valeur posée par le parent).
|
|
213
|
+
defineExpose({ resize })
|
|
183
214
|
|
|
184
|
-
|
|
215
|
+
// `resize` n'a de sens que sans auto-extension (sinon les deux pilotent la hauteur).
|
|
216
|
+
const textareaStyle = computed(() =>
|
|
217
|
+
props.resize === "none" || props.autogrow ? undefined : { resize: props.resize },
|
|
218
|
+
)
|
|
185
219
|
|
|
186
220
|
const onInput = (e: Event) => {
|
|
187
221
|
const el = e.target as HTMLInputElement | HTMLTextAreaElement
|
|
188
222
|
const raw = el.value
|
|
189
|
-
if (!
|
|
223
|
+
if (!maskEnabled.value) {
|
|
190
224
|
emit("update:modelValue", raw)
|
|
191
225
|
return
|
|
192
226
|
}
|
|
@@ -218,13 +252,17 @@ const showClear = computed(
|
|
|
218
252
|
() => props.clearable && !props.disable && !props.readonly && value.value !== "",
|
|
219
253
|
)
|
|
220
254
|
|
|
221
|
-
const
|
|
222
|
-
type: props.type,
|
|
255
|
+
const commonAttrs = computed(() => ({
|
|
223
256
|
maxlength: props.maxlength,
|
|
224
257
|
placeholder: props.placeholder,
|
|
258
|
+
autocomplete: props.autocomplete,
|
|
225
259
|
disabled: props.disable || undefined,
|
|
226
260
|
readonly: props.readonly || undefined,
|
|
227
261
|
}))
|
|
262
|
+
|
|
263
|
+
// `type` n'est valide que sur `<input>` : sur un `<textarea>` c'est un attribut
|
|
264
|
+
// invalide (et sans effet) — on ne le transmet pas.
|
|
265
|
+
const inputAttrs = computed(() => ({ ...commonAttrs.value, type: props.type }))
|
|
228
266
|
</script>
|
|
229
267
|
|
|
230
268
|
<template>
|
|
@@ -236,12 +274,13 @@ const inputAttrs = computed(() => ({
|
|
|
236
274
|
</slot>
|
|
237
275
|
<span v-if="prefix" class="q-field__prefix">{{ prefix }}</span>
|
|
238
276
|
<textarea
|
|
239
|
-
v-if="
|
|
277
|
+
v-if="isTextarea"
|
|
240
278
|
ref="nativeEl"
|
|
241
279
|
class="q-field__native"
|
|
242
280
|
:value="displayValue"
|
|
243
|
-
rows="
|
|
244
|
-
|
|
281
|
+
:rows="rows"
|
|
282
|
+
:style="textareaStyle"
|
|
283
|
+
v-bind="commonAttrs"
|
|
245
284
|
@input="onInput"
|
|
246
285
|
@focus="onFocus"
|
|
247
286
|
@blur="onBlur"
|
package/components/QInputOtp.vue
CHANGED
|
@@ -190,6 +190,8 @@ const rootClasses = computed(() =>
|
|
|
190
190
|
props.dense && "q-field--dense",
|
|
191
191
|
props.error && "q-field--error",
|
|
192
192
|
props.disable && "q-field--disabled",
|
|
193
|
+
// `radius: true` = pilule (forme native documentée)
|
|
194
|
+
effectiveRadius.value === true && "q-input-otp--rounded",
|
|
193
195
|
),
|
|
194
196
|
)
|
|
195
197
|
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QInputTextarea — champ multiligne dédié : <q-input-textarea v-model="bio" label="Bio" :rows="3" />
|
|
3
|
+
// Contrairement à `q-input` (qui bascule entre `<input>` et `<textarea>`), ce composant
|
|
4
|
+
// est un textarea **seul** : lignes visibles (`rows`), plafond d'auto-extension
|
|
5
|
+
// (`maxRows`), redimensionnement manuel (`resize`), compteur et états — avec le
|
|
6
|
+
// vocabulaire de champ commun (outlined / filled / borderless / dense / radius / error).
|
|
7
|
+
import { computed, ref } from "vue"
|
|
8
|
+
import { Icon } from "@iconify/vue"
|
|
9
|
+
import { icons } from "../lib/icons"
|
|
10
|
+
import { cn } from "../lib/utils"
|
|
11
|
+
import { radiusStyle, useRadius } from "../lib/useComponentProps"
|
|
12
|
+
import type { RadiusProp } from "../lib/useComponentProps"
|
|
13
|
+
import { useTextareaAutosize } from "../lib/useTextareaAutosize"
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
modelValue?: string | number | null
|
|
17
|
+
/** Label affiché au-dessus du champ */
|
|
18
|
+
label?: string
|
|
19
|
+
/** Aide affichée sous le champ */
|
|
20
|
+
hint?: string
|
|
21
|
+
/** État d'erreur */
|
|
22
|
+
error?: boolean
|
|
23
|
+
/** Message d'erreur (affiché quand error) */
|
|
24
|
+
errorMessage?: string
|
|
25
|
+
/** Affiche le compteur de caractères (nécessite maxlength) */
|
|
26
|
+
counter?: boolean
|
|
27
|
+
maxlength?: number
|
|
28
|
+
/** Nombre de lignes visibles (hauteur du champ hors auto-extension) */
|
|
29
|
+
rows?: number
|
|
30
|
+
/** Plafond d'auto-extension en lignes, 0 = illimité : au-delà, le champ défile */
|
|
31
|
+
maxRows?: number
|
|
32
|
+
/** Le champ grandit avec le contenu jusqu'à `maxRows`, puis défile */
|
|
33
|
+
autogrow?: boolean
|
|
34
|
+
/** Redimensionnement manuel (sans effet en `autogrow`) */
|
|
35
|
+
resize?: "none" | "vertical" | "both"
|
|
36
|
+
/** Bouton d'effacement */
|
|
37
|
+
clearable?: boolean
|
|
38
|
+
/** Bordure visible */
|
|
39
|
+
outlined?: boolean
|
|
40
|
+
/** Fond gris, soulignement */
|
|
41
|
+
filled?: boolean
|
|
42
|
+
/** Aucune bordure */
|
|
43
|
+
borderless?: boolean
|
|
44
|
+
/** Coins arrondis : true = pilule, ou échelle xs|sm|md|lg (none = carré) */
|
|
45
|
+
radius?: RadiusProp
|
|
46
|
+
/** Hauteur réduite */
|
|
47
|
+
dense?: boolean
|
|
48
|
+
disable?: boolean
|
|
49
|
+
readonly?: boolean
|
|
50
|
+
placeholder?: string
|
|
51
|
+
/** Préfixe dans le champ */
|
|
52
|
+
prefix?: string
|
|
53
|
+
/** Suffixe dans le champ */
|
|
54
|
+
suffix?: string
|
|
55
|
+
/** Icône Iconify à gauche (remplacée par le slot #prepend s'il est fourni) */
|
|
56
|
+
iconLeft?: string
|
|
57
|
+
/** Icône Iconify à droite (remplacée par le slot #append s'il est fourni) */
|
|
58
|
+
iconRight?: string
|
|
59
|
+
/** Attribut autocomplete du champ natif */
|
|
60
|
+
autocomplete?: string
|
|
61
|
+
/** Correction orthographique du navigateur */
|
|
62
|
+
spellcheck?: boolean
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
66
|
+
rows: 3,
|
|
67
|
+
maxRows: 0,
|
|
68
|
+
autogrow: false,
|
|
69
|
+
resize: "none",
|
|
70
|
+
error: false,
|
|
71
|
+
counter: false,
|
|
72
|
+
clearable: false,
|
|
73
|
+
outlined: false,
|
|
74
|
+
filled: false,
|
|
75
|
+
borderless: false,
|
|
76
|
+
dense: false,
|
|
77
|
+
disable: false,
|
|
78
|
+
readonly: false,
|
|
79
|
+
spellcheck: false,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const emit = defineEmits<{
|
|
83
|
+
"update:modelValue": [value: string]
|
|
84
|
+
focus: [event: FocusEvent]
|
|
85
|
+
blur: [event: FocusEvent]
|
|
86
|
+
clear: []
|
|
87
|
+
keyup: [event: KeyboardEvent]
|
|
88
|
+
keydown: [event: KeyboardEvent]
|
|
89
|
+
}>()
|
|
90
|
+
|
|
91
|
+
const focused = ref(false)
|
|
92
|
+
const nativeEl = ref<HTMLTextAreaElement | null>(null)
|
|
93
|
+
|
|
94
|
+
// radius : prop explicite > composantProps.QInputTextarea ; échelle → --q-radius
|
|
95
|
+
const effectiveRadius = useRadius("QInputTextarea", () => props.radius)
|
|
96
|
+
const roundedStyle = computed(() => radiusStyle(effectiveRadius.value))
|
|
97
|
+
|
|
98
|
+
const value = computed(() => String(props.modelValue ?? ""))
|
|
99
|
+
|
|
100
|
+
const fieldClasses = computed(() =>
|
|
101
|
+
cn(
|
|
102
|
+
"q-input-textarea",
|
|
103
|
+
props.outlined && "q-field--outlined",
|
|
104
|
+
props.filled && "q-field--filled",
|
|
105
|
+
props.borderless && "q-field--borderless",
|
|
106
|
+
effectiveRadius.value === true && "q-field--rounded",
|
|
107
|
+
props.dense && "q-field--dense",
|
|
108
|
+
props.error && "q-field--error",
|
|
109
|
+
props.disable && "q-field--disabled",
|
|
110
|
+
focused.value && "q-field--float",
|
|
111
|
+
props.autogrow && "q-input-textarea--autogrow",
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
// Auto-extension : mesure + plafond `maxRows` (calcul pur dans `lib/textarea.ts`),
|
|
116
|
+
// re-mesurée au montage, à chaque changement de valeur/lignes et au changement de
|
|
117
|
+
// largeur (le texte se replie autrement).
|
|
118
|
+
const { resize } = useTextareaAutosize(nativeEl, {
|
|
119
|
+
enabled: () => props.autogrow,
|
|
120
|
+
maxRows: () => props.maxRows,
|
|
121
|
+
sources: () => [value.value, props.rows, props.maxRows, props.autogrow],
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// `resize` manuel inutile en auto-extension : les deux pilotent la hauteur.
|
|
125
|
+
const textareaStyle = computed(() =>
|
|
126
|
+
props.resize === "none" || props.autogrow ? undefined : { resize: props.resize },
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const showClear = computed(
|
|
130
|
+
() => props.clearable && !props.disable && !props.readonly && value.value !== "",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
const onInput = (e: Event) => emit("update:modelValue", (e.target as HTMLTextAreaElement).value)
|
|
134
|
+
|
|
135
|
+
const onFocus = (e: FocusEvent) => {
|
|
136
|
+
focused.value = true
|
|
137
|
+
emit("focus", e)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const onBlur = (e: FocusEvent) => {
|
|
141
|
+
focused.value = false
|
|
142
|
+
emit("blur", e)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const onClear = () => {
|
|
146
|
+
emit("update:modelValue", "")
|
|
147
|
+
emit("clear")
|
|
148
|
+
void resize()
|
|
149
|
+
nativeEl.value?.focus()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
defineExpose({
|
|
153
|
+
focus: () => nativeEl.value?.focus(),
|
|
154
|
+
blur: () => nativeEl.value?.blur(),
|
|
155
|
+
resize: () => void resize(),
|
|
156
|
+
})
|
|
157
|
+
</script>
|
|
158
|
+
|
|
159
|
+
<template>
|
|
160
|
+
<div class="q-input-textarea" :class="fieldClasses" :style="roundedStyle">
|
|
161
|
+
<label v-if="label" class="q-field__label-stack">{{ label }}</label>
|
|
162
|
+
<div class="q-field__control">
|
|
163
|
+
<slot name="prepend">
|
|
164
|
+
<Icon v-if="iconLeft" :icon="iconLeft" class="q-field__icon" aria-hidden="true" />
|
|
165
|
+
</slot>
|
|
166
|
+
<span v-if="prefix" class="q-field__prefix">{{ prefix }}</span>
|
|
167
|
+
<textarea
|
|
168
|
+
ref="nativeEl"
|
|
169
|
+
class="q-field__native"
|
|
170
|
+
:value="value"
|
|
171
|
+
:rows="rows"
|
|
172
|
+
:style="textareaStyle"
|
|
173
|
+
:maxlength="maxlength"
|
|
174
|
+
:placeholder="placeholder"
|
|
175
|
+
:autocomplete="autocomplete"
|
|
176
|
+
:spellcheck="spellcheck"
|
|
177
|
+
:disabled="disable || undefined"
|
|
178
|
+
:readonly="readonly || undefined"
|
|
179
|
+
@input="onInput"
|
|
180
|
+
@focus="onFocus"
|
|
181
|
+
@blur="onBlur"
|
|
182
|
+
@keyup="(e) => emit('keyup', e)"
|
|
183
|
+
@keydown="(e) => emit('keydown', e)"
|
|
184
|
+
/>
|
|
185
|
+
<span v-if="suffix" class="q-field__suffix">{{ suffix }}</span>
|
|
186
|
+
<button
|
|
187
|
+
v-if="showClear"
|
|
188
|
+
class="q-field__clear"
|
|
189
|
+
type="button"
|
|
190
|
+
aria-label="Effacer"
|
|
191
|
+
@click="onClear"
|
|
192
|
+
>
|
|
193
|
+
<Icon :icon="icons.x" aria-hidden="true" />
|
|
194
|
+
</button>
|
|
195
|
+
<slot name="append">
|
|
196
|
+
<Icon v-if="iconRight" :icon="iconRight" class="q-field__icon" aria-hidden="true" />
|
|
197
|
+
</slot>
|
|
198
|
+
</div>
|
|
199
|
+
<div class="q-field__bottom">
|
|
200
|
+
<div v-if="error" class="q-field__error">
|
|
201
|
+
<slot name="error">{{ errorMessage }}</slot>
|
|
202
|
+
</div>
|
|
203
|
+
<div v-else-if="hint || $slots.hint" class="q-field__hint">
|
|
204
|
+
<slot name="hint">{{ hint }}</slot>
|
|
205
|
+
</div>
|
|
206
|
+
<div v-if="counter && maxlength" class="q-field__counter">
|
|
207
|
+
{{ value.length }}/{{ maxlength }}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</template>
|
package/components/QSyntax.vue
CHANGED
|
@@ -8,7 +8,7 @@ import { createHighlighter, type Highlighter } from "shiki"
|
|
|
8
8
|
import { createJavaScriptRegexEngine } from "shiki/engine/javascript"
|
|
9
9
|
import { Icon } from "@iconify/vue"
|
|
10
10
|
import { icons } from "../lib/icons"
|
|
11
|
-
import {
|
|
11
|
+
import { radiusStyleNative, useRadius } from "../lib/useComponentProps"
|
|
12
12
|
import type { RadiusProp } from "../lib/useComponentProps"
|
|
13
13
|
|
|
14
14
|
const DEFAULT_THEME = "github-dark-default"
|
|
@@ -44,7 +44,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
44
44
|
|
|
45
45
|
// radius : prop explicite > composantProps.QSyntax.radius > composantProps.default.radius
|
|
46
46
|
const effectiveRadius = useRadius("QSyntax", () => props.radius)
|
|
47
|
-
const roundedStyle = computed(() =>
|
|
47
|
+
const roundedStyle = computed(() => radiusStyleNative(effectiveRadius.value))
|
|
48
48
|
|
|
49
49
|
const slots = useSlots()
|
|
50
50
|
|
package/components/QTable.vue
CHANGED
|
@@ -33,7 +33,7 @@ import { computed, getCurrentInstance, onBeforeUnmount, onMounted, ref, watch }
|
|
|
33
33
|
import { Icon } from "@iconify/vue"
|
|
34
34
|
import { icons } from "../lib/icons"
|
|
35
35
|
import { cn } from "../lib/utils"
|
|
36
|
-
import {
|
|
36
|
+
import { radiusStyleNative, useRadius } from "../lib/useComponentProps"
|
|
37
37
|
import type { RadiusProp } from "../lib/useComponentProps"
|
|
38
38
|
import QSelect from "./QSelect.vue"
|
|
39
39
|
|
|
@@ -437,7 +437,7 @@ const tableClasses = computed(() =>
|
|
|
437
437
|
|
|
438
438
|
// radius : prop explicite > composantProps.QTable.radius > composantProps.default.radius
|
|
439
439
|
const effectiveRadius = useRadius("QTable", () => props.radius)
|
|
440
|
-
const roundedStyle = computed(() =>
|
|
440
|
+
const roundedStyle = computed(() => radiusStyleNative(effectiveRadius.value))
|
|
441
441
|
|
|
442
442
|
const sortThClass = computed(() => "q-table__sort")
|
|
443
443
|
|
package/index.ts
CHANGED
|
@@ -72,6 +72,7 @@ export { default as QInputCurrency } from "./components/QInputCurrency.vue"
|
|
|
72
72
|
export { default as QInputOtp } from "./components/QInputOtp.vue"
|
|
73
73
|
export { default as QInputPassword } from "./components/QInputPassword.vue"
|
|
74
74
|
export { default as QInputTag } from "./components/QInputTag.vue"
|
|
75
|
+
export { default as QInputTextarea } from "./components/QInputTextarea.vue"
|
|
75
76
|
export { default as QInteract } from "./components/QInteract.vue"
|
|
76
77
|
export { default as QIntersection } from "./components/QIntersection.vue"
|
|
77
78
|
export { default as QItem } from "./components/QItem.vue"
|
|
@@ -203,6 +204,14 @@ export { DEFAULT_LAYOUT_VIEW, parseView, qLayoutKey } from "./lib/layout"
|
|
|
203
204
|
export type { LayoutView, LayoutZone, LayoutZonePlacement, QLayoutContext } from "./lib/layout"
|
|
204
205
|
export { vRipple } from "./lib/ripple"
|
|
205
206
|
export type { RippleOptions, RippleValue } from "./lib/ripple"
|
|
207
|
+
export { textareaCap, textareaMetrics, textareaSizing } from "./lib/textarea"
|
|
208
|
+
export type { TextareaMetrics, TextareaSizing, TextareaComputedStyle } from "./lib/textarea"
|
|
209
|
+
export { useTextareaAutosize } from "./lib/useTextareaAutosize"
|
|
210
|
+
export type { TextareaAutosize, UseTextareaAutosizeOptions } from "./lib/useTextareaAutosize"
|
|
211
|
+
export { iconsRegistered, registerIconSet, registerIcons } from "./lib/registerIcons"
|
|
212
|
+
export { lucideIconSet } from "./lib/iconSet"
|
|
213
|
+
export { icons } from "./lib/icons"
|
|
214
|
+
export type { IconSetJson } from "./lib/icons"
|
|
206
215
|
export { chartToECharts, channelValue, pickFromEvent, tableCsv, tableModel, textValue, tokenCss } from "./lib/chart"
|
|
207
216
|
export type {
|
|
208
217
|
QChartAxis,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Icônes embarquées — garde-fou contre la dérive du fichier généré.
|
|
2
|
+
//
|
|
3
|
+
// `lib/iconSet.ts` est produit par `bun scripts/gen-icon-set.ts`. Si quelqu'un ajoute
|
|
4
|
+
// un `lucide:…` dans un composant sans relancer le script, l'icône repasse
|
|
5
|
+
// silencieusement par l'API Iconify (donc : absente du SSR et du mode hors ligne).
|
|
6
|
+
// Ces tests échouent dans ce cas, sans navigateur ni réseau.
|
|
7
|
+
import { readdirSync, readFileSync } from "node:fs"
|
|
8
|
+
import { join } from "node:path"
|
|
9
|
+
import { describe, expect, it } from "bun:test"
|
|
10
|
+
import { icons } from "./icons"
|
|
11
|
+
import { lucideIconSet } from "./iconSet"
|
|
12
|
+
|
|
13
|
+
const ICON_NAME_RE = /\blucide:[a-z0-9]+(?:-[a-z0-9]+)*/g
|
|
14
|
+
const componentsDir = join(import.meta.dir, "..", "components")
|
|
15
|
+
|
|
16
|
+
const has = (name: string): boolean =>
|
|
17
|
+
name in lucideIconSet.icons || Boolean(lucideIconSet.aliases && name in lucideIconSet.aliases)
|
|
18
|
+
|
|
19
|
+
const strip = (value: string): string => value.slice("lucide:".length)
|
|
20
|
+
|
|
21
|
+
describe("lucideIconSet (généré)", () => {
|
|
22
|
+
it("est une collection lucide valide", () => {
|
|
23
|
+
expect(lucideIconSet.prefix).toBe("lucide")
|
|
24
|
+
expect(lucideIconSet.width).toBe(24)
|
|
25
|
+
expect(lucideIconSet.height).toBe(24)
|
|
26
|
+
expect(Object.keys(lucideIconSet.icons).length).toBeGreaterThan(0)
|
|
27
|
+
for (const [name, icon] of Object.entries(lucideIconSet.icons)) {
|
|
28
|
+
expect(name).not.toContain(":")
|
|
29
|
+
expect(icon.body.length).toBeGreaterThan(0)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("contient toutes les icônes de `lib/icons.ts`", () => {
|
|
34
|
+
const missing = Object.values(icons)
|
|
35
|
+
.map(strip)
|
|
36
|
+
.filter((name) => !has(name))
|
|
37
|
+
expect(missing).toEqual([])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("contient tous les `lucide:…` référencés dans les composants", () => {
|
|
41
|
+
const files = readdirSync(componentsDir, { recursive: true })
|
|
42
|
+
.map(String)
|
|
43
|
+
.filter((path) => path.endsWith(".vue"))
|
|
44
|
+
.map((path) => join(componentsDir, path))
|
|
45
|
+
|
|
46
|
+
const missing = new Set<string>()
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
for (const match of readFileSync(file, "utf8").matchAll(ICON_NAME_RE)) {
|
|
49
|
+
const name = strip(match[0])
|
|
50
|
+
if (!has(name)) missing.add(name)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
expect([...missing]).toEqual([])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("embarque la parente de chaque alias", () => {
|
|
57
|
+
for (const alias of Object.values(lucideIconSet.aliases ?? {})) {
|
|
58
|
+
expect(alias.parent in lucideIconSet.icons).toBe(true)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
})
|