@uniweb/kit 0.7.20 → 0.7.22

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.22",
4
4
  "description": "Standard component library for Uniweb foundations",
5
5
  "type": "module",
6
6
  "exports": {
@@ -38,7 +38,7 @@
38
38
  "fuse.js": "^7.0.0",
39
39
  "shiki": "^3.0.0",
40
40
  "tailwind-merge": "^2.6.0",
41
- "@uniweb/core": "0.5.18"
41
+ "@uniweb/core": "0.5.19"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "react": "^18.0.0 || ^19.0.0",
@@ -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,151 @@ 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
+ return <Code content={element.text || ''} language={element.attrs?.language || ''} />
79
+ }
80
+
81
+ case 'dataBlock':
82
+ return null
83
+
84
+ case 'list': {
85
+ const Tag = element.style === 'ordered' ? 'ol' : 'ul'
86
+ return (
87
+ <Tag>
88
+ {element.children?.map((itemSeq, i) => (
89
+ <li key={i}>
90
+ {itemSeq.map((el, j) => (
91
+ <SequenceElement key={j} element={el} block={block} />
92
+ ))}
93
+ </li>
94
+ ))}
95
+ </Tag>
96
+ )
97
+ }
98
+
99
+ case 'blockquote': {
100
+ return (
101
+ <blockquote>
102
+ {element.children?.map((el, i) => (
103
+ <SequenceElement key={i} element={el} block={block} />
104
+ ))}
105
+ </blockquote>
106
+ )
107
+ }
108
+
109
+ case 'link': {
110
+ const { href, label, role } = element.attrs || {}
111
+ // Standalone links promoted from paragraphs
112
+ return <p><Link to={href}>{label}</Link></p>
113
+ }
114
+
115
+ case 'button': {
116
+ return <p><Link to={element.attrs?.href}>{element.text}</Link></p>
117
+ }
118
+
119
+ case 'divider':
120
+ return <hr />
121
+
122
+ case 'inset': {
123
+ if (!block || !element.refId) return null
124
+ const insetBlock = block.getInset(element.refId)
125
+ if (!insetBlock) return null
126
+ const InsetRenderer = getChildBlockRenderer()
127
+ if (!InsetRenderer) return null
128
+ return <InsetRenderer blocks={[insetBlock]} />
129
+ }
130
+
131
+ case 'icon': {
132
+ return <Icon {...element.attrs} />
133
+ }
134
+
135
+ default:
136
+ return null
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Prose - Renders the narrative content from a parsed content sequence
142
+ *
143
+ * Skips data blocks (tagged code blocks, dataBlocks) — those are structured
144
+ * data accessible via content.data, not prose to render.
26
145
  *
27
- * Applies Tailwind Typography (prose) classes for readable text styling.
28
- * Use for article bodies, documentation, or any long-form content.
146
+ * Pass `content` (the parsed content object from component props).
147
+ * Optionally pass `block` for inset resolution.
29
148
  *
30
149
  * @param {Object} props
150
+ * @param {Object} [props.content] - Parsed content object (has .sequence)
151
+ * @param {Object} [props.block] - Block instance (needed for inset resolution)
31
152
  * @param {string} [props.size='lg'] - Text size: sm, base, lg, xl, 2xl
32
153
  * @param {string} [props.as='div'] - HTML element to render as
33
154
  * @param {string} [props.className] - Additional CSS classes
34
- * @param {React.ReactNode} props.children - Content to render
155
+ * @param {React.ReactNode} [props.children] - Alternative to content (pure wrapper mode)
35
156
  *
36
157
  * @example
37
- * <Prose>
38
- * <h2>Article Title</h2>
39
- * <p>Article content...</p>
40
- * </Prose>
158
+ * // Typical usage in a section component
159
+ * function Lesson({ content, block }) {
160
+ * return <Prose content={content} block={block} />
161
+ * }
162
+ *
163
+ * @example
164
+ * // Access data blocks separately
165
+ * <Prose content={content} block={block} />
166
+ * {content.data.quiz && <Quiz data={content.data.quiz} />}
41
167
  *
42
168
  * @example
43
- * <Prose size="base" className="dark:prose-invert">
44
- * <Render content={proseMirrorContent} />
169
+ * // Pure typography wrapper (backward compatible)
170
+ * <Prose size="base">
171
+ * <h2>Title</h2>
172
+ * <p>Content...</p>
45
173
  * </Prose>
46
174
  */
47
175
  export function Prose({
176
+ block,
177
+ content,
48
178
  size = 'lg',
49
179
  as: Component = 'div',
50
180
  className,
@@ -52,13 +182,19 @@ export function Prose({
52
182
  ...props
53
183
  }) {
54
184
  const sizeClass = SIZE_CLASSES[size] || SIZE_CLASSES.lg
185
+ const sequence = content?.sequence
55
186
 
56
187
  return (
57
188
  <Component
58
189
  className={cn('prose', sizeClass, 'max-w-none', className)}
59
190
  {...props}
60
191
  >
61
- {children}
192
+ {sequence
193
+ ? sequence.map((element, i) => (
194
+ <SequenceElement key={i} element={element} block={block} />
195
+ ))
196
+ : children
197
+ }
62
198
  </Component>
63
199
  )
64
200
  }