@uniweb/core 0.1.10 → 0.1.11

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/README.md CHANGED
@@ -126,7 +126,6 @@ page.label // Short navigation label (or null)
126
126
  page.order // Sort order
127
127
  page.children // Child pages (for nested hierarchy)
128
128
  page.website // Back-reference to parent Website
129
- page.site // Alias for page.website
130
129
 
131
130
  // Navigation visibility
132
131
  page.hidden // Hidden from all navigation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/core",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
5
5
  "type": "module",
6
6
  "exports": {
@@ -27,6 +27,6 @@
27
27
  "node": ">=20.19"
28
28
  },
29
29
  "dependencies": {
30
- "@uniweb/semantic-parser": "1.0.7"
30
+ "@uniweb/semantic-parser": "1.0.8"
31
31
  }
32
32
  }
package/src/block.js CHANGED
@@ -175,17 +175,18 @@ export default class Block {
175
175
  return null
176
176
  }
177
177
 
178
- // Get component-level block configuration
179
- // Supports: Component.block (preferred), Component.blockDefaults (legacy)
180
- const blockConfig = this.Component.block || this.Component.blockDefaults || {}
178
+ // Get runtime metadata for this component (from meta.js, extracted at build time)
179
+ const meta = globalThis.uniweb?.getComponentMeta(this.type) || {}
181
180
 
182
181
  // Initialize state (dynamic, can change at runtime)
183
- const stateDefaults = blockConfig.state || this.Component.blockState
182
+ // Source: meta.js initialState field
183
+ const stateDefaults = meta.initialState
184
184
  this.startState = stateDefaults ? { ...stateDefaults } : null
185
185
  this.initState()
186
186
 
187
187
  // Initialize context (static, per component type)
188
- this.context = blockConfig.context ? { ...blockConfig.context } : null
188
+ // Source: meta.js context field
189
+ this.context = meta.context ? { ...meta.context } : null
189
190
 
190
191
  return this.Component
191
192
  }
