@uniweb/kit 0.7.19 → 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
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
|
}
|
|
@@ -247,7 +247,7 @@ function RenderNode({ node, block, ...props }) {
|
|
|
247
247
|
if (!insetBlock) return null
|
|
248
248
|
|
|
249
249
|
const InsetRenderer = getChildBlockRenderer()
|
|
250
|
-
return <InsetRenderer blocks={[insetBlock]}
|
|
250
|
+
return <InsetRenderer blocks={[insetBlock]} />
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
case 'button': {
|
|
@@ -37,7 +37,8 @@ import { getChildBlockRenderer } from "../../utils/index.js";
|
|
|
37
37
|
export function Visual({ inset, video, image, className, fallback = null }) {
|
|
38
38
|
if (inset) {
|
|
39
39
|
const Renderer = getChildBlockRenderer();
|
|
40
|
-
|
|
40
|
+
const rendered = <Renderer blocks={[inset]} />;
|
|
41
|
+
return className ? <div className={className}>{rendered}</div> : rendered;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
if (video) {
|
package/src/utils/index.js
CHANGED
|
@@ -149,15 +149,19 @@ export function getChildBlockRenderer() {
|
|
|
149
149
|
/**
|
|
150
150
|
* Renders child blocks or insets. Wrapper that defers runtime lookup to render time.
|
|
151
151
|
*
|
|
152
|
+
* By default renders each child as a bare component (no wrapper, no section chrome).
|
|
153
|
+
* Pass `wrapAs` to opt into full section treatment.
|
|
154
|
+
*
|
|
152
155
|
* @param {Object} props
|
|
153
156
|
* @param {Object} props.from - Parent block (renders block.childBlocks)
|
|
154
157
|
* @param {Array} props.blocks - Explicit array of blocks to render
|
|
158
|
+
* @param {string} [props.wrapAs] - Wrapper element tag for section treatment ('div', 'article', etc.)
|
|
155
159
|
*
|
|
156
160
|
* @example
|
|
157
161
|
* import { ChildBlocks } from '@uniweb/kit'
|
|
158
162
|
*
|
|
159
163
|
* <ChildBlocks from={block} />
|
|
160
|
-
* <ChildBlocks
|
|
164
|
+
* <ChildBlocks from={block} wrapAs="div" />
|
|
161
165
|
*/
|
|
162
166
|
export function ChildBlocks(props) {
|
|
163
167
|
const Renderer = globalThis.uniweb.childBlockRenderer
|