wave-ui 4.1.1 → 4.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": "wave-ui",
3
- "version": "4.1.1",
3
+ "version": "4.2.0",
4
4
  "description": "A UI framework for Vue.js 3 (and 2) with only the bright side. :sunny:",
5
5
  "author": "Antoni Andre <antoniandre.web@gmail.com>",
6
6
  "homepage": "https://antoniandre.github.io/wave-ui",
@@ -2,7 +2,7 @@ import { reactive, inject } from 'vue'
2
2
  import { mergeConfig } from './utils/config'
3
3
  import { consoleWarn } from './utils/console'
4
4
  import { colorPalette, generateColorShades, flattenColors } from './utils/colors'
5
- import { injectColorsCSSInDOM, injectCSSInDOM } from './utils/dynamic-css'
5
+ import { injectColorsCSSInDOM, injectCSSInDOM, generatePaletteVariables, generateColors } from './utils/dynamic-css'
6
6
  import { injectNotifManagerInDOM, NotificationManager } from './utils/notification-manager'
7
7
  import { waveRippleDirective } from './utils/wave-ripple-directive'
8
8
  import { scheduleFocus, registerVFocus, unregisterVFocus } from './utils/focus'
@@ -88,9 +88,12 @@ export default class WaveUI {
88
88
  * @param {string} theme - The theme to switch to.
89
89
  */
90
90
  switchTheme (theme) {
91
+ // Only remove the current colors stylesheet when the theme actually changes.
92
+ // This prevents a blink when switchTheme is called on first mount with the same theme
93
+ // that was already set during SSR (via getSSRStyles + useHead) or in the constructor.
94
+ if (this.theme !== theme) document.head.querySelector('#wave-ui-colors')?.remove?.()
91
95
  this.theme = theme
92
96
  document.documentElement.setAttribute('data-theme', theme)
93
- document.head.querySelector('#wave-ui-colors')?.remove?.()
94
97
  const themeColors = this.config.colors[this.theme]
95
98
  injectColorsCSSInDOM(themeColors, colorPalette, this.config.css.colorShadeCssVariables)
96
99
  this.colors = flattenColors(themeColors, colorPalette)
@@ -107,9 +110,92 @@ export default class WaveUI {
107
110
  wApp.className = 'w-app' // First reset the classes.
108
111
  if (classes.length && classes[0]) wApp.classList.add(...classes)
109
112
  }
113
+ },
114
+
115
+ /**
116
+ * Returns the CSS strings for both Wave UI stylesheets so they can be injected
117
+ * server-side (e.g. via Nuxt's useHead) to prevent FOUC.
118
+ * The returned strings map to the `#wave-ui-palette` and `#wave-ui-colors` <style> tags.
119
+ *
120
+ * When called without a `theme` argument (recommended), `colors` contains both themes'
121
+ * custom color variables each scoped to `[data-theme="light"]` / `[data-theme="dark"]`.
122
+ * Combined with `WaveUI.getThemeInitScript()` injected as a blocking <script> in <head>,
123
+ * this guarantees the correct colors are present at first paint — no flash of wrong theme.
124
+ *
125
+ * When called with an explicit `theme`, returns only that theme's variables on `:root`
126
+ * (legacy / single-theme use cases).
127
+ *
128
+ * @param {string} [theme] - Explicit theme ('light'|'dark'). Omit for dual-scoped output.
129
+ * @returns {{ theme: string, palette: string, colors: string }}
130
+ */
131
+ getSSRStyles (theme) {
132
+ const palette = generatePaletteVariables(colorPalette)
133
+ const { colorShadeCssVariables } = this.config.css
134
+
135
+ // No theme specified → emit both themes scoped to [data-theme="X"] so a blocking init
136
+ // script only needs to set the attribute — no color flash is possible.
137
+ if (!theme) {
138
+ return {
139
+ theme: this.theme || this.config?.theme || 'light',
140
+ palette,
141
+ colors:
142
+ generateColors(this.config.colors.light, colorShadeCssVariables, '[data-theme="light"]') +
143
+ generateColors(this.config.colors.dark, colorShadeCssVariables, '[data-theme="dark"]')
144
+ }
145
+ }
146
+
147
+ return {
148
+ theme,
149
+ palette,
150
+ colors: generateColors(this.config.colors[theme], colorShadeCssVariables)
151
+ }
110
152
  }
111
153
  }
112
154
 
