@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.
@@ -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="radius"
144
+ :radius="effectiveRadius"
138
145
  :flat="flat"
139
146
  :outline="outline"
140
147
  :unelevated="unelevated"
@@ -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 { radiusStyle, useRadius } from "../lib/useComponentProps"
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(() => radiusStyle(effectiveRadius.value))
40
+ const roundedStyle = computed(() => radiusStyleNative(effectiveRadius.value))
41
41
  </script>
42
42
 
43
43
  <template>
@@ -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 { radiusStyle, useRadius } from "../lib/useComponentProps"
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(() => radiusStyle(effectiveRadius.value))
153
+ const roundedStyle = computed(() => radiusStyleNative(effectiveRadius.value))
154
154
 
155
155
  const contentClasses = computed(() =>
156
156
  cn(
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- // QGridItem — cellule de la grille style Vant : <q-grid-item icon="lucide:photo" text="Photos" badge="3" />
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"
@@ -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"
@@ -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
- import { computed, nextTick, ref, watch } from "vue"
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 (!props.mask) return v
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
- const resizeTextarea = async () => {
176
- const el = nativeEl.value
177
- if (el && props.autogrow) {
178
- await nextTick()
179
- el.style.height = "auto"
180
- el.style.height = `${el.scrollHeight}px`
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
- watch(value, resizeTextarea)
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 (!props.mask) {
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 inputAttrs = computed(() => ({
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="autogrow"
277
+ v-if="isTextarea"
240
278
  ref="nativeEl"
241
279
  class="q-field__native"
242
280
  :value="displayValue"
243
- rows="1"
244
- v-bind="inputAttrs"
281
+ :rows="rows"
282
+ :style="textareaStyle"
283
+ v-bind="commonAttrs"
245
284
  @input="onInput"
246
285
  @focus="onFocus"
247
286
  @blur="onBlur"
@@ -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>
@@ -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 { radiusStyle, useRadius } from "../lib/useComponentProps"
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(() => radiusStyle(effectiveRadius.value))
47
+ const roundedStyle = computed(() => radiusStyleNative(effectiveRadius.value))
48
48
 
49
49
  const slots = useSlots()
50
50
 
@@ -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 { radiusStyle, useRadius } from "../lib/useComponentProps"
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(() => radiusStyle(effectiveRadius.value))
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
+ })