@marwes-ui/vue 1.0.1 → 1.0.3

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
@@ -1,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- <img alt="Marwes Design System" src="https://raw.githubusercontent.com/niklas-westman/marwes/main/.github/assets/banner-light.png" width="100%" style="border-radius: 40px;">
3
+ <img alt="Marwes Design System" src="https://raw.githubusercontent.com/niklas-westman/marwes/main/.github/assets/banner.png" width="100%" style="border-radius: 40px;">
4
4
 
5
5
  <br>
6
6
  <br>
@@ -309,6 +309,71 @@ import ThemeToggle from "./theme-toggle.vue"
309
309
 
310
310
  `default-mode` sets the initial uncontrolled mode. `toggleMode()` updates the provider, so Marwes components, preset CSS, and custom app styles that use `--mw-*` variables all move together without duplicating local theme state.
311
311
 
312
+ ### System And Persisted Preference
313
+
314
+ `mode` is always the rendered visual mode: `ThemeMode.light` or `ThemeMode.dark`. `preference` is the app or user choice: `ThemeMode.light`, `ThemeMode.dark`, or `"system"`. Use `preference` and `default-preference` when your UI needs a system option; keep using `mode`, `default-mode`, and `on-mode-change` when you only need concrete light/dark compatibility.
315
+
316
+ ```vue
317
+ <script setup lang="ts">
318
+ import { Button, ButtonVariant, ThemeMode, useThemeMode } from "@marwes-ui/vue"
319
+
320
+ const { mode, preference, systemMode, isSystem, setPreference, setMode } = useThemeMode()
321
+ </script>
322
+
323
+ <template>
324
+ <p>Rendering {{ mode }}; preference is {{ preference }}; system is {{ systemMode }}.</p>
325
+ <Button :variant="ButtonVariant.secondary" @click="setPreference('system')">
326
+ Use system
327
+ </Button>
328
+ <Button :variant="ButtonVariant.secondary" @click="setMode(ThemeMode.dark)">
329
+ Use dark
330
+ </Button>
331
+ <span v-if="isSystem">Following system</span>
332
+ </template>
333
+ ```
334
+
335
+ ```vue
336
+ <script setup lang="ts">
337
+ import { MarwesProvider } from "@marwes-ui/vue"
338
+ import ThemeMenu from "./theme-menu.vue"
339
+ </script>
340
+
341
+ <template>
342
+ <MarwesProvider default-preference="system" storage-key="marwes-theme" enable-system>
343
+ <ThemeMenu />
344
+ <AppShell />
345
+ </MarwesProvider>
346
+ </template>
347
+ ```
348
+
349
+ `useThemeMode().mode` never returns `"system"`; it is always the concrete mode Marwes rendered. `useThemeMode().preference` returns the active app/user preference, and `setPreference("system")` is the system-capable setter. `setMode(mode)` remains a concrete light/dark convenience.
350
+
351
+ `storage-key` is opt-in and defaults to `false`. Storage reads and writes are failure-safe, so private browsing or unavailable storage will fall back to normal provider state. `enable-system` defaults to `true`; set it to `false` to avoid `matchMedia` detection and resolve `"system"` through the light fallback.
352
+
353
+ This layer does not provide SSR no-flash behavior. Server output can still render the fallback mode before the client applies a stored or system preference. Use it for client-side preference and persistence, not for a no-flash SSR guarantee.
354
+
355
+ ### Root Target Sync
356
+
357
+ By default Marwes keeps theme state scoped to the provider element. If your app shell also needs the resolved mode on `html` or `body`, set `target` and `attribute`:
358
+
359
+ ```vue
360
+ <template>
361
+ <MarwesProvider
362
+ default-preference="system"
363
+ storage-key="marwes-theme"
364
+ target="html"
365
+ attribute="class"
366
+ disable-transition-on-change
367
+ >
368
+ <AppShell />
369
+ </MarwesProvider>
370
+ </template>
371
+ ```
372
+
373
+ `target` can be `"provider"`, `"html"`, or `"body"`. `attribute` can be `"class"`, `"data-theme"`, or `"data-mode"`. Class mode only adds the active `light` / `dark` class and removes the opposite one; unrelated classes are preserved. The provider root still keeps `mw-theme--light` / `mw-theme--dark` and provider-scoped `--mw-*` variables in every target mode.
374
+
375
+ Use one global-target provider per app shell. If multiple providers target `html` or `body`, deciding which provider owns that global element is app-owned behavior.
376
+
312
377
  For a simple brand pass, override shared values once and let Marwes fill the rest. If your product needs different brand colors in light and dark mode, control `mode` and switch between two small `ThemeInput` override objects:
