@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.
@@ -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
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Input
3
+ *
4
+ * Handles input/form data within blocks.
5
+ */
6
+
7
+ export default class Input {
8
+ constructor(inputData) {
9
+ this.data = inputData || {}
10
+ }
11
+
12
+ getData() {
13
+ return this.data
14
+ }
15
+ }
@@ -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
+ }