@uniweb/kit 0.7.20 → 0.7.21
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 +1 -1
- package/src/styled/Prose/index.jsx +153 -14
package/package.json
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Prose Component
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Renders the prose (narrative) portions of parsed content from content.sequence.
|
|
5
|
+
* Data blocks (tagged code blocks, dataBlocks) are skipped — they're structured
|
|
6
|
+
* data, not prose. Access them via content.data instead.
|
|
7
|
+
*
|
|
8
|
+
* Also works as a pure typography wrapper when given children instead of content.
|
|
6
9
|
*
|
|
7
10
|
* @module @uniweb/kit/styled/Prose
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
13
|
import React from 'react'
|
|
11
|
-
import { cn } from '../../utils/index.js'
|
|
14
|
+
import { cn, getChildBlockRenderer } from '../../utils/index.js'
|
|
15
|
+
import { SafeHtml } from '../../components/SafeHtml/index.js'
|
|
16
|
+
import { Image } from '../../components/Image/index.js'
|
|
17
|
+
import { Media } from '../../components/Media/index.js'
|
|
18
|
+
import { Icon } from '../../components/Icon/index.js'
|
|
19
|
+
import { Link } from '../../components/Link/index.js'
|
|
20
|
+
import { Code } from '../Section/renderers/Code.jsx'
|
|
12
21
|
|
|
13
22
|
/**
|
|
14
23
|
* Prose sizes
|
|
@@ -21,30 +30,154 @@ const SIZE_CLASSES = {
|
|
|
21
30
|
'2xl': 'prose-2xl'
|
|
22
31
|
}
|
|
23
32
|
|
|
33
|
+
function generateId(text) {
|
|
34
|
+
return text
|
|
35
|
+
.replace(/<[^>]*>/g, '')
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
38
|
+
.replace(/^-|-$/g, '')
|
|
39
|
+
}
|
|
40
|
+
|
|
24
41
|
/**
|
|
25
|
-
*
|
|
42
|
+
* Render a single sequence element to React
|
|
43
|
+
*/
|
|
44
|
+
function SequenceElement({ element, block }) {
|
|
45
|
+
if (!element) return null
|
|
46
|
+
|
|
47
|
+
switch (element.type) {
|
|
48
|
+
case 'heading': {
|
|
49
|
+
const level = Math.min(element.level || 1, 6)
|
|
50
|
+
const Tag = `h${level}`
|
|
51
|
+
const id = generateId(element.text || '')
|
|
52
|
+
return <Tag id={id}><SafeHtml value={element.text} as="span" /></Tag>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case 'paragraph': {
|
|
56
|
+
if (!element.text) return null
|
|
57
|
+
return <p><SafeHtml value={element.text} as="span" /></p>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
case 'image': {
|
|
61
|
+
const { url, alt, caption, role } = element.attrs || {}
|
|
62
|
+
if (role === 'icon') {
|
|
63
|
+
return <Icon {...element.attrs} />
|
|
64
|
+
}
|
|
65
|
+
return (
|
|
66
|
+
<figure>
|
|
67
|
+
<Image src={url} alt={alt || caption || ''} />
|
|
68
|
+
{caption && <figcaption>{caption}</figcaption>}
|
|
69
|
+
</figure>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
case 'video': {
|
|
74
|
+
return <Media src={element.attrs?.src} />
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case 'codeBlock': {
|
|
78
|
+
// Tagged code blocks are data, not prose — skip
|
|
79
|
+
if (element.attrs?.tag) return null
|
|
80
|
+
const code = typeof element.text === 'string' ? element.text : ''
|
|
81
|
+
return <Code content={code} language={element.attrs?.language || ''} />
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
case 'dataBlock':
|
|
85
|
+
return null
|
|
86
|
+
|
|
87
|
+
case 'list': {
|
|
88
|
+
const Tag = element.style === 'ordered' ? 'ol' : 'ul'
|
|
89
|
+
return (
|
|
90
|
+
<Tag>
|
|
91
|
+
{element.children?.map((itemSeq, i) => (
|
|
92
|
+
<li key={i}>
|
|
93
|
+
{itemSeq.map((el, j) => (
|
|
94
|
+
<SequenceElement key={j} element={el} block={block} />
|
|
95
|
+
))}
|
|
96
|
+
</li>
|
|
97
|
+
))}
|
|
98
|
+
</Tag>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case 'blockquote': {
|
|
103
|
+
return (
|
|
104
|
+
<blockquote>
|
|
105
|
+
{element.children?.map((el, i) => (
|
|
106
|
+
<SequenceElement key={i} element={el} block={block} />
|
|
107
|
+
))}
|
|
108
|
+
</blockquote>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case 'link': {
|
|
113
|
+
const { href, label, role } = element.attrs || {}
|
|
114
|
+
// Standalone links promoted from paragraphs
|
|
115
|
+
return <p><Link to={href}>{label}</Link></p>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case 'button': {
|
|
119
|
+
return <p><Link to={element.attrs?.href}>{element.text}</Link></p>
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
case 'divider':
|
|
123
|
+
return <hr />
|
|
124
|
+
|
|
125
|
+
case 'inset': {
|
|
126
|
+
if (!block || !element.refId) return null
|
|
127
|
+
const insetBlock = block.getInset(element.refId)
|
|
128
|
+
if (!insetBlock) return null
|
|
129
|
+
const InsetRenderer = getChildBlockRenderer()
|
|
130
|
+
if (!InsetRenderer) return null
|
|
131
|
+
return <InsetRenderer blocks={[insetBlock]} />
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case 'icon': {
|
|
135
|
+
return <Icon {...element.attrs} />
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
default:
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Prose - Renders the narrative content from a parsed content sequence
|
|
145
|
+
*
|
|
146
|
+
* Skips data blocks (tagged code blocks, dataBlocks) — those are structured
|
|
147
|
+
* data accessible via content.data, not prose to render.
|
|
26
148
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
149
|
+
* Pass `content` (the parsed content object from component props).
|
|
150
|
+
* Optionally pass `block` for inset resolution.
|
|
29
151
|
*
|
|
30
152
|
* @param {Object} props
|
|
153
|
+
* @param {Object} [props.content] - Parsed content object (has .sequence)
|
|
154
|
+
* @param {Object} [props.block] - Block instance (needed for inset resolution)
|
|
31
155
|
* @param {string} [props.size='lg'] - Text size: sm, base, lg, xl, 2xl
|
|
32
156
|
* @param {string} [props.as='div'] - HTML element to render as
|
|
33
157
|
* @param {string} [props.className] - Additional CSS classes
|
|
34
|
-
* @param {React.ReactNode} props.children -
|
|
158
|
+
* @param {React.ReactNode} [props.children] - Alternative to content (pure wrapper mode)
|
|
35
159
|
*
|
|
36
160
|
* @example
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* <
|
|
40
|
-
*
|
|
161
|
+
* // Typical usage in a section component
|
|
162
|
+
* function Lesson({ content, block }) {
|
|
163
|
+
* return <Prose content={content} block={block} />
|
|
164
|
+
* }
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* // Access data blocks separately
|
|
168
|
+
* <Prose content={content} block={block} />
|
|
169
|
+
* {content.data.quiz && <Quiz data={content.data.quiz} />}
|
|
41
170
|
*
|
|
42
171
|
* @example
|
|
43
|
-
*
|
|
44
|
-
*
|
|
172
|
+
* // Pure typography wrapper (backward compatible)
|
|
173
|
+
* <Prose size="base">
|
|
174
|
+
* <h2>Title</h2>
|
|
175
|
+
* <p>Content...</p>
|
|
45
176
|
* </Prose>
|
|
46
177
|
*/
|
|
47
178
|
export function Prose({
|
|
179
|
+
block,
|
|
180
|
+
content,
|
|
48
181
|
size = 'lg',
|
|
49
182
|
as: Component = 'div',
|
|
50
183
|
className,
|
|
@@ -52,13 +185,19 @@ export function Prose({
|
|
|
52
185
|
...props
|
|
53
186
|
}) {
|
|
54
187
|
const sizeClass = SIZE_CLASSES[size] || SIZE_CLASSES.lg
|
|
188
|
+
const sequence = content?.sequence
|
|
55
189
|
|
|
56
190
|
return (
|
|
57
191
|
<Component
|
|
58
192
|
className={cn('prose', sizeClass, 'max-w-none', className)}
|
|
59
193
|
{...props}
|
|
60
194
|
>
|
|
61
|
-
{
|
|
195
|
+
{sequence
|
|
196
|
+
? sequence.map((element, i) => (
|
|
197
|
+
<SequenceElement key={i} element={element} block={block} />
|
|
198
|
+
))
|
|
199
|
+
: children
|
|
200
|
+
}
|
|
62
201
|
</Component>
|
|
63
202
|
)
|
|
64
203
|
}
|