@uniweb/kit 0.1.1
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/LICENSE +201 -0
- package/README.md +345 -0
- package/package.json +47 -0
- package/src/components/Asset/Asset.jsx +161 -0
- package/src/components/Asset/index.js +1 -0
- package/src/components/Disclaimer/Disclaimer.jsx +198 -0
- package/src/components/Disclaimer/index.js +1 -0
- package/src/components/FileLogo/FileLogo.jsx +148 -0
- package/src/components/FileLogo/index.js +1 -0
- package/src/components/Icon/Icon.jsx +214 -0
- package/src/components/Icon/index.js +1 -0
- package/src/components/Image/Image.jsx +194 -0
- package/src/components/Image/index.js +1 -0
- package/src/components/Link/Link.jsx +261 -0
- package/src/components/Link/index.js +1 -0
- package/src/components/Media/Media.jsx +322 -0
- package/src/components/Media/index.js +1 -0
- package/src/components/MediaIcon/MediaIcon.jsx +95 -0
- package/src/components/MediaIcon/index.js +1 -0
- package/src/components/SafeHtml/SafeHtml.jsx +93 -0
- package/src/components/SafeHtml/index.js +1 -0
- package/src/components/Section/Render.jsx +245 -0
- package/src/components/Section/Section.jsx +131 -0
- package/src/components/Section/index.js +3 -0
- package/src/components/Section/renderers/Alert.jsx +101 -0
- package/src/components/Section/renderers/Code.jsx +70 -0
- package/src/components/Section/renderers/Details.jsx +77 -0
- package/src/components/Section/renderers/Divider.jsx +42 -0
- package/src/components/Section/renderers/Table.jsx +55 -0
- package/src/components/Section/renderers/index.js +11 -0
- package/src/components/Text/Text.jsx +207 -0
- package/src/components/Text/index.js +14 -0
- package/src/hooks/index.js +1 -0
- package/src/hooks/useWebsite.js +77 -0
- package/src/index.js +69 -0
- package/src/styles/index.css +8 -0
- package/src/utils/index.js +104 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset Component
|
|
3
|
+
*
|
|
4
|
+
* File asset preview with download functionality.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/kit/Asset
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useState, useCallback, forwardRef, useImperativeHandle } from 'react'
|
|
10
|
+
import { cn } from '../../utils/index.js'
|
|
11
|
+
import { FileLogo } from '../FileLogo/index.js'
|
|
12
|
+
import { Image } from '../Image/index.js'
|
|
13
|
+
import { useWebsite } from '../../hooks/useWebsite.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if file is an image
|
|
17
|
+
* @param {string} filename
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
function isImageFile(filename) {
|
|
21
|
+
if (!filename) return false
|
|
22
|
+
const ext = filename.toLowerCase().split('.').pop()
|
|
23
|
+
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Asset - File preview with download
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} props
|
|
30
|
+
* @param {string|Object} props.value - Asset identifier or object
|
|
31
|
+
* @param {Object} [props.profile] - Profile object for asset resolution
|
|
32
|
+
* @param {boolean} [props.withDownload=true] - Show download button
|
|
33
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* <Asset value="document.pdf" profile={profile} />
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* <Asset value={{ src: "/files/report.pdf", filename: "report.pdf" }} />
|
|
40
|
+
*/
|
|
41
|
+
export const Asset = forwardRef(function Asset(
|
|
42
|
+
{ value, profile, withDownload = true, className, ...props },
|
|
43
|
+
ref
|
|
44
|
+
) {
|
|
45
|
+
const { localize } = useWebsite()
|
|
46
|
+
const [imageError, setImageError] = useState(false)
|
|
47
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
48
|
+
|
|
49
|
+
// Resolve asset info
|
|
50
|
+
let src = ''
|
|
51
|
+
let filename = ''
|
|
52
|
+
let alt = ''
|
|
53
|
+
|
|
54
|
+
if (typeof value === 'string') {
|
|
55
|
+
src = value
|
|
56
|
+
filename = value.split('/').pop() || value
|
|
57
|
+
} else if (value && typeof value === 'object') {
|
|
58
|
+
src = value.src || value.url || ''
|
|
59
|
+
filename = value.filename || value.name || src.split('/').pop() || ''
|
|
60
|
+
alt = value.alt || filename
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Use profile to resolve asset if available
|
|
64
|
+
if (profile && typeof profile.getAssetInfo === 'function') {
|
|
65
|
+
const assetInfo = profile.getAssetInfo(value, true, filename)
|
|
66
|
+
src = assetInfo?.src || src
|
|
67
|
+
filename = assetInfo?.filename || filename
|
|
68
|
+
alt = assetInfo?.alt || alt
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const isImage = isImageFile(filename)
|
|
72
|
+
|
|
73
|
+
// Handle download
|
|
74
|
+
const handleDownload = useCallback(async () => {
|
|
75
|
+
if (!src) return
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// Try to trigger download
|
|
79
|
+
const downloadUrl = src.includes('?') ? `${src}&download=true` : `${src}?download=true`
|
|
80
|
+
const response = await fetch(downloadUrl)
|
|
81
|
+
const blob = await response.blob()
|
|
82
|
+
|
|
83
|
+
const link = document.createElement('a')
|
|
84
|
+
link.href = URL.createObjectURL(blob)
|
|
85
|
+
link.download = filename
|
|
86
|
+
document.body.appendChild(link)
|
|
87
|
+
link.click()
|
|
88
|
+
document.body.removeChild(link)
|
|
89
|
+
URL.revokeObjectURL(link.href)
|
|
90
|
+
} catch (error) {
|
|
91
|
+
// Fallback: open in new tab
|
|
92
|
+
window.open(src, '_blank')
|
|
93
|
+
}
|
|
94
|
+
}, [src, filename])
|
|
95
|
+
|
|
96
|
+
// Expose download method via ref
|
|
97
|
+
useImperativeHandle(ref, () => ({
|
|
98
|
+
triggerDownload: handleDownload
|
|
99
|
+
}), [handleDownload])
|
|
100
|
+
|
|
101
|
+
// Handle image error
|
|
102
|
+
const handleImageError = useCallback(() => {
|
|
103
|
+
setImageError(true)
|
|
104
|
+
}, [])
|
|
105
|
+
|
|
106
|
+
if (!src) return null
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div
|
|
110
|
+
className={cn(
|
|
111
|
+
'relative inline-block rounded-lg overflow-hidden border border-gray-200',
|
|
112
|
+
'transition-shadow hover:shadow-md',
|
|
113
|
+
className
|
|
114
|
+
)}
|
|
115
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
116
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
117
|
+
{...props}
|
|
118
|
+
>
|
|
119
|
+
{/* Preview */}
|
|
120
|
+
<div className="w-32 h-32 flex items-center justify-center bg-gray-50">
|
|
121
|
+
{isImage && !imageError ? (
|
|
122
|
+
<Image
|
|
123
|
+
src={src}
|
|
124
|
+
alt={alt}
|
|
125
|
+
className="w-full h-full object-cover"
|
|
126
|
+
onError={handleImageError}
|
|
127
|
+
/>
|
|
128
|
+
) : (
|
|
129
|
+
<FileLogo filename={filename} size="48" className="text-gray-400" />
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{/* Filename */}
|
|
134
|
+
<div className="px-2 py-1 text-xs text-gray-600 truncate max-w-[128px]" title={filename}>
|
|
135
|
+
{filename}
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* Download overlay */}
|
|
139
|
+
{withDownload && (
|
|
140
|
+
<button
|
|
141
|
+
onClick={handleDownload}
|
|
142
|
+
className={cn(
|
|
143
|
+
'absolute inset-0 flex items-center justify-center',
|
|
144
|
+
'bg-black/50 text-white transition-opacity',
|
|
145
|
+
isHovered ? 'opacity-100' : 'opacity-0'
|
|
146
|
+
)}
|
|
147
|
+
aria-label={localize({
|
|
148
|
+
en: 'Download file',
|
|
149
|
+
fr: 'Télécharger le fichier'
|
|
150
|
+
})}
|
|
151
|
+
>
|
|
152
|
+
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
153
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
154
|
+
</svg>
|
|
155
|
+
</button>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
export default Asset
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Asset, default } from './Asset.jsx'
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disclaimer Component
|
|
3
|
+
*
|
|
4
|
+
* Modal disclaimer dialog for terms, privacy notices, etc.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/kit/Disclaimer
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useState, useCallback, useEffect } from 'react'
|
|
10
|
+
import { cn } from '../../utils/index.js'
|
|
11
|
+
import { useWebsite } from '../../hooks/useWebsite.js'
|
|
12
|
+
import { SafeHtml } from '../SafeHtml/index.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Disclaimer Modal
|
|
16
|
+
*/
|
|
17
|
+
function DisclaimerModal({ isOpen, onClose, title, content, className }) {
|
|
18
|
+
// Handle escape key
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const handleEscape = (e) => {
|
|
21
|
+
if (e.key === 'Escape') onClose()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (isOpen) {
|
|
25
|
+
document.addEventListener('keydown', handleEscape)
|
|
26
|
+
document.body.style.overflow = 'hidden'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return () => {
|
|
30
|
+
document.removeEventListener('keydown', handleEscape)
|
|
31
|
+
document.body.style.overflow = ''
|
|
32
|
+
}
|
|
33
|
+
}, [isOpen, onClose])
|
|
34
|
+
|
|
35
|
+
if (!isOpen) return null
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="fixed inset-0 z-50 overflow-y-auto">
|
|
39
|
+
{/* Backdrop */}
|
|
40
|
+
<div
|
|
41
|
+
className="fixed inset-0 bg-black/50 transition-opacity"
|
|
42
|
+
onClick={onClose}
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
/>
|
|
45
|
+
|
|
46
|
+
{/* Dialog */}
|
|
47
|
+
<div className="flex min-h-full items-center justify-center p-4">
|
|
48
|
+
<div
|
|
49
|
+
className={cn(
|
|
50
|
+
'relative w-full max-w-lg transform overflow-hidden rounded-lg',
|
|
51
|
+
'bg-white shadow-xl transition-all',
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
role="dialog"
|
|
55
|
+
aria-modal="true"
|
|
56
|
+
aria-labelledby="disclaimer-title"
|
|
57
|
+
>
|
|
58
|
+
{/* Header */}
|
|
59
|
+
<div className="flex items-center justify-between border-b border-gray-200 px-4 py-3">
|
|
60
|
+
<h3 id="disclaimer-title" className="text-lg font-medium text-gray-900">
|
|
61
|
+
{title}
|
|
62
|
+
</h3>
|
|
63
|
+
<button
|
|
64
|
+
onClick={onClose}
|
|
65
|
+
className="rounded-md p-1 text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
66
|
+
aria-label="Close"
|
|
67
|
+
>
|
|
68
|
+
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
69
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
70
|
+
</svg>
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* Content */}
|
|
75
|
+
<div className="px-4 py-4 max-h-96 overflow-y-auto">
|
|
76
|
+
{typeof content === 'string' ? (
|
|
77
|
+
<SafeHtml value={content} className="prose prose-sm" />
|
|
78
|
+
) : (
|
|
79
|
+
content
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Footer */}
|
|
84
|
+
<div className="border-t border-gray-200 px-4 py-3 flex justify-end">
|
|
85
|
+
<button
|
|
86
|
+
onClick={onClose}
|
|
87
|
+
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
88
|
+
>
|
|
89
|
+
Close
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Disclaimer - Disclaimer trigger and modal
|
|
100
|
+
*
|
|
101
|
+
* @param {Object} props
|
|
102
|
+
* @param {string} [props.type='link'] - Trigger type: 'link', 'button', 'popup'
|
|
103
|
+
* @param {string} props.title - Disclaimer title
|
|
104
|
+
* @param {string|React.ReactNode} props.content - Disclaimer content
|
|
105
|
+
* @param {string} [props.triggerText] - Text for the trigger element
|
|
106
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
107
|
+
* @param {React.ReactNode} [props.children] - Custom trigger element
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* <Disclaimer
|
|
111
|
+
* title="Terms of Service"
|
|
112
|
+
* content="<p>Please read our terms...</p>"
|
|
113
|
+
* triggerText="View Terms"
|
|
114
|
+
* />
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* <Disclaimer title="Privacy Policy" content={<PrivacyContent />}>
|
|
118
|
+
* <button className="underline">Privacy Policy</button>
|
|
119
|
+
* </Disclaimer>
|
|
120
|
+
*/
|
|
121
|
+
export function Disclaimer({
|
|
122
|
+
type = 'link',
|
|
123
|
+
title,
|
|
124
|
+
content,
|
|
125
|
+
triggerText,
|
|
126
|
+
className,
|
|
127
|
+
children,
|
|
128
|
+
...props
|
|
129
|
+
}) {
|
|
130
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
131
|
+
const { localize } = useWebsite()
|
|
132
|
+
|
|
133
|
+
const handleOpen = useCallback(() => setIsOpen(true), [])
|
|
134
|
+
const handleClose = useCallback(() => setIsOpen(false), [])
|
|
135
|
+
|
|
136
|
+
// Auto-open for popup type
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (type === 'popup') {
|
|
139
|
+
setIsOpen(true)
|
|
140
|
+
}
|
|
141
|
+
}, [type])
|
|
142
|
+
|
|
143
|
+
// Localized title and content
|
|
144
|
+
const localizedTitle = typeof title === 'object' ? localize(title) : title
|
|
145
|
+
const localizedContent = typeof content === 'object' && !React.isValidElement(content)
|
|
146
|
+
? localize(content)
|
|
147
|
+
: content
|
|
148
|
+
const localizedTriggerText = typeof triggerText === 'object'
|
|
149
|
+
? localize(triggerText)
|
|
150
|
+
: (triggerText || localizedTitle)
|
|
151
|
+
|
|
152
|
+
// Render trigger
|
|
153
|
+
const trigger = children ? (
|
|
154
|
+
React.cloneElement(React.Children.only(children), {
|
|
155
|
+
onClick: handleOpen,
|
|
156
|
+
role: 'button',
|
|
157
|
+
'aria-haspopup': 'dialog'
|
|
158
|
+
})
|
|
159
|
+
) : type === 'button' ? (
|
|
160
|
+
<button
|
|
161
|
+
onClick={handleOpen}
|
|
162
|
+
className={cn(
|
|
163
|
+
'inline-flex items-center px-3 py-1.5 text-sm font-medium',
|
|
164
|
+
'text-blue-600 hover:text-blue-700',
|
|
165
|
+
'border border-blue-600 rounded-md hover:bg-blue-50',
|
|
166
|
+
className
|
|
167
|
+
)}
|
|
168
|
+
{...props}
|
|
169
|
+
>
|
|
170
|
+
{localizedTriggerText}
|
|
171
|
+
</button>
|
|
172
|
+
) : (
|
|
173
|
+
<button
|
|
174
|
+
onClick={handleOpen}
|
|
175
|
+
className={cn(
|
|
176
|
+
'text-blue-600 hover:text-blue-700 underline text-sm',
|
|
177
|
+
className
|
|
178
|
+
)}
|
|
179
|
+
{...props}
|
|
180
|
+
>
|
|
181
|
+
{localizedTriggerText}
|
|
182
|
+
</button>
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<>
|
|
187
|
+
{type !== 'popup' && trigger}
|
|
188
|
+
<DisclaimerModal
|
|
189
|
+
isOpen={isOpen}
|
|
190
|
+
onClose={handleClose}
|
|
191
|
+
title={localizedTitle}
|
|
192
|
+
content={localizedContent}
|
|
193
|
+
/>
|
|
194
|
+
</>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export default Disclaimer
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Disclaimer, default } from './Disclaimer.jsx'
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileLogo Component
|
|
3
|
+
*
|
|
4
|
+
* Displays file type icons based on filename extension.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/kit/FileLogo
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react'
|
|
10
|
+
import { cn } from '../../utils/index.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* File type to icon mapping
|
|
14
|
+
* Using simple SVG icons for common file types
|
|
15
|
+
*/
|
|
16
|
+
const FILE_ICONS = {
|
|
17
|
+
// Images
|
|
18
|
+
jpg: 'image',
|
|
19
|
+
jpeg: 'image',
|
|
20
|
+
png: 'image',
|
|
21
|
+
gif: 'image',
|
|
22
|
+
webp: 'image',
|
|
23
|
+
svg: 'image',
|
|
24
|
+
|
|
25
|
+
// Documents
|
|
26
|
+
pdf: 'pdf',
|
|
27
|
+
doc: 'word',
|
|
28
|
+
docx: 'word',
|
|
29
|
+
txt: 'text',
|
|
30
|
+
rtf: 'text',
|
|
31
|
+
|
|
32
|
+
// Spreadsheets
|
|
33
|
+
xls: 'excel',
|
|
34
|
+
xlsx: 'excel',
|
|
35
|
+
xlsm: 'excel',
|
|
36
|
+
xlsb: 'excel',
|
|
37
|
+
csv: 'excel',
|
|
38
|
+
|
|
39
|
+
// Presentations
|
|
40
|
+
ppt: 'powerpoint',
|
|
41
|
+
pptx: 'powerpoint',
|
|
42
|
+
|
|
43
|
+
// Code
|
|
44
|
+
html: 'code',
|
|
45
|
+
css: 'code',
|
|
46
|
+
js: 'code',
|
|
47
|
+
json: 'code',
|
|
48
|
+
xml: 'code',
|
|
49
|
+
|
|
50
|
+
// Archives
|
|
51
|
+
zip: 'archive',
|
|
52
|
+
rar: 'archive',
|
|
53
|
+
'7z': 'archive',
|
|
54
|
+
tar: 'archive',
|
|
55
|
+
gz: 'archive',
|
|
56
|
+
|
|
57
|
+
// Media
|
|
58
|
+
mp3: 'audio',
|
|
59
|
+
wav: 'audio',
|
|
60
|
+
mp4: 'video',
|
|
61
|
+
mov: 'video',
|
|
62
|
+
avi: 'video'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* SVG icons for each file type
|
|
67
|
+
*/
|
|
68
|
+
const ICONS = {
|
|
69
|
+
image: (
|
|
70
|
+
<path d="M4 5a2 2 0 012-2h12a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm14 0H6v14h12V5zm-3 4a1 1 0 11-2 0 1 1 0 012 0zm-9 10l3-3 2 2 4-4 3 3v2H6v-0z" />
|
|
71
|
+
),
|
|
72
|
+
pdf: (
|
|
73
|
+
<path d="M7 2a2 2 0 00-2 2v16a2 2 0 002 2h10a2 2 0 002-2V8l-6-6H7zm7 1.5L18.5 8H14V3.5zM9 13h1.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5H10v2H9v-5zm1 2h.5a.5.5 0 000-1H10v1zm3-2h1.75c.69 0 1.25.56 1.25 1.25v2.5c0 .69-.56 1.25-1.25 1.25H13v-5zm1 4h.75a.25.25 0 00.25-.25v-2.5a.25.25 0 00-.25-.25H14v3z" />
|
|
74
|
+
),
|
|
75
|
+
word: (
|
|
76
|
+
<path d="M7 2a2 2 0 00-2 2v16a2 2 0 002 2h10a2 2 0 002-2V8l-6-6H7zm7 1.5L18.5 8H14V3.5zM8 13h1l1.5 4 1.5-4h1l-2 6h-1l-2-6zm7 0h1v6h-1v-6z" />
|
|
77
|
+
),
|
|
78
|
+
excel: (
|
|
79
|
+
<path d="M7 2a2 2 0 00-2 2v16a2 2 0 002 2h10a2 2 0 002-2V8l-6-6H7zm7 1.5L18.5 8H14V3.5zM9 12h2l1 2.5 1-2.5h2l-2 3 2 3h-2l-1-2.5-1 2.5H9l2-3-2-3z" />
|
|
80
|
+
),
|
|
81
|
+
powerpoint: (
|
|
82
|
+
<path d="M7 2a2 2 0 00-2 2v16a2 2 0 002 2h10a2 2 0 002-2V8l-6-6H7zm7 1.5L18.5 8H14V3.5zM9 12h3c1.1 0 2 .9 2 2s-.9 2-2 2h-2v2H9v-6zm1 3h2a1 1 0 000-2h-2v2z" />
|
|
83
|
+
),
|
|
84
|
+
text: (
|
|
85
|
+
<path d="M7 2a2 2 0 00-2 2v16a2 2 0 002 2h10a2 2 0 002-2V8l-6-6H7zm7 1.5L18.5 8H14V3.5zM8 12h8v1H8v-1zm0 3h8v1H8v-1zm0 3h5v1H8v-1z" />
|
|
86
|
+
),
|
|
87
|
+
code: (
|
|
88
|
+
<path d="M7 2a2 2 0 00-2 2v16a2 2 0 002 2h10a2 2 0 002-2V8l-6-6H7zm7 1.5L18.5 8H14V3.5zM9.5 12l-2 3 2 3 .7-.7L8.5 15l1.7-2.3-.7-.7zm5 0l-.7.7 1.7 2.3-1.7 2.3.7.7 2-3-2-3z" />
|
|
89
|
+
),
|
|
90
|
+
archive: (
|
|
91
|
+
<path d="M7 2a2 2 0 00-2 2v16a2 2 0 002 2h10a2 2 0 002-2V8l-6-6H7zm7 1.5L18.5 8H14V3.5zM11 10h2v1h-2v-1zm0 2h2v1h-2v-1zm0 2h2v3h-2v-3z" />
|
|
92
|
+
),
|
|
93
|
+
audio: (
|
|
94
|
+
<path d="M7 2a2 2 0 00-2 2v16a2 2 0 002 2h10a2 2 0 002-2V8l-6-6H7zm7 1.5L18.5 8H14V3.5zM12 11a3 3 0 100 6 3 3 0 000-6zm0 1a2 2 0 110 4 2 2 0 010-4zm0 1a1 1 0 100 2 1 1 0 000-2z" />
|
|
95
|
+
),
|
|
96
|
+
video: (
|
|
97
|
+
<path d="M7 2a2 2 0 00-2 2v16a2 2 0 002 2h10a2 2 0 002-2V8l-6-6H7zm7 1.5L18.5 8H14V3.5zM9 12l6 3-6 3v-6z" />
|
|
98
|
+
),
|
|
99
|
+
default: (
|
|
100
|
+
<path d="M7 2a2 2 0 00-2 2v16a2 2 0 002 2h10a2 2 0 002-2V8l-6-6H7zm7 1.5L18.5 8H14V3.5z" />
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get file extension from filename
|
|
106
|
+
* @param {string} filename
|
|
107
|
+
* @returns {string}
|
|
108
|
+
*/
|
|
109
|
+
function getExtension(filename) {
|
|
110
|
+
if (!filename) return ''
|
|
111
|
+
const parts = filename.toLowerCase().split('.')
|
|
112
|
+
return parts.length > 1 ? parts.pop() : ''
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* FileLogo - File type icon component
|
|
117
|
+
*
|
|
118
|
+
* @param {Object} props
|
|
119
|
+
* @param {string} props.filename - Filename to determine icon
|
|
120
|
+
* @param {string} [props.size='24'] - Icon size in pixels
|
|
121
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* <FileLogo filename="report.pdf" size="32" />
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* <FileLogo filename="data.xlsx" className="text-green-600" />
|
|
128
|
+
*/
|
|
129
|
+
export function FileLogo({ filename, size = '24', className, ...props }) {
|
|
130
|
+
const ext = getExtension(filename)
|
|
131
|
+
const iconType = FILE_ICONS[ext] || 'default'
|
|
132
|
+
const icon = ICONS[iconType]
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<svg
|
|
136
|
+
viewBox="0 0 24 24"
|
|
137
|
+
fill="currentColor"
|
|
138
|
+
className={cn('inline-block', className)}
|
|
139
|
+
style={{ width: size, height: size }}
|
|
140
|
+
aria-hidden="true"
|
|
141
|
+
{...props}
|
|
142
|
+
>
|
|
143
|
+
{icon}
|
|
144
|
+
</svg>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default FileLogo
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FileLogo, default } from './FileLogo.jsx'
|