@uniweb/core 0.1.9 → 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 +0 -1
- package/package.json +2 -2
- package/src/block.js +6 -5
- package/src/page.js +97 -1
- package/src/uniweb.js +25 -1
- package/src/website.js +55 -10
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.
|
|
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.
|
|
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
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -11,6 +11,7 @@ export default class Page {
|
|
|
11
11
|
constructor(pageData, id, website, pageHeader, pageFooter, pageLeft, pageRight) {
|
|
12
12
|
this.id = id
|
|
13
13
|
this.route = pageData.route
|
|
14
|
+
this.isIndex = pageData.isIndex || false // True if this page is the index for its parent route
|
|
14
15
|
this.title = pageData.title || ''
|
|
15
16
|
this.description = pageData.description || ''
|
|
16
17
|
this.label = pageData.label || null // Short label for navigation (null = use title)
|
|
@@ -50,7 +51,6 @@ export default class Page {
|
|
|
50
51
|
|
|
51
52
|
// Back-reference to website
|
|
52
53
|
this.website = website
|
|
53
|
-
this.site = website // Alias
|
|
54
54
|
|
|
55
55
|
// Scroll position memory (for navigation restoration)
|
|
56
56
|
this.scrollY = 0
|
|
@@ -293,6 +293,26 @@ export default class Page {
|
|
|
293
293
|
// Navigation and Layout Helpers
|
|
294
294
|
// ─────────────────────────────────────────────────────────────────
|
|
295
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Get the navigation route (canonical route for links)
|
|
298
|
+
* For index pages, returns the parent route (e.g., '/' for homepage)
|
|
299
|
+
* For regular pages, returns the actual route
|
|
300
|
+
* @returns {string}
|
|
301
|
+
*/
|
|
302
|
+
getNavRoute() {
|
|
303
|
+
if (!this.isIndex) {
|
|
304
|
+
return this.route
|
|
305
|
+
}
|
|
306
|
+
// Index page - compute parent route
|
|
307
|
+
// /home -> /
|
|
308
|
+
// /docs/getting-started -> /docs
|
|
309
|
+
const segments = this.route.split('/').filter(Boolean)
|
|
310
|
+
if (segments.length <= 1) {
|
|
311
|
+
return '/'
|
|
312
|
+
}
|
|
313
|
+
return '/' + segments.slice(0, -1).join('/')
|
|
314
|
+
}
|
|
315
|
+
|
|
296
316
|
/**
|
|
297
317
|
* Get display label for navigation (short form of title)
|
|
298
318
|
* @returns {string}
|
|
@@ -364,4 +384,80 @@ export default class Page {
|
|
|
364
384
|
hasChildren() {
|
|
365
385
|
return this.children.length > 0
|
|
366
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
|
+
}
|
|
367
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
|
@@ -52,8 +52,9 @@ export default class Website {
|
|
|
52
52
|
// Build parent-child relationships based on route structure
|
|
53
53
|
this.buildPageHierarchy()
|
|
54
54
|
|
|
55
|
+
// Find the homepage (root-level index page)
|
|
55
56
|
this.activePage =
|
|
56
|
-
this.pages.find((page) => page.
|
|
57
|
+
this.pages.find((page) => page.isIndex && page.getNavRoute() === '/') ||
|
|
57
58
|
this.pages[0]
|
|
58
59
|
|
|
59
60
|
this.pageRoutes = this.pages.map((page) => page.route)
|
|
@@ -115,22 +116,28 @@ export default class Website {
|
|
|
115
116
|
})
|
|
116
117
|
|
|
117
118
|
// Build a map of route to page for quick lookup
|
|
119
|
+
// Include both actual routes and nav routes (for index pages)
|
|
118
120
|
const pageMap = new Map()
|
|
119
121
|
for (const page of sortedPages) {
|
|
120
122
|
pageMap.set(page.route, page)
|
|
123
|
+
// Also map the nav route for index pages so parent lookup works
|
|
124
|
+
if (page.isIndex) {
|
|
125
|
+
const navRoute = page.getNavRoute()
|
|
126
|
+
if (navRoute !== page.route) {
|
|
127
|
+
pageMap.set(navRoute, page)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
// For each page, find its parent and add it as a child
|
|
124
133
|
for (const page of sortedPages) {
|
|
125
134
|
const route = page.route
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// Find parent route by removing the last segment
|
|
129
|
-
// /getting-started/installation -> /getting-started
|
|
135
|
+
// Skip root-level pages (single segment like /home, /about)
|
|
130
136
|
const segments = route.split('/').filter(Boolean)
|
|
131
|
-
if (segments.length <= 1) continue
|
|
137
|
+
if (segments.length <= 1) continue
|
|
132
138
|
|
|
133
|
-
// Build parent route
|
|
139
|
+
// Build parent route by removing the last segment
|
|
140
|
+
// /docs/getting-started -> /docs
|
|
134
141
|
const parentRoute = '/' + segments.slice(0, -1).join('/')
|
|
135
142
|
const parent = pageMap.get(parentRoute)
|
|
136
143
|
|
|
@@ -150,11 +157,17 @@ export default class Website {
|
|
|
150
157
|
|
|
151
158
|
/**
|
|
152
159
|
* Get page by route
|
|
160
|
+
* Matches both actual routes and nav routes (for index pages)
|
|
153
161
|
* @param {string} route
|
|
154
162
|
* @returns {Page|undefined}
|
|
155
163
|
*/
|
|
156
164
|
getPage(route) {
|
|
157
|
-
|
|
165
|
+
// First try exact match on actual route
|
|
166
|
+
const exactMatch = this.pages.find((page) => page.route === route)
|
|
167
|
+
if (exactMatch) return exactMatch
|
|
168
|
+
|
|
169
|
+
// Then try matching nav route (for index pages accessible at parent route)
|
|
170
|
+
return this.pages.find((page) => page.isIndex && page.getNavRoute() === route)
|
|
158
171
|
}
|
|
159
172
|
|
|
160
173
|
/**
|
|
@@ -475,12 +488,13 @@ export default class Website {
|
|
|
475
488
|
// Build page info objects
|
|
476
489
|
const buildPageInfo = (page) => ({
|
|
477
490
|
id: page.id,
|
|
478
|
-
route: page.route
|
|
491
|
+
route: page.getNavRoute(), // Use canonical nav route (e.g., '/' for index pages)
|
|
492
|
+
navigableRoute: page.getNavigableRoute(), // First route with content (for links)
|
|
479
493
|
title: page.title,
|
|
480
494
|
label: page.getLabel(),
|
|
481
495
|
description: page.description,
|
|
482
496
|
order: page.order,
|
|
483
|
-
hasContent: page.
|
|
497
|
+
hasContent: page.hasContent(),
|
|
484
498
|
children: nested && page.hasChildren()
|
|
485
499
|
? page.children.filter(isPageVisible).map(buildPageInfo)
|
|
486
500
|
: []
|
|
@@ -515,4 +529,35 @@ export default class Website {
|
|
|
515
529
|
getAllPages(includeHidden = false) {
|
|
516
530
|
return this.getPageHierarchy({ nested: false, includeHidden })
|
|
517
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
|
+
}
|
|
518
563
|
}
|