package/src/page.js CHANGED
@@ -51,7 +51,6 @@ export default class Page {
51
51
 
52
52
  // Back-reference to website
53
53
  this.website = website
54
- this.site = website // Alias
55
54
 
56
55
  // Scroll position memory (for navigation restoration)
57
56
  this.scrollY = 0
@@ -385,4 +384,80 @@ export default class Page {
385
384
  hasChildren() {
386
385
  return this.children.length > 0
387
386
  }
387
+
388
+ /**
389
+ * Check if page has body content (sections)
390
+ * @returns {boolean}
391
+ */
392
+ hasContent() {
393
+ return this.pageBlocks.body.length > 0
394
+ }
395
+
396
+ // ─────────────────────────────────────────────────────────────────
397
+ // Active Route Detection
398
+ // ─────────────────────────────────────────────────────────────────
399
+
400
+ /**
401
+ * Get the first navigable route for this page.
402
+ * If page has no content, recursively finds first child with content.
403
+ * Useful for category pages that are just navigation containers.
404
+ *
405
+ * @returns {string} The route to navigate to
406
+ *
407
+ * @example
408
+ * // For a "Docs" category page with no content but children:
409
+ * // page.route = '/docs'
410
+ * // page.hasContent() = false
411
+ * // First child with content: '/docs/getting-started'
412
+ * page.getNavigableRoute() // Returns '/docs/getting-started'
413
+ */
414
+ getNavigableRoute() {
415
+ if (this.hasContent()) return this.route
416
+ for (const child of this.children || []) {
417
+ const route = child.getNavigableRoute()
418
+ if (route) return route
419
+ }
420
+ return this.route // Fallback to own route
421
+ }
422
+
423
+ /**
424
+ * Get route without leading/trailing slashes.
425
+ * Useful for route comparisons.
426
+ *
427
+ * @returns {string} Normalized route (e.g., 'docs/getting-started')
428
+ */
429
+ getNormalizedRoute() {
430
+ return (this.route || '').replace(/^\//, '').replace(/\/$/, '')
431
+ }
432
+
433
+ /**
434
+ * Check if this page matches the given route exactly.
435
+ *
436
+ * @param {string} route - Normalized route (no leading/trailing slashes)
437
+ * @returns {boolean} True if this page's route matches
438
+ */
439
+ isActiveFor(route) {
440
+ return this.getNormalizedRoute() === route
441
+ }
442
+
443
+ /**
444
+ * Check if this page or any descendant matches the given route.
445
+ * Useful for highlighting parent nav items when a child page is active.
446
+ *
447
+ * @param {string} route - Normalized route (no leading/trailing slashes)
448
+ * @returns {boolean} True if this page or a descendant is active
449
+ *
450
+ * @example
451
+ * // Page route: '/docs'
452
+ * // Current route: 'docs/getting-started/installation'
453
+ * page.isActiveOrAncestor('docs/getting-started/installation') // true
454
+ */
455
+ isActiveOrAncestor(route) {
456
+ const pageRoute = this.getNormalizedRoute()
457
+ if (pageRoute === route) return true
458
+ // Check if route starts with this page's route followed by /
459
+ // Handle empty pageRoute (root) specially
460
+ if (pageRoute === '') return true // Root is ancestor of all
461
+ return route.startsWith(pageRoute + '/')
462
+ }
388
463
  }
package/src/uniweb.js CHANGED
@@ -14,7 +14,8 @@ export default class Uniweb {
14
14
  this.childBlockRenderer = null // Function to render child blocks
15
15
  this.routingComponents = {} // Link, SafeHtml, useNavigate, etc.
16
16
  this.foundation = null // The loaded foundation module
17
- this.foundationConfig = {} // Configuration from foundation
17
+ this.foundationConfig = {} // Configuration from foundation (capabilities)
18
+ this.meta = {} // Per-component runtime metadata (from meta.js)
18
19
  this.language = 'en'
19
20
 
20
21
  // Initialize analytics (disabled by default, configure via site config)
@@ -27,6 +28,29 @@ export default class Uniweb {
27
28
  */
28
29
  setFoundation(foundation) {
29
30
  this.foundation = foundation
31
+
32
+ // Store per-component metadata if present
33
+ if (foundation.meta) {
34
+ this.meta = foundation.meta
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Get runtime metadata for a component
40
+ * @param {string} componentName
41
+ * @returns {Object|null} Meta with defaults, context, initialState, background, data
42
+ */
43
+ getComponentMeta(componentName) {
44
+ return this.meta[componentName] || null
45
+ }
46
+
47
+ /**
48
+ * Get default param values for a component
49
+ * @param {string} componentName
50
+ * @returns {Object} Default values (empty object if none)
51
+ */
52
+ getComponentDefaults(componentName) {
53
+ return this.meta[componentName]?.defaults || {}
30
54
  }
31
55
 
32
56
  /**
package/src/website.js CHANGED
@@ -489,11 +489,12 @@ export default class Website {
489
489
  const buildPageInfo = (page) => ({
490
490
  id: page.id,
491
491
  route: page.getNavRoute(), // Use canonical nav route (e.g., '/' for index pages)
492
+ navigableRoute: page.getNavigableRoute(), // First route with content (for links)
492
493
  title: page.title,
493
494
  label: page.getLabel(),
494
495
  description: page.description,
495
496
  order: page.order,
496
- hasContent: page.getBodyBlocks().length > 0,
497
+ hasContent: page.hasContent(),
497
498
  children: nested && page.hasChildren()
498
499
  ? page.children.filter(isPageVisible).map(buildPageInfo)
499
500
  : []
@@ -528,4 +529,35 @@ export default class Website {
528
529
  getAllPages(includeHidden = false) {
529
530
  return this.getPageHierarchy({ nested: false, includeHidden })
530
531
  }
532
+
533
+ // ─────────────────────────────────────────────────────────────────
534
+ // Active Route API (for navigation components)
535
+ // ─────────────────────────────────────────────────────────────────
536
+
537
+ /**
538
+ * Get the current active route, normalized (no leading/trailing slashes).
539
+ * Works in both SSR (from activePage) and client (from activePage).
540
+ *
541
+ * @returns {string} Normalized route (e.g., 'docs/getting-started')
542
+ *
543
+ * @example
544
+ * website.getActiveRoute() // 'docs/getting-started'
545
+ */
546
+ getActiveRoute() {
547
+ return this.activePage?.getNormalizedRoute() || ''
548
+ }
549
+
550
+ /**
551
+ * Get the first segment of the active route.
552
+ * Useful for root-level navigation highlighting.
553
+ *
554
+ * @returns {string} First segment (e.g., 'docs' for 'docs/getting-started')
555
+ *
556
+ * @example
557
+ * // Active route: 'docs/getting-started/installation'
558
+ * website.getActiveRootSegment() // 'docs'
559
+ */
560
+ getActiveRootSegment() {
561
+ return this.getActiveRoute().split('/')[0]
562
+ }
531
563
  }