@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.
Files changed (31) hide show
  1. package/package.json +3 -2
  2. package/src/components/Asset/Asset.jsx +31 -81
  3. package/src/components/Media/Media.jsx +27 -125
  4. package/src/components/SocialIcon/index.jsx +146 -0
  5. package/src/hooks/index.js +7 -0
  6. package/src/hooks/useAccordion.js +143 -0
  7. package/src/hooks/useActiveRoute.js +97 -0
  8. package/src/hooks/useGridLayout.js +71 -0
  9. package/src/hooks/useMobileMenu.js +58 -0
  10. package/src/hooks/useRouting.js +119 -0
  11. package/src/hooks/useScrolled.js +48 -0
  12. package/src/hooks/useTheme.js +205 -0
  13. package/src/index.js +29 -10
  14. package/src/styled/Asset/Asset.jsx +161 -0
  15. package/src/styled/Asset/index.js +1 -0
  16. package/src/{components → styled}/Disclaimer/Disclaimer.jsx +1 -1
  17. package/src/styled/Media/Media.jsx +322 -0
  18. package/src/styled/Media/index.js +1 -0
  19. package/src/{components → styled}/Section/Render.jsx +4 -4
  20. package/src/{components → styled}/Section/index.js +6 -0
  21. package/src/{components → styled}/Section/renderers/Alert.jsx +1 -1
  22. package/src/{components → styled}/Section/renderers/Details.jsx +1 -1
  23. package/src/{components → styled}/Section/renderers/Table.jsx +1 -1
  24. package/src/{components → styled}/Section/renderers/index.js +1 -1
  25. package/src/styled/SidebarLayout/SidebarLayout.jsx +310 -0
  26. package/src/styled/SidebarLayout/index.js +1 -0
  27. package/src/styled/index.js +40 -0
  28. /package/src/{components → styled}/Disclaimer/index.js +0 -0
  29. /package/src/{components → styled}/Section/Section.jsx +0 -0
  30. /package/src/{components → styled}/Section/renderers/Code.jsx +0 -0
  31. /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
- // Primitives
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
- // Content
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
- export { Disclaimer } from './components/Disclaimer/index.js'
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 { useWebsite } from './hooks/index.js'
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 '../SafeHtml/index.js'
12
+ import { SafeHtml } from '../../components/SafeHtml/index.js'
13
13
 
14
14
  /**
15
15
  * Disclaimer Modal