@uniweb/kit 0.1.4 → 0.1.6
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/package.json +3 -2
- package/src/components/Asset/Asset.jsx +31 -81
- package/src/components/Media/Media.jsx +27 -125
- package/src/components/SocialIcon/index.jsx +146 -0
- package/src/hooks/index.js +7 -0
- package/src/hooks/useAccordion.js +143 -0
- package/src/hooks/useActiveRoute.js +97 -0
- package/src/hooks/useGridLayout.js +71 -0
- package/src/hooks/useMobileMenu.js +58 -0
- package/src/hooks/useRouting.js +119 -0
- package/src/hooks/useScrolled.js +48 -0
- package/src/hooks/useTheme.js +205 -0
- package/src/index.js +29 -10
- package/src/styled/Asset/Asset.jsx +161 -0
- package/src/styled/Asset/index.js +1 -0
- package/src/{components → styled}/Disclaimer/Disclaimer.jsx +1 -1
- package/src/styled/Media/Media.jsx +322 -0
- package/src/styled/Media/index.js +1 -0
- package/src/{components → styled}/Section/Render.jsx +4 -4
- package/src/{components → styled}/Section/index.js +6 -0
- package/src/{components → styled}/Section/renderers/Alert.jsx +1 -1
- package/src/{components → styled}/Section/renderers/Details.jsx +1 -1
- package/src/{components → styled}/Section/renderers/Table.jsx +1 -1
- package/src/{components → styled}/Section/renderers/index.js +1 -1
- package/src/styled/SidebarLayout/SidebarLayout.jsx +310 -0
- package/src/styled/SidebarLayout/index.js +1 -0
- package/src/styled/index.js +40 -0
- /package/src/{components → styled}/Disclaimer/index.js +0 -0
- /package/src/{components → styled}/Section/Section.jsx +0 -0
- /package/src/{components → styled}/Section/renderers/Code.jsx +0 -0
- /package/src/{components → styled}/Section/renderers/Divider.jsx +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTheme Hook
|
|
3
|
+
*
|
|
4
|
+
* Provides standardized theme classes for components.
|
|
5
|
+
* Eliminates duplicated theme objects across 25+ components.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* function Hero({ params }) {
|
|
9
|
+
* const t = useTheme(params.theme)
|
|
10
|
+
*
|
|
11
|
+
* return (
|
|
12
|
+
* <section className={t.section}>
|
|
13
|
+
* <h1 className={t.title}>...</h1>
|
|
14
|
+
* <p className={t.body}>...</p>
|
|
15
|
+
* </section>
|
|
16
|
+
* )
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // With custom overrides
|
|
21
|
+
* const t = useTheme('dark', {
|
|
22
|
+
* card: 'bg-gray-800 rounded-xl',
|
|
23
|
+
* cardHover: 'hover:bg-gray-700'
|
|
24
|
+
* })
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Standard theme definitions.
|
|
29
|
+
* Keys are standardized across all components.
|
|
30
|
+
*/
|
|
31
|
+
const THEMES = {
|
|
32
|
+
light: {
|
|
33
|
+
// Section backgrounds
|
|
34
|
+
section: 'bg-white',
|
|
35
|
+
sectionAlt: 'bg-gray-50',
|
|
36
|
+
|
|
37
|
+
// Typography
|
|
38
|
+
title: 'text-gray-900',
|
|
39
|
+
subtitle: 'text-gray-700',
|
|
40
|
+
pretitle: 'text-primary',
|
|
41
|
+
body: 'text-gray-600',
|
|
42
|
+
muted: 'text-gray-500',
|
|
43
|
+
|
|
44
|
+
// Cards
|
|
45
|
+
card: 'bg-gray-50',
|
|
46
|
+
cardHover: 'hover:shadow-lg',
|
|
47
|
+
cardBorder: 'border border-gray-200',
|
|
48
|
+
|
|
49
|
+
// Interactive
|
|
50
|
+
link: 'text-primary hover:text-primary-dark',
|
|
51
|
+
linkMuted: 'text-gray-600 hover:text-gray-900',
|
|
52
|
+
|
|
53
|
+
// Borders & Dividers
|
|
54
|
+
border: 'border-gray-200',
|
|
55
|
+
divider: 'bg-gray-200',
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
gray: {
|
|
59
|
+
section: 'bg-gray-50',
|
|
60
|
+
sectionAlt: 'bg-gray-100',
|
|
61
|
+
|
|
62
|
+
title: 'text-gray-900',
|
|
63
|
+
subtitle: 'text-gray-700',
|
|
64
|
+
pretitle: 'text-primary',
|
|
65
|
+
body: 'text-gray-600',
|
|
66
|
+
muted: 'text-gray-500',
|
|
67
|
+
|
|
68
|
+
card: 'bg-white shadow-sm',
|
|
69
|
+
cardHover: 'hover:shadow-lg',
|
|
70
|
+
cardBorder: 'border border-gray-200',
|
|
71
|
+
|
|
72
|
+
link: 'text-primary hover:text-primary-dark',
|
|
73
|
+
linkMuted: 'text-gray-600 hover:text-gray-900',
|
|
74
|
+
|
|
75
|
+
border: 'border-gray-200',
|
|
76
|
+
divider: 'bg-gray-300',
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
dark: {
|
|
80
|
+
section: 'bg-gray-900',
|
|
81
|
+
sectionAlt: 'bg-gray-800',
|
|
82
|
+
|
|
83
|
+
title: 'text-white',
|
|
84
|
+
subtitle: 'text-gray-300',
|
|
85
|
+
pretitle: 'text-primary',
|
|
86
|
+
body: 'text-gray-400',
|
|
87
|
+
muted: 'text-gray-500',
|
|
88
|
+
|
|
89
|
+
card: 'bg-gray-800',
|
|
90
|
+
cardHover: 'hover:bg-gray-750',
|
|
91
|
+
cardBorder: 'border border-gray-700',
|
|
92
|
+
|
|
93
|
+
link: 'text-primary hover:text-primary-light',
|
|
94
|
+
linkMuted: 'text-gray-400 hover:text-white',
|
|
95
|
+
|
|
96
|
+
border: 'border-gray-700',
|
|
97
|
+
divider: 'bg-gray-700',
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
primary: {
|
|
101
|
+
section: 'bg-primary',
|
|
102
|
+
sectionAlt: 'bg-primary-dark',
|
|
103
|
+
|
|
104
|
+
title: 'text-white',
|
|
105
|
+
subtitle: 'text-white/90',
|
|
106
|
+
pretitle: 'text-white/80',
|
|
107
|
+
body: 'text-white/80',
|
|
108
|
+
muted: 'text-white/60',
|
|
109
|
+
|
|
110
|
+
card: 'bg-white/10',
|
|
111
|
+
cardHover: 'hover:bg-white/20',
|
|
112
|
+
cardBorder: 'border border-white/20',
|
|
113
|
+
|
|
114
|
+
link: 'text-white hover:text-white/80',
|
|
115
|
+
linkMuted: 'text-white/70 hover:text-white',
|
|
116
|
+
|
|
117
|
+
border: 'border-white/20',
|
|
118
|
+
divider: 'bg-white/20',
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
gradient: {
|
|
122
|
+
section: 'bg-gradient-to-br from-primary to-indigo-600',
|
|
123
|
+
sectionAlt: 'bg-gradient-to-br from-primary-dark to-indigo-700',
|
|
124
|
+
|
|
125
|
+
title: 'text-white',
|
|
126
|
+
subtitle: 'text-white/90',
|
|
127
|
+
pretitle: 'text-white/80',
|
|
128
|
+
body: 'text-white/80',
|
|
129
|
+
muted: 'text-white/60',
|
|
130
|
+
|
|
131
|
+
card: 'bg-white/10 backdrop-blur-sm',
|
|
132
|
+
cardHover: 'hover:bg-white/20',
|
|
133
|
+
cardBorder: 'border border-white/20',
|
|
134
|
+
|
|
135
|
+
link: 'text-white hover:text-white/80',
|
|
136
|
+
linkMuted: 'text-white/70 hover:text-white',
|
|
137
|
+
|
|
138
|
+
border: 'border-white/20',
|
|
139
|
+
divider: 'bg-white/20',
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
glass: {
|
|
143
|
+
section: 'bg-white/10 backdrop-blur-lg',
|
|
144
|
+
sectionAlt: 'bg-white/5 backdrop-blur-lg',
|
|
145
|
+
|
|
146
|
+
title: 'text-white',
|
|
147
|
+
subtitle: 'text-white/90',
|
|
148
|
+
pretitle: 'text-primary',
|
|
149
|
+
body: 'text-white/80',
|
|
150
|
+
muted: 'text-white/60',
|
|
151
|
+
|
|
152
|
+
card: 'bg-white/10 backdrop-blur-sm',
|
|
153
|
+
cardHover: 'hover:bg-white/20',
|
|
154
|
+
cardBorder: 'border border-white/20',
|
|
155
|
+
|
|
156
|
+
link: 'text-white hover:text-primary',
|
|
157
|
+
linkMuted: 'text-white/70 hover:text-white',
|
|
158
|
+
|
|
159
|
+
border: 'border-white/20',
|
|
160
|
+
divider: 'bg-white/20',
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Hook to get theme classes by name.
|
|
166
|
+
*
|
|
167
|
+
* @param {string} themeName - Theme name (light, gray, dark, primary, gradient, glass)
|
|
168
|
+
* @param {Object} overrides - Custom class overrides
|
|
169
|
+
* @returns {Object} Theme classes object
|
|
170
|
+
*/
|
|
171
|
+
export function useTheme(themeName = 'light', overrides = {}) {
|
|
172
|
+
const baseTheme = THEMES[themeName] || THEMES.light
|
|
173
|
+
|
|
174
|
+
if (Object.keys(overrides).length === 0) {
|
|
175
|
+
return baseTheme
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
...baseTheme,
|
|
180
|
+
...overrides,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get theme classes without the hook (for non-React contexts)
|
|
186
|
+
* @param {string} themeName
|
|
187
|
+
* @param {Object} overrides
|
|
188
|
+
* @returns {Object}
|
|
189
|
+
*/
|
|
190
|
+
export function getThemeClasses(themeName = 'light', overrides = {}) {
|
|
191
|
+
const baseTheme = THEMES[themeName] || THEMES.light
|
|
192
|
+
return { ...baseTheme, ...overrides }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* All available theme names
|
|
197
|
+
*/
|
|
198
|
+
export const THEME_NAMES = Object.keys(THEMES)
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Export theme definitions for customization
|
|
202
|
+
*/
|
|
203
|
+
export { THEMES }
|
|
204
|
+
|
|
205
|
+
export default useTheme
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Standard component library for Uniweb foundations.
|
|
5
5
|
*
|
|
6
|
+
* For pre-styled components (Section, SidebarLayout, Disclaimer, etc.),
|
|
7
|
+
* import from '@uniweb/kit/styled'.
|
|
8
|
+
*
|
|
6
9
|
* @example
|
|
7
10
|
* import { Link, Image, useWebsite } from '@uniweb/kit'
|
|
8
11
|
*
|
|
@@ -18,10 +21,10 @@
|
|
|
18
21
|
*/
|
|
19
22
|
|
|
20
23
|
// ============================================================================
|
|
21
|
-
// Components
|
|
24
|
+
// Components (Primitives - no Tailwind dependency)
|
|
22
25
|
// ============================================================================
|
|
23
26
|
|
|
24
|
-
//
|
|
27
|
+
// Navigation
|
|
25
28
|
export { Link } from './components/Link/index.js'
|
|
26
29
|
export { Image } from './components/Image/index.js'
|
|
27
30
|
export { SafeHtml } from './components/SafeHtml/index.js'
|
|
@@ -35,24 +38,40 @@ export {
|
|
|
35
38
|
PlainText
|
|
36
39
|
} from './components/Text/index.js'
|
|
37
40
|
|
|
38
|
-
// Media
|
|
41
|
+
// Media (plain version - for styled facade, use @uniweb/kit/tailwind)
|
|
39
42
|
export { Media } from './components/Media/index.js'
|
|
40
43
|
export { FileLogo } from './components/FileLogo/index.js'
|
|
41
44
|
export { MediaIcon } from './components/MediaIcon/index.js'
|
|
42
45
|
|
|
43
|
-
//
|
|
44
|
-
export { Section, Render } from './components/Section/index.js'
|
|
45
|
-
export { Code, Alert, Warning, Table, Details, Divider } from './components/Section/renderers/index.js'
|
|
46
|
-
|
|
47
|
-
// Utilities
|
|
46
|
+
// Files (plain version - for styled card, use @uniweb/kit/tailwind)
|
|
48
47
|
export { Asset } from './components/Asset/index.js'
|
|
49
|
-
|
|
48
|
+
|
|
49
|
+
// Social
|
|
50
|
+
export {
|
|
51
|
+
SocialIcon,
|
|
52
|
+
getSocialPlatform,
|
|
53
|
+
isSocialLink,
|
|
54
|
+
filterSocialLinks
|
|
55
|
+
} from './components/SocialIcon/index.jsx'
|
|
50
56
|
|
|
51
57
|
// ============================================================================
|
|
52
58
|
// Hooks
|
|
53
59
|
// ============================================================================
|
|
54
60
|
|
|
55
|
-
export {
|
|
61
|
+
export {
|
|
62
|
+
useWebsite,
|
|
63
|
+
useRouting,
|
|
64
|
+
useActiveRoute,
|
|
65
|
+
useScrolled,
|
|
66
|
+
useMobileMenu,
|
|
67
|
+
useAccordion,
|
|
68
|
+
useGridLayout,
|
|
69
|
+
getGridClasses,
|
|
70
|
+
useTheme,
|
|
71
|
+
getThemeClasses,
|
|
72
|
+
THEMES,
|
|
73
|
+
THEME_NAMES
|
|
74
|
+
} from './hooks/index.js'
|
|
56
75
|
|
|
57
76
|
// ============================================================================
|
|
58
77
|
// Utilities
|
|
@@ -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'
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import React, { useState, useCallback, useEffect } from 'react'
|
|
10
10
|
import { cn } from '../../utils/index.js'
|
|
11
11
|
import { useWebsite } from '../../hooks/useWebsite.js'
|
|
12
|
-
import { SafeHtml } from '
|
|
12
|
+
import { SafeHtml } from '../../components/SafeHtml/index.js'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Disclaimer Modal
|