@uniweb/theming 0.1.15 → 0.2.0

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/theming",
3
- "version": "0.1.15",
3
+ "version": "0.2.0",
4
4
  "description": "Theming utilities for the Uniweb platform — shade generation, CSS generation, theme processing",
5
5
  "type": "module",
6
6
  "exports": {
@@ -12,32 +12,7 @@ import { generatePalettes } from './shade-generator.js'
12
12
  import { normalizeTokenValue } from './normalize.js'
13
13
 
14
14
  /**
15
- * Check if a block has any section-level overrides worth emitting.
16
- *
17
- * @param {Object} standardOptions - Block's standardOptions
18
- * @returns {boolean}
19
- */
20
- function hasOverrides(standardOptions) {
21
- if (!standardOptions) return false
22
-
23
- const { colors, foundationStyles } = standardOptions
24
-
25
- if (foundationStyles && Object.keys(foundationStyles).length > 0) return true
26
- if (!colors) return false
27
-
28
- const hasColors =
29
- colors.colors?.light && Object.keys(colors.colors.light).length > 0
30
- const hasElements =
31
- colors.elements &&
32
- Object.values(colors.elements).some(
33
- (ctx) => ctx && Object.keys(ctx).length > 0
34
- )
35
- return hasColors || hasElements
36
- }
37
-
38
- /**
39
- * Build CSS variable declarations for base palette overrides.
40
- * Base palette is context-independent — always stored under the 'light' key.
15
+ * Build CSS variable declarations for base palette overrides — context-independent.
41
16
  *
42
17
  * @param {Object} baseColors - e.g. { primary: "#3b82f6", accent: "#f59e0b" }
43
18
  * @returns {string[]} CSS declaration lines
@@ -75,7 +50,7 @@ function buildElementVars(elements) {
75
50
  }
76
51
 
77
52
  /**
78
- * Build CSS variable declarations for foundation style overrides.
53
+ * Build CSS variable declarations for variable overrides — a section's `vars`, a component's.
79
54
  *
80
55
  * @param {Object} styles - e.g. { "border-radius": "0.5rem" }
81
56
  * @returns {string[]} CSS declaration lines
@@ -92,13 +67,12 @@ function buildFoundationVars(styles) {
92
67
  }
93
68
 
94
69
  /**
95
- * Split foundation styles into flat (context-independent) and
96
- * context-keyed (color/gradient) values.
70
+ * Split variables into flat (context-independent) and context-keyed (color/gradient) values.
97
71
  *
98
72
  * Flat values: { "radius-xl": "1.5rem" }
99
73
  * Context-keyed values: { "colorful-bg": { light: "linear-gradient(...)", dark: "..." } }
100
74
  *
101
- * @param {Object} styles - Foundation styles object
75
+ * @param {Object} styles - a section's `vars`
102
76
  * @returns {{ flat: Object, contexts: Object }} - flat styles + { light: {...}, dark: {...} }
103
77
  */
104
78
  function splitFoundationStyles(styles) {
@@ -145,8 +119,21 @@ function buildRule(selector, vars) {
145
119
  * Works with both Block instances and plain data objects — only reads:
146
120
  * - block.stableId || block.id — for CSS selector
147
121
  * - block.themeName — '' for Auto, 'light'/'medium'/'dark' for Pinned
148
- * - block.standardOptions — { colors, foundationStyles }
122
+ * - block.themeOverrides — the section's `theme:`, as @uniweb/core normalizes it:
123
+ * `{ colors, contexts, vars, tokens }` — a palette, tokens per color context, the
124
+ * foundation's variables, and tokens for the section in any context
149
125
  * - block.componentVars — merged meta.js defaults + frontmatter overrides
126
+ * - block.childBlocks — child sections get the same, at any depth: rendered as sections,
127
+ * they carry the `#section-{id}` these rules select [2026-09-28]
128
+ *
129
+ * ⛔ An editor's older envelope, `block.standardOptions` — `{ colors, foundationStyles }` — is
130
+ * no longer read (2026-09-28): an editor writes the section's `theme`, and @uniweb/core no longer
131
+ * sets it.
132
+ *
133
+ * ⭐ Which context's values apply, for the section's `theme`: a pinned section uses its own
134
+ * context's; a section that follows the site (Auto) uses `light` under a light scheme and
135
+ * `dark` under `.scheme-dark` — two rules, so no knowledge of whether the site can go dark
136
+ * is needed. A context's own token beats one written beside `mode`, being more specific.
150
137
  *
151
138
  * @param {Array<Object>} blocks - Block data objects
152
139
  * @param {Object} appearance - Theme appearance config
@@ -158,59 +145,53 @@ export function buildSectionOverrides(blocks, appearance = {}) {
158
145
  if (!blocks || blocks.length === 0) return ''
159
146
 
160
147
  let css = ''
161
-
162
148
  for (const block of blocks) {
163
- const hasStandardOverrides = hasOverrides(block.standardOptions)
164
- const hasComponentVars = block.componentVars && Object.keys(block.componentVars).length > 0
165
-
166
- if (!hasStandardOverrides && !hasComponentVars) continue
167
-
168
- const { colors, foundationStyles } = block.standardOptions || {}
169
- const sectionId = block.stableId || block.id
170
- const selector = `#section-${sectionId}`
171
-
172
- const isAuto = !block.themeName
173
- const hasToggle = appearance.allowToggle
174
-
175
- // Base palette is context-independent (always under 'light' key)
176
- const paletteVars = buildPaletteVars(colors?.colors?.light)
177
-
178
- // Split foundation styles: flat (context-independent) + context-keyed (color/gradient)
179
- const { flat: flatFoundation, contexts: ctxFoundation } = splitFoundationStyles(foundationStyles)
180
- const foundationVars = buildFoundationVars(flatFoundation)
181
-
182
- // Component-level CSS vars are context-independent
183
- const compVars = buildFoundationVars(block.componentVars)
184
-
185
- if (isAuto && hasToggle) {
186
- // Dual rules: light-scoped elements + dark-scoped elements
187
- const lightElementVars = buildElementVars(colors?.elements?.light)
188
- const darkElementVars = buildElementVars(colors?.elements?.dark)
189
-
190
- // Context-aware foundation vars (color/gradient types)
191
- const lightFoundationCtx = buildFoundationVars(ctxFoundation.light)
192
- const darkFoundationCtx = buildFoundationVars(ctxFoundation.dark)
193
-
194
- // Context-independent rule: palette + flat foundation + component vars (apply in both schemes)
195
- const sharedVars = [...paletteVars, ...foundationVars, ...compVars]
196
- css += buildRule(selector, sharedVars)
197
-
198
- // Light-only overrides (elements + context-aware foundation vars)
199
- css += buildRule(`:root:not(.scheme-dark) ${selector}`, [...lightElementVars, ...lightFoundationCtx])
149
+ css += buildBlockRules(block, appearance || {})
150
+ if (Array.isArray(block.childBlocks) && block.childBlocks.length > 0) {
151
+ css += buildSectionOverrides(block.childBlocks, appearance)
152
+ }
153
+ }
154
+ return css
155
+ }
200
156
 
201
- // Dark-only overrides (elements + context-aware foundation vars)
202
- css += buildRule(`.scheme-dark ${selector}`, [...darkElementVars, ...darkFoundationCtx])
157
+ /**
158
+ * The rules for one section: declarations for any scheme, and for an Auto section under a
159
+ * light and under a dark scheme.
160
+ */
161
+ function buildBlockRules(block, appearance) {
162
+ const selector = `#section-${block.stableId || block.id}`
163
+ const isAuto = !block.themeName
164
+ const always = []
165
+ const light = []
166
+ const dark = []
167
+
168
+ // The component's variables: `meta.js` defaults with the section's values over them.
169
+ always.push(...buildFoundationVars(block.componentVars))
170
+
171
+ // The section's `theme:` — theme.yml's keys, scoped to the section.
172
+ const theme = block.themeOverrides
173
+ const themeDark = []
174
+ if (theme) {
175
+ const { flat, contexts: ctxVars } = splitFoundationStyles(theme.vars)
176
+ always.push(...buildPaletteVars(theme.colors), ...buildFoundationVars(flat), ...buildElementVars(theme.tokens))
177
+ if (isAuto) {
178
+ light.push(...buildElementVars(theme.contexts?.light), ...buildFoundationVars(ctxVars.light))
179
+ themeDark.push(...buildElementVars(theme.contexts?.dark), ...buildFoundationVars(ctxVars.dark))
180
+ dark.push(...themeDark)
203
181
  } else {
204
- // Single context: when toggle is off, always use 'light' bucket
205
- // (overrides are context-independent); when toggle is on, use pinned context
206
- const ctx = hasToggle ? (block.themeName || 'light') : 'light'
207
- const elementVars = buildElementVars(colors?.elements?.[ctx])
208
- const ctxFoundationVars = buildFoundationVars(ctxFoundation[ctx])
209
-
210
- const allVars = [...paletteVars, ...elementVars, ...foundationVars, ...compVars, ...ctxFoundationVars]
211
- css += buildRule(selector, allVars)
182
+ always.push(...buildElementVars(theme.contexts?.[block.themeName]), ...buildFoundationVars(ctxVars[block.themeName]))
212
183
  }
213
184
  }
214
185
 
215
- return css
186
+ return (
187
+ buildRule(selector, always) +
188
+ buildRule(`:root:not(.scheme-dark) ${selector}`, light) +
189
+ buildRule(`.scheme-dark ${selector}`, dark) +
190
+ // A site that follows the visitor's system goes dark through a media query before any
191
+ // class is set — as the theme's own dark tokens do (`generateDarkSchemeCSS`) — so the
192
+ // section's dark values follow it there too. Same specificity as the light rule, later.
193
+ (appearance.default === 'system' && themeDark.length > 0
194
+ ? `@media (prefers-color-scheme: dark) {\n${buildRule(`:root:not(.scheme-light) ${selector}`, themeDark)}}\n`
195
+ : '')
196
+ )
216
197
  }