@uniweb/core 0.1.14 → 0.1.16
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 +7 -1
- package/src/index.js +1 -0
- package/src/theme.js +403 -0
- package/src/website.js +14 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -26,7 +26,13 @@
|
|
|
26
26
|
"engines": {
|
|
27
27
|
"node": ">=20.19"
|
|
28
28
|
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"jest": "^29.7.0"
|
|
31
|
+
},
|
|
29
32
|
"dependencies": {
|
|
30
33
|
"@uniweb/semantic-parser": "1.0.12"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
|
31
37
|
}
|
|
32
38
|
}
|
package/src/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export { default as Page } from './page.js'
|
|
|
14
14
|
export { default as Block } from './block.js'
|
|
15
15
|
export { default as Input } from './input.js'
|
|
16
16
|
export { default as Analytics } from './analytics.js'
|
|
17
|
+
export { default as Theme } from './theme.js'
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* The singleton Uniweb instance.
|
package/src/theme.js
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Class
|
|
3
|
+
*
|
|
4
|
+
* Runtime representation of a site's theme configuration.
|
|
5
|
+
* Provides access to colors, semantic tokens, and appearance settings.
|
|
6
|
+
*
|
|
7
|
+
* @module @uniweb/core/theme
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Standard shade levels
|
|
11
|
+
const SHADE_LEVELS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
|
|
12
|
+
|
|
13
|
+
// Valid color contexts
|
|
14
|
+
const VALID_CONTEXTS = ['light', 'medium', 'dark']
|
|
15
|
+
|
|
16
|
+
// Default semantic tokens by context
|
|
17
|
+
const DEFAULT_CONTEXT_TOKENS = {
|
|
18
|
+
light: {
|
|
19
|
+
bg: 'var(--neutral-50)',
|
|
20
|
+
text: 'var(--neutral-950)',
|
|
21
|
+
heading: 'var(--neutral-900)',
|
|
22
|
+
link: 'var(--primary-600)',
|
|
23
|
+
},
|
|
24
|
+
medium: {
|
|
25
|
+
bg: 'var(--neutral-100)',
|
|
26
|
+
text: 'var(--neutral-950)',
|
|
27
|
+
heading: 'var(--neutral-900)',
|
|
28
|
+
link: 'var(--primary-600)',
|
|
29
|
+
},
|
|
30
|
+
dark: {
|
|
31
|
+
bg: 'var(--neutral-900)',
|
|
32
|
+
text: 'var(--neutral-50)',
|
|
33
|
+
heading: 'white',
|
|
34
|
+
link: 'var(--primary-400)',
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Theme class for runtime theme access
|
|
40
|
+
*/
|
|
41
|
+
export default class Theme {
|
|
42
|
+
/**
|
|
43
|
+
* Create a Theme instance
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} themeData - Processed theme data from build
|
|
46
|
+
* @param {Object} themeData.palettes - Generated color palettes (name → { shade → value })
|
|
47
|
+
* @param {Object} themeData.colors - Raw colors (for reference)
|
|
48
|
+
* @param {Object} themeData.contexts - Context token overrides
|
|
49
|
+
* @param {Object} themeData.fonts - Font configuration
|
|
50
|
+
* @param {Object} themeData.appearance - Appearance settings
|
|
51
|
+
* @param {Object} themeData.foundationVars - Foundation variables
|
|
52
|
+
* @param {string} themeData.css - Pre-generated CSS (optional)
|
|
53
|
+
*/
|
|
54
|
+
constructor(themeData = {}) {
|
|
55
|
+
this._data = themeData
|
|
56
|
+
// Use palettes if available, fall back to colors for backwards compatibility
|
|
57
|
+
this._palettes = themeData.palettes || themeData.colors || {}
|
|
58
|
+
this._rawColors = themeData.colors || {}
|
|
59
|
+
this._contexts = themeData.contexts || {}
|
|
60
|
+
this._fonts = themeData.fonts || {}
|
|
61
|
+
this._appearance = themeData.appearance || { default: 'light' }
|
|
62
|
+
this._foundationVars = themeData.foundationVars || {}
|
|
63
|
+
this._css = themeData.css || null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the pre-generated CSS string
|
|
68
|
+
* @returns {string|null}
|
|
69
|
+
*/
|
|
70
|
+
get css() {
|
|
71
|
+
return this._css
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get the raw theme data
|
|
76
|
+
* @returns {Object}
|
|
77
|
+
*/
|
|
78
|
+
get data() {
|
|
79
|
+
return this._data
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============================================================
|
|
83
|
+
// Color Access
|
|
84
|
+
// ============================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get a color value from a palette
|
|
88
|
+
*
|
|
89
|
+
* @param {string} name - Palette name (e.g., 'primary', 'neutral')
|
|
90
|
+
* @param {number} shade - Shade level (50-950), defaults to 500
|
|
91
|
+
* @returns {string|null} Color value (oklch string) or null if not found
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* theme.getColor('primary', 500) // → "oklch(55.0% 0.2000 260.0)"
|
|
95
|
+
* theme.getColor('primary') // → same as above (500 is default)
|
|
96
|
+
*/
|
|
97
|
+
getColor(name, shade = 500) {
|
|
98
|
+
const palette = this._palettes[name]
|
|
99
|
+
if (!palette) return null
|
|
100
|
+
|
|
101
|
+
return palette[shade] || null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get all shades for a color palette
|
|
106
|
+
*
|
|
107
|
+
* @param {string} name - Palette name
|
|
108
|
+
* @returns {Object|null} Object with shade levels as keys, or null
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* theme.getPalette('primary')
|
|
112
|
+
* // → { 50: "oklch(...)", 100: "oklch(...)", ... }
|
|
113
|
+
*/
|
|
114
|
+
getPalette(name) {
|
|
115
|
+
return this._palettes[name] || null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get all available palette names
|
|
120
|
+
*
|
|
121
|
+
* @returns {string[]} Array of palette names
|
|
122
|
+
*/
|
|
123
|
+
getPaletteNames() {
|
|
124
|
+
return Object.keys(this._palettes)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if a palette exists
|
|
129
|
+
*
|
|
130
|
+
* @param {string} name - Palette name
|
|
131
|
+
* @returns {boolean}
|
|
132
|
+
*/
|
|
133
|
+
hasPalette(name) {
|
|
134
|
+
return name in this._palettes
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get a CSS variable reference for a color
|
|
139
|
+
*
|
|
140
|
+
* @param {string} name - Palette name
|
|
141
|
+
* @param {number} shade - Shade level
|
|
142
|
+
* @returns {string} CSS var() reference
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* theme.getColorVar('primary', 600) // → "var(--primary-600)"
|
|
146
|
+
*/
|
|
147
|
+
getColorVar(name, shade = 500) {
|
|
148
|
+
return `var(--${name}-${shade})`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ============================================================
|
|
152
|
+
// Context Access
|
|
153
|
+
// ============================================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get a semantic token value for a context
|
|
157
|
+
*
|
|
158
|
+
* @param {string} context - Context name ('light', 'medium', 'dark')
|
|
159
|
+
* @param {string} token - Token name (e.g., 'bg', 'text', 'link')
|
|
160
|
+
* @returns {string|null} Token value or null
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* theme.getContextToken('light', 'bg') // → "var(--neutral-50)"
|
|
164
|
+
* theme.getContextToken('dark', 'text') // → "var(--neutral-50)"
|
|
165
|
+
*/
|
|
166
|
+
getContextToken(context, token) {
|
|
167
|
+
// Check custom context tokens first
|
|
168
|
+
const customContext = this._contexts[context]
|
|
169
|
+
if (customContext && customContext[token]) {
|
|
170
|
+
return customContext[token]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Fall back to defaults
|
|
174
|
+
const defaults = DEFAULT_CONTEXT_TOKENS[context]
|
|
175
|
+
return defaults?.[token] || null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get all tokens for a context
|
|
180
|
+
*
|
|
181
|
+
* @param {string} context - Context name
|
|
182
|
+
* @returns {Object} Token name → value mapping
|
|
183
|
+
*/
|
|
184
|
+
getContextTokens(context) {
|
|
185
|
+
const defaults = DEFAULT_CONTEXT_TOKENS[context] || {}
|
|
186
|
+
const custom = this._contexts[context] || {}
|
|
187
|
+
return { ...defaults, ...custom }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get the CSS class name for a context
|
|
192
|
+
*
|
|
193
|
+
* @param {string} context - Context name
|
|
194
|
+
* @returns {string} CSS class name
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* theme.getContextClass('dark') // → "context-dark"
|
|
198
|
+
*/
|
|
199
|
+
getContextClass(context) {
|
|
200
|
+
if (!VALID_CONTEXTS.includes(context)) {
|
|
201
|
+
console.warn(`Invalid context: ${context}. Using 'light'.`)
|
|
202
|
+
return 'context-light'
|
|
203
|
+
}
|
|
204
|
+
return `context-${context}`
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Check if a context is valid
|
|
209
|
+
*
|
|
210
|
+
* @param {string} context - Context name
|
|
211
|
+
* @returns {boolean}
|
|
212
|
+
*/
|
|
213
|
+
isValidContext(context) {
|
|
214
|
+
return VALID_CONTEXTS.includes(context)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get all valid context names
|
|
219
|
+
*
|
|
220
|
+
* @returns {string[]}
|
|
221
|
+
*/
|
|
222
|
+
getValidContexts() {
|
|
223
|
+
return [...VALID_CONTEXTS]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ============================================================
|
|
227
|
+
// Appearance (Color Scheme)
|
|
228
|
+
// ============================================================
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get appearance configuration
|
|
232
|
+
*
|
|
233
|
+
* @returns {Object} Appearance settings
|
|
234
|
+
* @property {string} default - Default scheme ('light', 'dark', 'system')
|
|
235
|
+
* @property {boolean} allowToggle - Whether scheme toggle is enabled
|
|
236
|
+
* @property {boolean} respectSystemPreference - Honor prefers-color-scheme
|
|
237
|
+
* @property {string[]} schemes - Available schemes
|
|
238
|
+
*/
|
|
239
|
+
getAppearance() {
|
|
240
|
+
return {
|
|
241
|
+
default: this._appearance.default || 'light',
|
|
242
|
+
allowToggle: this._appearance.allowToggle || false,
|
|
243
|
+
respectSystemPreference: this._appearance.respectSystemPreference ?? true,
|
|
244
|
+
schemes: this._appearance.schemes || ['light'],
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get the default color scheme
|
|
250
|
+
*
|
|
251
|
+
* @returns {string} 'light', 'dark', or 'system'
|
|
252
|
+
*/
|
|
253
|
+
getDefaultScheme() {
|
|
254
|
+
return this._appearance.default || 'light'
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Check if a color scheme is supported
|
|
259
|
+
*
|
|
260
|
+
* @param {string} scheme - Scheme name
|
|
261
|
+
* @returns {boolean}
|
|
262
|
+
*/
|
|
263
|
+
supportsScheme(scheme) {
|
|
264
|
+
const schemes = this._appearance.schemes || ['light']
|
|
265
|
+
return schemes.includes(scheme)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Check if scheme toggle is enabled
|
|
270
|
+
*
|
|
271
|
+
* @returns {boolean}
|
|
272
|
+
*/
|
|
273
|
+
hasSchemeToggle() {
|
|
274
|
+
return this._appearance.allowToggle === true
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get the CSS class for a scheme
|
|
279
|
+
*
|
|
280
|
+
* @param {string} scheme - Scheme name
|
|
281
|
+
* @returns {string} CSS class name
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* theme.getSchemeClass('dark') // → "scheme-dark"
|
|
285
|
+
*/
|
|
286
|
+
getSchemeClass(scheme) {
|
|
287
|
+
return `scheme-${scheme}`
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ============================================================
|
|
291
|
+
// Fonts
|
|
292
|
+
// ============================================================
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get font configuration
|
|
296
|
+
*
|
|
297
|
+
* @returns {Object} Font settings
|
|
298
|
+
*/
|
|
299
|
+
getFonts() {
|
|
300
|
+
return { ...this._fonts }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get a specific font family
|
|
305
|
+
*
|
|
306
|
+
* @param {string} type - Font type ('body', 'heading', 'mono')
|
|
307
|
+
* @returns {string|null} Font family string or null
|
|
308
|
+
*/
|
|
309
|
+
getFont(type) {
|
|
310
|
+
return this._fonts[type] || null
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Get CSS variable reference for a font
|
|
315
|
+
*
|
|
316
|
+
* @param {string} type - Font type
|
|
317
|
+
* @returns {string} CSS var() reference
|
|
318
|
+
*/
|
|
319
|
+
getFontVar(type) {
|
|
320
|
+
return `var(--font-${type})`
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ============================================================
|
|
324
|
+
// Foundation Variables
|
|
325
|
+
// ============================================================
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get a foundation variable value
|
|
329
|
+
*
|
|
330
|
+
* @param {string} name - Variable name
|
|
331
|
+
* @returns {string|null} Variable value or null
|
|
332
|
+
*/
|
|
333
|
+
getFoundationVar(name) {
|
|
334
|
+
const config = this._foundationVars[name]
|
|
335
|
+
if (!config) return null
|
|
336
|
+
return typeof config === 'object' ? config.default : config
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Get all foundation variables
|
|
341
|
+
*
|
|
342
|
+
* @returns {Object} Variable name → value mapping
|
|
343
|
+
*/
|
|
344
|
+
getFoundationVars() {
|
|
345
|
+
const vars = {}
|
|
346
|
+
for (const [name, config] of Object.entries(this._foundationVars)) {
|
|
347
|
+
vars[name] = typeof config === 'object' ? config.default : config
|
|
348
|
+
}
|
|
349
|
+
return vars
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Get CSS variable reference for a foundation variable
|
|
354
|
+
*
|
|
355
|
+
* @param {string} name - Variable name
|
|
356
|
+
* @returns {string} CSS var() reference
|
|
357
|
+
*/
|
|
358
|
+
getFoundationVarRef(name) {
|
|
359
|
+
return `var(--${name})`
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ============================================================
|
|
363
|
+
// Utility Methods
|
|
364
|
+
// ============================================================
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Get all shade levels
|
|
368
|
+
*
|
|
369
|
+
* @returns {number[]}
|
|
370
|
+
*/
|
|
371
|
+
getShadeLevels() {
|
|
372
|
+
return [...SHADE_LEVELS]
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Check if theme has any custom configuration
|
|
377
|
+
*
|
|
378
|
+
* @returns {boolean}
|
|
379
|
+
*/
|
|
380
|
+
hasCustomization() {
|
|
381
|
+
return (
|
|
382
|
+
Object.keys(this._palettes).length > 0 ||
|
|
383
|
+
Object.keys(this._contexts).length > 0 ||
|
|
384
|
+
Object.keys(this._fonts).length > 0
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Convert theme to a plain object (for serialization)
|
|
390
|
+
*
|
|
391
|
+
* @returns {Object}
|
|
392
|
+
*/
|
|
393
|
+
toJSON() {
|
|
394
|
+
return {
|
|
395
|
+
palettes: this._palettes,
|
|
396
|
+
colors: this._rawColors,
|
|
397
|
+
contexts: this._contexts,
|
|
398
|
+
fonts: this._fonts,
|
|
399
|
+
appearance: this._appearance,
|
|
400
|
+
foundationVars: this._foundationVars,
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
package/src/website.js
CHANGED
|
@@ -25,7 +25,7 @@ const LOCALE_NAMES = {
|
|
|
25
25
|
|
|
26
26
|
export default class Website {
|
|
27
27
|
constructor(websiteData) {
|
|
28
|
-
const { pages = [], theme = {}, config = {}, header, footer, left, right } = websiteData
|
|
28
|
+
const { pages = [], theme = {}, config = {}, header, footer, left, right, notFound } = websiteData
|
|
29
29
|
|
|
30
30
|
// Site metadata
|
|
31
31
|
this.name = config.name || ''
|
|
@@ -40,8 +40,12 @@ export default class Website {
|
|
|
40
40
|
this.leftPage = left || pages.find((p) => p.route === '/@left') || null
|
|
41
41
|
this.rightPage = right || pages.find((p) => p.route === '/@right') || null
|
|
42
42
|
|
|
43
|
+
// Store 404 page (for SPA routing)
|
|
44
|
+
// Convention: pages/404/ directory
|
|
45
|
+
this.notFoundPage = notFound || pages.find((p) => p.route === '/404') || null
|
|
46
|
+
|
|
43
47
|
// Filter out special pages from regular pages array
|
|
44
|
-
const specialRoutes = ['/@header', '/@footer', '/@left', '/@right']
|
|
48
|
+
const specialRoutes = ['/@header', '/@footer', '/@left', '/@right', '/404']
|
|
45
49
|
const regularPages = pages.filter((page) => !specialRoutes.includes(page.route))
|
|
46
50
|
|
|
47
51
|
// Store original page data for dynamic pages (needed to create instances on-demand)
|
|
@@ -769,6 +773,14 @@ export default class Website {
|
|
|
769
773
|
return this.getPageHierarchy({ nested: false, includeHidden })
|
|
770
774
|
}
|
|
771
775
|
|
|
776
|
+
/**
|
|
777
|
+
* Get the 404 (not found) page if defined
|
|
778
|
+
* @returns {Page|null} The 404 page or null
|
|
779
|
+
*/
|
|
780
|
+
getNotFoundPage() {
|
|
781
|
+
return this.notFoundPage
|
|
782
|
+
}
|
|
783
|
+
|
|
772
784
|
// ─────────────────────────────────────────────────────────────────
|
|
773
785
|
// Active Route API (for navigation components)
|
|
774
786
|
// ─────────────────────────────────────────────────────────────────
|