155
+ /**
156
+ * Resolves the initial theme from localStorage or OS preference.
157
+ * Use this as the `theme` install option so Wave UI is initialized with the correct
158
+ * theme from the very first render — no flash, no manual localStorage reading needed.
159
+ *
160
+ * app.use(WaveUI, { theme: WaveUI.resolveInitialTheme() })
161
+ *
162
+ * On the server (SSR) where localStorage/window are unavailable, always returns 'light'
163
+ * as the safe fallback — pair with `getSSRStyles()` + `getThemeInitScript()` to handle
164
+ * the server → client handoff without FOUC.
165
+ *
166
+ * @param {string} [storageKey='waveui-theme'] - The localStorage key to read.
167
+ * @returns {'light'|'dark'}
168
+ */
169
+ static resolveInitialTheme (storageKey = 'waveui-theme') {
170
+ if (typeof window === 'undefined') return 'light'
171
+ try {
172
+ const stored = localStorage.getItem(storageKey)
173
+ if (stored === 'light' || stored === 'dark') return stored
174
+ }
175
+ catch { }
176
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
177
+ }
178
+
179
+ /**
180
+ * Returns a minified inline script that sets `data-theme` on `<html>` synchronously,
181
+ * before any CSS is parsed or rendered — eliminating theme FOUC entirely.
182
+ *
183
+ * Inject it as a blocking (no async/defer) <script> in <head> before any CSS:
184
+ *
185
+ * Nuxt — nuxt.config.ts:
186
+ * app: { head: { script: [{ innerHTML: WaveUI.getThemeInitScript() }] } }
187
+ *
188
+ * Plain HTML:
189
+ * <script><%= WaveUI.getThemeInitScript() %></script>
190
+ *
191
+ * @param {string} [storageKey='waveui-theme'] - Must match the key used when saving the theme.
192
+ * @returns {string} Minified inline script string (no <script> tags).
193
+ */
194
+ static getThemeInitScript (storageKey = 'waveui-theme') {
195
+ const key = JSON.stringify(storageKey)
196
+ return `(function(){try{var t=localStorage.getItem(${key});if(t==='dark'||t==='light'){document.documentElement.setAttribute('data-theme',t);return}}catch(e){}if(window.matchMedia('(prefers-color-scheme:dark)').matches)document.documentElement.setAttribute('data-theme','dark')})()`
197
+ }
198
+
113
199
  static install (app, options = {}) {
114
200
  // Register directives.
115
201
  app.directive('focus', {
@@ -159,7 +245,12 @@ export default class WaveUI {
159
245
  }
160
246
 
161
247
  if (config.theme === 'auto') detectOSDarkMode($waveui) // Also switches the theme.
162
- else $waveui.switchTheme(config.theme, true)
248
+ else {
249
+ // Respect any data-theme already set on <html> (e.g. by getThemeInitScript) so a
250
+ // blocking init script is enough — no need to pass `theme` to app.use(WaveUI, ...).
251
+ const docTheme = document.documentElement.getAttribute('data-theme')
252
+ $waveui.switchTheme(docTheme === 'light' || docTheme === 'dark' ? docTheme : config.theme)
253
+ }
163
254
 
164
255
  injectCSSInDOM($waveui)
165
256
  injectNotifManagerInDOM(app)
@@ -203,6 +294,7 @@ export default class WaveUI {
203
294
  app.provide('$waveui', $waveui)
204
295
 
205
296
  if (config.theme !== 'auto') {
297
+ this.$waveui.theme = config.theme
206
298
  this.$waveui.colors = flattenColors(config.colors[config.theme], colorPalette)
207
299
  }
208
300
  }
@@ -9,7 +9,7 @@ const cssVars = {
9
9
  let breakpointsDef = { keys: [], values: [] }
10
10
  let currentBreakpoint = null
11
11
 
12
- const generatePaletteVariables = colorPalette => {
12
+ export const generatePaletteVariables = colorPalette => {
13
13
  let cssVariablesString = ''
14
14
 
15
15
  colorPalette.forEach(({ label, color, shades = [] }) => {
@@ -26,7 +26,7 @@ const generatePaletteVariables = colorPalette => {
26
26
  // :root {[color1-variable], [color2-variable]}
27
27
  // .color1--bg {background-color: [color1-variable]}
28
28
  // .color1 {color: [color1-variable]}
29
- const generateColors = (themeColors, generateShadeCssVariables) => {
29
+ export const generateColors = (themeColors, generateShadeCssVariables, scope = ':root') => {
30
30
  let styles = ''
31
31
  let cssVariablesString = ''
32
32
 
@@ -62,7 +62,7 @@ const generateColors = (themeColors, generateShadeCssVariables) => {
62
62
  for (const colorName in shades) cssVariablesString += `--w-${colorName}-color: ${shades[colorName]};`
63
63
  }
64
64
 
65
- return `:root{${cssVariablesString}}${styles}`
65
+ return `${scope}{${cssVariablesString}}${styles}`
66
66
  }
67
67
 
68
68
  // Generate the layout grid. E.g. xs1, xs2, ..., xl12.