@uniweb/theming 0.1.15 → 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/theming",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "Theming utilities for the Uniweb platform — shade generation, CSS generation, theme processing",
5
5
  "type": "module",
6
6
  "exports": {
@@ -145,8 +145,19 @@ function buildRule(selector, vars) {
145
145
  * Works with both Block instances and plain data objects — only reads:
146
146
  * - block.stableId || block.id — for CSS selector
147
147
  * - block.themeName — '' for Auto, 'light'/'medium'/'dark' for Pinned
148
- * - block.standardOptions — { colors, foundationStyles }
148
+ * - block.themeOverrides — the section's `theme:`, as @uniweb/core normalizes it:
149
+ * `{ colors, contexts, vars, tokens }` — a palette, tokens per color context, the
150
+ * foundation's variables, and tokens for the section in any context
149
151
  * - block.componentVars — merged meta.js defaults + frontmatter overrides
152
+ * - block.standardOptions — ⚠️ the older editor envelope, `{ colors, foundationStyles }`,
153
+ * read by its own rules until an editor writes the section's `theme` instead
154
+ * - block.childBlocks — child sections get the same, at any depth: rendered as sections,
155
+ * they carry the `#section-{id}` these rules select [2026-09-28]
156
+ *
157
+ * ⭐ Which context's values apply, for the section's `theme`: a pinned section uses its own
158
+ * context's; a section that follows the site (Auto) uses `light` under a light scheme and
159
+ * `dark` under `.scheme-dark` — two rules, so no knowledge of whether the site can go dark
160
+ * is needed. A context's own token beats one written beside `mode`, being more specific.
150
161
  *
151
162
  * @param {Array<Object>} blocks - Block data objects
152
163
  * @param {Object} appearance - Theme appearance config
@@ -158,59 +169,78 @@ export function buildSectionOverrides(blocks, appearance = {}) {
158
169
  if (!blocks || blocks.length === 0) return ''
159
170
 
160
171
  let css = ''
161
-
162
172
  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}`
173
+ css += buildBlockRules(block, appearance || {})
174
+ if (Array.isArray(block.childBlocks) && block.childBlocks.length > 0) {
175
+ css += buildSectionOverrides(block.childBlocks, appearance)
176
+ }
177
+ }
178
+ return css
179
+ }
171
180
 
172
- const isAuto = !block.themeName
181
+ /**
182
+ * The rules for one section: declarations for any scheme, and for an Auto section under a
183
+ * light and under a dark scheme.
184
+ */
185
+ function buildBlockRules(block, appearance) {
186
+ const selector = `#section-${block.stableId || block.id}`
187
+ const isAuto = !block.themeName
188
+ const always = []
189
+ const light = []
190
+ const dark = []
191
+
192
+ // ⚠️ The older editor envelope — its rules unchanged, the `light` bucket without a toggle
193
+ // included, so an editor sending it renders as before.
194
+ const legacy = block.standardOptions
195
+ const compVars = buildFoundationVars(block.componentVars)
196
+ if (hasOverrides(legacy)) {
197
+ const { colors, foundationStyles } = legacy
173
198
  const hasToggle = appearance.allowToggle
174
-
175
- // Base palette is context-independent (always under 'light' key)
176
199
  const paletteVars = buildPaletteVars(colors?.colors?.light)
177
-
178
- // Split foundation styles: flat (context-independent) + context-keyed (color/gradient)
179
200
  const { flat: flatFoundation, contexts: ctxFoundation } = splitFoundationStyles(foundationStyles)
180
201
  const foundationVars = buildFoundationVars(flatFoundation)
181
-
182
- // Component-level CSS vars are context-independent
183
- const compVars = buildFoundationVars(block.componentVars)
184
-
185
202
  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])
200
-
201
- // Dark-only overrides (elements + context-aware foundation vars)
202
- css += buildRule(`.scheme-dark ${selector}`, [...darkElementVars, ...darkFoundationCtx])
203
+ always.push(...paletteVars, ...foundationVars, ...compVars)
204
+ light.push(...buildElementVars(colors?.elements?.light), ...buildFoundationVars(ctxFoundation.light))
205
+ dark.push(...buildElementVars(colors?.elements?.dark), ...buildFoundationVars(ctxFoundation.dark))
203
206
  } 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
207
  const ctx = hasToggle ? (block.themeName || 'light') : 'light'
207
- const elementVars = buildElementVars(colors?.elements?.[ctx])
208
- const ctxFoundationVars = buildFoundationVars(ctxFoundation[ctx])
208
+ always.push(
209
+ ...paletteVars,
210
+ ...buildElementVars(colors?.elements?.[ctx]),
211
+ ...foundationVars,
212
+ ...compVars,
213
+ ...buildFoundationVars(ctxFoundation[ctx])
214
+ )
215
+ }
216
+ } else {
217
+ always.push(...compVars)
218
+ }
209
219
 
210
- const allVars = [...paletteVars, ...elementVars, ...foundationVars, ...compVars, ...ctxFoundationVars]
211
- css += buildRule(selector, allVars)
220
+ // The section's `theme:` — theme.yml's keys, scoped to the section.
221
+ const theme = block.themeOverrides
222
+ const themeDark = []
223
+ if (theme) {
224
+ const { flat, contexts: ctxVars } = splitFoundationStyles(theme.vars)
225
+ always.push(...buildPaletteVars(theme.colors), ...buildFoundationVars(flat), ...buildElementVars(theme.tokens))
226
+ if (isAuto) {
227
+ light.push(...buildElementVars(theme.contexts?.light), ...buildFoundationVars(ctxVars.light))
228
+ themeDark.push(...buildElementVars(theme.contexts?.dark), ...buildFoundationVars(ctxVars.dark))
229
+ dark.push(...themeDark)
230
+ } else {
231
+ always.push(...buildElementVars(theme.contexts?.[block.themeName]), ...buildFoundationVars(ctxVars[block.themeName]))
212
232
  }
213
233
  }
214
234
 
215
- return css
235
+ return (
236
+ buildRule(selector, always) +
237
+ buildRule(`:root:not(.scheme-dark) ${selector}`, light) +
238
+ buildRule(`.scheme-dark ${selector}`, dark) +
239
+ // A site that follows the visitor's system goes dark through a media query before any
240
+ // class is set — as the theme's own dark tokens do (`generateDarkSchemeCSS`) — so the
241
+ // section's dark values follow it there too. Same specificity as the light rule, later.
242
+ (appearance.default === 'system' && themeDark.length > 0
243
+ ? `@media (prefers-color-scheme: dark) {\n${buildRule(`:root:not(.scheme-light) ${selector}`, themeDark)}}\n`
244
+ : '')
245
+ )
216
246
  }