@uniweb/core 0.1.5 → 0.1.6

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/core",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
5
5
  "type": "module",
6
6
  "exports": {
package/src/block.js CHANGED
@@ -10,9 +10,14 @@ import { parseContent as parseSemanticContent } from '@uniweb/semantic-parser'
10
10
  export default class Block {
11
11
  constructor(blockData, id) {
12
12
  this.id = id
13
- this.component = blockData.component || 'Section'
13
+ // 'type' matches frontmatter convention; 'component' supported for backwards compatibility
14
+ this.type = blockData.type || blockData.component || 'Section'
14
15
  this.Component = null
15
16
 
17
+ // Back-references (set by Page when creating blocks)
18
+ this.page = null
19
+ this.website = null
20
+
16
21
  // Content structure
17
22
  // The content can be:
18
23
  // 1. Raw ProseMirror content (from content collection)
@@ -28,7 +33,7 @@ export default class Block {
28
33
  // Block configuration
29
34
  const blockConfig = blockData.params || blockData.config || {}
30
35
  this.preset = blockData.preset
31
- this.themeName = `context__${blockConfig.theme || 'light'}`
36
+ this.themeName = blockConfig.theme || 'light'
32
37
  this.standardOptions = blockConfig.standardOptions || {}
33
38
  this.properties = blockConfig.properties || blockConfig
34
39
 
@@ -40,10 +45,13 @@ export default class Block {
40
45
  // Input data
41
46
  this.input = blockData.input || null
42
47
 
43
- // State management
48
+ // State management (dynamic, can change at runtime)
44
49
  this.startState = null
45
50
  this.state = null
46
51
  this.resetStateHook = null
52
+
53
+ // Context (static, defined per component type)
54
+ this.context = null
47
55
  }
48
56
 
49
57
  /**
@@ -160,18 +168,25 @@ export default class Block {
160
168
  initComponent() {
161
169
  if (this.Component) return this.Component
162
170
 
163
- this.Component = globalThis.uniweb?.getComponent(this.component)
171
+ this.Component = globalThis.uniweb?.getComponent(this.type)
164
172
 
165
173
  if (!this.Component) {
166
- console.warn(`[Block] Component not found: ${this.component}`)
174
+ console.warn(`[Block] Component not found: ${this.type}`)
167
175
  return null
168
176
  }
169
177
 
170
- // Initialize state from component defaults
171
- const defaults = this.Component.blockDefaults || { state: this.Component.blockState }
172
- this.startState = defaults.state ? { ...defaults.state } : null
178
+ // Get component-level block configuration
179
+ // Supports: Component.block (preferred), Component.blockDefaults (legacy)
180
+ const blockConfig = this.Component.block || this.Component.blockDefaults || {}
181
+
182
+ // Initialize state (dynamic, can change at runtime)
183
+ const stateDefaults = blockConfig.state || this.Component.blockState
184
+ this.startState = stateDefaults ? { ...stateDefaults } : null
173
185
  this.initState()
174
186
 
187
+ // Initialize context (static, per component type)
188
+ this.context = blockConfig.context ? { ...blockConfig.context } : null
189
+
175
190
  return this.Component
176
191
  }
177
192
 
@@ -243,6 +258,59 @@ export default class Block {
243
258
  if (this.resetStateHook) this.resetStateHook()
244
259
  }
245
260
 
261
+ // ─────────────────────────────────────────────────────────────────
262
+ // Cross-Block Communication
263
+ // ─────────────────────────────────────────────────────────────────
264
+
265
+ /**
266
+ * Get this block's index within its page.
267
+ * Useful for finding neighboring blocks.
268
+ *
269
+ * @returns {number} The index, or -1 if not found
270
+ */
271
+ getIndex() {
272
+ if (!this.page) return -1
273
+ return this.page.getBlockIndex(this)
274
+ }
275
+
276
+ /**
277
+ * Get information about this block for cross-component communication.
278
+ * Other components (like NavBar) can use this to adapt their behavior.
279
+ *
280
+ * @returns {Object} Block info: { type, theme, state, context }
281
+ */
282
+ getBlockInfo() {
283
+ return {
284
+ type: this.type,
285
+ theme: this.themeName,
286
+ state: this.state,
287
+ context: this.context
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Get information about the next block in the page.
293
+ * Commonly used by headers/navbars to adapt to the first content section.
294
+ *
295
+ * @returns {Object|null} Next block's info or null
296
+ */
297
+ getNextBlockInfo() {
298
+ const index = this.getIndex()
299
+ if (index < 0 || !this.page) return null
300
+ return this.page.getBlockInfo(index + 1)
301
+ }
302
+
303
+ /**
304
+ * Get information about the previous block in the page.
305
+ *
306
+ * @returns {Object|null} Previous block's info or null
307
+ */
308
+ getPrevBlockInfo() {
309
+ const index = this.getIndex()
310
+ if (index <= 0 || !this.page) return null
311
+ return this.page.getBlockInfo(index - 1)
312
+ }
313
+
246
314
  /**
247
315
  * React hook for block state management
248
316
  * @param {Function} useState - React useState hook
package/src/page.js CHANGED
@@ -8,7 +8,7 @@
8
8
  import Block from './block.js'
9
9
 
10
10
  export default class Page {
11
- constructor(pageData, id, pageHeader, pageFooter, pageLeft, pageRight) {
11
+ constructor(pageData, id, website, pageHeader, pageFooter, pageLeft, pageRight) {
12
12
  this.id = id
13
13
  this.route = pageData.route
14
14
  this.title = pageData.title || ''
@@ -48,9 +48,9 @@ export default class Page {
48
48
  // Child pages (for nested hierarchy) - populated by Website
49
49
  this.children = []
50
50
 
51
- // Back-reference to website (set by Website constructor)
52
- this.website = null
53
- this.site = null // Alias
51
+ // Back-reference to website
52
+ this.website = website
53
+ this.site = website // Alias
54
54
 
55
55
  // Scroll position memory (for navigation restoration)
56
56
  this.scrollY = 0
@@ -99,18 +99,46 @@ export default class Page {
99
99
  buildPageBlocks(body, header, footer, left, right) {
100
100
  const buildBlocks = (sections, prefix) => {
101
101
  if (!sections || sections.length === 0) return null
102
- return sections.map((section, index) => new Block(section, `${prefix}-${index}`))
102
+ return sections.map((section, index) => {
103
+ const block = new Block(section, `${prefix}-${index}`)
104
+ this.initBlockReferences(block)
105
+ return block
106
+ })
103
107
  }
104
108
 
109
+ const bodyBlocks = (body || []).map((section, index) => {
110
+ const block = new Block(section, index)
111
+ this.initBlockReferences(block)
112
+ return block
113
+ })
114
+
105
115
  return {
106
116
  header: buildBlocks(header, 'header'),
107
- body: (body || []).map((section, index) => new Block(section, index)),
117
+ body: bodyBlocks,
108
118
  footer: buildBlocks(footer, 'footer'),
109
119
  left: buildBlocks(left, 'left'),
110
120
  right: buildBlocks(right, 'right')
111
121
  }
112
122
  }
113
123
 
124
+ /**
125
+ * Initialize block back-references to page and website.
126
+ * Also recursively sets references for child blocks.
127
+ *
128
+ * @param {Block} block - The block to initialize
129
+ */
130
+ initBlockReferences(block) {
131
+ block.page = this
132
+ block.website = this.website
133
+
134
+ // Recursively set references for child blocks
135
+ if (block.childBlocks?.length) {
136
+ for (const childBlock of block.childBlocks) {
137
+ this.initBlockReferences(childBlock)
138
+ }
139
+ }
140
+ }
141
+
114
142
  /**
115
143
  * Get all block groups (for Layout component)
116
144
  * @returns {Object} { header, body, footer, left, right }
@@ -119,6 +147,46 @@ export default class Page {
119
147
  return this.pageBlocks
120
148
  }
121
149
 
150
+ // ─────────────────────────────────────────────────────────────────
151
+ // Cross-Block Communication
152
+ // ─────────────────────────────────────────────────────────────────
153
+
154
+ /**
155
+ * Find a block's index within the page's block list.
156
+ * Searches across all layout areas (header, body, footer, left, right).
157
+ *
158
+ * @param {Block} block - The block to find
159
+ * @returns {number} The index in the flat list, or -1 if not found
160
+ */
161
+ getBlockIndex(block) {
162
+ const allBlocks = this.getPageBlocks()
163
+ return allBlocks.indexOf(block)
164
+ }
165
+
166
+ /**
167
+ * Get information about a block at a specific index.
168
+ * Used for cross-component communication (e.g., NavBar checking Hero's theme).
169
+ *
170
+ * @param {number} index - The block index
171
+ * @returns {Object|null} Block info { theme, component, state } or null
172
+ */
173
+ getBlockInfo(index) {
174
+ const allBlocks = this.getPageBlocks()
175
+ const block = allBlocks[index]
176
+ return block?.getBlockInfo() || null
177
+ }
178
+
179
+ /**
180
+ * Get the first body block's info.
181
+ * Common use case: NavBar checking if first section supports overlay.
182
+ *
183
+ * @returns {Object|null} First body block's info or null
184
+ */
185
+ getFirstBodyBlockInfo() {
186
+ const bodyBlocks = this.pageBlocks.body
187
+ return bodyBlocks?.[0]?.getBlockInfo() || null
188
+ }
189
+
122
190
  /**
123
191
  * Get all blocks (header, body, footer) as flat array
124
192
  * Respects page layout preferences (hasHeader, hasFooter, etc.)
package/src/website.js CHANGED
@@ -46,15 +46,9 @@ export default class Website {
46
46
  .filter((page) => !specialRoutes.includes(page.route))
47
47
  .map(
48
48
  (page, index) =>
49
- new Page(page, index, this.headerPage, this.footerPage, this.leftPage, this.rightPage)
49
+ new Page(page, index, this, this.headerPage, this.footerPage, this.leftPage, this.rightPage)
50
50
  )
51
51
 
52
- // Set reference from pages back to website
53
- for (const page of this.pages) {
54
- page.website = this
55
- page.site = this // Alias
56
- }
57
-
58
52
  this.activePage =
59
53
  this.pages.find((page) => page.route === '/' || page.route === '/index') ||
60
54
  this.pages[0]
@@ -287,8 +281,60 @@ export default class Website {
287
281
  return defaultVal
288
282
  }
289
283
 
284
+ // ─────────────────────────────────────────────────────────────────
285
+ // Search API
286
+ // ─────────────────────────────────────────────────────────────────
287
+
288
+ /**
289
+ * Check if search is enabled for this site
290
+ * @returns {boolean}
291
+ */
292
+ isSearchEnabled() {
293
+ // Search is enabled by default unless explicitly disabled
294
+ return this.config?.search?.enabled !== false
295
+ }
296
+
297
+ /**
298
+ * Get search configuration
299
+ * @returns {Object} Search configuration
300
+ */
301
+ getSearchConfig() {
302
+ const config = this.config?.search || {}
303
+
304
+ return {
305
+ enabled: this.isSearchEnabled(),
306
+ indexUrl: this.getSearchIndexUrl(),
307
+ locale: this.getActiveLocale(),
308
+ include: {
309
+ pages: config.include?.pages !== false,
310
+ sections: config.include?.sections !== false,
311
+ headings: config.include?.headings !== false,
312
+ paragraphs: config.include?.paragraphs !== false,
313
+ links: config.include?.links !== false,
314
+ lists: config.include?.lists !== false
315
+ },
316
+ exclude: {
317
+ routes: config.exclude?.routes || [],
318
+ components: config.exclude?.components || []
319
+ }
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Get the URL for the search index file
325
+ * @returns {string} URL to fetch the search index
326
+ */
327
+ getSearchIndexUrl() {
328
+ const locale = this.getActiveLocale()
329
+ const isDefault = locale === this.getDefaultLocale()
330
+
331
+ // Default locale uses root path, others use locale prefix
332
+ return isDefault ? '/search-index.json' : `/${locale}/search-index.json`
333
+ }
334
+
290
335
  /**
291
336
  * Get search data for all pages
337
+ * @deprecated Use getSearchConfig() and fetch the search index instead
292
338
  */
293
339
  getSearchData() {
294
340
  return this.pages.map((page) => ({