@traky-app/ui 1.6.6 → 1.6.8
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/README.md +3 -1
- package/RELEASE.md +27 -0
- package/package.json +3 -2
- package/registry/traky/json/file-manager.json +25 -0
- package/registry/traky/json/internal-chat-panel.json +2 -1
- package/registry/traky/json/lib-file-items.json +14 -0
- package/registry/traky/json/lib-file-manager-utils.json +14 -0
- package/registry/traky/lib/file-items.ts +99 -0
- package/registry/traky/lib/file-manager-utils.ts +116 -0
- package/registry/traky/registry.json +30 -0
- package/registry/traky/ui/file-manager.tsx +646 -0
- package/registry/traky/ui/internal-chat-panel.tsx +44 -6
- package/src/lib/file-items.ts +99 -0
- package/src/lib/file-manager-utils.test.tsx +71 -0
- package/src/lib/file-manager-utils.ts +116 -0
- package/src/ui/file-manager.tsx +646 -0
- package/src/ui/internal-chat-panel.tsx +44 -6
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export const STORAGE_FILE_PROVIDERS = ["traky", "legacy"] as const
|
|
2
|
+
|
|
3
|
+
export type FileStorageProvider = (typeof STORAGE_FILE_PROVIDERS)[number]
|
|
4
|
+
|
|
5
|
+
export type FileItem = {
|
|
6
|
+
name: string
|
|
7
|
+
url: string
|
|
8
|
+
type: string
|
|
9
|
+
/** `traky` = objeto nuevo; `legacy` = externo. El host decide el valor. */
|
|
10
|
+
provider?: FileStorageProvider
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function inferMimeFromName(name: string) {
|
|
14
|
+
const lower = name.toLowerCase()
|
|
15
|
+
if (/\.pdf$/i.test(lower)) return "application/pdf"
|
|
16
|
+
if (/\.(webm)$/i.test(lower)) return "video/webm"
|
|
17
|
+
if (/\.(mp4|mov|mkv)$/i.test(lower)) return "video/mp4"
|
|
18
|
+
if (/\.(png|jpe?g|webp|gif|bmp|svg)$/i.test(lower)) return "image/jpeg"
|
|
19
|
+
return ""
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveFileProvider(explicit?: unknown): FileStorageProvider | undefined {
|
|
23
|
+
if (explicit === "traky" || explicit === "legacy") return explicit
|
|
24
|
+
return undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Normaliza entradas (strings, objetos incompletos o sin `type`). */
|
|
28
|
+
export function normalizeFileItem(file: unknown): FileItem | null {
|
|
29
|
+
if (typeof file === "string" && file.trim()) {
|
|
30
|
+
const url = file.trim()
|
|
31
|
+
const name = url.split("/").pop()?.split("?")[0] || "archivo"
|
|
32
|
+
return {
|
|
33
|
+
name,
|
|
34
|
+
url,
|
|
35
|
+
type: inferMimeFromName(name),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!file || typeof file !== "object") return null
|
|
40
|
+
|
|
41
|
+
const item = file as Record<string, unknown>
|
|
42
|
+
const urlCandidate = [item.url, item.fileUrl, item.href, item.path, item.src].find(
|
|
43
|
+
(value) => typeof value === "string" && value.trim(),
|
|
44
|
+
)
|
|
45
|
+
if (typeof urlCandidate !== "string") return null
|
|
46
|
+
|
|
47
|
+
const url = urlCandidate.trim()
|
|
48
|
+
const name =
|
|
49
|
+
(typeof item.name === "string" && item.name.trim()) ||
|
|
50
|
+
(typeof item.fileName === "string" && item.fileName.trim()) ||
|
|
51
|
+
url.split("/").pop()?.split("?")[0] ||
|
|
52
|
+
"archivo"
|
|
53
|
+
const type =
|
|
54
|
+
(typeof item.type === "string" && item.type) ||
|
|
55
|
+
(typeof item.mimeType === "string" && item.mimeType) ||
|
|
56
|
+
inferMimeFromName(name) ||
|
|
57
|
+
inferMimeFromName(url)
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
name,
|
|
61
|
+
url,
|
|
62
|
+
type,
|
|
63
|
+
provider: resolveFileProvider(item.provider),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function normalizeFileItems(files: unknown): FileItem[] {
|
|
68
|
+
if (typeof files === "string") {
|
|
69
|
+
const trimmed = files.trim()
|
|
70
|
+
if (!trimmed) return []
|
|
71
|
+
|
|
72
|
+
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
73
|
+
try {
|
|
74
|
+
return normalizeFileItems(JSON.parse(trimmed) as unknown)
|
|
75
|
+
} catch {
|
|
76
|
+
// JSON inválido: tratar como URL suelta abajo.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const single = normalizeFileItem(trimmed)
|
|
81
|
+
return single ? [single] : []
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (files && typeof files === "object" && !Array.isArray(files)) {
|
|
85
|
+
const single = normalizeFileItem(files)
|
|
86
|
+
return single ? [single] : []
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!Array.isArray(files)) return []
|
|
90
|
+
return files
|
|
91
|
+
.map(normalizeFileItem)
|
|
92
|
+
.filter((file): file is FileItem => file !== null)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function fileListSignature(files: unknown): string {
|
|
96
|
+
return normalizeFileItems(files)
|
|
97
|
+
.map((file) => `${file.url}|${file.name}|${file.type}`)
|
|
98
|
+
.join("\n")
|
|
99
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { FileItem, FileStorageProvider } from "./file-items"
|
|
2
|
+
|
|
3
|
+
export const ACCEPTED_TYPES = ".png,.jpg,.jpeg,.webp,.gif,.pdf"
|
|
4
|
+
export const ACCEPTED_TYPES_WITH_VIDEO =
|
|
5
|
+
".png,.jpg,.jpeg,.webp,.gif,.pdf,.mp4,.webm,.mov,.mkv"
|
|
6
|
+
export const ACCEPTED_MIME = [
|
|
7
|
+
"image/png",
|
|
8
|
+
"image/jpeg",
|
|
9
|
+
"image/webp",
|
|
10
|
+
"image/gif",
|
|
11
|
+
"application/pdf",
|
|
12
|
+
]
|
|
13
|
+
export const VIDEO_MIME = [
|
|
14
|
+
"video/mp4",
|
|
15
|
+
"video/webm",
|
|
16
|
+
"video/quicktime",
|
|
17
|
+
"video/x-matroska",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
export function isImageFile(file: FileItem) {
|
|
21
|
+
const type = file.type ?? ""
|
|
22
|
+
const name = file.name ?? ""
|
|
23
|
+
const url = file.url ?? ""
|
|
24
|
+
return (
|
|
25
|
+
type.startsWith("image/") ||
|
|
26
|
+
/\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(name) ||
|
|
27
|
+
/\.(png|jpe?g|gif|webp|bmp|svg)(\?|$)/i.test(url)
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isPdfFile(file: FileItem) {
|
|
32
|
+
const type = file.type ?? ""
|
|
33
|
+
const name = file.name ?? ""
|
|
34
|
+
const url = file.url ?? ""
|
|
35
|
+
return (
|
|
36
|
+
type.includes("pdf") || /\.pdf$/i.test(name) || /\.pdf(\?|$)/i.test(url)
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isVideoFile(file: FileItem) {
|
|
41
|
+
const type = file.type ?? ""
|
|
42
|
+
const name = file.name ?? ""
|
|
43
|
+
const url = file.url ?? ""
|
|
44
|
+
return (
|
|
45
|
+
type.startsWith("video/") ||
|
|
46
|
+
/\.(mp4|webm|mov|mkv)$/i.test(name) ||
|
|
47
|
+
/\.(mp4|webm|mov|mkv)(\?|$)/i.test(url)
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isAcceptedFile(file: File, acceptVideos: boolean) {
|
|
52
|
+
const type = file.type ?? ""
|
|
53
|
+
if (ACCEPTED_MIME.includes(type)) return true
|
|
54
|
+
if (acceptVideos && (VIDEO_MIME.includes(type) || type.startsWith("video/"))) {
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
if (/\.(png|jpe?g|webp|gif|pdf)$/i.test(file.name)) return true
|
|
58
|
+
if (acceptVideos && /\.(mp4|webm|mov|mkv)$/i.test(file.name)) return true
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getFileDisplayName(file: FileItem, maxLength = 40): string {
|
|
63
|
+
let label = file.name?.trim() ?? ""
|
|
64
|
+
const looksLikeUrl =
|
|
65
|
+
/^https?:\/\//i.test(label) || (label.length > 72 && !label.includes(" "))
|
|
66
|
+
|
|
67
|
+
if (!label || looksLikeUrl) {
|
|
68
|
+
try {
|
|
69
|
+
const pathname = decodeURIComponent(new URL(file.url ?? "").pathname)
|
|
70
|
+
const fromUrl = pathname.split("/").filter(Boolean).pop() ?? ""
|
|
71
|
+
if (fromUrl) label = fromUrl
|
|
72
|
+
} catch {
|
|
73
|
+
// ignore invalid URL
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!label) return "Archivo"
|
|
78
|
+
if (label.length <= maxLength) return label
|
|
79
|
+
|
|
80
|
+
const dot = label.lastIndexOf(".")
|
|
81
|
+
const ext = dot > 0 && label.length - dot <= 8 ? label.slice(dot) : ""
|
|
82
|
+
const stem = ext ? label.slice(0, dot) : label
|
|
83
|
+
const budget = maxLength - ext.length - 1
|
|
84
|
+
if (budget < 8) return `${label.slice(0, maxLength - 1)}…`
|
|
85
|
+
return `${stem.slice(0, budget)}…${ext}`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getFileFullName(file: FileItem): string {
|
|
89
|
+
const label = file.name?.trim()
|
|
90
|
+
if (label && !/^https?:\/\//i.test(label)) return label
|
|
91
|
+
return getFileDisplayName(file, 200)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function downloadFile(file: FileItem) {
|
|
95
|
+
const link = document.createElement("a")
|
|
96
|
+
link.href = file.url
|
|
97
|
+
link.download = file.name || "archivo"
|
|
98
|
+
link.target = "_blank"
|
|
99
|
+
link.rel = "noopener noreferrer"
|
|
100
|
+
document.body.appendChild(link)
|
|
101
|
+
link.click()
|
|
102
|
+
link.remove()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function toFileItem(
|
|
106
|
+
file: File,
|
|
107
|
+
url: string,
|
|
108
|
+
provider?: FileStorageProvider,
|
|
109
|
+
): FileItem {
|
|
110
|
+
return {
|
|
111
|
+
name: file.name,
|
|
112
|
+
url,
|
|
113
|
+
type: file.type,
|
|
114
|
+
...(provider ? { provider } : {}),
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -303,6 +303,16 @@
|
|
|
303
303
|
}
|
|
304
304
|
]
|
|
305
305
|
},
|
|
306
|
+
{
|
|
307
|
+
"name": "file-manager",
|
|
308
|
+
"type": "registry:ui",
|
|
309
|
+
"files": [
|
|
310
|
+
{
|
|
311
|
+
"path": "json/file-manager.json",
|
|
312
|
+
"type": "registry:file"
|
|
313
|
+
}
|
|
314
|
+
]
|
|
315
|
+
},
|
|
306
316
|
{
|
|
307
317
|
"name": "geo-search-select",
|
|
308
318
|
"type": "registry:ui",
|
|
@@ -793,6 +803,26 @@
|
|
|
793
803
|
}
|
|
794
804
|
]
|
|
795
805
|
},
|
|
806
|
+
{
|
|
807
|
+
"name": "lib-file-items",
|
|
808
|
+
"type": "registry:lib",
|
|
809
|
+
"files": [
|
|
810
|
+
{
|
|
811
|
+
"path": "json/lib-file-items.json",
|
|
812
|
+
"type": "registry:file"
|
|
813
|
+
}
|
|
814
|
+
]
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
"name": "lib-file-manager-utils",
|
|
818
|
+
"type": "registry:lib",
|
|
819
|
+
"files": [
|
|
820
|
+
{
|
|
821
|
+
"path": "json/lib-file-manager-utils.json",
|
|
822
|
+
"type": "registry:file"
|
|
823
|
+
}
|
|
824
|
+
]
|
|
825
|
+
},
|
|
796
826
|
{
|
|
797
827
|
"name": "lib-geo-city-codes",
|
|
798
828
|
"type": "registry:lib",
|