@uniweb/theming 0.1.10 → 0.1.12

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.10",
3
+ "version": "0.1.12",
4
4
  "description": "Theming utilities for the Uniweb platform — shade generation, CSS generation, theme processing",
5
5
  "type": "module",
6
6
  "exports": {
@@ -26,7 +26,12 @@ const DEFAULT_CONTEXT_TOKENS = {
26
26
  'ring': 'var(--primary-500)',
27
27
  'link': 'var(--primary-600)',
28
28
  'link-hover': 'var(--primary-700)',
29
- 'accent': 'var(--accent-600)',
29
+ // Shade 500 is the site's colour exactly as authored (the palette anchors
30
+ // the input there). `link` and `primary` sit at 600 because they carry the
31
+ // strict contrast burden — body-size links, and white text on a fill. An
32
+ // accent is decorative emphasis, usually at display size, so it keeps the
33
+ // brand colour unmuddied instead of paying for contrast it doesn't need.
34
+ 'accent': 'var(--accent-500)',
30
35
  'primary': 'var(--primary-600)',
31
36
  'primary-foreground': 'white',
32
37
  'primary-hover': 'var(--primary-700)',
@@ -55,7 +60,7 @@ const DEFAULT_CONTEXT_TOKENS = {
55
60
  'ring': 'var(--primary-500)',
56
61
  'link': 'var(--primary-600)',
57
62
  'link-hover': 'var(--primary-700)',
58
- 'accent': 'var(--accent-600)',
63
+ 'accent': 'var(--accent-500)', // exact brand — see the light context
59
64
  'primary': 'var(--primary-600)',
60
65
  'primary-foreground': 'white',
61
66
  'primary-hover': 'var(--primary-700)',
@@ -84,6 +89,11 @@ const DEFAULT_CONTEXT_TOKENS = {
84
89
  'ring': 'var(--primary-500)',
85
90
  'link': 'var(--primary-400)',
86
91
  'link-hover': 'var(--primary-300)',
92
+ // Stays lightened, unlike the light/medium contexts which use the exact
93
+ // brand at 500. Every text token here is lightened (body, subtle, link)
94
+ // because a mid-brand hue on a near-black surface often isn't legible —
95
+ // a navy or forest-green accent at 500 would effectively disappear. A site
96
+ // whose brand does read on dark can set `contexts.dark.accent` to it.
87
97
  'accent': 'var(--accent-400)',
88
98
  'primary': 'var(--primary-500)',
89
99
  'primary-foreground': 'white',
@@ -108,6 +118,8 @@ const DEFAULT_CONTEXT_TOKENS = {
108
118
  const DEFAULT_COLORS = {
109
119
  primary: '#3b82f6', // Blue
110
120
  secondary: '#64748b', // Slate
121
+ // Last resort only. A site that names no accent inherits its primary (see
122
+ // processTheme) — this literal is reached solely when the primary is unusable.
111
123
  accent: '#8b5cf6', // Purple
112
124
  neutral: '#78716c', // Stone
113
125
  }
package/src/processor.js CHANGED
@@ -26,10 +26,19 @@ const NEUTRAL_PRESETS = {
26
26
  /**
27
27
  * Default inline text styles (content-author markdown: [text]{accent})
28
28
  * These reference semantic tokens so they adapt to context automatically
29
+ *
30
+ * `accent` resolves to --accent, not --link. It used to be --link, which
31
+ * conflated two roles that have genuinely different constraints: --link means
32
+ * "navigable text" and must stay readable at body size (4.5:1), while an accent
33
+ * is decorative emphasis, usually at display size (3:1 suffices). Welding them
34
+ * together meant a site that responsibly darkened its link colour also drained
35
+ * the brand colour out of its headlines, where the darker value buys nothing.
36
+ * It also left the `accent` colour family nearly unreachable from markdown, and
37
+ * contradicted `callout` right below, which already used --accent.
29
38
  */
30
39
  const DEFAULT_INLINE = {
31
40
  accent: {
32
- color: 'var(--link)',
41
+ color: 'var(--accent)',
33
42
  'font-weight': '600',
34
43
  },
35
44
  callout: {
@@ -369,6 +378,49 @@ export function validateThemeConfig(config) {
369
378
  }
370
379
  }
371
380
 
381
+ /**
382
+ * Merge inline text styles over the framework defaults, property by property.
383
+ *
384
+ * Everything else in theme.yml merges at the granularity of the leaf value —
385
+ * `contexts[name]` merges your tokens into the default context object, `colors`
386
+ * and `appearance` merge per key. This does the same one level down, so a site
387
+ * declares only the deltas:
388
+ *
389
+ * inline:
390
+ * accent:
391
+ * font-weight: inherit # keeps the default colour
392
+ *
393
+ * A shallow merge replaced the whole style object instead, so the example above
394
+ * silently produced `span[accent] { font-weight: inherit }` with no colour at
395
+ * all, and accented text rendered in the surrounding heading colour. The failure
396
+ * was subtractive and invisible in the config — you had to know what the default
397
+ * contained to notice it was gone. Three of the shipped templates carried a
398
+ * verbatim restatement of the default `accent` style purely to work around this.
399
+ *
400
+ * To *drop* a default property rather than change it, give it a neutral CSS
401
+ * value — `font-weight: inherit` to follow the surrounding text, or `initial` /
402
+ * `unset` / `revert`. Setting a whole style to null still removes it entirely
403
+ * (the CSS generator skips non-object entries).
404
+ *
405
+ * @param {Object} defaults - DEFAULT_INLINE
406
+ * @param {Object} [overrides] - rawConfig.inline
407
+ * @returns {Object} merged style map
408
+ */
409
+ function mergeInlineStyles(defaults, overrides) {
410
+ const merged = { ...defaults }
411
+
412
+ for (const [name, style] of Object.entries(overrides || {})) {
413
+ // Non-objects (null/false) pass through so a site can delete a default
414
+ // style outright; anything else layers over the default of the same name.
415
+ merged[name] =
416
+ style && typeof style === 'object' && !Array.isArray(style)
417
+ ? { ...(defaults[name] || {}), ...style }
418
+ : style
419
+ }
420
+
421
+ return merged
422
+ }
423
+
372
424
  /**
373
425
  * Normalize appearance configuration
374
426
  *
@@ -486,6 +538,19 @@ export function processTheme(rawConfig = {}, options = {}) {
486
538
  rawColors.neutral = NEUTRAL_PRESETS[rawColors.neutral]
487
539
  }
488
540
 
541
+ // A site that declares no accent colour almost always means "my brand
542
+ // colour" — an accent is an optional second voice, not a required one. The
543
+ // alternative is a fixed hue nobody chose showing up wherever markdown says
544
+ // [text]{accent} or {callout}. Falling back to primary also makes the
545
+ // --accent token safe to use as a default elsewhere: it is always *a* brand
546
+ // colour rather than a stock purple. The literal in DEFAULT_COLORS survives
547
+ // only as a last resort for a site whose primary is unusable.
548
+ const brandColor = rawColors.primary ?? defaultColors.primary
549
+ const isUsableColor = (v) => (typeof v === 'object' && v !== null) || isValidColor(v)
550
+ if (rawColors.accent === undefined && isUsableColor(brandColor)) {
551
+ rawColors.accent = brandColor
552
+ }
553
+
489
554
  // Filter to only valid colors (skip invalid ones in non-strict mode)
490
555
  const validColors = {}
491
556
  for (const [name, value] of Object.entries({ ...defaultColors, ...rawColors })) {
@@ -637,8 +702,7 @@ export function processTheme(rawConfig = {}, options = {}) {
637
702
  const background = rawConfig.background || null
638
703
 
639
704
  // Inline text styles (semantic names → CSS declarations)
640
- // Merge framework defaults with user overrides (user values win)
641
- const inline = { ...DEFAULT_INLINE, ...(rawConfig.inline || {}) }
705
+ const inline = mergeInlineStyles(DEFAULT_INLINE, rawConfig.inline)
642
706
 
643
707
  const config = {
644
708
  colors, // Raw colors for CSS generator