@m3ui-vue/nuxt 0.1.0 → 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/README.md CHANGED
@@ -10,6 +10,11 @@ Nuxt module for [m3ui-vue](https://m3ui.dev), the Material Design 3 component li
10
10
  It installs the library's plugin, auto-imports its components and composables, adds its stylesheet, and puts the
11
11
  user's theme (dark mode, palette, contrast) on the server-rendered page before the first paint.
12
12
 
13
+ > **Beta.** Server-side rendering and this module are new. Every component renders on the server and is tested
14
+ > there, but real apps will find cases we haven't: if something breaks, renders differently on the server, or Vue
15
+ > warns about a hydration mismatch, please [open an issue](https://github.com/SrWither/m3ui-vue-nuxt/issues) (or on
16
+ > [m3ui-vue](https://github.com/SrWither/m3ui-vue/issues) for a component).
17
+
13
18
  ## Install
14
19
 
15
20
  ```bash
@@ -32,6 +37,9 @@ export default defineNuxtConfig({
32
37
  })
33
38
  ```
34
39
 
40
+ Keep `vue` in your app's own dependencies, as Nuxt's starter does: with pnpm, an app that only gets Vue through
41
+ `nuxt` can end up loading a second copy on the server, which fails to render.
42
+
35
43
  The theme and the palettes are Tailwind CSS, so they go in your own stylesheet, after Tailwind:
36
44
 
37
45
  ```css
@@ -84,27 +92,60 @@ Everything `createM3UI()` takes, plus what the module itself does:
84
92
  | Option | Default | |
85
93
  |---|---|---|
86
94
  | `palette`, `seed`, `seedOptions`, `contrast`, `persistPalette`, `customPalettes`, `themeColor` | | As in `createM3UI()` |
87
- | `locale` | English | A preset's code (`'es'`, `'fr'`, `'pt'`, `'de'`, `'ja'`, `'zh'`, `'ko'`) or your own strings |
95
+ | `locale` | English | A preset's code (`'es'`, `'fr'`, `'pt'`, `'de'`, `'ja'`, `'zh'`, `'ko'`), your own strings, or `'auto'` to follow `@nuxtjs/i18n` |
88
96
  | `components` | `true` | Auto-import the components |
89
97
  | `composables` | `{ prefix: 'M3' }` | Auto-import the composables with this prefix; `false` to import them yourself |
90
98
  | `css` | `true` | Add `@m3ui-vue/m3ui-vue/styles` |
99
+ | `cookies` | `true` | Keep the theme, palette and contrast in cookies too, so the server renders with them |
91
100
  | `themeScript` | `true` | Inline `themeInitScript()` in `<head>` |
92
101
 
102
+ ## With @nuxtjs/i18n
103
+
104
+ `locale: 'auto'` makes the components' built-in text (pagination, pickers, dialogs…) follow `@nuxtjs/i18n`'s current
105
+ language, on the server and when the user switches it. A language tag picks its language's preset (`es-AR` → Spanish);
106
+ languages without one fall back to English.
107
+
108
+ ```ts
109
+ export default defineNuxtConfig({
110
+ modules: ['@m3ui-vue/nuxt', '@nuxtjs/i18n'],
111
+ i18n: {
112
+ locales: [{ code: 'en', language: 'en-US' }, { code: 'es', language: 'es-AR' }],
113
+ defaultLocale: 'en',
114
+ },
115
+ m3ui: { locale: 'auto' },
116
+ })
117
+ ```
118
+
119
+ With `'auto'` all seven presets are bundled (about 11 kB gzipped), since the language can change at any time.
120
+
93
121
  ## Server-side rendering
94
122
 
95
- The user's theme lives in `localStorage`, which the server can't read. The inlined script applies it to `<html>`
96
- before the page paints, so there's no flash of the default theme, but the composables still start from the defaults
97
- on the server (`theme` is `'system'`, `palette` is `'purple'`) and switch to the user's choice when the app hydrates.
98
- Render anything that depends on them inside `<ClientOnly>`, like an icon showing the current theme:
123
+ The library keeps the user's theme, palette and contrast in `localStorage`, and with `cookies` (on by default) in
124
+ cookies too, which the server reads. So `useM3Theme()` and `useM3ColorPalette()` report the user's choice while the
125
+ page renders on the server, and anything built from them (a theme toggle's icon, the selected palette swatch) hydrates
126
+ without mismatches, no `<ClientOnly>` needed:
99
127
 
100
128
  ```vue
101
- <ClientOnly>
129
+ <template>
102
130
  <MIconButton :icon="theme === 'dark' ? 'dark_mode' : 'light_mode'" label="Theme" @click="cycle" />
103
- </ClientOnly>
131
+ </template>
132
+
133
+ <script setup lang="ts">
134
+ const { theme, cycle } = useM3Theme()
135
+ </script>
104
136
  ```
105
137
 
138
+ The inlined script puts dark mode, the palette and the contrast on `<html>` before the page paints. What the server
139
+ can't know is the system's preferences: with the `'system'` theme or contrast, `theme` is `'system'` there (whether
140
+ the page ends up dark is decided in the browser), and `activeContrast` is `'standard'`. If cookies were cleared but
141
+ `localStorage` wasn't, the first render shows the defaults and switches to the stored choice once hydrated. The other way
142
+ around, a choice only in the cookies is used too: your server can set `m3-theme`, `m3-palette` or `m3-contrast` to apply
143
+ a theme saved in the user's account, for instance.
144
+
106
145
  The adaptive layouts (`MNavigationSuite`, `MListDetail`, `MSupportingPane`) measure their own size, which the server
107
- doesn't know either: they render their compact layout there and adapt on the client.
146
+ can't. With `cookies`, the window's size is kept in a cookie too (`m3-viewport`), so from the second visit on they
147
+ render on the server for the user's screen (one in a narrower column corrects itself on the client). On a first visit
148
+ they render their compact layout and adapt once the page is interactive.
108
149
 
109
150
  A `seed` default can't be generated before the app loads: on the first visit the page shows the baseline palette
110
151
  until it does (later visits use the cached scheme).
@@ -115,10 +156,18 @@ until it does (later visits use the cached scheme).
115
156
  pnpm install
116
157
  pnpm dev:prepare # stub the module and generate types
117
158
  pnpm dev # the playground, with the library linked from ../m3ui-vue
118
- pnpm test # builds a fixture app and checks its server-rendered HTML
159
+ pnpm test # builds fixture apps, checks their server-rendered HTML and hydrates one in Chromium
119
160
  pnpm test:types
120
161
  ```
121
162
 
163
+ The hydration test (`test/browser.test.ts`) drives the Chromium already on the machine through `playwright-core` (no
164
+ browser download): `/usr/bin/chromium` and the usual paths, or `CHROMIUM_PATH`. It fails on any hydration warning
165
+ or page error, including overlays open on load; without a Chromium it's skipped.
166
+
167
+ `dev:prepare` stubs the module (it loads `src/module.ts` on the fly), and with the stub the editor types the `m3ui`
168
+ options in the playground's `nuxt.config.ts` loosely (no autocompletion of palette ids, for instance). `pnpm build`
169
+ followed by `nuxi prepare playground` types them like the published package.
170
+
122
171
  ## License
123
172
 
124
173
  MIT
package/dist/module.d.mts CHANGED
@@ -15,9 +15,10 @@ type LocalePreset = keyof typeof LOCALE_PRESETS | 'en';
15
15
  interface ModuleOptions extends Omit<M3UIOptions, 'locale'> {
16
16
  /**
17
17
  * Built-in text: a preset's language code (`'es'`, `'fr'`, `'pt'`, `'de'`, `'ja'`, `'zh'`, `'ko'`; `'en'` is the
18
- * default) or your own strings, which override English.
18
+ * default), your own strings, which override English, or `'auto'` to follow `@nuxtjs/i18n`'s current language
19
+ * (by its language tag, `es-AR` → `es`; English where there's no preset), switching with it.
19
20
  */
20
- locale?: LocalePreset | Partial<M3Locale>;
21
+ locale?: LocalePreset | 'auto' | Partial<M3Locale>;
21
22
  /** Auto-import the components (`<MButton>`…). Each one is bundled only where it's used. Default `true`. */
22
23
  components?: boolean;
23
24
  /**
@@ -30,6 +31,13 @@ interface ModuleOptions extends Omit<M3UIOptions, 'locale'> {
30
31
  };
31
32
  /** Add the components' own stylesheet (`@m3ui-vue/m3ui-vue/styles`). Default `true`. */
32
33
  css?: boolean;
34
+ /**
35
+ * Remember the theme, the palette and the contrast level in cookies too, so the server renders with the user's
36
+ * choice: `useM3Theme()` and `useM3ColorPalette()` report it during server rendering and hydration, and UI built
37
+ * from them needs no `<ClientOnly>`. The window's size goes in one too, so the adaptive layouts (`MNavigationSuite`,
38
+ * `MListDetail`, `MSupportingPane`) render for the user's screen instead of compact. Default `true`.
39
+ */
40
+ cookies?: boolean;
33
41
  /**
34
42
  * Inline `themeInitScript()` in `<head>`, so dark mode, the palette and the contrast level the user picked are
35
43
  * on the server-rendered page before the first paint. Default `true`.
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.0.0"
6
6
  },
7
- "version": "0.1.0",
7
+ "version": "0.2.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join, dirname } from 'node:path';
3
- import { defineNuxtModule, useLogger, createResolver, resolvePath, tryResolveModule, addComponent, addImports, addTemplate, addTypeTemplate, addPlugin } from '@nuxt/kit';
3
+ import { defineNuxtModule, useLogger, createResolver, resolvePath, tryResolveModule, addComponent, addImports, hasNuxtModule, addTemplate, addTypeTemplate, addPlugin } from '@nuxt/kit';
4
4
 
5
5
  const LIB = "@m3ui-vue/m3ui-vue";
6
6
  const LOCALE_PRESETS = { es: "esLocale", fr: "frLocale", pt: "ptLocale", de: "deLocale", ja: "jaLocale", zh: "zhLocale", ko: "koLocale" };
@@ -23,12 +23,13 @@ const module$1 = defineNuxtModule({
23
23
  components: true,
24
24
  composables: { prefix: "M3" },
25
25
  css: true,
26
+ cookies: true,
26
27
  themeScript: true
27
28
  },
28
29
  async setup(options, nuxt) {
29
30
  const logger = useLogger("@m3ui-vue/nuxt");
30
31
  const resolver = createResolver(import.meta.url);
31
- const { components, composables, css, themeScript, locale, ...plugin } = options;
32
+ const { components, composables, css, cookies, themeScript, locale, ...plugin } = options;
32
33
  const entry = await resolvePath(LIB, { extensions: [".js", ".mjs"] });
33
34
  let api;
34
35
  try {
@@ -49,14 +50,24 @@ const module$1 = defineNuxtModule({
49
50
  addImports(api.values.filter((value) => /^use[A-Z]/.test(value.name)).map((value) => ({ name: value.name, as: `use${prefix}${value.name.slice(3)}`, from: value.import })));
50
51
  }
51
52
  if (css) nuxt.options.css.push(`${LIB}/styles`);
52
- const preset = typeof locale === "string" && locale !== "en" ? LOCALE_PRESETS[locale] : void 0;
53
- if (typeof locale === "string" && locale !== "en" && !preset) logger.warn(`Unknown locale preset '${locale}', using English`);
53
+ const auto = locale === "auto";
54
+ const preset = typeof locale === "string" && locale !== "en" && !auto ? LOCALE_PRESETS[locale] : void 0;
55
+ if (typeof locale === "string" && locale !== "en" && !auto && !preset) logger.warn(`Unknown locale preset '${locale}', using English`);
56
+ if (auto) {
57
+ nuxt.hook("modules:done", () => {
58
+ if (!hasNuxtModule("@nuxtjs/i18n")) logger.warn(`locale: 'auto' follows @nuxtjs/i18n, which isn't installed: using English`);
59
+ });
60
+ }
54
61
  addTemplate({
55
62
  filename: "m3ui.options.mjs",
56
63
  getContents: () => [
57
64
  preset ? `import { ${preset} } from '${LIB}/locales'` : "",
65
+ auto ? `import * as presets from '${LIB}/locales'` : "",
58
66
  `export const options = ${JSON.stringify(plugin)}`,
59
67
  `export const locale = ${preset ?? (typeof locale === "object" ? JSON.stringify(locale) : "undefined")}`,
68
+ `export const autoLocale = ${auto}`,
69
+ `export const localePresets = ${auto ? `{ ${Object.entries(LOCALE_PRESETS).map(([code, name]) => `${code}: presets.${name}`).join(", ")} }` : "{}"}`,
70
+ `export const cookies = ${JSON.stringify(cookies)}`,
60
71
  `export const themeScript = ${JSON.stringify(themeScript)}`
61
72
  ].join("\n")
62
73
  });
@@ -66,6 +77,9 @@ const module$1 = defineNuxtModule({
66
77
  `declare module '#build/m3ui.options.mjs' {`,
67
78
  ` export const options: import('${LIB}').M3UIOptions`,
68
79
  ` export const locale: Partial<import('${LIB}').M3Locale> | undefined`,
80
+ ` export const autoLocale: boolean`,
81
+ ` export const localePresets: Record<string, Partial<import('${LIB}').M3Locale>>`,
82
+ ` export const cookies: boolean`,
69
83
  ` export const themeScript: boolean`,
70
84
  `}`
71
85
  ].join("\n")
@@ -1,11 +1,25 @@
1
+ import { computed } from "vue";
1
2
  import { createM3UI, themeInitScript } from "@m3ui-vue/m3ui-vue";
2
- import { defineNuxtPlugin, useHead } from "#app";
3
- import { locale, options, themeScript } from "#build/m3ui.options.mjs";
3
+ import { defineNuxtPlugin, useCookie, useHead } from "#app";
4
+ import { autoLocale, cookies, locale, localePresets, options, themeScript } from "#build/m3ui.options.mjs";
4
5
  export default defineNuxtPlugin({
5
6
  name: "m3ui",
6
7
  enforce: "pre",
7
8
  setup(nuxtApp) {
8
- nuxtApp.vueApp.use(createM3UI({ ...options, locale, isHydrating: () => !!nuxtApp.isHydrating }));
9
+ const read = (name) => useCookie(name, { decode: decodeURIComponent, readonly: true }).value;
10
+ const [width, height] = (read("m3-viewport") ?? "").split("x").map(Number);
11
+ const initialState = cookies ? {
12
+ theme: read("m3-theme"),
13
+ palette: read("m3-palette"),
14
+ contrast: read("m3-contrast"),
15
+ viewport: width && height ? { width, height } : void 0
16
+ } : void 0;
17
+ const appLocale = autoLocale ? computed(() => {
18
+ const i18n = nuxtApp.$i18n;
19
+ const tag = i18n?.localeProperties?.value.language || i18n?.locale.value || "";
20
+ return localePresets[tag.toLowerCase().split("-")[0]] ?? {};
21
+ }) : locale;
22
+ nuxtApp.vueApp.use(createM3UI({ ...options, locale: appLocale, cookies, initialState, isHydrating: () => !!nuxtApp.isHydrating }));
9
23
  if (import.meta.server && themeScript) {
10
24
  useHead({
11
25
  script: [{
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "@m3ui-vue/nuxt",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Nuxt module for m3ui-vue, the Material Design 3 component library for Vue 3 + Tailwind CSS v4",
5
- "keywords": ["nuxt", "nuxt-module", "material-design", "material-3", "m3", "vue", "tailwindcss"],
5
+ "keywords": [
6
+ "nuxt",
7
+ "nuxt-module",
8
+ "material-design",
9
+ "material-3",
10
+ "m3",
11
+ "vue",
12
+ "tailwindcss"
13
+ ],
6
14
  "homepage": "https://m3ui.dev",
7
15
  "repository": {
8
16
  "type": "git",
@@ -41,19 +49,22 @@
41
49
  "@nuxt/kit": "^4.6.2"
42
50
  },
43
51
  "peerDependencies": {
44
- "@m3ui-vue/m3ui-vue": "^1.0.2"
52
+ "@m3ui-vue/m3ui-vue": "^1.0.3"
45
53
  },
46
54
  "devDependencies": {
47
- "@m3ui-vue/m3ui-vue": "^1.0.2",
55
+ "@m3ui-vue/m3ui-vue": "^1.0.3",
48
56
  "@nuxt/module-builder": "^1.0.3",
49
57
  "@nuxt/schema": "^4.6.2",
50
58
  "@nuxt/test-utils": "^4.3.4",
59
+ "@nuxtjs/i18n": "^10.6.0",
51
60
  "@tailwindcss/vite": "^4.3.3",
52
61
  "@types/node": "^24.0.0",
53
62
  "nuxt": "^4.6.2",
63
+ "playwright-core": "^1.64.1",
54
64
  "tailwindcss": "^4.3.3",
55
65
  "typescript": "^6.0.3",
56
66
  "vitest": "^5.0.5",
67
+ "vue": "^3.5.44",
57
68
  "vue-tsc": "^3.3.12"
58
69
  }
59
70
  }