@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,143 @@
1
+ /**
2
+ * useAccordion Hook
3
+ *
4
+ * Manages expand/collapse state for accordion-style UIs.
5
+ * Used in FAQ components and collapsible navigation (LeftPanel).
6
+ *
7
+ * @example
8
+ * // Single-select accordion (only one item open at a time)
9
+ * function FAQ({ items }) {
10
+ * const { isOpen, toggle } = useAccordion()
11
+ *
12
+ * return items.map((item, i) => (
13
+ * <div key={i}>
14
+ * <button onClick={() => toggle(i)}>{item.question}</button>
15
+ * {isOpen(i) && <p>{item.answer}</p>}
16
+ * </div>
17
+ * ))
18
+ * }
19
+ *
20
+ * @example
21
+ * // Multi-select with first item open
22
+ * const { isOpen, toggle } = useAccordion({
23
+ * multiple: true,
24
+ * defaultOpen: [0]
25
+ * })
26
+ *
27
+ * @example
28
+ * // All items open by default
29
+ * const { isOpen, toggle, openAll, closeAll } = useAccordion({
30
+ * multiple: true,
31
+ * defaultOpen: 'all',
32
+ * items: faqItems // needed when using 'all'
33
+ * })
34
+ */
35
+
36
+ import { useState, useCallback } from 'react'
37
+
38
+ /**
39
+ * Hook to manage accordion expand/collapse state.
40
+ *
41
+ * @param {Object} options - Configuration options
42
+ * @param {boolean} [options.multiple=false] - Allow multiple items open at once
43
+ * @param {Array|string} [options.defaultOpen=[]] - Initially open items (indices or 'all')
44
+ * @param {Array} [options.items] - Items array (needed for defaultOpen: 'all')
45
+ * @returns {Object} Accordion state and controls
46
+ */
47
+ export function useAccordion(options = {}) {
48
+ const {
49
+ multiple = false,
50
+ defaultOpen = [],
51
+ items = [],
52
+ } = options
53
+
54
+ // Compute initial open state
55
+ const getInitialOpen = () => {
56
+ if (defaultOpen === 'all' && items.length > 0) {
57
+ return items.map((_, i) => i)
58
+ }
59
+ if (Array.isArray(defaultOpen)) {
60
+ return defaultOpen
61
+ }
62
+ return []
63
+ }
64
+
65
+ const [openItems, setOpenItems] = useState(getInitialOpen)
66
+
67
+ /**
68
+ * Check if an item is open
69
+ * @param {number|string} id - Item identifier (index or key)
70
+ * @returns {boolean}
71
+ */
72
+ const isOpen = useCallback((id) => {
73
+ return openItems.includes(id)
74
+ }, [openItems])
75
+
76
+ /**
77
+ * Toggle an item open/closed
78
+ * @param {number|string} id - Item identifier
79
+ */
80
+ const toggle = useCallback((id) => {
81
+ setOpenItems(prev => {
82
+ if (prev.includes(id)) {
83
+ // Close this item
84
+ return prev.filter(item => item !== id)
85
+ }
86
+ if (multiple) {
87
+ // Add to open items
88
+ return [...prev, id]
89
+ }
90
+ // Single select: replace open item
91
+ return [id]
92
+ })
93
+ }, [multiple])
94
+
95
+ /**
96
+ * Open a specific item
97
+ * @param {number|string} id - Item identifier
98
+ */
99
+ const open = useCallback((id) => {
100
+ setOpenItems(prev => {
101
+ if (prev.includes(id)) return prev
102
+ if (multiple) return [...prev, id]
103
+ return [id]
104
+ })
105
+ }, [multiple])
106
+
107
+ /**
108
+ * Close a specific item
109
+ * @param {number|string} id - Item identifier
110
+ */
111
+ const close = useCallback((id) => {
112
+ setOpenItems(prev => prev.filter(item => item !== id))
113
+ }, [])
114
+
115
+ /**
116
+ * Open all items (only works in multiple mode)
117
+ * @param {Array} allIds - All item identifiers
118
+ */
119
+ const openAll = useCallback((allIds) => {
120
+ if (multiple && Array.isArray(allIds)) {
121
+ setOpenItems(allIds)
122
+ }
123
+ }, [multiple])
124
+
125
+ /**
126
+ * Close all items
127
+ */
128
+ const closeAll = useCallback(() => {
129
+ setOpenItems([])
130
+ }, [])
131
+
132
+ return {
133
+ openItems,
134
+ isOpen,
135
+ toggle,
136
+ open,
137
+ close,
138
+ openAll,
139
+ closeAll,
140
+ }
141
+ }
142
+
143
+ export default useAccordion
@@ -0,0 +1,97 @@
1
+ /**
2
+ * useActiveRoute Hook
3
+ *
4
+ * SSG-safe hook for active route detection in navigation components.
5
+ * Provides utilities for checking if pages are active or ancestors of the current route.
6
+ *
7
+ * @example
8
+ * function NavItem({ page }) {
9
+ * const { isActiveOrAncestor } = useActiveRoute()
10
+ *
11
+ * return (
12
+ * <Link
13
+ * href={page.getNavigableRoute()}
14
+ * className={isActiveOrAncestor(page) ? 'active' : ''}
15
+ * >
16
+ * {page.label}
17
+ * </Link>
18
+ * )
19
+ * }
20
+ */
21
+
22
+ import { useRouting } from './useRouting.js'
23
+
24
+ /**
25
+ * Normalize a route by removing leading/trailing slashes
26
+ * @param {string} route
27
+ * @returns {string}
28
+ */
29
+ function normalizeRoute(route) {
30
+ return (route || '').replace(/^\//, '').replace(/\/$/, '')
31
+ }
32
+
33
+ /**
34
+ * Hook for active route detection with SSG-safe fallbacks.
35
+ *
36
+ * @returns {Object} Route utilities
37
+ * @property {string} route - Current normalized route (e.g., 'docs/getting-started')
38
+ * @property {string} rootSegment - First segment of route (e.g., 'docs')
39
+ * @property {function} isActive - Check if a page is the current page
40
+ * @property {function} isActiveOrAncestor - Check if a page or its descendants are active
41
+ */
42
+ export function useActiveRoute() {
43
+ const { useLocation } = useRouting()
44
+ const location = useLocation()
45
+
46
+ const route = normalizeRoute(location?.pathname)
47
+ const rootSegment = route.split('/')[0]
48
+
49
+ return {
50
+ /**
51
+ * Current normalized route (no leading/trailing slashes)
52
+ * @type {string}
53
+ */
54
+ route,
55
+
56
+ /**
57
+ * First segment of the current route
58
+ * Useful for root-level navigation highlighting
59
+ * @type {string}
60
+ */
61
+ rootSegment,
62
+
63
+ /**
64
+ * Check if a page is the current active page (exact match)
65
+ *
66
+ * @param {Object} page - Page object with getNormalizedRoute() or route property
67
+ * @returns {boolean}
68
+ */
69
+ isActive: (page) => {
70
+ if (typeof page.getNormalizedRoute === 'function') {
71
+ return page.getNormalizedRoute() === route
72
+ }
73
+ // Fallback for page info objects from getPageHierarchy
74
+ return normalizeRoute(page.route) === route
75
+ },
76
+
77
+ /**
78
+ * Check if a page or any of its descendants is active
79
+ * Useful for highlighting parent nav items when child is active
80
+ *
81
+ * @param {Object} page - Page object with isActiveOrAncestor() or route property
82
+ * @returns {boolean}
83
+ */
84
+ isActiveOrAncestor: (page) => {
85
+ if (typeof page.isActiveOrAncestor === 'function') {
86
+ return page.isActiveOrAncestor(route)
87
+ }
88
+ // Fallback for page info objects from getPageHierarchy
89
+ const pageRoute = normalizeRoute(page.route)
90
+ if (pageRoute === route) return true
91
+ if (pageRoute === '') return true // Root is ancestor of all
92
+ return route.startsWith(pageRoute + '/')
93
+ },
94
+ }
95
+ }
96
+
97
+ export default useActiveRoute
@@ -0,0 +1,71 @@
1
+ /**
2
+ * useGridLayout Hook
3
+ *
4
+ * Returns Tailwind CSS classes for responsive grid layouts.
5
+ * Standardizes the grid column patterns used across components.
6
+ *
7
+ * @example
8
+ * function Features({ items, params }) {
9
+ * const gridClass = useGridLayout(params.columns)
10
+ *
11
+ * return (
12
+ * <div className={gridClass}>
13
+ * {items.map(item => <FeatureCard key={item.id} {...item} />)}
14
+ * </div>
15
+ * )
16
+ * }
17
+ *
18
+ * @example
19
+ * // With custom gap
20
+ * const gridClass = useGridLayout(3, { gap: 12 })
21
+ * // Returns: "grid gap-12 sm:grid-cols-2 lg:grid-cols-3"
22
+ */
23
+
24
+ /**
25
+ * Standard responsive grid column configurations.
26
+ * Keys are column counts, values are Tailwind classes.
27
+ */
28
+ const GRID_CONFIGS = {
29
+ 1: '',
30
+ 2: 'sm:grid-cols-2',
31
+ 3: 'sm:grid-cols-2 lg:grid-cols-3',
32
+ 4: 'sm:grid-cols-2 lg:grid-cols-4',
33
+ 5: 'sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5',
34
+ 6: 'sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-6',
35
+ }
36
+
37
+ /**
38
+ * Hook to generate responsive grid layout classes.
39
+ *
40
+ * @param {number} columns - Number of columns (1-6)
41
+ * @param {Object} options - Configuration options
42
+ * @param {number} [options.gap=8] - Gap size (Tailwind scale: 4, 6, 8, 10, 12)
43
+ * @param {string} [options.baseClass='grid'] - Base class to include
44
+ * @returns {string} Tailwind CSS classes for the grid
45
+ */
46
+ export function useGridLayout(columns = 3, options = {}) {
47
+ const {
48
+ gap = 8,
49
+ baseClass = 'grid',
50
+ } = options
51
+
52
+ const colConfig = GRID_CONFIGS[columns] || GRID_CONFIGS[3]
53
+ const gapClass = `gap-${gap}`
54
+
55
+ return [baseClass, gapClass, colConfig].filter(Boolean).join(' ')
56
+ }
57
+
58
+ /**
59
+ * Get grid classes without the hook (for non-React contexts)
60
+ * @param {number} columns
61
+ * @param {Object} options
62
+ * @returns {string}
63
+ */
64
+ export function getGridClasses(columns = 3, options = {}) {
65
+ const { gap = 8, baseClass = 'grid' } = options
66
+ const colConfig = GRID_CONFIGS[columns] || GRID_CONFIGS[3]
67
+ const gapClass = `gap-${gap}`
68
+ return [baseClass, gapClass, colConfig].filter(Boolean).join(' ')
69
+ }
70
+
71
+ export default useGridLayout
@@ -0,0 +1,58 @@
1
+ /**
2
+ * useMobileMenu Hook
3
+ *
4
+ * Manages mobile menu state with automatic close on route change.
5
+ * Common pattern in Header/Navbar components across all templates.
6
+ *
7
+ * @example
8
+ * function Header() {
9
+ * const { isOpen, toggle, close } = useMobileMenu()
10
+ *
11
+ * return (
12
+ * <>
13
+ * <button onClick={toggle}>Menu</button>
14
+ * {isOpen && (
15
+ * <nav>
16
+ * <Link href="/about" onClick={close}>About</Link>
17
+ * </nav>
18
+ * )}
19
+ * </>
20
+ * )
21
+ * }
22
+ */
23
+
24
+ import { useState, useEffect, useCallback } from 'react'
25
+ import { useActiveRoute } from './useActiveRoute.js'
26
+
27
+ /**
28
+ * Hook to manage mobile menu state.
29
+ * Automatically closes menu on route change.
30
+ *
31
+ * @returns {Object} Menu state and controls
32
+ * @property {boolean} isOpen - Whether menu is open
33
+ * @property {function} open - Open the menu
34
+ * @property {function} close - Close the menu
35
+ * @property {function} toggle - Toggle menu open/closed
36
+ */
37
+ export function useMobileMenu() {
38
+ const [isOpen, setIsOpen] = useState(false)
39
+ const { route } = useActiveRoute()
40
+
41
+ // Close menu on route change
42
+ useEffect(() => {
43
+ setIsOpen(false)
44
+ }, [route])
45
+
46
+ const open = useCallback(() => setIsOpen(true), [])
47
+ const close = useCallback(() => setIsOpen(false), [])
48
+ const toggle = useCallback(() => setIsOpen(prev => !prev), [])
49
+
50
+ return {
51
+ isOpen,
52
+ open,
53
+ close,
54
+ toggle,
55
+ }
56
+ }
57
+
58
+ export default useMobileMenu
@@ -0,0 +1,119 @@
1
+ /**
2
+ * useRouting Hook
3
+ *
4
+ * Provides SSG-safe access to routing functionality.
5
+ *
6
+ * The runtime registers routing components (Link, useNavigate, useLocation, useParams)
7
+ * via the bridge pattern. This hook provides safe access that gracefully handles
8
+ * SSG/SSR contexts where the Router context isn't available.
9
+ *
10
+ * @example
11
+ * function NavItem({ route }) {
12
+ * const { useLocation } = useRouting()
13
+ * const location = useLocation()
14
+ * const isActive = location.pathname === route
15
+ * return <Link to={route} className={isActive ? 'active' : ''}>...</Link>
16
+ * }
17
+ */
18
+
19
+ import { getUniweb } from '@uniweb/core'
20
+
21
+ /**
22
+ * Default location object for SSG/SSR contexts
23
+ */
24
+ const DEFAULT_LOCATION = {
25
+ pathname: '/',
26
+ search: '',
27
+ hash: '',
28
+ state: null,
29
+ key: 'default'
30
+ }
31
+
32
+ /**
33
+ * Default params object for SSG/SSR contexts
34
+ */
35
+ const DEFAULT_PARAMS = {}
36
+
37
+ /**
38
+ * Get routing utilities with SSG-safe fallbacks
39
+ * @returns {Object} Routing utilities
40
+ */
41
+ export function useRouting() {
42
+ const uniweb = getUniweb()
43
+ const routing = uniweb?.routingComponents || {}
44
+
45
+ return {
46
+ /**
47
+ * SSG-safe useLocation hook
48
+ * Returns current location or defaults during SSG
49
+ * @returns {Object} Location object { pathname, search, hash, state, key }
50
+ */
51
+ useLocation: () => {
52
+ if (!routing.useLocation) {
53
+ return DEFAULT_LOCATION
54
+ }
55
+ try {
56
+ return routing.useLocation()
57
+ } catch {
58
+ // Router context not available (SSG/SSR)
59
+ return DEFAULT_LOCATION
60
+ }
61
+ },
62
+
63
+ /**
64
+ * SSG-safe useParams hook
65
+ * Returns route params or empty object during SSG
66
+ * @returns {Object} Params object
67
+ */
68
+ useParams: () => {
69
+ if (!routing.useParams) {
70
+ return DEFAULT_PARAMS
71
+ }
72
+ try {
73
+ return routing.useParams()
74
+ } catch {
75
+ // Router context not available (SSG/SSR)
76
+ return DEFAULT_PARAMS
77
+ }
78
+ },
79
+
80
+ /**
81
+ * SSG-safe useNavigate hook
82
+ * Returns navigate function or no-op during SSG
83
+ * @returns {Function} Navigate function
84
+ */
85
+ useNavigate: () => {
86
+ if (!routing.useNavigate) {
87
+ return () => {} // No-op during SSG
88
+ }
89
+ try {
90
+ return routing.useNavigate()
91
+ } catch {
92
+ // Router context not available (SSG/SSR)
93
+ return () => {}
94
+ }
95
+ },
96
+
97
+ /**
98
+ * Router Link component (or fallback to 'a')
99
+ * Use Kit's Link component instead for most cases
100
+ */
101
+ Link: routing.Link || 'a',
102
+
103
+ /**
104
+ * Check if routing is available (browser with Router context)
105
+ * @returns {boolean}
106
+ */
107
+ isRoutingAvailable: () => {
108
+ if (!routing.useLocation) return false
109
+ try {
110
+ routing.useLocation()
111
+ return true
112
+ } catch {
113
+ return false
114
+ }
115
+ }
116
+ }
117
+ }
118
+
119
+ export default useRouting
@@ -0,0 +1,48 @@
1
+ /**
2
+ * useScrolled Hook
3
+ *
4
+ * Detects scroll position for sticky header effects.
5
+ * Common pattern in Header components across all templates.
6
+ *
7
+ * @example
8
+ * function Header() {
9
+ * const scrolled = useScrolled()
10
+ * return (
11
+ * <header className={scrolled ? 'bg-white shadow' : 'bg-transparent'}>
12
+ * ...
13
+ * </header>
14
+ * )
15
+ * }
16
+ *
17
+ * @example
18
+ * // With custom threshold
19
+ * const scrolled = useScrolled(50) // triggers after 50px scroll
20
+ */
21
+
22
+ import { useState, useEffect } from 'react'
23
+
24
+ /**
25
+ * Hook to detect if page has scrolled past a threshold.
26
+ *
27
+ * @param {number} threshold - Scroll position threshold in pixels (default: 0)
28
+ * @returns {boolean} Whether scroll position is past threshold
29
+ */
30
+ export function useScrolled(threshold = 0) {
31
+ const [scrolled, setScrolled] = useState(false)
32
+
33
+ useEffect(() => {
34
+ const handleScroll = () => {
35
+ setScrolled(window.scrollY > threshold)
36
+ }
37
+
38
+ // Check initial state
39
+ handleScroll()
40
+
41
+ window.addEventListener('scroll', handleScroll, { passive: true })
42
+ return () => window.removeEventListener('scroll', handleScroll)
43
+ }, [threshold])
44
+
45
+ return scrolled
46
+ }
47
+
48
+ export default useScrolled