@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,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/kit",
3
- "version": "0.7.19",
3
+ "version": "0.7.21",
4
4
  "description": "Standard component library for Uniweb foundations",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,14 +1,23 @@
1
1
  /**
2
2
  * Prose Component
3
3
  *
4
- * Typography wrapper for long-form content.
5
- * Applies prose styling for readable text.
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
- * Prose - Typography wrapper for long-form content
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
- * Applies Tailwind Typography (prose) classes for readable text styling.
28
- * Use for article bodies, documentation, or any long-form content.
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 - Content to render
158
+ * @param {React.ReactNode} [props.children] - Alternative to content (pure wrapper mode)
35
159
  *
36
160
  * @example
37
- * <Prose>
38
- * <h2>Article Title</h2>
39
- * <p>Article content...</p>
40
- * </Prose>
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
- * <Prose size="base" className="dark:prose-invert">
44
- * <Render content={proseMirrorContent} />
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
- {children}
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]} as="div" />
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
- return <Renderer blocks={[inset]} as="div" extra={{ className }} />;
40
+ const rendered = <Renderer blocks={[inset]} />;
41
+ return className ? <div className={className}>{rendered}</div> : rendered;
41
42
  }
42
43
 
43
44
  if (video) {
@@ -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 blocks={block.insets} />
164
+ * <ChildBlocks from={block} wrapAs="div" />
161
165
  */
162
166
  export function ChildBlocks(props) {
163
167
  const Renderer = globalThis.uniweb.childBlockRenderer