@uniweb/kit 0.9.45 → 0.10.0
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 +3 -2
- package/src/hooks/useEntityDetail.js +19 -6
- package/src/hooks/useThemeData.js +6 -1
- package/src/index.js +2 -1
- package/src/styled/Article/index.jsx +28 -58
- package/src/styled/Media/Media.jsx +26 -2
- package/src/styled/Prose/index.jsx +31 -235
- package/src/styled/Render/index.jsx +377 -0
- package/src/styled/Section/Section.jsx +12 -6
- package/src/styled/Section/index.js +0 -1
- package/src/styled/index.js +2 -1
- package/src/styled/Section/Render.jsx +0 -367
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Standard component library for Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"fuse.js": "^7.0.0",
|
|
41
41
|
"shiki": "^3.0.0",
|
|
42
42
|
"tailwind-merge": "^3.6.0",
|
|
43
|
-
"@uniweb/
|
|
43
|
+
"@uniweb/semantic-parser": "1.2.0",
|
|
44
|
+
"@uniweb/core": "0.8.0",
|
|
44
45
|
"@uniweb/scene": "0.1.2"
|
|
45
46
|
},
|
|
46
47
|
"peerDependencies": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* }
|
|
45
45
|
*/
|
|
46
46
|
|
|
47
|
-
import { getUniweb } from '@uniweb/core'
|
|
47
|
+
import { getUniweb, recordDataUrl } from '@uniweb/core'
|
|
48
48
|
import { useFetched } from './useFetched.js'
|
|
49
49
|
|
|
50
50
|
/**
|
|
@@ -63,7 +63,19 @@ export function useEntityDetail(record, options = {}) {
|
|
|
63
63
|
return useFetched(request)
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Build the fetch request for one record's full payload.
|
|
68
|
+
*
|
|
69
|
+
* Exported for tests only — not re-exported from the package index. The
|
|
70
|
+
* static-file branch has to resolve to the same URL the build writes and the
|
|
71
|
+
* same URL core's dynamic-route auto-detail injects; a test can only assert
|
|
72
|
+
* that if it can call this without a React tree.
|
|
73
|
+
*
|
|
74
|
+
* @param {Object|null} record
|
|
75
|
+
* @param {string} collection
|
|
76
|
+
* @returns {{path?: string, url?: string, schema: string}|null}
|
|
77
|
+
*/
|
|
78
|
+
export function buildDetailRequest(record, collection) {
|
|
67
79
|
const slug = record?.slug
|
|
68
80
|
if (!slug || !collection) return null
|
|
69
81
|
|
|
@@ -83,10 +95,11 @@ function buildDetailRequest(record, collection) {
|
|
|
83
95
|
return { url, schema: collection }
|
|
84
96
|
}
|
|
85
97
|
|
|
86
|
-
// Static-file default — the build emitted per-record JSON files.
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
98
|
+
// Static-file default — the build emitted per-record JSON files. Shares
|
|
99
|
+
// `recordDataUrl` with the build that writes them and with core's
|
|
100
|
+
// dynamic-route auto-detail injection, which is the other half of this
|
|
101
|
+
// same feature: the three must resolve one record to one URL.
|
|
102
|
+
return { path: recordDataUrl(collection, slug), schema: collection }
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
export default useEntityDetail
|
|
@@ -55,9 +55,14 @@ function readAppliedScheme() {
|
|
|
55
55
|
* Returns the Theme instance which provides methods like:
|
|
56
56
|
* - getColor(name, shade) - Get a color value
|
|
57
57
|
* - getPalette(name) - Get all shades for a color
|
|
58
|
-
* - getContextToken(context, token) - Get semantic token
|
|
59
58
|
* - getAppearance() - Get appearance configuration
|
|
60
59
|
*
|
|
60
|
+
* For a semantic token's ACTUAL value at some element, read the CSS variable
|
|
61
|
+
* rather than asking the theme: getComputedStyle(el).getPropertyValue('--heading').
|
|
62
|
+
* Only that accounts for the section's own `theme:` overrides and the active
|
|
63
|
+
* light/dark scheme. (`getContextToken`/`getContextTokens` were removed in
|
|
64
|
+
* 2026-07-28 for exactly this reason — see the note in @uniweb/core's theme.js.)
|
|
65
|
+
*
|
|
61
66
|
* @returns {Theme|null} Theme instance or null if not available
|
|
62
67
|
*
|
|
63
68
|
* @example
|
package/src/index.js
CHANGED
|
@@ -131,7 +131,8 @@ export {
|
|
|
131
131
|
// Styled Components (Tailwind-based)
|
|
132
132
|
// ============================================================================
|
|
133
133
|
|
|
134
|
-
export { Section
|
|
134
|
+
export { Section } from './styled/Section/index.js'
|
|
135
|
+
export { Render, SequenceElement } from './styled/Render/index.jsx'
|
|
135
136
|
export { Prose } from './styled/Prose/index.jsx'
|
|
136
137
|
export { Article } from './styled/Article/index.jsx'
|
|
137
138
|
export { Code, Alert, Warning, Table, Details, Divider } from './styled/Section/renderers/index.js'
|
|
@@ -1,84 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Article
|
|
2
|
+
* Article — a document, in a semantic <article>.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* `<Prose>` with the right tag. That is the whole component now, and the point
|
|
5
|
+
* of it is the element: an article is a self-contained composition, and saying
|
|
6
|
+
* so in markup is what assistive technology and reader modes act on.
|
|
7
|
+
*
|
|
8
|
+
* It used to be its own thing — a prose container wrapped around a SECOND node
|
|
9
|
+
* walker that read raw ProseMirror. That is why articles rendered with no bold,
|
|
10
|
+
* no links and no math until 2026-07-30: the walker's `paragraph` case
|
|
11
|
+
* flattened inline content to plain text, and it had no case for math at all.
|
|
12
|
+
* Going through `<Render>` means there is one walk, and inline content arrives
|
|
13
|
+
* from the parser already correct.
|
|
6
14
|
*
|
|
7
15
|
* @module @uniweb/kit/styled/Article
|
|
8
16
|
*/
|
|
9
17
|
|
|
10
18
|
import React from 'react'
|
|
11
|
-
import {
|
|
12
|
-
import { Render } from '../Section/Render.jsx'
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Prose sizes
|
|
16
|
-
*/
|
|
17
|
-
const SIZE_CLASSES = {
|
|
18
|
-
sm: 'prose-sm',
|
|
19
|
-
base: 'prose-base',
|
|
20
|
-
lg: 'prose-lg',
|
|
21
|
-
xl: 'prose-xl',
|
|
22
|
-
'2xl': 'prose-2xl'
|
|
23
|
-
}
|
|
19
|
+
import { Prose } from '../Prose/index.jsx'
|
|
24
20
|
|
|
25
21
|
/**
|
|
26
22
|
* Article - Semantic article with prose typography
|
|
27
23
|
*
|
|
28
|
-
* Renders content inside a semantic <article> tag with prose styling.
|
|
29
|
-
* Can accept either children or a content prop (ProseMirror JSON).
|
|
30
|
-
*
|
|
31
24
|
* @param {Object} props
|
|
32
|
-
* @param {Object|Array} [props.content] -
|
|
25
|
+
* @param {Object|Array} [props.content] - Parsed content (`.sequence`), a
|
|
26
|
+
* ProseMirror doc, or an array of ProseMirror nodes
|
|
27
|
+
* @param {Object} [props.block] - Block instance (supplies content; resolves insets)
|
|
28
|
+
* @param {Object} [props.components] - Per-element-type renderer overrides
|
|
33
29
|
* @param {string} [props.size='lg'] - Text size: sm, base, lg, xl, 2xl
|
|
34
30
|
* @param {string} [props.className] - Additional CSS classes
|
|
35
|
-
* @param {React.ReactNode} [props.children] - Content to render (alternative to content
|
|
31
|
+
* @param {React.ReactNode} [props.children] - Content to render (alternative to content)
|
|
36
32
|
*
|
|
37
33
|
* @example
|
|
38
|
-
* // With ProseMirror content
|
|
39
34
|
* <Article content={articleData.content} />
|
|
40
35
|
*
|
|
41
36
|
* @example
|
|
42
|
-
*
|
|
43
|
-
* <Article>
|
|
44
|
-
* <h1>My Article</h1>
|
|
45
|
-
* <p>Article content...</p>
|
|
46
|
-
* </Article>
|
|
47
|
-
*
|
|
48
|
-
* @example
|
|
49
|
-
* // Composing with Render
|
|
50
|
-
* <Article size="base" className="dark:prose-invert">
|
|
51
|
-
* <Render content={proseMirrorContent} />
|
|
52
|
-
* </Article>
|
|
37
|
+
* <Article block={block} components={{ blockquote: PullQuote }} />
|
|
53
38
|
*/
|
|
54
|
-
export function Article({
|
|
55
|
-
content,
|
|
56
|
-
block,
|
|
57
|
-
size = 'lg',
|
|
58
|
-
className,
|
|
59
|
-
children,
|
|
60
|
-
...props
|
|
61
|
-
}) {
|
|
62
|
-
const sizeClass = SIZE_CLASSES[size] || SIZE_CLASSES.lg
|
|
63
|
-
|
|
64
|
-
// Get content from block if not provided directly
|
|
65
|
-
let resolvedContent = content
|
|
66
|
-
if (!resolvedContent && block) {
|
|
67
|
-
resolvedContent = block.rawContent
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// If it's a ProseMirror doc, get the content array
|
|
71
|
-
if (resolvedContent?.type === 'doc') {
|
|
72
|
-
resolvedContent = resolvedContent.content
|
|
73
|
-
}
|
|
74
|
-
|
|
39
|
+
export function Article({ content, block, components, size = 'lg', className, children, ...props }) {
|
|
75
40
|
return (
|
|
76
|
-
<
|
|
77
|
-
|
|
41
|
+
<Prose
|
|
42
|
+
as="article"
|
|
43
|
+
content={content}
|
|
44
|
+
block={block}
|
|
45
|
+
components={components}
|
|
46
|
+
size={size}
|
|
47
|
+
className={className}
|
|
78
48
|
{...props}
|
|
79
49
|
>
|
|
80
|
-
{children
|
|
81
|
-
</
|
|
50
|
+
{children}
|
|
51
|
+
</Prose>
|
|
82
52
|
)
|
|
83
53
|
}
|
|
84
54
|
|
|
@@ -202,11 +202,34 @@ function PlayButton({ onClick, className }) {
|
|
|
202
202
|
* @example
|
|
203
203
|
* // Local video
|
|
204
204
|
* <Media src="/videos/intro.mp4" controls autoplay={false} />
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* // A video authored in markdown, delivered via content.videos[]
|
|
208
|
+
* // {role=video poster=./thumb.jpg autoplay muted loop}
|
|
209
|
+
* <Visual video={content.videos[0]} />
|
|
205
210
|
*/
|
|
206
211
|
export function Media({
|
|
207
212
|
src,
|
|
208
213
|
media,
|
|
209
214
|
thumbnail,
|
|
215
|
+
// Same concept, three producers. `thumbnail` is this component's own prop;
|
|
216
|
+
// `poster` is the authored markdown spelling (`{role=video poster=…}`, and
|
|
217
|
+
// what @uniweb/build auto-generates when it is absent); `coverImg` is what
|
|
218
|
+
// the editor's Video node carries. All three named here because a video
|
|
219
|
+
// entry from `content.videos[]` is spread straight in — `<Media {...video} />`
|
|
220
|
+
// via <Visual> — so an unrecognised one would land on the wrapper div, where
|
|
221
|
+
// it does nothing and React warns.
|
|
222
|
+
poster,
|
|
223
|
+
coverImg,
|
|
224
|
+
// Also part of the delivered video shape. Absorbed rather than forwarded:
|
|
225
|
+
// the reference documents wrapping media in a link as the FOUNDATION's job
|
|
226
|
+
// (docs/reference/content-structure.md, "Clickable Images and Videos"), so
|
|
227
|
+
// these are the author's to use, not this component's to consume.
|
|
228
|
+
alt,
|
|
229
|
+
role,
|
|
230
|
+
href,
|
|
231
|
+
target,
|
|
232
|
+
caption: captionProp,
|
|
210
233
|
autoplay = false,
|
|
211
234
|
muted = false,
|
|
212
235
|
loop = false,
|
|
@@ -222,13 +245,14 @@ export function Media({
|
|
|
222
245
|
|
|
223
246
|
// Normalize source
|
|
224
247
|
const videoSrc = typeof src === 'string' ? src : (src?.src || media?.src || '')
|
|
225
|
-
const caption = media?.caption || src?.caption || ''
|
|
248
|
+
const caption = captionProp || media?.caption || src?.caption || ''
|
|
226
249
|
|
|
227
250
|
// Detect video type
|
|
228
251
|
const mediaType = detectMediaType(videoSrc)
|
|
229
252
|
|
|
230
253
|
// Get thumbnail
|
|
231
|
-
const thumbnailSrc =
|
|
254
|
+
const thumbnailSrc =
|
|
255
|
+
thumbnail || poster || coverImg || getVideoThumbnail(videoSrc, mediaType)
|
|
232
256
|
|
|
233
257
|
// Handle play click (for facade mode)
|
|
234
258
|
const handlePlay = useCallback(() => {
|
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Prose
|
|
2
|
+
* Prose — content plus prose typography.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* `<Render>` walks the sequence; this adds the Tailwind Typography container
|
|
5
|
+
* that styles the semantic markup it emits. Two concerns, deliberately separate:
|
|
6
|
+
* a foundation with its own type system uses `<Render>` and lays out the
|
|
7
|
+
* elements itself.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
+
* Typography comes from a plugin the FOUNDATION installs — kit ships no
|
|
10
|
+
* stylesheet — and answers to the site's `theme.yml` through
|
|
11
|
+
* `@uniweb/kit/prose-tokens.css`. See that file for the two rules that bite:
|
|
12
|
+
* one prose container per subtree (the variables are inherited, so a nested one
|
|
13
|
+
* silently resets its subtree), and no palette modifier (`prose-gray` and
|
|
14
|
+
* friends re-declare every variable and defeat the theme bridge).
|
|
15
|
+
*
|
|
16
|
+
* Also works as a pure typography wrapper when given children instead of
|
|
17
|
+
* content.
|
|
9
18
|
*
|
|
10
19
|
* @module @uniweb/kit/styled/Prose
|
|
11
20
|
*/
|
|
12
21
|
|
|
13
22
|
import React from 'react'
|
|
14
|
-
import { cn
|
|
15
|
-
import {
|
|
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'
|
|
23
|
+
import { cn } from '../../utils/index.js'
|
|
24
|
+
import { Render } from '../Render/index.jsx'
|
|
21
25
|
|
|
22
|
-
/**
|
|
23
|
-
* Prose sizes
|
|
24
|
-
*/
|
|
26
|
+
/** Prose sizes */
|
|
25
27
|
const SIZE_CLASSES = {
|
|
26
28
|
sm: 'prose-sm',
|
|
27
29
|
base: 'prose-base',
|
|
@@ -30,215 +32,14 @@ const SIZE_CLASSES = {
|
|
|
30
32
|
'2xl': 'prose-2xl'
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
const INLINE_INSET_RE = /<uniweb-inset data-ref-id="([^"]+)"><\/uniweb-inset>/g
|
|
35
|
-
|
|
36
35
|
/**
|
|
37
|
-
*
|
|
38
|
-
* (`<uniweb-inset data-ref-id="…">`). The fragment is split at marker
|
|
39
|
-
* boundaries; HTML chunks render via SafeHtml, marker positions render
|
|
40
|
-
* via the framework's child-block renderer (the same path block-level
|
|
41
|
-
* insets use, scoped to a single inset block).
|
|
42
|
-
*
|
|
43
|
-
* Components rendered through this path return an inline element
|
|
44
|
-
* (typically a `<span>`) so the paragraph stays a single line of prose.
|
|
45
|
-
*/
|
|
46
|
-
function renderParagraphWithInsets(html, block) {
|
|
47
|
-
if (!block) return <SafeHtml value={html} as="span" />
|
|
48
|
-
const InsetRenderer = getChildBlockRenderer()
|
|
49
|
-
const parts = []
|
|
50
|
-
let lastIdx = 0
|
|
51
|
-
INLINE_INSET_RE.lastIndex = 0
|
|
52
|
-
let match
|
|
53
|
-
while ((match = INLINE_INSET_RE.exec(html)) !== null) {
|
|
54
|
-
if (match.index > lastIdx) {
|
|
55
|
-
parts.push(
|
|
56
|
-
<SafeHtml
|
|
57
|
-
key={`t${lastIdx}`}
|
|
58
|
-
value={html.slice(lastIdx, match.index)}
|
|
59
|
-
as="span"
|
|
60
|
-
/>
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
const refId = match[1]
|
|
64
|
-
const insetBlock = block.getInset?.(refId)
|
|
65
|
-
if (insetBlock && InsetRenderer) {
|
|
66
|
-
parts.push(<InsetRenderer key={`i${refId}`} blocks={[insetBlock]} />)
|
|
67
|
-
}
|
|
68
|
-
lastIdx = match.index + match[0].length
|
|
69
|
-
}
|
|
70
|
-
if (lastIdx < html.length) {
|
|
71
|
-
parts.push(
|
|
72
|
-
<SafeHtml key={`t${lastIdx}`} value={html.slice(lastIdx)} as="span" />
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
return parts
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Render a single sequence element to React
|
|
80
|
-
*/
|
|
81
|
-
function SequenceElement({ element, block }) {
|
|
82
|
-
if (!element) return null
|
|
83
|
-
|
|
84
|
-
switch (element.type) {
|
|
85
|
-
case 'heading': {
|
|
86
|
-
const level = Math.min(element.level || 1, 6)
|
|
87
|
-
const Tag = `h${level}`
|
|
88
|
-
const id = headingId(element.text || '')
|
|
89
|
-
return <Tag id={id}><SafeHtml value={element.text} as="span" /></Tag>
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
case 'paragraph': {
|
|
93
|
-
if (!element.text) return null
|
|
94
|
-
// Inline insets ride through the paragraph text as
|
|
95
|
-
// `<uniweb-inset data-ref-id="…"></uniweb-inset>` markers (see
|
|
96
|
-
// semantic-parser's getTextContent). When any are present, walk
|
|
97
|
-
// the text once and intersperse React-rendered insets at the
|
|
98
|
-
// marker positions; otherwise stick to the fast SafeHtml path.
|
|
99
|
-
if (/<uniweb-inset/.test(element.text)) {
|
|
100
|
-
return <p>{renderParagraphWithInsets(element.text, block)}</p>
|
|
101
|
-
}
|
|
102
|
-
return <p><SafeHtml value={element.text} as="span" /></p>
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
case 'image': {
|
|
106
|
-
const { url, alt, caption, role } = element.attrs || {}
|
|
107
|
-
if (role === 'icon') {
|
|
108
|
-
return <Icon {...element.attrs} />
|
|
109
|
-
}
|
|
110
|
-
return (
|
|
111
|
-
<figure>
|
|
112
|
-
<Image src={url} alt={alt || caption || ''} />
|
|
113
|
-
{caption && <figcaption>{caption}</figcaption>}
|
|
114
|
-
</figure>
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
case 'video': {
|
|
119
|
-
return <Media src={element.attrs?.src} />
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
case 'codeBlock': {
|
|
123
|
-
return <Code content={element.text || ''} language={element.attrs?.language || ''} />
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
case 'dataBlock':
|
|
127
|
-
return null
|
|
128
|
-
|
|
129
|
-
case 'math': {
|
|
130
|
-
// Math from $$...$$ on its own line, ```math fence, or inline
|
|
131
|
-
// $...$ (the inline form doesn't reach the sequence walker — it
|
|
132
|
-
// rides inside paragraph HTML — but render the same way for
|
|
133
|
-
// safety in case a future caller surfaces it here). Mathml is
|
|
134
|
-
// pre-compiled at parse time; the browser renders real MathML
|
|
135
|
-
// natively. Foundations that want to style errors can target
|
|
136
|
-
// .temml-error inside this wrapper.
|
|
137
|
-
if (!element.mathml) return null
|
|
138
|
-
const display = element.display !== false
|
|
139
|
-
const Tag = display ? 'div' : 'span'
|
|
140
|
-
return (
|
|
141
|
-
<Tag
|
|
142
|
-
className={display ? 'math-display' : 'math-inline'}
|
|
143
|
-
dangerouslySetInnerHTML={{ __html: element.mathml }}
|
|
144
|
-
/>
|
|
145
|
-
)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
case 'list': {
|
|
149
|
-
const Tag = element.style === 'ordered' ? 'ol' : 'ul'
|
|
150
|
-
return (
|
|
151
|
-
<Tag>
|
|
152
|
-
{element.children?.map((itemSeq, i) => (
|
|
153
|
-
<li key={i}>
|
|
154
|
-
{itemSeq.map((el, j) => (
|
|
155
|
-
<SequenceElement key={j} element={el} block={block} />
|
|
156
|
-
))}
|
|
157
|
-
</li>
|
|
158
|
-
))}
|
|
159
|
-
</Tag>
|
|
160
|
-
)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
case 'blockquote': {
|
|
164
|
-
return (
|
|
165
|
-
<blockquote>
|
|
166
|
-
{element.children?.map((el, i) => (
|
|
167
|
-
<SequenceElement key={i} element={el} block={block} />
|
|
168
|
-
))}
|
|
169
|
-
</blockquote>
|
|
170
|
-
)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
case 'inset_block': {
|
|
174
|
-
// A component reference that carries block content — the block form of
|
|
175
|
-
// an inset. Children recurse; flattening them to text is what the
|
|
176
|
-
// sequence's default branch used to do, and it lost the body.
|
|
177
|
-
//
|
|
178
|
-
// Reaching this case means the container was NOT lifted — `@uniweb/core`
|
|
179
|
-
// rewrites `inset_block` to `inset_placeholder` when it builds the render
|
|
180
|
-
// graph, and the `inset` case resolves that against the foundation. This
|
|
181
|
-
// is the fallback for a document rendered without a Block behind it.
|
|
182
|
-
//
|
|
183
|
-
// kit does NOT map the name to one of its own components — see the longer
|
|
184
|
-
// note in Section/Render. A visible generic box; never a drop.
|
|
185
|
-
const body = element.children?.map((el, i) => (
|
|
186
|
-
<SequenceElement key={i} element={el} block={block} />
|
|
187
|
-
))
|
|
188
|
-
|
|
189
|
-
return (
|
|
190
|
-
<div
|
|
191
|
-
data-inset-block={element.component || 'unknown'}
|
|
192
|
-
className="border border-border rounded-md p-4 my-4"
|
|
193
|
-
>
|
|
194
|
-
{body}
|
|
195
|
-
</div>
|
|
196
|
-
)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
case 'link': {
|
|
200
|
-
const { href, label, role } = element.attrs || {}
|
|
201
|
-
// Standalone links promoted from paragraphs
|
|
202
|
-
return <p><Link to={href}>{label}</Link></p>
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
case 'button': {
|
|
206
|
-
return <p><Link to={element.attrs?.href}>{element.text}</Link></p>
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
case 'divider':
|
|
210
|
-
return <hr />
|
|
211
|
-
|
|
212
|
-
case 'inset': {
|
|
213
|
-
if (!block || !element.refId) return null
|
|
214
|
-
const insetBlock = block.getInset(element.refId)
|
|
215
|
-
if (!insetBlock) return null
|
|
216
|
-
const InsetRenderer = getChildBlockRenderer()
|
|
217
|
-
if (!InsetRenderer) return null
|
|
218
|
-
return <InsetRenderer blocks={[insetBlock]} />
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
case 'icon': {
|
|
222
|
-
return <Icon {...element.attrs} />
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
default:
|
|
226
|
-
return null
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Prose - Renders the narrative content from a parsed content sequence
|
|
232
|
-
*
|
|
233
|
-
* Skips data blocks (tagged code blocks, dataBlocks) — those are structured
|
|
234
|
-
* data accessible via content.data, not prose to render.
|
|
235
|
-
*
|
|
236
|
-
* Pass `content` (the parsed content object from component props).
|
|
237
|
-
* Optionally pass `block` for inset resolution.
|
|
36
|
+
* Prose - Rendered content with prose typography
|
|
238
37
|
*
|
|
239
38
|
* @param {Object} props
|
|
240
|
-
* @param {Object} [props.content] - Parsed content
|
|
241
|
-
*
|
|
39
|
+
* @param {Object|Array} [props.content] - Parsed content (`.sequence`), a
|
|
40
|
+
* ProseMirror doc, or an array of ProseMirror nodes
|
|
41
|
+
* @param {Object} [props.block] - Block instance (supplies content; resolves insets)
|
|
42
|
+
* @param {Object} [props.components] - Per-element-type renderer overrides
|
|
242
43
|
* @param {string} [props.size='lg'] - Text size: sm, base, lg, xl, 2xl
|
|
243
44
|
* @param {string} [props.as='div'] - HTML element to render as
|
|
244
45
|
* @param {string} [props.className] - Additional CSS classes
|
|
@@ -251,20 +52,24 @@ function SequenceElement({ element, block }) {
|
|
|
251
52
|
* }
|
|
252
53
|
*
|
|
253
54
|
* @example
|
|
254
|
-
* //
|
|
55
|
+
* // Render one element type your own way — the foundation's design, kit's walk
|
|
56
|
+
* <Prose content={content} block={block} components={{ concept_block: Faq }} />
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* // Access data blocks separately — they are deliberately not rendered
|
|
255
60
|
* <Prose content={content} block={block} />
|
|
256
61
|
* {content.data.quiz && <Quiz data={content.data.quiz} />}
|
|
257
62
|
*
|
|
258
63
|
* @example
|
|
259
|
-
* // Pure typography wrapper
|
|
64
|
+
* // Pure typography wrapper
|
|
260
65
|
* <Prose size="base">
|
|
261
66
|
* <h2>Title</h2>
|
|
262
|
-
* <p>Content...</p>
|
|
263
67
|
* </Prose>
|
|
264
68
|
*/
|
|
265
69
|
export function Prose({
|
|
266
70
|
block,
|
|
267
71
|
content,
|
|
72
|
+
components,
|
|
268
73
|
size = 'lg',
|
|
269
74
|
as: Component = 'div',
|
|
270
75
|
className,
|
|
@@ -272,19 +77,10 @@ export function Prose({
|
|
|
272
77
|
...props
|
|
273
78
|
}) {
|
|
274
79
|
const sizeClass = SIZE_CLASSES[size] || SIZE_CLASSES.lg
|
|
275
|
-
const sequence = content?.sequence
|
|
276
80
|
|
|
277
81
|
return (
|
|
278
|
-
<Component
|
|
279
|
-
|
|
280
|
-
{...props}
|
|
281
|
-
>
|
|
282
|
-
{sequence
|
|
283
|
-
? sequence.map((element, i) => (
|
|
284
|
-
<SequenceElement key={i} element={element} block={block} />
|
|
285
|
-
))
|
|
286
|
-
: children
|
|
287
|
-
}
|
|
82
|
+
<Component className={cn('prose', sizeClass, 'max-w-none', className)} {...props}>
|
|
83
|
+
{children ?? <Render content={content} block={block} components={components} />}
|
|
288
84
|
</Component>
|
|
289
85
|
)
|
|
290
86
|
}
|