313
378
 
314
379
  ```vue
package/dist/index.d.ts CHANGED
@@ -1,40 +1,71 @@
1
1
  import * as vue from 'vue';
2
2
  import { ComputedRef, VNodeChild } from 'vue';
3
3
  import * as _marwes_ui_core from '@marwes-ui/core';
4
- import { ThemeInput, ThemeMode, FontLoadingConfig, ResolvedTheme, ButtonOptions, InputOptions, SelectOptions, SelectAppearance as SelectAppearance$1, SelectOption as SelectOption$1, TextareaOptions, RichTextOptions, InputOtpOptions, CurrencyCode, iconRegistry, IconSize, IconStrokeWidth, AvatarOptions, CheckboxProps as CheckboxProps$1, DividerOptions, DividerSize, DividerOrientation, SpinnerOptions, SpacingOptions, SpacingSize, HeadingOptions, HeadingSize, ParagraphOptions, ParagraphSize, ToastOptions, ToastVariant, ToastIntent, ToastPlacement, TooltipOptions, IconNameType, RadioOptions, BadgeOptions, SwitchOptions, SliderOptions, TabOptions, DialogOptions } from '@marwes-ui/core';
5
- export { AvatarSize, AvatarType, BadgeVariant, ButtonAction, ButtonSize, ButtonVariant, Density, FontLoadingConfig, FontLoadingMode, FontLoadingOptions, IconName, MwAvailableFont, MwFontFallback, MwGoogleFontFamily, MwStyledTheme, MwThemeVarName, MwThemeVarNames, MwThemeVarReference, MwThemeVars, ResolvedTheme, SpacingSize, Spacings, SwitchSize, Theme, ThemeInput, ThemeMode, ToneName, createFontStack, mwAvailableFonts, mwFontFallbacks, mwGoogleFontFamilies, mwStyledTheme, mwThemeVarNames, mwThemeVars, mwVar } from '@marwes-ui/core';
4
+ import { ThemeInput, ThemePreference, ThemeMode, FontLoadingConfig, ResolvedTheme, ButtonOptions, InputOptions, SelectOptions, SelectAppearance as SelectAppearance$1, SelectOption as SelectOption$1, TextareaOptions, RichTextOptions, InputOtpOptions, CurrencyCode, iconRegistry, IconSize, IconStrokeWidth, AvatarOptions, CheckboxProps as CheckboxProps$1, DividerOptions, DividerSize, DividerOrientation, SpinnerOptions, SpacingOptions, SpacingSize, HeadingOptions, HeadingSize, ParagraphOptions, ParagraphSize, ToastOptions, ToastVariant, ToastIntent, ToastPlacement, TooltipOptions, IconNameType, RadioOptions, BadgeOptions, SwitchOptions, SliderOptions, TabOptions, DialogOptions } from '@marwes-ui/core';
5
+ export { AvatarSize, AvatarType, BadgeVariant, ButtonAction, ButtonSize, ButtonVariant, Density, FontLoadingConfig, FontLoadingMode, FontLoadingOptions, IconName, MwAvailableFont, MwFontFallback, MwGoogleFontFamily, MwStyledTheme, MwThemeVarName, MwThemeVarNames, MwThemeVarReference, MwThemeVars, ResolvedTheme, SpacingSize, Spacings, SwitchSize, Theme, ThemeInput, ThemeMode, ThemePreference, ToneName, createFontStack, mwAvailableFonts, mwFontFallbacks, mwGoogleFontFamilies, mwStyledTheme, mwThemeVarNames, mwThemeVars, mwVar } from '@marwes-ui/core';
6
+
7
+ type ThemeTarget = "provider" | "html" | "body";
8
+ type ThemeAttribute = "class" | "data-theme" | "data-mode";
6
9
 
7
10
  type MarwesProviderProps = {
8
11
  theme?: ThemeInput;
12
+ defaultPreference?: ThemePreference;
13
+ preference?: ThemePreference;
9
14
  defaultMode?: ThemeMode;
10
15
  mode?: ThemeMode;
11
16
  fontLoading?: FontLoadingConfig;
17
+ onPreferenceChange?: (preference: ThemePreference) => void;
12
18
  onModeChange?: (mode: ThemeMode) => void;
19
+ storageKey?: string | false;
20
+ enableSystem?: boolean;
21
+ target?: ThemeTarget;
22
+ attribute?: ThemeAttribute;
23
+ disableTransitionOnChange?: boolean;
13
24
  };
14
25
  declare const MarwesProvider: vue.DefineComponent<{
15
26
  mode?: any;
27
+ target?: any;
28
+ attribute?: any;
16
29
  theme?: any;
30
+ defaultPreference?: any;
31
+ preference?: any;
17
32
  defaultMode?: any;
18
33
  fontLoading?: any;
34
+ onPreferenceChange?: any;
19
35
  onModeChange?: any;
36
+ storageKey?: any;
37
+ enableSystem?: any;
38
+ disableTransitionOnChange?: any;
20
39
  }, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
21
40
  [key: string]: any;
22
41
  }>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, string, vue.PublicProps, Readonly<{
23
42
  mode?: any;
43
+ target?: any;
44
+ attribute?: any;
24
45
  theme?: any;
46
+ defaultPreference?: any;
47
+ preference?: any;
25
48
  defaultMode?: any;
26
49
  fontLoading?: any;
50
+ onPreferenceChange?: any;
27
51
  onModeChange?: any;
52
+ storageKey?: any;
53
+ enableSystem?: any;
54
+ disableTransitionOnChange?: any;
28
55
  }> & Readonly<{}>, {}, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
29
56
 
30
57
  declare function useTheme(): ResolvedTheme;
31
58
 
32
59
  type ThemeModeContextValue = {
33
60
  mode: ComputedRef<ThemeMode>;
61
+ preference: ComputedRef<ThemePreference>;
62
+ systemMode: ComputedRef<ThemeMode>;
34
63
  setMode: (mode: ThemeMode) => void;
64
+ setPreference: (preference: ThemePreference) => void;
35
65
  toggleMode: () => void;
36
66
  isDark: ComputedRef<boolean>;
37
67
  isLight: ComputedRef<boolean>;
68
+ isSystem: ComputedRef<boolean>;
38
69
  };
39
70
  declare function useThemeMode(): ThemeModeContextValue;
40
71
 
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import '@marwes-ui/presets/firstEdition/styles.css';
2
- import { ThemeMode, resolveThemeInput, resolveIconSize, resolveIconStrokeWidth, iconRegistry, resolveIconA11y, createSpinnerRecipe, createButtonRecipe, ButtonAction, ButtonVariant, IconName, createInputRecipe, createSelectRecipe, resolveSelectMode, createTextareaRecipe, createRichTextRecipe, resolveRichTextAllowedFormats, normalizeRichTextHtml, isRichTextHtmlEmpty, paragraphRecipe, buildInputFieldA11yIds, sanitizeInputOtpValue, createInputOtpRecipe, buildRichTextFieldA11yIds, buildCurrencyHelperText, getCurrencySymbol, sanitizeCurrencyValue, createAvatarRecipe, createPurposeSemanticAttributes, checkboxRecipe, buildCheckboxFieldA11yIds, buildCheckboxGroupFieldA11yIds, createDividerRecipe, createSpacingRecipe, createCardRecipe, createToastRecipe, createTooltipRecipe, createAccordionRecipe, buildAccordionFieldA11yIds, radioRecipe, buildRadioGroupFieldA11yIds, createBadgeRecipe, createSwitchRecipe, buildSwitchFieldA11yIds, createSliderRecipe, buildSliderFieldA11yIds, createTabRecipe, resolveTabValue, buildTabGroupA11yIds, createDialogRecipe, themeToCSSVars, richTextCommandByFormat, headingRecipe, moveTabSelection, extractUsedWeights, extractFontFamilyName, shouldLoadGoogleFont, buildGoogleFontsUrl } from '@marwes-ui/core';
2
+ import { ThemeMode, resolveThemePreference, resolveThemeInput, resolveIconSize, resolveIconStrokeWidth, iconRegistry, resolveIconA11y, createSpinnerRecipe, createButtonRecipe, ButtonAction, ButtonVariant, IconName, createInputRecipe, createSelectRecipe, resolveSelectMode, createTextareaRecipe, createRichTextRecipe, resolveRichTextAllowedFormats, normalizeRichTextHtml, isRichTextHtmlEmpty, paragraphRecipe, buildInputFieldA11yIds, sanitizeInputOtpValue, createInputOtpRecipe, buildRichTextFieldA11yIds, buildCurrencyHelperText, getCurrencySymbol, sanitizeCurrencyValue, createAvatarRecipe, createPurposeSemanticAttributes, checkboxRecipe, buildCheckboxFieldA11yIds, buildCheckboxGroupFieldA11yIds, createDividerRecipe, createSpacingRecipe, createCardRecipe, createToastRecipe, createTooltipRecipe, createAccordionRecipe, buildAccordionFieldA11yIds, radioRecipe, buildRadioGroupFieldA11yIds, createBadgeRecipe, createSwitchRecipe, buildSwitchFieldA11yIds, createSliderRecipe, buildSliderFieldA11yIds, createTabRecipe, resolveTabValue, buildTabGroupA11yIds, createDialogRecipe, isThemePreference, nextThemeMode, themeToCSSVars, richTextCommandByFormat, headingRecipe, moveTabSelection, extractUsedWeights, extractFontFamilyName, shouldLoadGoogleFont, buildGoogleFontsUrl } from '@marwes-ui/core';
3
3
  export { AvatarSize, AvatarType, BadgeVariant, ButtonAction, ButtonSize, ButtonVariant, IconName, Spacings, SwitchSize, ThemeMode, createFontStack, mwAvailableFonts, mwFontFallbacks, mwGoogleFontFamilies, mwStyledTheme, mwThemeVarNames, mwThemeVars, mwVar } from '@marwes-ui/core';
4
- import { defineComponent, ref, computed, onMounted, watch, provide, h, useAttrs, onBeforeUnmount, shallowRef, Fragment, nextTick, Teleport, inject } from 'vue';
4
+ import { defineComponent, ref, computed, onMounted, onUnmounted, watch, provide, h, useAttrs, onBeforeUnmount, shallowRef, Fragment, nextTick, Teleport, inject } from 'vue';
5
5
 
6
6
  // src/index.ts
7
7
 
@@ -61,32 +61,141 @@ function loadThemeFonts(theme, config = "auto") {
61
61
  }
62
62
  }
63
63
  }
64
+ var mediaQuery = "(prefers-color-scheme: dark)";
65
+ function getSystemThemeMode() {
66
+ if (typeof window === "undefined") return ThemeMode.light;
67
+ return window.matchMedia?.(mediaQuery).matches ? ThemeMode.dark : ThemeMode.light;
68
+ }
69
+ function subscribeToSystemThemeMode(onChange) {
70
+ if (typeof window === "undefined" || !window.matchMedia) return () => {
71
+ };
72
+ const media = window.matchMedia(mediaQuery);
73
+ const listener = (event) => {
74
+ onChange(event.matches ? ThemeMode.dark : ThemeMode.light);
75
+ };
76
+ if (media.addEventListener) {
77
+ media.addEventListener("change", listener);
78
+ } else {
79
+ media.addListener?.(listener);
80
+ }
81
+ return () => {
82
+ if (media.removeEventListener) {
83
+ media.removeEventListener("change", listener);
84
+ } else {
85
+ media.removeListener?.(listener);
86
+ }
87
+ };
88
+ }
89
+ function readStoredThemePreference(storageKey) {
90
+ if (!storageKey || typeof window === "undefined") return void 0;
91
+ try {
92
+ const value = window.localStorage.getItem(storageKey);
93
+ return isThemePreference(value) ? value : void 0;
94
+ } catch {
95
+ return void 0;
96
+ }
97
+ }
98
+ function writeStoredThemePreference(storageKey, preference) {
99
+ if (!storageKey || typeof window === "undefined") return;
100
+ try {
101
+ window.localStorage.setItem(storageKey, preference);
102
+ } catch {
103
+ }
104
+ }
105
+ function applyModeAttribute({
106
+ target,
107
+ providerElement,
108
+ mode,
109
+ attribute
110
+ }) {
111
+ const element = getTargetElement(target, providerElement);
112
+ if (!element) return;
113
+ if (attribute === "class") {
114
+ const oppositeMode = mode === ThemeMode.dark ? ThemeMode.light : ThemeMode.dark;
115
+ element.classList.remove(oppositeMode);
116
+ element.classList.add(mode);
117
+ return;
118
+ }
119
+ element.setAttribute(attribute, mode);
120
+ }
121
+ function withoutModeTransitions(callback) {
122
+ if (typeof document === "undefined" || typeof window === "undefined") {
123
+ callback();
124
+ return;
125
+ }
126
+ const style = document.createElement("style");
127
+ style.dataset.marwesDisableTransitions = "true";
128
+ style.textContent = "* { transition: none !important; }";
129
+ document.head.appendChild(style);
130
+ callback();
131
+ window.getComputedStyle(document.body);
132
+ window.requestAnimationFrame(() => {
133
+ style.remove();
134
+ });
135
+ }
136
+ function getTargetElement(target, providerElement) {
137
+ if (target === "provider") return providerElement;
138
+ if (typeof document === "undefined") return null;
139
+ return target === "html" ? document.documentElement : document.body;
140
+ }
64
141
 
65
142
  // src/provider/marwes-provider.ts
66
143
  var MarwesProvider = defineComponent({
67
144
  name: "MarwesProvider",
68
- props: ["theme", "defaultMode", "mode", "fontLoading", "onModeChange"],
145
+ props: [
146
+ "theme",
147
+ "defaultPreference",
148
+ "preference",
149
+ "defaultMode",
150
+ "mode",
151
+ "fontLoading",
152
+ "onPreferenceChange",
153
+ "onModeChange",
154
+ "storageKey",
155
+ "enableSystem",
156
+ "target",
157
+ "attribute",
158
+ "disableTransitionOnChange"
159
+ ],
69
160
  setup(rawProps, { slots }) {
70
161
  const props = rawProps;
71
- const internalMode = ref(props.defaultMode ?? ThemeMode.light);
72
- const activeMode = computed(
73
- () => props.mode ?? props.theme?.mode ?? internalMode.value
162
+ const internalPreference = ref(
163
+ props.defaultPreference ?? props.defaultMode ?? ThemeMode.light
74
164
  );
75
- const isModeControlled = computed(
76
- () => props.mode !== void 0 || props.theme?.mode !== void 0
165
+ const systemMode = ref(
166
+ props.enableSystem === false ? ThemeMode.light : getSystemThemeMode()
167
+ );
168
+ const enableSystem = computed(() => props.enableSystem !== false);
169
+ const storageKey = computed(() => props.storageKey ?? false);
170
+ const target = computed(() => props.target ?? "provider");
171
+ const attribute = computed(() => props.attribute ?? "class");
172
+ const disableTransitionOnChange = computed(() => props.disableTransitionOnChange === true);
173
+ const isPreferenceControlled = computed(
174
+ () => props.preference !== void 0 || props.mode !== void 0 || props.theme?.mode !== void 0
175
+ );
176
+ const activePreference = computed(
177
+ () => props.preference ?? props.mode ?? props.theme?.mode ?? internalPreference.value
178
+ );
179
+ const activeMode = computed(
180
+ () => resolveThemePreference(activePreference.value, systemMode.value)
77
181
  );
78
182
  const resolved = computed(
79
183
  () => resolveThemeInput({ ...props.theme ?? {}, mode: activeMode.value })
80
184
  );
81
185
  const rootRef = ref(null);
82
- function setMode(nextMode) {
83
- if (!isModeControlled.value) {
84
- internalMode.value = nextMode;
186
+ function setPreference(nextPreference) {
187
+ if (!isPreferenceControlled.value) {
188
+ internalPreference.value = nextPreference;
85
189
  }
190
+ writeStoredThemePreference(storageKey.value, nextPreference);
191
+ props.onPreferenceChange?.(nextPreference);
192
+ }
193
+ function setMode(nextMode) {
194
+ setPreference(nextMode);
86
195
  props.onModeChange?.(nextMode);
87
196
  }
88
197
  function toggleMode() {
89
- setMode(activeMode.value === ThemeMode.dark ? ThemeMode.light : ThemeMode.dark);
198
+ setMode(nextThemeMode(activeMode.value));
90
199
  }
91
200
  function syncThemeToRuntime() {
92
201
  if (rootRef.value) {
@@ -94,12 +203,59 @@ var MarwesProvider = defineComponent({
94
203
  }
95
204
  loadThemeFonts(resolved.value, props.fontLoading ?? "auto");
96
205
  }
97
- onMounted(syncThemeToRuntime);
206
+ function syncTargetModeAttribute() {
207
+ if (target.value === "provider") return;
208
+ const apply = () => {
209
+ applyModeAttribute({
210
+ target: target.value,
211
+ providerElement: rootRef.value,
212
+ mode: activeMode.value,
213
+ attribute: attribute.value
214
+ });
215
+ };
216
+ if (disableTransitionOnChange.value) {
217
+ withoutModeTransitions(apply);
218
+ return;
219
+ }
220
+ apply();
221
+ }
222
+ let unsubscribeSystemThemeMode;
223
+ function syncSystemThemeSubscription() {
224
+ unsubscribeSystemThemeMode?.();
225
+ unsubscribeSystemThemeMode = void 0;
226
+ if (!enableSystem.value) {
227
+ systemMode.value = ThemeMode.light;
228
+ return;
229
+ }
230
+ systemMode.value = getSystemThemeMode();
231
+ if (activePreference.value === "system") {
232
+ unsubscribeSystemThemeMode = subscribeToSystemThemeMode((nextMode) => {
233
+ systemMode.value = nextMode;
234
+ });
235
+ }
236
+ }
237
+ onMounted(() => {
238
+ const storedPreference = readStoredThemePreference(storageKey.value);
239
+ if (storedPreference !== void 0 && !isPreferenceControlled.value) {
240
+ internalPreference.value = storedPreference;
241
+ }
242
+ syncSystemThemeSubscription();
243
+ syncThemeToRuntime();
244
+ syncTargetModeAttribute();
245
+ });
246
+ onUnmounted(() => {
247
+ unsubscribeSystemThemeMode?.();
248
+ });
98
249
  watch(resolved, syncThemeToRuntime);
250
+ watch([activeMode, target, attribute, disableTransitionOnChange], syncTargetModeAttribute);
251
+ watch([activePreference, enableSystem], syncSystemThemeSubscription);
99
252
  provide(marwesContextKey, {
100
253
  theme: resolved,
101
254
  mode: activeMode,
255
+ preference: activePreference,
256
+ systemMode: computed(() => systemMode.value),
102
257
  setMode,
258
+ setPreference,
103
259
  toggleMode
104
260
  });
105
261
  return () => h(
@@ -128,10 +284,14 @@ function useThemeMode() {
128
284
  }
129
285
  return {
130
286
  mode: context.mode,
287
+ preference: context.preference,
288
+ systemMode: context.systemMode,
131
289
  setMode: context.setMode,
290
+ setPreference: context.setPreference,
132
291
  toggleMode: context.toggleMode,
133
292
  isDark: computed(() => context.mode.value === ThemeMode.dark),
134
- isLight: computed(() => context.mode.value === ThemeMode.light)
293
+ isLight: computed(() => context.mode.value === ThemeMode.light),
294
+ isSystem: computed(() => context.preference.value === "system")
135
295
  };
136
296
  }
137
297