@uniweb/runtime 0.1.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/LICENSE +201 -0
- package/README.md +229 -0
- package/package.json +50 -0
- package/src/components/BlockRenderer.jsx +124 -0
- package/src/components/ErrorBoundary.jsx +51 -0
- package/src/components/Link.jsx +28 -0
- package/src/components/PageRenderer.jsx +57 -0
- package/src/components/SafeHtml.jsx +22 -0
- package/src/components/WebsiteRenderer.jsx +84 -0
- package/src/core/block.js +279 -0
- package/src/core/input.js +15 -0
- package/src/core/page.js +75 -0
- package/src/core/uniweb.js +103 -0
- package/src/core/website.js +157 -0
- package/src/index.js +245 -0
- package/src/vite/content-collector.js +269 -0
- package/src/vite/foundation-plugin.js +194 -0
- package/src/vite/index.js +7 -0
- package/src/vite/site-content-plugin.js +135 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PageRenderer
|
|
3
|
+
*
|
|
4
|
+
* Renders a page by iterating through its blocks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useEffect } from 'react'
|
|
8
|
+
import BlockRenderer from './BlockRenderer.jsx'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ChildBlocks - renders child blocks of a block
|
|
12
|
+
* Exposed for use by foundation components
|
|
13
|
+
*/
|
|
14
|
+
export function ChildBlocks({ block, childBlocks, pure = false, extra = {} }) {
|
|
15
|
+
const blocks = childBlocks || block?.childBlocks || []
|
|
16
|
+
|
|
17
|
+
return blocks.map((childBlock, index) => (
|
|
18
|
+
<React.Fragment key={childBlock.id || index}>
|
|
19
|
+
<BlockRenderer block={childBlock} pure={pure} extra={extra} />
|
|
20
|
+
</React.Fragment>
|
|
21
|
+
))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* PageRenderer component
|
|
26
|
+
*/
|
|
27
|
+
export default function PageRenderer() {
|
|
28
|
+
const page = globalThis.uniweb?.activeWebsite?.activePage
|
|
29
|
+
const pageTitle = page?.title || 'Website'
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
document.title = pageTitle
|
|
33
|
+
return () => {
|
|
34
|
+
document.title = 'Website'
|
|
35
|
+
}
|
|
36
|
+
}, [pageTitle])
|
|
37
|
+
|
|
38
|
+
if (!page) {
|
|
39
|
+
return (
|
|
40
|
+
<div className="page-loading" style={{ padding: '2rem', textAlign: 'center', color: '#64748b' }}>
|
|
41
|
+
No page loaded
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const blocks = page.getPageBlocks()
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
{blocks.map((block, index) => (
|
|
51
|
+
<React.Fragment key={block.id || index}>
|
|
52
|
+
<BlockRenderer block={block} />
|
|
53
|
+
</React.Fragment>
|
|
54
|
+
))}
|
|
55
|
+
</>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SafeHtml
|
|
3
|
+
*
|
|
4
|
+
* Safely render HTML content with sanitization.
|
|
5
|
+
* TODO: Add DOMPurify for production use
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react'
|
|
9
|
+
|
|
10
|
+
export default function SafeHtml({ html, className, as: Component = 'div', ...props }) {
|
|
11
|
+
if (!html) return null
|
|
12
|
+
|
|
13
|
+
// For now, render directly
|
|
14
|
+
// TODO: Integrate DOMPurify for sanitization
|
|
15
|
+
return (
|
|
16
|
+
<Component
|
|
17
|
+
className={className}
|
|
18
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebsiteRenderer
|
|
3
|
+
*
|
|
4
|
+
* Top-level renderer that sets up theme styles and renders pages.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react'
|
|
8
|
+
import PageRenderer from './PageRenderer.jsx'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build CSS custom properties from theme data
|
|
12
|
+
*/
|
|
13
|
+
function buildThemeStyles(themeData) {
|
|
14
|
+
if (!themeData) return ''
|
|
15
|
+
|
|
16
|
+
const { contexts = {} } = themeData
|
|
17
|
+
const styles = []
|
|
18
|
+
|
|
19
|
+
// Generate CSS for each context (light, medium, dark)
|
|
20
|
+
for (const [contextName, contextData] of Object.entries(contexts)) {
|
|
21
|
+
const selector = `.context__${contextName}`
|
|
22
|
+
const vars = []
|
|
23
|
+
|
|
24
|
+
if (contextData.colors) {
|
|
25
|
+
for (const [key, value] of Object.entries(contextData.colors)) {
|
|
26
|
+
vars.push(` --${key}: ${value};`)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (vars.length > 0) {
|
|
31
|
+
styles.push(`${selector} {\n${vars.join('\n')}\n}`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return styles.join('\n\n')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fonts component - loads custom fonts
|
|
40
|
+
*/
|
|
41
|
+
function Fonts({ fontsData }) {
|
|
42
|
+
if (!fontsData || !fontsData.length) return null
|
|
43
|
+
|
|
44
|
+
const fontLinks = fontsData.map((font, index) => {
|
|
45
|
+
if (font.url) {
|
|
46
|
+
return <link key={index} rel="stylesheet" href={font.url} />
|
|
47
|
+
}
|
|
48
|
+
return null
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return <>{fontLinks.filter(Boolean)}</>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* WebsiteRenderer component
|
|
56
|
+
*/
|
|
57
|
+
export default function WebsiteRenderer() {
|
|
58
|
+
const website = globalThis.uniweb?.activeWebsite
|
|
59
|
+
|
|
60
|
+
if (!website) {
|
|
61
|
+
return (
|
|
62
|
+
<div className="website-loading" style={{ padding: '2rem', textAlign: 'center', color: '#64748b' }}>
|
|
63
|
+
Loading website...
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const themeStyles = buildThemeStyles(website.themeData)
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
{/* Load custom fonts */}
|
|
73
|
+
<Fonts fontsData={website.themeData?.importedFonts} />
|
|
74
|
+
|
|
75
|
+
{/* Inject theme CSS variables */}
|
|
76
|
+
{themeStyles && (
|
|
77
|
+
<style dangerouslySetInnerHTML={{ __html: themeStyles }} />
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{/* Render the page */}
|
|
81
|
+
<PageRenderer />
|
|
82
|
+
</>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block
|
|
3
|
+
*
|
|
4
|
+
* Represents a section/block on a page. Contains content, properties,
|
|
5
|
+
* child blocks, and state management. Connects to foundation components.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default class Block {
|
|
9
|
+
constructor(blockData, id) {
|
|
10
|
+
this.id = id
|
|
11
|
+
this.component = blockData.component || 'Section'
|
|
12
|
+
this.Component = null
|
|
13
|
+
|
|
14
|
+
// Content structure
|
|
15
|
+
// The content can be:
|
|
16
|
+
// 1. Raw ProseMirror content (from content collection)
|
|
17
|
+
// 2. Pre-parsed content with main/items structure
|
|
18
|
+
// For now, store raw and parse on demand
|
|
19
|
+
this.rawContent = blockData.content || {}
|
|
20
|
+
this.parsedContent = this.parseContent(blockData.content)
|
|
21
|
+
|
|
22
|
+
const { main, items } = this.parsedContent
|
|
23
|
+
this.main = main
|
|
24
|
+
this.items = items
|
|
25
|
+
|
|
26
|
+
// Block configuration
|
|
27
|
+
const blockConfig = blockData.params || blockData.config || {}
|
|
28
|
+
this.preset = blockData.preset
|
|
29
|
+
this.themeName = `context__${blockConfig.theme || 'light'}`
|
|
30
|
+
this.standardOptions = blockConfig.standardOptions || {}
|
|
31
|
+
this.properties = blockConfig.properties || blockConfig
|
|
32
|
+
|
|
33
|
+
// Child blocks (subsections)
|
|
34
|
+
this.childBlocks = blockData.subsections
|
|
35
|
+
? blockData.subsections.map((block, i) => new Block(block, `${id}_${i}`))
|
|
36
|
+
: []
|
|
37
|
+
|
|
38
|
+
// Input data
|
|
39
|
+
this.input = blockData.input || null
|
|
40
|
+
|
|
41
|
+
// State management
|
|
42
|
+
this.startState = null
|
|
43
|
+
this.state = null
|
|
44
|
+
this.resetStateHook = null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse content into structured format
|
|
49
|
+
* Supports multiple content formats:
|
|
50
|
+
* 1. Pre-parsed groups structure
|
|
51
|
+
* 2. ProseMirror document
|
|
52
|
+
* 3. Simple key-value content (PoC style)
|
|
53
|
+
*
|
|
54
|
+
* TODO: Integrate @uniwebcms/semantic-parser for full parsing
|
|
55
|
+
*/
|
|
56
|
+
parseContent(content) {
|
|
57
|
+
// If content is already parsed with groups structure
|
|
58
|
+
if (content?.groups) {
|
|
59
|
+
return content.groups
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ProseMirror document
|
|
63
|
+
if (content?.type === 'doc') {
|
|
64
|
+
return this.extractFromProseMirror(content)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Simple key-value content (PoC style) - pass through directly
|
|
68
|
+
// This allows components to receive content like { title, subtitle, items }
|
|
69
|
+
if (content && typeof content === 'object' && !Array.isArray(content)) {
|
|
70
|
+
return {
|
|
71
|
+
main: { header: {}, body: {} },
|
|
72
|
+
items: [],
|
|
73
|
+
// Store raw content for direct access
|
|
74
|
+
raw: content
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Fallback
|
|
79
|
+
return {
|
|
80
|
+
main: { header: {}, body: {} },
|
|
81
|
+
items: []
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Basic extraction from ProseMirror content
|
|
87
|
+
* This is simplified - full implementation uses semantic-parser
|
|
88
|
+
*/
|
|
89
|
+
extractFromProseMirror(doc) {
|
|
90
|
+
const main = { header: {}, body: {} }
|
|
91
|
+
const items = []
|
|
92
|
+
|
|
93
|
+
if (!doc.content) return { main, items }
|
|
94
|
+
|
|
95
|
+
for (const node of doc.content) {
|
|
96
|
+
if (node.type === 'heading') {
|
|
97
|
+
const text = this.extractText(node)
|
|
98
|
+
if (node.attrs?.level === 1) {
|
|
99
|
+
main.header.title = text
|
|
100
|
+
} else if (node.attrs?.level === 2) {
|
|
101
|
+
main.header.subtitle = text
|
|
102
|
+
}
|
|
103
|
+
} else if (node.type === 'paragraph') {
|
|
104
|
+
const text = this.extractText(node)
|
|
105
|
+
if (!main.body.paragraphs) main.body.paragraphs = []
|
|
106
|
+
main.body.paragraphs.push(text)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { main, items }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Extract text from a node
|
|
115
|
+
*/
|
|
116
|
+
extractText(node) {
|
|
117
|
+
if (!node.content) return ''
|
|
118
|
+
return node.content
|
|
119
|
+
.filter((n) => n.type === 'text')
|
|
120
|
+
.map((n) => n.text)
|
|
121
|
+
.join('')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Initialize the component from the foundation
|
|
126
|
+
* @returns {React.ComponentType|null}
|
|
127
|
+
*/
|
|
128
|
+
initComponent() {
|
|
129
|
+
if (this.Component) return this.Component
|
|
130
|
+
|
|
131
|
+
this.Component = globalThis.uniweb?.getComponent(this.component)
|
|
132
|
+
|
|
133
|
+
if (!this.Component) {
|
|
134
|
+
console.warn(`[Block] Component not found: ${this.component}`)
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Initialize state from component defaults
|
|
139
|
+
const defaults = this.Component.blockDefaults || { state: this.Component.blockState }
|
|
140
|
+
this.startState = defaults.state ? { ...defaults.state } : null
|
|
141
|
+
this.initState()
|
|
142
|
+
|
|
143
|
+
return this.Component
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get structured block content for components
|
|
148
|
+
*/
|
|
149
|
+
getBlockContent() {
|
|
150
|
+
const mainHeader = this.main?.header || {}
|
|
151
|
+
const mainBody = this.main?.body || {}
|
|
152
|
+
const banner = this.main?.banner || null
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
banner,
|
|
156
|
+
pretitle: mainHeader.pretitle || '',
|
|
157
|
+
title: mainHeader.title || '',
|
|
158
|
+
subtitle: mainHeader.subtitle || '',
|
|
159
|
+
description: mainHeader.description || '',
|
|
160
|
+
paragraphs: mainBody.paragraphs || [],
|
|
161
|
+
images: mainBody.imgs || mainBody.images || [],
|
|
162
|
+
links: mainBody.links || [],
|
|
163
|
+
icons: mainBody.icons || [],
|
|
164
|
+
properties: mainBody.propertyBlocks?.[0] || {},
|
|
165
|
+
videos: mainBody.videos || [],
|
|
166
|
+
lists: mainBody.lists || [],
|
|
167
|
+
buttons: mainBody.buttons || []
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get block properties
|
|
173
|
+
*/
|
|
174
|
+
getBlockProperties() {
|
|
175
|
+
return this.properties
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get child block renderer from runtime
|
|
180
|
+
*/
|
|
181
|
+
getChildBlockRenderer() {
|
|
182
|
+
return globalThis.uniweb?.childBlockRenderer
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get links from block content
|
|
187
|
+
* @param {Object} options
|
|
188
|
+
* @returns {Array}
|
|
189
|
+
*/
|
|
190
|
+
getBlockLinks(options = {}) {
|
|
191
|
+
const website = globalThis.uniweb?.activeWebsite
|
|
192
|
+
|
|
193
|
+
if (options.nested) {
|
|
194
|
+
const lists = this.main?.body?.lists || []
|
|
195
|
+
const links = lists[0]
|
|
196
|
+
return Block.parseNestedLinks(links, website)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const links = this.main?.body?.links || []
|
|
200
|
+
return links.map((link) => ({
|
|
201
|
+
route: website?.makeHref(link.href) || link.href,
|
|
202
|
+
label: link.label
|
|
203
|
+
}))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Initialize block state
|
|
208
|
+
*/
|
|
209
|
+
initState() {
|
|
210
|
+
this.state = this.startState
|
|
211
|
+
if (this.resetStateHook) this.resetStateHook()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* React hook for block state management
|
|
216
|
+
* @param {Function} useState - React useState hook
|
|
217
|
+
* @param {any} initState - Initial state
|
|
218
|
+
* @returns {[any, Function]}
|
|
219
|
+
*/
|
|
220
|
+
useBlockState(useState, initState) {
|
|
221
|
+
if (initState !== undefined && this.startState === null) {
|
|
222
|
+
this.startState = initState
|
|
223
|
+
this.state = initState
|
|
224
|
+
} else {
|
|
225
|
+
initState = this.startState
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const [state, setState] = useState(initState)
|
|
229
|
+
|
|
230
|
+
this.resetStateHook = () => setState(initState)
|
|
231
|
+
|
|
232
|
+
return [state, (newState) => setState((this.state = newState))]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Parse nested links structure
|
|
237
|
+
*/
|
|
238
|
+
static parseNestedLinks(list, website) {
|
|
239
|
+
const parsed = []
|
|
240
|
+
|
|
241
|
+
if (!list?.length) return parsed
|
|
242
|
+
|
|
243
|
+
for (const listItem of list) {
|
|
244
|
+
const { links = [], lists = [], paragraphs = [] } = listItem
|
|
245
|
+
|
|
246
|
+
const link = links[0]
|
|
247
|
+
const nestedList = lists[0]
|
|
248
|
+
const text = paragraphs[0]
|
|
249
|
+
|
|
250
|
+
let label = ''
|
|
251
|
+
let href = ''
|
|
252
|
+
let subLinks = []
|
|
253
|
+
let hasData = true
|
|
254
|
+
|
|
255
|
+
if (link) {
|
|
256
|
+
label = link.label
|
|
257
|
+
href = link.href
|
|
258
|
+
if (nestedList) {
|
|
259
|
+
subLinks = Block.parseNestedLinks(nestedList, website)
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
label = text
|
|
263
|
+
hasData = false
|
|
264
|
+
if (nestedList) {
|
|
265
|
+
subLinks = Block.parseNestedLinks(nestedList, website)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
parsed.push({
|
|
270
|
+
label,
|
|
271
|
+
route: website?.makeHref(href) || href,
|
|
272
|
+
child_items: subLinks,
|
|
273
|
+
hasData
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return parsed
|
|
278
|
+
}
|
|
279
|
+
}
|
package/src/core/page.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page
|
|
3
|
+
*
|
|
4
|
+
* Represents a single page with header, body sections, and footer.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Block from './block.js'
|
|
8
|
+
|
|
9
|
+
export default class Page {
|
|
10
|
+
constructor(pageData, id, pageHeader, pageFooter) {
|
|
11
|
+
this.id = id
|
|
12
|
+
this.route = pageData.route
|
|
13
|
+
this.title = pageData.title || ''
|
|
14
|
+
this.description = pageData.description || ''
|
|
15
|
+
|
|
16
|
+
this.pageBlocks = this.buildPageBlocks(
|
|
17
|
+
pageData.sections,
|
|
18
|
+
pageHeader?.sections,
|
|
19
|
+
pageFooter?.sections
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build the page block structure
|
|
25
|
+
*/
|
|
26
|
+
buildPageBlocks(body, header, footer) {
|
|
27
|
+
const headerSection = header?.[0]
|
|
28
|
+
const footerSection = footer?.[0]
|
|
29
|
+
const bodySections = body || []
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
header: headerSection ? new Block(headerSection, 'header') : null,
|
|
33
|
+
body: bodySections.map((section, index) => new Block(section, index)),
|
|
34
|
+
footer: footerSection ? new Block(footerSection, 'footer') : null,
|
|
35
|
+
leftPanel: null,
|
|
36
|
+
rightPanel: null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get all blocks (header, body, footer) as flat array
|
|
42
|
+
* @returns {Block[]}
|
|
43
|
+
*/
|
|
44
|
+
getPageBlocks() {
|
|
45
|
+
return [
|
|
46
|
+
this.pageBlocks.header,
|
|
47
|
+
...this.pageBlocks.body,
|
|
48
|
+
this.pageBlocks.footer
|
|
49
|
+
].filter(Boolean)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get just body blocks
|
|
54
|
+
* @returns {Block[]}
|
|
55
|
+
*/
|
|
56
|
+
getBodyBlocks() {
|
|
57
|
+
return this.pageBlocks.body
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get header block
|
|
62
|
+
* @returns {Block|null}
|
|
63
|
+
*/
|
|
64
|
+
getHeader() {
|
|
65
|
+
return this.pageBlocks.header
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get footer block
|
|
70
|
+
* @returns {Block|null}
|
|
71
|
+
*/
|
|
72
|
+
getFooter() {
|
|
73
|
+
return this.pageBlocks.footer
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uniweb Core Runtime
|
|
3
|
+
*
|
|
4
|
+
* The main runtime instance that manages the website, foundation components,
|
|
5
|
+
* and provides utilities to components.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import Website from './website.js'
|
|
9
|
+
|
|
10
|
+
export default class Uniweb {
|
|
11
|
+
constructor(configData) {
|
|
12
|
+
this.activeWebsite = new Website(configData)
|
|
13
|
+
this.childBlockRenderer = null // Function to render child blocks
|
|
14
|
+
this.routingComponents = {} // Link, SafeHtml, useNavigate, etc.
|
|
15
|
+
this.foundation = null // The loaded foundation module
|
|
16
|
+
this.foundationConfig = {} // Configuration from foundation
|
|
17
|
+
this.language = 'en'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Set the foundation module after loading
|
|
22
|
+
* @param {Object} foundation - The loaded ESM foundation module
|
|
23
|
+
*/
|
|
24
|
+
setFoundation(foundation) {
|
|
25
|
+
this.foundation = foundation
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get a component from the foundation by name
|
|
30
|
+
* @param {string} name - Component name
|
|
31
|
+
* @returns {React.ComponentType|undefined}
|
|
32
|
+
*/
|
|
33
|
+
getComponent(name) {
|
|
34
|
+
if (!this.foundation) {
|
|
35
|
+
console.warn('[Runtime] No foundation loaded')
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Use foundation's getComponent interface
|
|
40
|
+
if (typeof this.foundation.getComponent === 'function') {
|
|
41
|
+
return this.foundation.getComponent(name)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Fallback: direct component access
|
|
45
|
+
return this.foundation[name]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* List available components from the foundation
|
|
50
|
+
* @returns {string[]}
|
|
51
|
+
*/
|
|
52
|
+
listComponents() {
|
|
53
|
+
if (!this.foundation) return []
|
|
54
|
+
|
|
55
|
+
if (typeof this.foundation.listComponents === 'function') {
|
|
56
|
+
return this.foundation.listComponents()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return []
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get component schema
|
|
64
|
+
* @param {string} name - Component name
|
|
65
|
+
* @returns {Object|undefined}
|
|
66
|
+
*/
|
|
67
|
+
getSchema(name) {
|
|
68
|
+
if (!this.foundation) return undefined
|
|
69
|
+
|
|
70
|
+
if (typeof this.foundation.getSchema === 'function') {
|
|
71
|
+
return this.foundation.getSchema(name)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Set foundation configuration
|
|
79
|
+
* @param {Object} config
|
|
80
|
+
*/
|
|
81
|
+
setFoundationConfig(config) {
|
|
82
|
+
this.foundationConfig = config
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Legacy compatibility - maps to new method names
|
|
86
|
+
getRemoteComponent(name) {
|
|
87
|
+
return this.getComponent(name)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
setRemoteComponents(components) {
|
|
91
|
+
// Legacy: components was an object map
|
|
92
|
+
// Convert to foundation-like interface
|
|
93
|
+
this.foundation = {
|
|
94
|
+
getComponent: (name) => components[name],
|
|
95
|
+
listComponents: () => Object.keys(components),
|
|
96
|
+
components
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
setRemoteConfig(config) {
|
|
101
|
+
this.setFoundationConfig(config)
|
|
102
|
+
}
|
|
103
|
+
}
|