@uniweb/core 0.5.9 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/core",
3
- "version": "0.5.9",
3
+ "version": "0.5.11",
4
4
  "description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
5
5
  "type": "module",
6
6
  "exports": {
@@ -30,7 +30,8 @@
30
30
  "jest": "^29.7.0"
31
31
  },
32
32
  "dependencies": {
33
- "@uniweb/semantic-parser": "1.1.6"
33
+ "@uniweb/semantic-parser": "1.1.6",
34
+ "@uniweb/theming": "0.1.1"
34
35
  },
35
36
  "scripts": {
36
37
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
package/src/block.js CHANGED
@@ -6,21 +6,7 @@
6
6
  */
7
7
 
8
8
  import { parseContent as parseSemanticContent } from '@uniweb/semantic-parser'
9
-
10
- /**
11
- * Resolve bare palette references to var() in theme overrides.
12
- * Allows content authors to write `primary: neutral-900` in frontmatter
13
- * instead of `primary: var(--neutral-900)`.
14
- */
15
- const SHADE_LEVELS = new Set([50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950])
16
-
17
- function resolveOverrideValue(value) {
18
- if (typeof value !== 'string' || value.includes('(') || value.startsWith('#')) return value
19
- const bare = value.replace(/^-{0,2}/, '')
20
- const match = bare.match(/^([a-z][a-z0-9]*)-(\d+)$/)
21
- if (match && SHADE_LEVELS.has(parseInt(match[2], 10))) return `var(--${bare})`
22
- return value
23
- }
9
+ import { normalizeTokenValue } from '@uniweb/theming'
24
10
 
25
11
  export default class Block {
26
12
  constructor(blockData, id, page) {
@@ -58,20 +44,26 @@ export default class Block {
58
44
 
59
45
  // Normalize theme: supports string ("light") or object ({ mode, ...tokenOverrides })
60
46
  // Resolve bare palette refs (e.g. "primary: neutral-900" → var(--neutral-900))
47
+ //
48
+ // themeName values:
49
+ // '' (empty) = Auto — section inherits from site's appearance/scheme
50
+ // 'light' = Pinned to light context
51
+ // 'medium' = Pinned to dim context
52
+ // 'dark' = Pinned to dark context
61
53
  const rawTheme = blockConfig.theme
62
54
  if (rawTheme && typeof rawTheme === 'object') {
63
55
  const { mode, ...overrides } = rawTheme
64
- this.themeName = mode || 'light'
56
+ this.themeName = mode || ''
65
57
  if (Object.keys(overrides).length > 0) {
66
58
  for (const key of Object.keys(overrides)) {
67
- overrides[key] = resolveOverrideValue(overrides[key])
59
+ overrides[key] = normalizeTokenValue(overrides[key])
68
60
  }
69
61
  this.contextOverrides = overrides
70
62
  } else {
71
63
  this.contextOverrides = null
72
64
  }
73
65
  } else {
74
- this.themeName = rawTheme || 'light'
66
+ this.themeName = rawTheme ?? ''
75
67
  this.contextOverrides = null
76
68
  }
77
69
 
@@ -149,6 +141,10 @@ export default class Block {
149
141
  // Context (static, defined per component type)
150
142
  this.context = null
151
143
 
144
+ // Component-level CSS variables (merged meta.js defaults + frontmatter overrides)
145
+ // Populated by initComponent() — context-independent, emitted on #section-{id}
146
+ this.componentVars = null
147
+
152
148
  Object.seal(this)
153
149
  }
154
150
 
@@ -176,6 +172,11 @@ export default class Block {
176
172
  return this.extractFromProseMirror(content)
177
173
  }
178
174
 
175
+ // Wrapped ProseMirror document (Content API format: { doc: { type: "doc", ... } })
176
+ if (content?.doc?.type === 'doc') {
177
+ return this.extractFromProseMirror(content.doc)
178
+ }
179
+
179
180
  // Plain object content — pass through directly.
180
181
  // guaranteeContentStructure() in prepare-props will fill in missing fields.
181
182
  if (content && typeof content === 'object' && !Array.isArray(content)) {
@@ -284,6 +285,12 @@ export default class Block {
284
285
  // Source: meta.js context field
285
286
  this.context = meta.context ? { ...meta.context } : null
286
287
 
288
+ // Merge component-level CSS vars: meta.js defaults + frontmatter overrides
289
+ // Source: meta.js vars field (defaults), section frontmatter vars: key (overrides)
290
+ if (meta.vars) {
291
+ this.componentVars = Block.mergeComponentVars(meta.vars, this.properties.vars)
292
+ }
293
+
287
294
  return this.Component
288
295
  }
289
296
 
@@ -434,6 +441,38 @@ export default class Block {
434
441
  return this.dynamicContext
435
442
  }
436
443
 
444
+ /**
445
+ * Merge component-level CSS variable defaults with frontmatter overrides.
446
+ *
447
+ * Schema vars (from meta.js) can be:
448
+ * - String shorthand: 'card-gap': '1.5rem' → { default: '1.5rem' }
449
+ * - Object: 'card-gap': { default: '1.5rem', label: 'Card Gap' }
450
+ *
451
+ * @param {Object} schemaVars - Var definitions from meta.js
452
+ * @param {Object} [frontmatterVars] - Overrides from section frontmatter
453
+ * @returns {Object} Flat { name: value } object for CSS emission
454
+ */
455
+ static mergeComponentVars(schemaVars, frontmatterVars = {}) {
456
+ const merged = {}
457
+
458
+ for (const [name, config] of Object.entries(schemaVars)) {
459
+ const defaultVal = typeof config === 'string' ? config : config?.default
460
+ if (defaultVal != null) {
461
+ merged[name] = defaultVal
462
+ }
463
+ }
464
+
465
+ if (frontmatterVars && typeof frontmatterVars === 'object') {
466
+ for (const [name, value] of Object.entries(frontmatterVars)) {
467
+ if (value != null && name in schemaVars) {
468
+ merged[name] = String(value)
469
+ }
470
+ }
471
+ }
472
+
473
+ return Object.keys(merged).length > 0 ? merged : null
474
+ }
475
+
437
476
  /**
438
477
  * Normalize a background value from section frontmatter
439
478
  *
@@ -464,7 +503,7 @@ export default class Block {
464
503
 
465
504
  // Anything else → CSS color (hex, rgb, hsl, oklch, named color, var())
466
505
  // Resolve bare palette refs (e.g. "primary-900" → "var(--primary-900)")
467
- return { mode: 'color', color: resolveOverrideValue(raw) }
506
+ return { mode: 'color', color: normalizeTokenValue(raw) }
468
507
  }
469
508
 
470
509
  // Object with explicit mode — pass through
package/src/website.js CHANGED
@@ -74,7 +74,7 @@ export default class Website {
74
74
 
75
75
  // Legacy language support (for editor multilingual)
76
76
  this.activeLang = this.activeLocale
77
- this.langs = config.languages || this.locales.map(l => ({
77
+ this.langs = this.locales.map(l => ({
78
78
  label: l.label || l.code,
79
79
  value: l.code
80
80
  }))
@@ -107,7 +107,7 @@ export default class Website {
107
107
  */
108
108
  buildLocalesList(config) {
109
109
  const defaultLocale = config.defaultLanguage || 'en'
110
- const i18nLocales = config.i18n?.locales || []
110
+ const languages = config.languages || []
111
111
 
112
112
  // Normalize input: convert strings to objects, keep objects as-is
113
113
  const normalizeLocale = (locale) => {
@@ -118,14 +118,14 @@ export default class Website {
118
118
  return { code: locale.code, ...(locale.label && { label: locale.label }) }
119
119
  }
120
120
 
121
- // Start with default locale (may not be in i18nLocales)
121
+ // Start with default locale (may not be in languages list)
122
122
  const localeMap = new Map()
123
123
  localeMap.set(defaultLocale, { code: defaultLocale })
124
124
 
125
- // Add i18n locales (may include objects with labels)
126
- for (const locale of i18nLocales) {
125
+ // Add configured languages (may include objects with labels)
126
+ for (const locale of languages) {
127
127
  const normalized = normalizeLocale(locale)
128
- // Merge with existing (to preserve labels if default locale also in i18n with label)
128
+ // Merge with existing (to preserve labels if default locale also in languages with label)
129
129
  if (localeMap.has(normalized.code)) {
130
130
  const existing = localeMap.get(normalized.code)
131
131
  localeMap.set(normalized.code, { ...existing, ...normalized })
@@ -577,12 +577,19 @@ export default class Website {
577
577
  * makeHref('/about') // → '/about' (passthrough)
578
578
  */
579
579
  makeHref(href) {
580
- if (!href || !href.startsWith('page:')) {
580
+ if (!href) return href
581
+
582
+ // Support both page: (current) and topic: (legacy) prefixes
583
+ let withoutPrefix
584
+ if (href.startsWith('page:')) {
585
+ withoutPrefix = href.slice(5)
586
+ } else if (href.startsWith('topic:')) {
587
+ withoutPrefix = href.slice(6)
588
+ } else {
581
589
  return href
582
590
  }
583
591
 
584
592
  // Parse page reference: page:pageId#sectionId
585
- const withoutPrefix = href.slice(5) // Remove 'page:'
586
593
  const [pageId, sectionId] = withoutPrefix.split('#')
587
594
 
588
595
  // Look up page by ID (explicit or route-based)
@@ -878,6 +885,10 @@ export default class Website {
878
885
  if (navType === 'footer' && page.hideInFooter) return false
879
886
  }
880
887
 
888
+ // Skip empty folders (no content) that have no visible children.
889
+ // Folders with children still appear as dropdown parents.
890
+ if (!page.hasContent() && !page.children?.some(isPageVisible)) return false
891
+
881
892
  // Apply custom filter if provided
882
893
  if (customFilter && !customFilter(page)) return false
883
894