@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/core",
3
- "version": "0.1.14",
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
  // ─────────────────────────────────────────────────────────────────