@dnax/ui 0.0.75 → 0.0.77

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.
@@ -6,6 +6,8 @@ import { computed, onBeforeUnmount, reactive, ref, watch } from "vue"
6
6
  import { Icon } from "@iconify/vue"
7
7
  import { icons } from "../lib/icons"
8
8
  import { cn } from "../lib/utils"
9
+ import { fileIconKind, formatFileSize, selectFiles } from "../lib/fileList"
10
+ import type { FileIconKind } from "../lib/fileList"
9
11
 
10
12
  interface Props {
11
13
  /** Fichier(s) sélectionné(s) — File (simple) ou File[] (multiple) */
@@ -100,51 +102,20 @@ const fileKey = (file: File): number => {
100
102
  return id
101
103
  }
102
104
 
103
- // — Icône par type de fichier (fallback générique) —
104
- const ARCHIVE_EXT = /\.(zip|rar|7z|tar|gz|bz2|xz|tgz)$/
105
- const SPREADSHEET_EXT = /\.(xlsx|xls|csv|ods|tsv)$/
106
- const CODE_EXT = /\.(json|js|mjs|cjs|ts|jsx|tsx|html?|xml|css|scss|sass|less|vue|py|java|c|cpp|h|hpp|sh|bash|yml|yaml|toml|ini|go|rs|php|rb|swift|kt)$/
107
-
108
- const iconFor = (file: File): string => {
109
- const type = file.type
110
- const name = file.name.toLowerCase()
111
- if (type.startsWith("video/")) return icons.fileVideo
112
- if (type.startsWith("audio/")) return icons.fileAudio
113
- if (ARCHIVE_EXT.test(name) || /zip|compressed/.test(type)) return icons.fileArchive
114
- if (SPREADSHEET_EXT.test(name) || type.includes("spreadsheet") || type.includes("excel")) {
115
- return icons.fileSpreadsheet
116
- }
117
- if (CODE_EXT.test(name)) return icons.fileCode
118
- if (type.startsWith("image/")) return icons.fileImage
119
- return icons.fileText
105
+ // — Icône par type de fichier (règles partagées avec QSpreadsheet `attachment`) —
106
+ const ICON_BY_KIND: Record<FileIconKind, string> = {
107
+ image: icons.fileImage,
108
+ video: icons.fileVideo,
109
+ audio: icons.fileAudio,
110
+ archive: icons.fileArchive,
111
+ spreadsheet: icons.fileSpreadsheet,
112
+ code: icons.fileCode,
113
+ file: icons.fileText,
120
114
  }
121
115
 
122
- const formatSize = (bytes: number): string => {
123
- if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} Go`
124
- if (bytes >= 1024 * 1024) return `${Math.round(bytes / (1024 * 1024))} Mo`
125
- if (bytes >= 1024) return `${Math.round(bytes / 1024)} Ko`
126
- return `${bytes} o`
127
- }
116
+ const iconFor = (file: File): string => ICON_BY_KIND[fileIconKind(file)]
128
117
 
129
- // — Validation —
130
- const acceptsFile = (file: File): boolean => {
131
- const accept = props.accept.trim()
132
- if (!accept) return true
133
- const patterns = accept
134
- .split(",")
135
- .map((p) => p.trim().toLowerCase())
136
- .filter(Boolean)
137
- if (patterns.length === 0) return true
138
- const name = file.name.toLowerCase()
139
- return patterns.some((p) => {
140
- if (p === "image/*") return file.type.startsWith("image/")
141
- if (p === "video/*") return file.type.startsWith("video/")
142
- if (p === "audio/*") return file.type.startsWith("audio/")
143
- if (p.startsWith(".")) return name.endsWith(p)
144
- if (p.includes("/")) return file.type.toLowerCase() === p
145
- return false
146
- })
147
- }
118
+ const formatSize = formatFileSize
148
119
 
149
120
  const formatMaxSize = () => {
150
121
  const bytes = props.maxFileSize
@@ -176,26 +147,12 @@ const onInputChange = (e: Event) => {
176
147
 
177
148
  const addFiles = (incoming: File[]) => {
178
149
  const current = files.value
179
- const valid: File[] = []
180
- const rejections: { file: File; reason: "size" | "type" | "count" }[] = []
181
-
182
- for (const file of incoming) {
183
- if (props.maxFileSize > 0 && file.size > props.maxFileSize) {
184
- rejections.push({ file, reason: "size" })
185
- continue
186
- }
187
- if (!acceptsFile(file)) {
188
- rejections.push({ file, reason: "type" })
189
- continue
190
- }
191
- valid.push(file)
192
- }
193
-
194
- const quota = props.maxFiles > 0 ? props.maxFiles : Infinity
195
- const remaining = quota === Infinity ? Infinity : Math.max(0, quota - current.length)
196
- const accepted = remaining === Infinity ? valid : valid.slice(0, remaining)
197
- const overflow = remaining === Infinity ? [] : valid.slice(remaining)
198
- for (const file of overflow) rejections.push({ file, reason: "count" })
150
+ const { accepted, rejected } = selectFiles(incoming, {
151
+ accept: props.accept,
152
+ maxSize: props.maxFileSize,
153
+ maxFiles: props.maxFiles,
154
+ current: current.length,
155
+ })
199
156
 
200
157
  if (accepted.length > 0) {
201
158
  const next = props.multiple ? [...current, ...accepted] : accepted[0]!
@@ -203,14 +160,14 @@ const addFiles = (incoming: File[]) => {
203
160
  for (const file of accepted) emit("add", file)
204
161
  }
205
162
 
206
- if (rejections.length > 0) {
207
- const reasons = [...new Set(rejections.map((r) => r.reason))]
163
+ if (rejected.length > 0) {
164
+ const reasons = [...new Set(rejected.map((r) => r.reason))]
208
165
  const parts: string[] = []
209
166
  if (reasons.includes("size")) parts.push(`Fichier trop lourd (max ${formatMaxSize()})`)
210
167
  if (reasons.includes("type")) parts.push("Type de fichier non accepté")
211
168
  if (reasons.includes("count")) parts.push(`Nombre maximum : ${props.maxFiles}`)
212
169
  internalError.value = parts.join(" · ")
213
- for (const r of rejections) emit("rejected", r.file, r.reason)
170
+ for (const r of rejected) emit("rejected", r.file, r.reason)
214
171
  }
215
172
  }
216
173