@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,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render Component
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates rendering of content blocks within a Section.
|
|
5
|
+
* Dispatches to appropriate renderers based on content type.
|
|
6
|
+
*
|
|
7
|
+
* @module @uniweb/kit/Section/Render
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react'
|
|
11
|
+
import { cn } from '../../utils/index.js'
|
|
12
|
+
import { SafeHtml } from '../SafeHtml/index.js'
|
|
13
|
+
import { Image } from '../Image/index.js'
|
|
14
|
+
import { Media } from '../Media/index.js'
|
|
15
|
+
import { Link } from '../Link/index.js'
|
|
16
|
+
import { Code } from './renderers/Code.jsx'
|
|
17
|
+
import { Alert } from './renderers/Alert.jsx'
|
|
18
|
+
import { Table } from './renderers/Table.jsx'
|
|
19
|
+
import { Details } from './renderers/Details.jsx'
|
|
20
|
+
import { Divider } from './renderers/Divider.jsx'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extract text content from a node
|
|
24
|
+
*/
|
|
25
|
+
function extractText(node) {
|
|
26
|
+
if (!node) return ''
|
|
27
|
+
if (typeof node === 'string') return node
|
|
28
|
+
if (node.text) return node.text
|
|
29
|
+
if (node.content) {
|
|
30
|
+
return node.content.map(extractText).join('')
|
|
31
|
+
}
|
|
32
|
+
return ''
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate ID from heading text
|
|
37
|
+
*/
|
|
38
|
+
function generateId(text) {
|
|
39
|
+
return text
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
42
|
+
.replace(/^-|-$/g, '')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Render a list (ordered or unordered)
|
|
47
|
+
*/
|
|
48
|
+
function renderList(items, ordered = false) {
|
|
49
|
+
const Tag = ordered ? 'ol' : 'ul'
|
|
50
|
+
const listClass = ordered ? 'list-decimal' : 'list-disc'
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Tag className={cn('pl-6 space-y-1', listClass)}>
|
|
54
|
+
{items?.map((item, i) => (
|
|
55
|
+
<li key={i}>
|
|
56
|
+
{item.content?.map((child, j) => (
|
|
57
|
+
<RenderNode key={j} node={child} />
|
|
58
|
+
))}
|
|
59
|
+
</li>
|
|
60
|
+
))}
|
|
61
|
+
</Tag>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Render a single content node
|
|
67
|
+
*/
|
|
68
|
+
function RenderNode({ node, ...props }) {
|
|
69
|
+
if (!node) return null
|
|
70
|
+
|
|
71
|
+
const { type, attrs, content } = node
|
|
72
|
+
|
|
73
|
+
switch (type) {
|
|
74
|
+
case 'paragraph': {
|
|
75
|
+
const html = extractText(node)
|
|
76
|
+
if (!html) return null
|
|
77
|
+
return <p><SafeHtml value={html} as="span" /></p>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
case 'heading': {
|
|
81
|
+
const level = attrs?.level || 1
|
|
82
|
+
const text = extractText(node)
|
|
83
|
+
const id = generateId(text)
|
|
84
|
+
const Tag = `h${Math.min(level, 6)}`
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Tag id={id} className="scroll-mt-20">
|
|
88
|
+
{text}
|
|
89
|
+
</Tag>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case 'image': {
|
|
94
|
+
const src = attrs?.src || ''
|
|
95
|
+
const alt = attrs?.alt || ''
|
|
96
|
+
const caption = attrs?.caption || ''
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<figure className="my-4">
|
|
100
|
+
<Image src={src} alt={alt} className="rounded-lg" />
|
|
101
|
+
{caption && (
|
|
102
|
+
<figcaption className="mt-2 text-sm text-gray-500 text-center">
|
|
103
|
+
{caption}
|
|
104
|
+
</figcaption>
|
|
105
|
+
)}
|
|
106
|
+
</figure>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case 'video': {
|
|
111
|
+
const src = attrs?.src || ''
|
|
112
|
+
return <Media src={src} className="my-4 rounded-lg overflow-hidden" />
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case 'codeBlock': {
|
|
116
|
+
const language = attrs?.language || 'plaintext'
|
|
117
|
+
const code = extractText(node)
|
|
118
|
+
return <Code content={code} language={language} className="my-4" />
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case 'warning':
|
|
122
|
+
case 'alert': {
|
|
123
|
+
const alertType = attrs?.type || 'info'
|
|
124
|
+
const alertContent = content?.map(extractText).join('') || ''
|
|
125
|
+
return <Alert type={alertType} content={alertContent} className="my-4" />
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case 'blockquote': {
|
|
129
|
+
return (
|
|
130
|
+
<blockquote className="border-l-4 border-gray-300 pl-4 italic text-gray-600 my-4">
|
|
131
|
+
{content?.map((child, i) => (
|
|
132
|
+
<RenderNode key={i} node={child} />
|
|
133
|
+
))}
|
|
134
|
+
</blockquote>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case 'bulletList': {
|
|
139
|
+
return renderList(content, false)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
case 'orderedList': {
|
|
143
|
+
return renderList(content, true)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
case 'table': {
|
|
147
|
+
return <Table content={content} className="my-4" />
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
case 'details': {
|
|
151
|
+
const summary = attrs?.summary || 'Details'
|
|
152
|
+
const detailsContent = content?.map(extractText).join('') || ''
|
|
153
|
+
return (
|
|
154
|
+
<Details
|
|
155
|
+
summary={summary}
|
|
156
|
+
content={detailsContent}
|
|
157
|
+
open={attrs?.open}
|
|
158
|
+
className="my-4"
|
|
159
|
+
/>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
case 'horizontalRule':
|
|
164
|
+
case 'divider': {
|
|
165
|
+
return <Divider type={attrs?.type} className="my-6" />
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
case 'button': {
|
|
169
|
+
const href = attrs?.href || '#'
|
|
170
|
+
const label = extractText(node) || attrs?.label || 'Button'
|
|
171
|
+
return (
|
|
172
|
+
<Link
|
|
173
|
+
to={href}
|
|
174
|
+
className="inline-block px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors my-2"
|
|
175
|
+
>
|
|
176
|
+
{label}
|
|
177
|
+
</Link>
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
case 'text': {
|
|
182
|
+
// Handle inline marks (bold, italic, etc.)
|
|
183
|
+
let text = node.text || ''
|
|
184
|
+
|
|
185
|
+
if (node.marks) {
|
|
186
|
+
node.marks.forEach((mark) => {
|
|
187
|
+
switch (mark.type) {
|
|
188
|
+
case 'bold':
|
|
189
|
+
case 'strong':
|
|
190
|
+
text = `<strong>${text}</strong>`
|
|
191
|
+
break
|
|
192
|
+
case 'italic':
|
|
193
|
+
case 'em':
|
|
194
|
+
text = `<em>${text}</em>`
|
|
195
|
+
break
|
|
196
|
+
case 'code':
|
|
197
|
+
text = `<code class="px-1 py-0.5 bg-gray-100 rounded text-sm">${text}</code>`
|
|
198
|
+
break
|
|
199
|
+
case 'link':
|
|
200
|
+
text = `<a href="${mark.attrs?.href || '#'}" class="text-blue-600 hover:underline">${text}</a>`
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return <SafeHtml value={text} as="span" />
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
default:
|
|
210
|
+
// Try to render children if they exist
|
|
211
|
+
if (content && Array.isArray(content)) {
|
|
212
|
+
return (
|
|
213
|
+
<>
|
|
214
|
+
{content.map((child, i) => (
|
|
215
|
+
<RenderNode key={i} node={child} />
|
|
216
|
+
))}
|
|
217
|
+
</>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
return null
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Render - Content block renderer
|
|
226
|
+
*
|
|
227
|
+
* @param {Object} props
|
|
228
|
+
* @param {Array|Object} props.content - Content to render (array of nodes or single node)
|
|
229
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
230
|
+
*/
|
|
231
|
+
export function Render({ content, className, ...props }) {
|
|
232
|
+
if (!content) return null
|
|
233
|
+
|
|
234
|
+
const nodes = Array.isArray(content) ? content : [content]
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<div className={cn('space-y-4', className)} {...props}>
|
|
238
|
+
{nodes.map((node, i) => (
|
|
239
|
+
<RenderNode key={i} node={node} />
|
|
240
|
+
))}
|
|
241
|
+
</div>
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export default Render
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section Component
|
|
3
|
+
*
|
|
4
|
+
* Rich content section renderer for Uniweb pages.
|
|
5
|
+
* Handles content parsing, layout, and rendering.
|
|
6
|
+
*
|
|
7
|
+
* @module @uniweb/kit/Section
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react'
|
|
11
|
+
import { cn } from '../../utils/index.js'
|
|
12
|
+
import { useWebsite } from '../../hooks/useWebsite.js'
|
|
13
|
+
import { Render } from './Render.jsx'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Width presets
|
|
17
|
+
*/
|
|
18
|
+
const WIDTH_CLASSES = {
|
|
19
|
+
sm: 'max-w-2xl',
|
|
20
|
+
md: 'max-w-4xl',
|
|
21
|
+
lg: 'max-w-5xl',
|
|
22
|
+
xl: 'max-w-6xl',
|
|
23
|
+
'2xl': 'max-w-7xl',
|
|
24
|
+
full: 'max-w-none'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Column layouts
|
|
29
|
+
*/
|
|
30
|
+
const COLUMN_CLASSES = {
|
|
31
|
+
'1': 'grid-cols-1',
|
|
32
|
+
'2': 'grid-cols-1 md:grid-cols-2',
|
|
33
|
+
'3': 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
|
34
|
+
'4': 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Padding presets
|
|
39
|
+
*/
|
|
40
|
+
const PADDING_CLASSES = {
|
|
41
|
+
none: '',
|
|
42
|
+
sm: 'py-8',
|
|
43
|
+
md: 'py-12',
|
|
44
|
+
lg: 'py-16',
|
|
45
|
+
xl: 'py-24'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Section - Rich content section
|
|
50
|
+
*
|
|
51
|
+
* @param {Object} props
|
|
52
|
+
* @param {Object} [props.block] - Block object from content
|
|
53
|
+
* @param {Object|Array} [props.content] - Content to render
|
|
54
|
+
* @param {string} [props.width='lg'] - Content width: sm, md, lg, xl, 2xl, full
|
|
55
|
+
* @param {string} [props.columns='1'] - Column layout: 1, 2, 3, 4
|
|
56
|
+
* @param {string} [props.padding='lg'] - Vertical padding: none, sm, md, lg, xl
|
|
57
|
+
* @param {string} [props.as='section'] - HTML element to render as
|
|
58
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
59
|
+
* @param {React.ReactNode} [props.children] - Child elements
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* <Section content={blockContent} width="lg" padding="md" />
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* <Section width="xl" columns="2" className="bg-gray-50">
|
|
66
|
+
* <div>Column 1</div>
|
|
67
|
+
* <div>Column 2</div>
|
|
68
|
+
* </Section>
|
|
69
|
+
*/
|
|
70
|
+
export function Section({
|
|
71
|
+
block,
|
|
72
|
+
content,
|
|
73
|
+
width = 'lg',
|
|
74
|
+
columns = '1',
|
|
75
|
+
padding = 'lg',
|
|
76
|
+
as: Component = 'section',
|
|
77
|
+
className,
|
|
78
|
+
children,
|
|
79
|
+
...props
|
|
80
|
+
}) {
|
|
81
|
+
const { website } = useWebsite()
|
|
82
|
+
|
|
83
|
+
// Get properties from block if provided
|
|
84
|
+
const blockProps = block?.getBlockProperties?.() || {}
|
|
85
|
+
const resolvedWidth = blockProps.width || width
|
|
86
|
+
const resolvedColumns = blockProps.columns || columns
|
|
87
|
+
const resolvedPadding = blockProps.vertical_padding || padding
|
|
88
|
+
|
|
89
|
+
// Get content from block if not provided directly
|
|
90
|
+
let resolvedContent = content
|
|
91
|
+
|
|
92
|
+
if (!resolvedContent && block) {
|
|
93
|
+
// Get parsed content from block
|
|
94
|
+
resolvedContent = block.parsedContent || block.content
|
|
95
|
+
|
|
96
|
+
// If it's a ProseMirror doc, get the content array
|
|
97
|
+
if (resolvedContent?.type === 'doc') {
|
|
98
|
+
resolvedContent = resolvedContent.content
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Build classes
|
|
103
|
+
const widthClass = WIDTH_CLASSES[resolvedWidth] || WIDTH_CLASSES.lg
|
|
104
|
+
const columnClass = COLUMN_CLASSES[resolvedColumns] || COLUMN_CLASSES['1']
|
|
105
|
+
const paddingClass = PADDING_CLASSES[resolvedPadding] || PADDING_CLASSES.lg
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Component
|
|
109
|
+
className={cn(paddingClass, className)}
|
|
110
|
+
{...props}
|
|
111
|
+
>
|
|
112
|
+
<div className={cn('mx-auto px-4 sm:px-6 lg:px-8', widthClass)}>
|
|
113
|
+
{/* Content grid */}
|
|
114
|
+
{(resolvedContent || children) && (
|
|
115
|
+
<div className={cn(
|
|
116
|
+
columns !== '1' && 'grid gap-8',
|
|
117
|
+
columns !== '1' && columnClass
|
|
118
|
+
)}>
|
|
119
|
+
{children || (
|
|
120
|
+
<div className="prose prose-gray max-w-none">
|
|
121
|
+
<Render content={resolvedContent} />
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
</Component>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default Section
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alert/Warning Renderer
|
|
3
|
+
*
|
|
4
|
+
* Renders alert boxes for warnings, info, success, and error messages.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/kit/Section/renderers/Alert
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react'
|
|
10
|
+
import { cn } from '../../../utils/index.js'
|
|
11
|
+
import { SafeHtml } from '../../SafeHtml/index.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Alert type configurations
|
|
15
|
+
*/
|
|
16
|
+
const ALERT_STYLES = {
|
|
17
|
+
info: {
|
|
18
|
+
container: 'bg-blue-50 border-blue-200 text-blue-800',
|
|
19
|
+
icon: (
|
|
20
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
21
|
+
),
|
|
22
|
+
iconColor: 'text-blue-500'
|
|
23
|
+
},
|
|
24
|
+
success: {
|
|
25
|
+
container: 'bg-green-50 border-green-200 text-green-800',
|
|
26
|
+
icon: (
|
|
27
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
28
|
+
),
|
|
29
|
+
iconColor: 'text-green-500'
|
|
30
|
+
},
|
|
31
|
+
warning: {
|
|
32
|
+
container: 'bg-yellow-50 border-yellow-200 text-yellow-800',
|
|
33
|
+
icon: (
|
|
34
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
35
|
+
),
|
|
36
|
+
iconColor: 'text-yellow-500'
|
|
37
|
+
},
|
|
38
|
+
danger: {
|
|
39
|
+
container: 'bg-red-50 border-red-200 text-red-800',
|
|
40
|
+
icon: (
|
|
41
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
42
|
+
),
|
|
43
|
+
iconColor: 'text-red-500'
|
|
44
|
+
},
|
|
45
|
+
error: {
|
|
46
|
+
container: 'bg-red-50 border-red-200 text-red-800',
|
|
47
|
+
icon: (
|
|
48
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
49
|
+
),
|
|
50
|
+
iconColor: 'text-red-500'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Alert - Alert/warning box
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} props
|
|
58
|
+
* @param {string} [props.type='info'] - Alert type: info, success, warning, danger, error
|
|
59
|
+
* @param {string|React.ReactNode} props.content - Alert content
|
|
60
|
+
* @param {string} [props.title] - Optional title
|
|
61
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
62
|
+
*/
|
|
63
|
+
export function Alert({ type = 'info', content, title, className, ...props }) {
|
|
64
|
+
const styles = ALERT_STYLES[type] || ALERT_STYLES.info
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
className={cn(
|
|
69
|
+
'flex gap-3 rounded-lg border p-4',
|
|
70
|
+
styles.container,
|
|
71
|
+
className
|
|
72
|
+
)}
|
|
73
|
+
role="alert"
|
|
74
|
+
{...props}
|
|
75
|
+
>
|
|
76
|
+
{/* Icon */}
|
|
77
|
+
<div className={cn('flex-shrink-0', styles.iconColor)}>
|
|
78
|
+
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
79
|
+
{styles.icon}
|
|
80
|
+
</svg>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Content */}
|
|
84
|
+
<div className="flex-1">
|
|
85
|
+
{title && (
|
|
86
|
+
<h4 className="font-medium mb-1">{title}</h4>
|
|
87
|
+
)}
|
|
88
|
+
{typeof content === 'string' ? (
|
|
89
|
+
<SafeHtml value={content} className="text-sm" />
|
|
90
|
+
) : (
|
|
91
|
+
<div className="text-sm">{content}</div>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Alias for backwards compatibility
|
|
99
|
+
export const Warning = Alert
|
|
100
|
+
|
|
101
|
+
export default Alert
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Block Renderer
|
|
3
|
+
*
|
|
4
|
+
* Renders syntax-highlighted code blocks.
|
|
5
|
+
* Uses CSS classes for highlighting (bring your own Prism/Highlight.js CSS).
|
|
6
|
+
*
|
|
7
|
+
* @module @uniweb/kit/Section/renderers/Code
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { useEffect, useRef } from 'react'
|
|
11
|
+
import { cn } from '../../../utils/index.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Attempt to highlight code using Prism if available
|
|
15
|
+
*/
|
|
16
|
+
function highlightCode(code, language, element) {
|
|
17
|
+
if (typeof window !== 'undefined' && window.Prism && element) {
|
|
18
|
+
try {
|
|
19
|
+
const grammar = window.Prism.languages[language]
|
|
20
|
+
if (grammar) {
|
|
21
|
+
element.innerHTML = window.Prism.highlight(code, grammar, language)
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.warn('[Code] Prism highlighting failed:', e)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Code - Syntax highlighted code block
|
|
33
|
+
*
|
|
34
|
+
* @param {Object} props
|
|
35
|
+
* @param {string} props.content - Code content
|
|
36
|
+
* @param {string} [props.language='plaintext'] - Programming language
|
|
37
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
38
|
+
*/
|
|
39
|
+
export function Code({ content, language = 'plaintext', className, ...props }) {
|
|
40
|
+
const codeRef = useRef(null)
|
|
41
|
+
|
|
42
|
+
// Normalize language
|
|
43
|
+
const lang = language?.toLowerCase() || 'plaintext'
|
|
44
|
+
|
|
45
|
+
// Try to highlight on mount
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (codeRef.current && content) {
|
|
48
|
+
highlightCode(content, lang, codeRef.current)
|
|
49
|
+
}
|
|
50
|
+
}, [content, lang])
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<pre
|
|
54
|
+
className={cn(
|
|
55
|
+
'overflow-x-auto rounded-lg bg-gray-900 p-4 text-sm',
|
|
56
|
+
className
|
|
57
|
+
)}
|
|
58
|
+
{...props}
|
|
59
|
+
>
|
|
60
|
+
<code
|
|
61
|
+
ref={codeRef}
|
|
62
|
+
className={`language-${lang} text-gray-100`}
|
|
63
|
+
>
|
|
64
|
+
{content}
|
|
65
|
+
</code>
|
|
66
|
+
</pre>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default Code
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Details/Collapsible Renderer
|
|
3
|
+
*
|
|
4
|
+
* Renders collapsible content sections.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/kit/Section/renderers/Details
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useState } from 'react'
|
|
10
|
+
import { cn } from '../../../utils/index.js'
|
|
11
|
+
import { SafeHtml } from '../../SafeHtml/index.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Details - Collapsible section
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} props
|
|
17
|
+
* @param {string} props.summary - Summary/title text
|
|
18
|
+
* @param {string|React.ReactNode} props.content - Collapsible content
|
|
19
|
+
* @param {boolean} [props.open=false] - Initially open
|
|
20
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
21
|
+
*/
|
|
22
|
+
export function Details({
|
|
23
|
+
summary,
|
|
24
|
+
content,
|
|
25
|
+
open = false,
|
|
26
|
+
className,
|
|
27
|
+
...props
|
|
28
|
+
}) {
|
|
29
|
+
const [isOpen, setIsOpen] = useState(open)
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
className={cn(
|
|
34
|
+
'border border-gray-200 rounded-lg overflow-hidden',
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
{/* Summary/Toggle */}
|
|
40
|
+
<button
|
|
41
|
+
className={cn(
|
|
42
|
+
'w-full flex items-center justify-between px-4 py-3',
|
|
43
|
+
'text-left font-medium text-gray-900 bg-gray-50',
|
|
44
|
+
'hover:bg-gray-100 transition-colors'
|
|
45
|
+
)}
|
|
46
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
47
|
+
aria-expanded={isOpen}
|
|
48
|
+
>
|
|
49
|
+
<span>{summary}</span>
|
|
50
|
+
<svg
|
|
51
|
+
className={cn(
|
|
52
|
+
'w-5 h-5 text-gray-500 transition-transform',
|
|
53
|
+
isOpen && 'rotate-180'
|
|
54
|
+
)}
|
|
55
|
+
fill="none"
|
|
56
|
+
viewBox="0 0 24 24"
|
|
57
|
+
stroke="currentColor"
|
|
58
|
+
>
|
|
59
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
60
|
+
</svg>
|
|
61
|
+
</button>
|
|
62
|
+
|
|
63
|
+
{/* Content */}
|
|
64
|
+
{isOpen && (
|
|
65
|
+
<div className="px-4 py-3 border-t border-gray-200">
|
|
66
|
+
{typeof content === 'string' ? (
|
|
67
|
+
<SafeHtml value={content} className="prose prose-sm" />
|
|
68
|
+
) : (
|
|
69
|
+
content
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default Details
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Divider Renderer
|
|
3
|
+
*
|
|
4
|
+
* Renders horizontal dividers.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/kit/Section/renderers/Divider
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react'
|
|
10
|
+
import { cn } from '../../../utils/index.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Divider - Horizontal divider
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} props
|
|
16
|
+
* @param {string} [props.type='hr'] - Divider type: 'hr' or 'dots'
|
|
17
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
18
|
+
*/
|
|
19
|
+
export function Divider({ type = 'hr', className, ...props }) {
|
|
20
|
+
if (type === 'dots') {
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
className={cn('flex justify-center gap-2 py-4', className)}
|
|
24
|
+
role="separator"
|
|
25
|
+
{...props}
|
|
26
|
+
>
|
|
27
|
+
<span className="w-2 h-2 bg-gray-300 rounded-full" />
|
|
28
|
+
<span className="w-2 h-2 bg-gray-300 rounded-full" />
|
|
29
|
+
<span className="w-2 h-2 bg-gray-300 rounded-full" />
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<hr
|
|
36
|
+
className={cn('border-gray-200 my-6', className)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default Divider
|