@uniweb/kit 0.1.9 → 0.1.11
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 +2 -2
- package/src/hooks/index.js +10 -0
- package/src/hooks/useInView.js +200 -0
- package/src/hooks/useThemeData.js +256 -0
- package/src/index.js +10 -1
- package/src/styled/Article/index.jsx +79 -0
- package/src/styled/Asset/Asset.jsx +2 -2
- package/src/styled/Prose/index.jsx +66 -0
- package/src/styled/index.js +7 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Standard component library for Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"tailwind-merge": "^2.6.0",
|
|
40
|
-
"@uniweb/core": "0.1.
|
|
40
|
+
"@uniweb/core": "0.1.16"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"react": "^18.0.0 || ^19.0.0",
|
package/src/hooks/index.js
CHANGED
|
@@ -6,3 +6,13 @@ export { useMobileMenu } from './useMobileMenu.js'
|
|
|
6
6
|
export { useAccordion } from './useAccordion.js'
|
|
7
7
|
export { useGridLayout, getGridClasses } from './useGridLayout.js'
|
|
8
8
|
export { useTheme, getThemeClasses, THEMES, THEME_NAMES } from './useTheme.js'
|
|
9
|
+
export { useInView, useIsInView } from './useInView.js'
|
|
10
|
+
|
|
11
|
+
// Theme data hooks (runtime theme access)
|
|
12
|
+
export {
|
|
13
|
+
useThemeData,
|
|
14
|
+
useColorContext,
|
|
15
|
+
useAppearance,
|
|
16
|
+
useThemeColor,
|
|
17
|
+
useThemeColorVar
|
|
18
|
+
} from './useThemeData.js'
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useInView Hook
|
|
3
|
+
*
|
|
4
|
+
* Detects when an element enters or leaves the viewport using IntersectionObserver.
|
|
5
|
+
* Useful for lazy loading, animations on scroll, and infinite scroll.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // Basic usage - trigger animation when element enters viewport
|
|
9
|
+
* function AnimatedSection() {
|
|
10
|
+
* const { ref, inView } = useInView()
|
|
11
|
+
* return (
|
|
12
|
+
* <div ref={ref} className={inView ? 'animate-fade-in' : 'opacity-0'}>
|
|
13
|
+
* Content appears when scrolled into view
|
|
14
|
+
* </div>
|
|
15
|
+
* )
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Lazy load image when near viewport
|
|
20
|
+
* function LazyImage({ src, alt }) {
|
|
21
|
+
* const { ref, inView } = useInView({
|
|
22
|
+
* triggerOnce: true,
|
|
23
|
+
* rootMargin: '200px', // Load 200px before entering viewport
|
|
24
|
+
* })
|
|
25
|
+
* return (
|
|
26
|
+
* <div ref={ref}>
|
|
27
|
+
* {inView ? <img src={src} alt={alt} /> : <div className="placeholder" />}
|
|
28
|
+
* </div>
|
|
29
|
+
* )
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // Threshold for partial visibility
|
|
34
|
+
* const { ref, inView } = useInView({
|
|
35
|
+
* threshold: 0.5, // Trigger when 50% visible
|
|
36
|
+
* })
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Default options for IntersectionObserver
|
|
43
|
+
*/
|
|
44
|
+
const DEFAULT_OPTIONS = {
|
|
45
|
+
threshold: 0,
|
|
46
|
+
rootMargin: '0px',
|
|
47
|
+
triggerOnce: false,
|
|
48
|
+
root: null,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Hook to detect when an element enters/leaves the viewport.
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} options - Configuration options
|
|
55
|
+
* @param {number|number[]} options.threshold - Visibility ratio(s) to trigger (0-1). Default: 0
|
|
56
|
+
* @param {string} options.rootMargin - Margin around root. Default: '0px'
|
|
57
|
+
* @param {boolean} options.triggerOnce - Only trigger once, then stop observing. Default: false
|
|
58
|
+
* @param {Element|null} options.root - Scroll container (null = viewport). Default: null
|
|
59
|
+
* @param {boolean} options.initialInView - Initial inView state for SSR. Default: false
|
|
60
|
+
*
|
|
61
|
+
* @returns {Object} { ref, inView, entry }
|
|
62
|
+
* @returns {Function} ref - Callback ref to attach to the target element
|
|
63
|
+
* @returns {boolean} inView - Whether element is currently in viewport
|
|
64
|
+
* @returns {IntersectionObserverEntry|null} entry - Latest IntersectionObserver entry
|
|
65
|
+
*/
|
|
66
|
+
export function useInView(options = {}) {
|
|
67
|
+
const {
|
|
68
|
+
threshold = DEFAULT_OPTIONS.threshold,
|
|
69
|
+
rootMargin = DEFAULT_OPTIONS.rootMargin,
|
|
70
|
+
triggerOnce = DEFAULT_OPTIONS.triggerOnce,
|
|
71
|
+
root = DEFAULT_OPTIONS.root,
|
|
72
|
+
initialInView = false,
|
|
73
|
+
} = options
|
|
74
|
+
|
|
75
|
+
const [inView, setInView] = useState(initialInView)
|
|
76
|
+
const [entry, setEntry] = useState(null)
|
|
77
|
+
|
|
78
|
+
// Track if we've already triggered (for triggerOnce)
|
|
79
|
+
const hasTriggered = useRef(false)
|
|
80
|
+
|
|
81
|
+
// Store the observer instance
|
|
82
|
+
const observerRef = useRef(null)
|
|
83
|
+
|
|
84
|
+
// Store the current element
|
|
85
|
+
const elementRef = useRef(null)
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Callback ref that handles element changes
|
|
89
|
+
*/
|
|
90
|
+
const ref = useCallback(
|
|
91
|
+
(node) => {
|
|
92
|
+
// Clean up previous observer
|
|
93
|
+
if (observerRef.current) {
|
|
94
|
+
observerRef.current.disconnect()
|
|
95
|
+
observerRef.current = null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Store the new element
|
|
99
|
+
elementRef.current = node
|
|
100
|
+
|
|
101
|
+
// Don't observe if:
|
|
102
|
+
// - No element
|
|
103
|
+
// - SSR (no IntersectionObserver)
|
|
104
|
+
// - Already triggered and triggerOnce is true
|
|
105
|
+
if (!node) return
|
|
106
|
+
if (typeof IntersectionObserver === 'undefined') return
|
|
107
|
+
if (triggerOnce && hasTriggered.current) return
|
|
108
|
+
|
|
109
|
+
// Create new observer
|
|
110
|
+
const observer = new IntersectionObserver(
|
|
111
|
+
([observerEntry]) => {
|
|
112
|
+
const isIntersecting = observerEntry.isIntersecting
|
|
113
|
+
|
|
114
|
+
setInView(isIntersecting)
|
|
115
|
+
setEntry(observerEntry)
|
|
116
|
+
|
|
117
|
+
// Handle triggerOnce
|
|
118
|
+
if (isIntersecting && triggerOnce) {
|
|
119
|
+
hasTriggered.current = true
|
|
120
|
+
observer.disconnect()
|
|
121
|
+
observerRef.current = null
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
threshold,
|
|
126
|
+
rootMargin,
|
|
127
|
+
root,
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
observer.observe(node)
|
|
132
|
+
observerRef.current = observer
|
|
133
|
+
},
|
|
134
|
+
[threshold, rootMargin, root, triggerOnce]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
// Clean up on unmount
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
return () => {
|
|
140
|
+
if (observerRef.current) {
|
|
141
|
+
observerRef.current.disconnect()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}, [])
|
|
145
|
+
|
|
146
|
+
return { ref, inView, entry }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Simpler hook that just returns inView state for a ref'd element.
|
|
151
|
+
* Use when you don't need the entry details.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* const ref = useRef()
|
|
155
|
+
* const inView = useIsInView(ref, { triggerOnce: true })
|
|
156
|
+
*
|
|
157
|
+
* @param {React.RefObject} targetRef - Ref to the target element
|
|
158
|
+
* @param {Object} options - Same options as useInView
|
|
159
|
+
* @returns {boolean} Whether element is in viewport
|
|
160
|
+
*/
|
|
161
|
+
export function useIsInView(targetRef, options = {}) {
|
|
162
|
+
const {
|
|
163
|
+
threshold = DEFAULT_OPTIONS.threshold,
|
|
164
|
+
rootMargin = DEFAULT_OPTIONS.rootMargin,
|
|
165
|
+
triggerOnce = DEFAULT_OPTIONS.triggerOnce,
|
|
166
|
+
root = DEFAULT_OPTIONS.root,
|
|
167
|
+
initialInView = false,
|
|
168
|
+
} = options
|
|
169
|
+
|
|
170
|
+
const [inView, setInView] = useState(initialInView)
|
|
171
|
+
const hasTriggered = useRef(false)
|
|
172
|
+
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
const element = targetRef.current
|
|
175
|
+
if (!element) return
|
|
176
|
+
if (typeof IntersectionObserver === 'undefined') return
|
|
177
|
+
if (triggerOnce && hasTriggered.current) return
|
|
178
|
+
|
|
179
|
+
const observer = new IntersectionObserver(
|
|
180
|
+
([entry]) => {
|
|
181
|
+
const isIntersecting = entry.isIntersecting
|
|
182
|
+
setInView(isIntersecting)
|
|
183
|
+
|
|
184
|
+
if (isIntersecting && triggerOnce) {
|
|
185
|
+
hasTriggered.current = true
|
|
186
|
+
observer.disconnect()
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
{ threshold, rootMargin, root }
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
observer.observe(element)
|
|
193
|
+
|
|
194
|
+
return () => observer.disconnect()
|
|
195
|
+
}, [targetRef, threshold, rootMargin, root, triggerOnce])
|
|
196
|
+
|
|
197
|
+
return inView
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export default useInView
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Data Hooks
|
|
3
|
+
*
|
|
4
|
+
* Provides access to runtime theme configuration and appearance settings.
|
|
5
|
+
* These hooks are for accessing theme data from the Uniweb runtime.
|
|
6
|
+
*
|
|
7
|
+
* Note: For component styling classes (light/dark/gray themes), use useTheme() instead.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* function ColorPicker() {
|
|
11
|
+
* const theme = useThemeData()
|
|
12
|
+
* const colors = theme?.getPaletteNames() || []
|
|
13
|
+
* return <div>{colors.join(', ')}</div>
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* function DarkModeToggle() {
|
|
18
|
+
* const { scheme, toggle, canToggle } = useAppearance()
|
|
19
|
+
* if (!canToggle) return null
|
|
20
|
+
* return <button onClick={toggle}>{scheme === 'dark' ? '☀️' : '🌙'}</button>
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
25
|
+
import { getUniweb, Theme } from '@uniweb/core'
|
|
26
|
+
|
|
27
|
+
// Storage key for appearance preference
|
|
28
|
+
const APPEARANCE_STORAGE_KEY = 'uniweb-appearance'
|
|
29
|
+
|
|
30
|
+
// CSS class for dark scheme
|
|
31
|
+
const DARK_SCHEME_CLASS = 'scheme-dark'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the initial scheme from storage or system preference
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} appearance - Theme appearance configuration
|
|
37
|
+
* @returns {string} 'light' or 'dark'
|
|
38
|
+
*/
|
|
39
|
+
function getInitialScheme(appearance) {
|
|
40
|
+
// Check localStorage first
|
|
41
|
+
if (typeof localStorage !== 'undefined') {
|
|
42
|
+
const stored = localStorage.getItem(APPEARANCE_STORAGE_KEY)
|
|
43
|
+
if (stored === 'light' || stored === 'dark') {
|
|
44
|
+
return stored
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check system preference if respectSystemPreference is enabled
|
|
49
|
+
if (appearance?.respectSystemPreference && typeof window !== 'undefined') {
|
|
50
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
51
|
+
if (prefersDark && appearance.schemes?.includes('dark')) {
|
|
52
|
+
return 'dark'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Fall back to default
|
|
57
|
+
const defaultScheme = appearance?.default || 'light'
|
|
58
|
+
return defaultScheme === 'system' ? 'light' : defaultScheme
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Access the full Theme object from the active website.
|
|
63
|
+
*
|
|
64
|
+
* Returns the Theme instance which provides methods like:
|
|
65
|
+
* - getColor(name, shade) - Get a color value
|
|
66
|
+
* - getPalette(name) - Get all shades for a color
|
|
67
|
+
* - getContextToken(context, token) - Get semantic token
|
|
68
|
+
* - getAppearance() - Get appearance configuration
|
|
69
|
+
*
|
|
70
|
+
* @returns {Theme|null} Theme instance or null if not available
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* const theme = useThemeData()
|
|
74
|
+
* const primaryColor = theme?.getColor('primary', 500)
|
|
75
|
+
*/
|
|
76
|
+
export function useThemeData() {
|
|
77
|
+
const uniweb = getUniweb()
|
|
78
|
+
const website = uniweb?.activeWebsite
|
|
79
|
+
const themeData = website?.themeData
|
|
80
|
+
|
|
81
|
+
if (!themeData) {
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Return a Theme instance if we have theme data
|
|
86
|
+
// The website may already have a Theme instance, or we create one
|
|
87
|
+
if (website.theme instanceof Theme) {
|
|
88
|
+
return website.theme
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Create a new Theme instance from themeData
|
|
92
|
+
return new Theme(themeData)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get the current section's color context.
|
|
97
|
+
*
|
|
98
|
+
* Note: This requires the component to receive the block prop.
|
|
99
|
+
* For components that don't have block access, use a default or pass context explicitly.
|
|
100
|
+
*
|
|
101
|
+
* @param {Object} block - Block instance (optional)
|
|
102
|
+
* @returns {string} Context name ('light', 'medium', or 'dark')
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* function MyComponent({ block }) {
|
|
106
|
+
* const context = useColorContext(block)
|
|
107
|
+
* return <div className={`context-${context}`}>...</div>
|
|
108
|
+
* }
|
|
109
|
+
*/
|
|
110
|
+
export function useColorContext(block) {
|
|
111
|
+
// Get context from block's theme property
|
|
112
|
+
const context = block?.themeName || block?.theme || 'light'
|
|
113
|
+
|
|
114
|
+
// Validate context name
|
|
115
|
+
const validContexts = ['light', 'medium', 'dark']
|
|
116
|
+
if (validContexts.includes(context)) {
|
|
117
|
+
return context
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return 'light'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check and toggle site-wide appearance scheme (light/dark mode).
|
|
125
|
+
*
|
|
126
|
+
* This hook manages the site's color scheme preference and provides
|
|
127
|
+
* utilities for toggling between light and dark modes.
|
|
128
|
+
*
|
|
129
|
+
* @returns {Object} Appearance utilities
|
|
130
|
+
* @property {string} scheme - Current scheme ('light' or 'dark')
|
|
131
|
+
* @property {Function} setScheme - Set scheme explicitly
|
|
132
|
+
* @property {Function} toggle - Toggle between light and dark
|
|
133
|
+
* @property {boolean} canToggle - Whether toggling is enabled
|
|
134
|
+
* @property {string[]} schemes - Available schemes
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* function DarkModeToggle() {
|
|
138
|
+
* const { scheme, toggle, canToggle } = useAppearance()
|
|
139
|
+
*
|
|
140
|
+
* if (!canToggle) return null
|
|
141
|
+
*
|
|
142
|
+
* return (
|
|
143
|
+
* <button onClick={toggle}>
|
|
144
|
+
* {scheme === 'dark' ? 'Switch to Light' : 'Switch to Dark'}
|
|
145
|
+
* </button>
|
|
146
|
+
* )
|
|
147
|
+
* }
|
|
148
|
+
*/
|
|
149
|
+
export function useAppearance() {
|
|
150
|
+
const theme = useThemeData()
|
|
151
|
+
const appearance = theme?.getAppearance() || { default: 'light', allowToggle: false }
|
|
152
|
+
|
|
153
|
+
const [scheme, setSchemeState] = useState(() => getInitialScheme(appearance))
|
|
154
|
+
|
|
155
|
+
// Apply scheme to document
|
|
156
|
+
const applyScheme = useCallback((newScheme) => {
|
|
157
|
+
if (typeof document !== 'undefined') {
|
|
158
|
+
const root = document.documentElement
|
|
159
|
+
if (newScheme === 'dark') {
|
|
160
|
+
root.classList.add(DARK_SCHEME_CLASS)
|
|
161
|
+
} else {
|
|
162
|
+
root.classList.remove(DARK_SCHEME_CLASS)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}, [])
|
|
166
|
+
|
|
167
|
+
// Set scheme with persistence and DOM update
|
|
168
|
+
const setScheme = useCallback((newScheme) => {
|
|
169
|
+
if (newScheme !== 'light' && newScheme !== 'dark') {
|
|
170
|
+
console.warn(`[useAppearance] Invalid scheme: ${newScheme}. Use 'light' or 'dark'.`)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
setSchemeState(newScheme)
|
|
175
|
+
applyScheme(newScheme)
|
|
176
|
+
|
|
177
|
+
// Persist to localStorage
|
|
178
|
+
if (typeof localStorage !== 'undefined') {
|
|
179
|
+
localStorage.setItem(APPEARANCE_STORAGE_KEY, newScheme)
|
|
180
|
+
}
|
|
181
|
+
}, [applyScheme])
|
|
182
|
+
|
|
183
|
+
// Toggle between light and dark
|
|
184
|
+
const toggle = useCallback(() => {
|
|
185
|
+
const newScheme = scheme === 'light' ? 'dark' : 'light'
|
|
186
|
+
setScheme(newScheme)
|
|
187
|
+
}, [scheme, setScheme])
|
|
188
|
+
|
|
189
|
+
// Apply initial scheme on mount
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
applyScheme(scheme)
|
|
192
|
+
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
193
|
+
|
|
194
|
+
// Listen for system preference changes
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
if (!appearance.respectSystemPreference || typeof window === 'undefined') {
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
201
|
+
|
|
202
|
+
const handleChange = (e) => {
|
|
203
|
+
// Only auto-switch if user hasn't manually set preference
|
|
204
|
+
const stored = localStorage.getItem(APPEARANCE_STORAGE_KEY)
|
|
205
|
+
if (!stored) {
|
|
206
|
+
const newScheme = e.matches ? 'dark' : 'light'
|
|
207
|
+
if (appearance.schemes?.includes(newScheme)) {
|
|
208
|
+
setScheme(newScheme)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
mediaQuery.addEventListener('change', handleChange)
|
|
214
|
+
return () => mediaQuery.removeEventListener('change', handleChange)
|
|
215
|
+
}, [appearance.respectSystemPreference, appearance.schemes, setScheme])
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
scheme,
|
|
219
|
+
setScheme,
|
|
220
|
+
toggle,
|
|
221
|
+
canToggle: appearance.allowToggle === true,
|
|
222
|
+
schemes: appearance.schemes || ['light'],
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get a specific color from the theme.
|
|
228
|
+
* Convenience hook that combines useThemeData with getColor.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} name - Color name (e.g., 'primary', 'neutral')
|
|
231
|
+
* @param {number} shade - Shade level (50-950), defaults to 500
|
|
232
|
+
* @returns {string|null} Color value or null
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* const primaryColor = useThemeColor('primary', 600)
|
|
236
|
+
*/
|
|
237
|
+
export function useThemeColor(name, shade = 500) {
|
|
238
|
+
const theme = useThemeData()
|
|
239
|
+
return theme?.getColor(name, shade) || null
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get CSS variable reference for a color.
|
|
244
|
+
* Useful for inline styles that reference theme colors.
|
|
245
|
+
*
|
|
246
|
+
* @param {string} name - Color name
|
|
247
|
+
* @param {number} shade - Shade level
|
|
248
|
+
* @returns {string} CSS var() reference
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* const style = { color: useThemeColorVar('primary', 600) }
|
|
252
|
+
* // Returns: 'var(--primary-600)'
|
|
253
|
+
*/
|
|
254
|
+
export function useThemeColorVar(name, shade = 500) {
|
|
255
|
+
return `var(--${name}-${shade})`
|
|
256
|
+
}
|
package/src/index.js
CHANGED
|
@@ -70,7 +70,16 @@ export {
|
|
|
70
70
|
useTheme,
|
|
71
71
|
getThemeClasses,
|
|
72
72
|
THEMES,
|
|
73
|
-
THEME_NAMES
|
|
73
|
+
THEME_NAMES,
|
|
74
|
+
// Viewport detection
|
|
75
|
+
useInView,
|
|
76
|
+
useIsInView,
|
|
77
|
+
// Theme data hooks (runtime theme access)
|
|
78
|
+
useThemeData,
|
|
79
|
+
useColorContext,
|
|
80
|
+
useAppearance,
|
|
81
|
+
useThemeColor,
|
|
82
|
+
useThemeColorVar
|
|
74
83
|
} from './hooks/index.js'
|
|
75
84
|
|
|
76
85
|
// ============================================================================
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Article Component
|
|
3
|
+
*
|
|
4
|
+
* Semantic article wrapper with prose typography.
|
|
5
|
+
* Use for blog posts, news articles, documentation pages, etc.
|
|
6
|
+
*
|
|
7
|
+
* @module @uniweb/kit/styled/Article
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react'
|
|
11
|
+
import { cn } from '../../utils/index.js'
|
|
12
|
+
import { Render } from '../Section/Render.jsx'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Prose sizes
|
|
16
|
+
*/
|
|
17
|
+
const SIZE_CLASSES = {
|
|
18
|
+
sm: 'prose-sm',
|
|
19
|
+
base: 'prose-base',
|
|
20
|
+
lg: 'prose-lg',
|
|
21
|
+
xl: 'prose-xl',
|
|
22
|
+
'2xl': 'prose-2xl'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Article - Semantic article with prose typography
|
|
27
|
+
*
|
|
28
|
+
* Renders content inside a semantic <article> tag with prose styling.
|
|
29
|
+
* Can accept either children or a content prop (ProseMirror JSON).
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} props
|
|
32
|
+
* @param {Object|Array} [props.content] - ProseMirror content to render
|
|
33
|
+
* @param {string} [props.size='lg'] - Text size: sm, base, lg, xl, 2xl
|
|
34
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
35
|
+
* @param {React.ReactNode} [props.children] - Content to render (alternative to content prop)
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // With ProseMirror content
|
|
39
|
+
* <Article content={articleData.content} />
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // With children
|
|
43
|
+
* <Article>
|
|
44
|
+
* <h1>My Article</h1>
|
|
45
|
+
* <p>Article content...</p>
|
|
46
|
+
* </Article>
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* // Composing with Render
|
|
50
|
+
* <Article size="base" className="dark:prose-invert">
|
|
51
|
+
* <Render content={proseMirrorContent} />
|
|
52
|
+
* </Article>
|
|
53
|
+
*/
|
|
54
|
+
export function Article({
|
|
55
|
+
content,
|
|
56
|
+
size = 'lg',
|
|
57
|
+
className,
|
|
58
|
+
children,
|
|
59
|
+
...props
|
|
60
|
+
}) {
|
|
61
|
+
const sizeClass = SIZE_CLASSES[size] || SIZE_CLASSES.lg
|
|
62
|
+
|
|
63
|
+
// Resolve content - if it's a ProseMirror doc, get the content array
|
|
64
|
+
let resolvedContent = content
|
|
65
|
+
if (resolvedContent?.type === 'doc') {
|
|
66
|
+
resolvedContent = resolvedContent.content
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<article
|
|
71
|
+
className={cn('prose', sizeClass, 'max-w-none', className)}
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
{children || (resolvedContent && <Render content={resolvedContent} />)}
|
|
75
|
+
</article>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default Article
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import React, { useState, useCallback, forwardRef, useImperativeHandle } from 'react'
|
|
10
10
|
import { cn } from '../../utils/index.js'
|
|
11
|
-
import { FileLogo } from '
|
|
12
|
-
import { Image } from '
|
|
11
|
+
import { FileLogo } from '../../components/FileLogo/index.js'
|
|
12
|
+
import { Image } from '../../components/Image/index.js'
|
|
13
13
|
import { useWebsite } from '../../hooks/useWebsite.js'
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prose Component
|
|
3
|
+
*
|
|
4
|
+
* Typography wrapper for long-form content.
|
|
5
|
+
* Applies prose styling for readable text.
|
|
6
|
+
*
|
|
7
|
+
* @module @uniweb/kit/styled/Prose
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react'
|
|
11
|
+
import { cn } from '../../utils/index.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Prose sizes
|
|
15
|
+
*/
|
|
16
|
+
const SIZE_CLASSES = {
|
|
17
|
+
sm: 'prose-sm',
|
|
18
|
+
base: 'prose-base',
|
|
19
|
+
lg: 'prose-lg',
|
|
20
|
+
xl: 'prose-xl',
|
|
21
|
+
'2xl': 'prose-2xl'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Prose - Typography wrapper for long-form content
|
|
26
|
+
*
|
|
27
|
+
* Applies Tailwind Typography (prose) classes for readable text styling.
|
|
28
|
+
* Use for article bodies, documentation, or any long-form content.
|
|
29
|
+
*
|
|
30
|
+
* @param {Object} props
|
|
31
|
+
* @param {string} [props.size='lg'] - Text size: sm, base, lg, xl, 2xl
|
|
32
|
+
* @param {string} [props.as='div'] - HTML element to render as
|
|
33
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
34
|
+
* @param {React.ReactNode} props.children - Content to render
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* <Prose>
|
|
38
|
+
* <h2>Article Title</h2>
|
|
39
|
+
* <p>Article content...</p>
|
|
40
|
+
* </Prose>
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* <Prose size="base" className="dark:prose-invert">
|
|
44
|
+
* <Render content={proseMirrorContent} />
|
|
45
|
+
* </Prose>
|
|
46
|
+
*/
|
|
47
|
+
export function Prose({
|
|
48
|
+
size = 'lg',
|
|
49
|
+
as: Component = 'div',
|
|
50
|
+
className,
|
|
51
|
+
children,
|
|
52
|
+
...props
|
|
53
|
+
}) {
|
|
54
|
+
const sizeClass = SIZE_CLASSES[size] || SIZE_CLASSES.lg
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Component
|
|
58
|
+
className={cn('prose', sizeClass, 'max-w-none', className)}
|
|
59
|
+
{...props}
|
|
60
|
+
>
|
|
61
|
+
{children}
|
|
62
|
+
</Component>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default Prose
|
package/src/styled/index.js
CHANGED
|
@@ -20,9 +20,15 @@ export { SidebarLayout } from './SidebarLayout/index.js'
|
|
|
20
20
|
// Content Rendering
|
|
21
21
|
// ============================================================================
|
|
22
22
|
|
|
23
|
-
// Section - Rich content section
|
|
23
|
+
// Section - Rich content section layout container
|
|
24
24
|
export { Section, Render } from './Section/index.js'
|
|
25
25
|
|
|
26
|
+
// Prose - Typography wrapper for long-form content
|
|
27
|
+
export { Prose } from './Prose/index.jsx'
|
|
28
|
+
|
|
29
|
+
// Article - Semantic article with prose typography
|
|
30
|
+
export { Article } from './Article/index.jsx'
|
|
31
|
+
|
|
26
32
|
// Renderers - Individual content type renderers
|
|
27
33
|
export { Code, Alert, Warning, Table, Details, Divider } from './Section/renderers/index.js'
|
|
28
34
|
|