@marwes-ui/vue 1.0.1 → 1.0.2

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,49 @@ 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
+
312
355
  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
356
 
314
357
  ```vue
package/dist/index.d.ts CHANGED
@@ -1,40 +1,59 @@
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
6
 
7
7
  type MarwesProviderProps = {
8
8
  theme?: ThemeInput;
9
+ defaultPreference?: ThemePreference;
10
+ preference?: ThemePreference;
9
11
  defaultMode?: ThemeMode;
10
12
  mode?: ThemeMode;
11
13
  fontLoading?: FontLoadingConfig;
14
+ onPreferenceChange?: (preference: ThemePreference) => void;
12
15
  onModeChange?: (mode: ThemeMode) => void;
16
+ storageKey?: string | false;
17
+ enableSystem?: boolean;
13
18
  };
14
19
  declare const MarwesProvider: vue.DefineComponent<{
15
20
  mode?: any;
16
21
  theme?: any;
22
+ defaultPreference?: any;
23
+ preference?: any;
17
24
  defaultMode?: any;
18
25
  fontLoading?: any;
26
+ onPreferenceChange?: any;
19
27
  onModeChange?: any;
28
+ storageKey?: any;
29
+ enableSystem?: any;
20
30
  }, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
21
31
  [key: string]: any;
22
32
  }>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, string, vue.PublicProps, Readonly<{
23
33
  mode?: any;
24
34
  theme?: any;
35
+ defaultPreference?: any;
36
+ preference?: any;
25
37
  defaultMode?: any;
26
38
  fontLoading?: any;
39
+ onPreferenceChange?: any;
27
40
  onModeChange?: any;
41
+ storageKey?: any;
42
+ enableSystem?: any;
28
43
  }> & Readonly<{}>, {}, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
29
44
 
30
45
  declare function useTheme(): ResolvedTheme;
31
46
 
32
47
  type ThemeModeContextValue = {
33
48
  mode: ComputedRef<ThemeMode>;
49
+ preference: ComputedRef<ThemePreference>;
50
+ systemMode: ComputedRef<ThemeMode>;
34
51
  setMode: (mode: ThemeMode) => void;
52
+ setPreference: (preference: ThemePreference) => void;
35
53
  toggleMode: () => void;
36
54
  isDark: ComputedRef<boolean>;
37
55
  isLight: ComputedRef<boolean>;
56
+ isSystem: ComputedRef<boolean>;
38
57
  };
39
58
  declare function useThemeMode(): ThemeModeContextValue;
40
59
 
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,99 @@ 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
+ }
64
105
 
65
106
  // src/provider/marwes-provider.ts
66
107
  var MarwesProvider = defineComponent({
67
108
  name: "MarwesProvider",
68
- props: ["theme", "defaultMode", "mode", "fontLoading", "onModeChange"],
109
+ props: [
110
+ "theme",
111
+ "defaultPreference",
112
+ "preference",
113
+ "defaultMode",
114
+ "mode",
115
+ "fontLoading",
116
+ "onPreferenceChange",
117
+ "onModeChange",
118
+ "storageKey",
119
+ "enableSystem"
120
+ ],
69
121
  setup(rawProps, { slots }) {
70
122
  const props = rawProps;
71
- const internalMode = ref(props.defaultMode ?? ThemeMode.light);
72
- const activeMode = computed(
73
- () => props.mode ?? props.theme?.mode ?? internalMode.value
123
+ const internalPreference = ref(
124
+ props.defaultPreference ?? props.defaultMode ?? ThemeMode.light
125
+ );
126
+ const systemMode = ref(
127
+ props.enableSystem === false ? ThemeMode.light : getSystemThemeMode()
74
128
  );
75
- const isModeControlled = computed(
76
- () => props.mode !== void 0 || props.theme?.mode !== void 0
129
+ const enableSystem = computed(() => props.enableSystem !== false);
130
+ const storageKey = computed(() => props.storageKey ?? false);
131
+ const isPreferenceControlled = computed(
132
+ () => props.preference !== void 0 || props.mode !== void 0 || props.theme?.mode !== void 0
133
+ );
134
+ const activePreference = computed(
135
+ () => props.preference ?? props.mode ?? props.theme?.mode ?? internalPreference.value
136
+ );
137
+ const activeMode = computed(
138
+ () => resolveThemePreference(activePreference.value, systemMode.value)
77
139
  );
78
140
  const resolved = computed(
79
141
  () => resolveThemeInput({ ...props.theme ?? {}, mode: activeMode.value })
80
142
  );
81
143
  const rootRef = ref(null);
82
- function setMode(nextMode) {
83
- if (!isModeControlled.value) {
84
- internalMode.value = nextMode;
144
+ function setPreference(nextPreference) {
145
+ if (!isPreferenceControlled.value) {
146
+ internalPreference.value = nextPreference;
85
147
  }
148
+ writeStoredThemePreference(storageKey.value, nextPreference);
149
+ props.onPreferenceChange?.(nextPreference);
150
+ }
151
+ function setMode(nextMode) {
152
+ setPreference(nextMode);
86
153
  props.onModeChange?.(nextMode);
87
154
  }
88
155
  function toggleMode() {
89
- setMode(activeMode.value === ThemeMode.dark ? ThemeMode.light : ThemeMode.dark);
156
+ setMode(nextThemeMode(activeMode.value));
90
157
  }
91
158
  function syncThemeToRuntime() {
92
159
  if (rootRef.value) {
@@ -94,12 +161,41 @@ var MarwesProvider = defineComponent({
94
161
  }
95
162
  loadThemeFonts(resolved.value, props.fontLoading ?? "auto");
96
163
  }
97
- onMounted(syncThemeToRuntime);
164
+ let unsubscribeSystemThemeMode;
165
+ function syncSystemThemeSubscription() {
166
+ unsubscribeSystemThemeMode?.();
167
+ unsubscribeSystemThemeMode = void 0;
168
+ if (!enableSystem.value) {
169
+ systemMode.value = ThemeMode.light;
170
+ return;
171
+ }
172
+ systemMode.value = getSystemThemeMode();
173
+ if (activePreference.value === "system") {
174
+ unsubscribeSystemThemeMode = subscribeToSystemThemeMode((nextMode) => {
175
+ systemMode.value = nextMode;
176
+ });
177
+ }
178
+ }
179
+ onMounted(() => {
180
+ const storedPreference = readStoredThemePreference(storageKey.value);
181
+ if (storedPreference !== void 0 && !isPreferenceControlled.value) {
182
+ internalPreference.value = storedPreference;
183
+ }
184
+ syncSystemThemeSubscription();
185
+ syncThemeToRuntime();
186
+ });
187
+ onUnmounted(() => {
188
+ unsubscribeSystemThemeMode?.();
189
+ });
98
190
  watch(resolved, syncThemeToRuntime);
191
+ watch([activePreference, enableSystem], syncSystemThemeSubscription);
99
192
  provide(marwesContextKey, {
100
193
  theme: resolved,
101
194
  mode: activeMode,
195
+ preference: activePreference,
196
+ systemMode: computed(() => systemMode.value),
102
197
  setMode,
198
+ setPreference,
103
199
  toggleMode
104
200
  });
105
201
  return () => h(
@@ -128,10 +224,14 @@ function useThemeMode() {
128
224
  }
129
225
  return {
130
226
  mode: context.mode,
227
+ preference: context.preference,
228
+ systemMode: context.systemMode,
131
229
  setMode: context.setMode,
230
+ setPreference: context.setPreference,
132
231
  toggleMode: context.toggleMode,
133
232
  isDark: computed(() => context.mode.value === ThemeMode.dark),
134
- isLight: computed(() => context.mode.value === ThemeMode.light)
233
+ isLight: computed(() => context.mode.value === ThemeMode.light),
234
+ isSystem: computed(() => context.preference.value === "system")
135
235
  };
136
236
  }
137
237