@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/kit",
3
- "version": "0.7.20",
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
  }