vue-multiple-themes 4.2.0 → 5.0.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.
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const vueDemi = require("vue-demi");
4
3
  const vue = require("vue");
4
+ const vueDemi = require("vue-demi");
5
5
  const STYLE_ID = "vmt-theme-styles";
6
6
  function toKebab(str) {
7
7
  return str.replace(/([A-Z])/g, (_, c) => `-${c.toLowerCase()}`);
@@ -79,8 +79,11 @@ function applyThemeToDom(name, previousName, options) {
79
79
  el.setAttribute(attribute, name);
80
80
  }
81
81
  if (strategy === "class" || strategy === "both") {
82
- if (previousName) {
83
- el.classList.remove(`${classPrefix}${previousName}`);
82
+ const toRemove = Array.from(el.classList).filter(
83
+ (cls) => cls.startsWith(classPrefix) && cls !== `${classPrefix}${name}`
84
+ );
85
+ for (const cls of toRemove) {
86
+ el.classList.remove(cls);
84
87
  }
85
88
  el.classList.add(`${classPrefix}${name}`);
86
89
  }
@@ -216,155 +219,103 @@ const _sfc_main$1 = vueDemi.defineComponent({
216
219
  );
217
220
  }
218
221
  });
219
- const _sfc_main = vueDemi.defineComponent({
220
- name: "VueMultipleThemes",
221
- components: { VmtIcon: _sfc_main$1 },
222
+ const _hoisted_1 = ["aria-label", "title"];
223
+ const _hoisted_2 = { class: "vmt-icon" };
224
+ const _hoisted_3 = {
225
+ key: 0,
226
+ class: "vmt-label"
227
+ };
228
+ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
229
+ __name: "VueMultipleThemes",
222
230
  props: {
223
- /** All available theme definitions */
224
- themes: {
225
- type: Array,
226
- default: () => [
227
- {
228
- name: "light",
229
- label: "Light",
230
- icon: "sun",
231
- colors: {
232
- primary: "#3b82f6",
233
- secondary: "#8b5cf6",
234
- accent: "#f59e0b",
235
- background: "#ffffff",
236
- surface: "#f8fafc",
237
- surfaceElevated: "#f1f5f9",
238
- text: "#111827",
239
- textMuted: "#6b7280",
240
- textInverse: "#ffffff",
241
- border: "#e5e7eb",
242
- ring: "#3b82f6",
243
- success: "#10b981",
244
- warning: "#f59e0b",
245
- error: "#ef4444",
246
- info: "#3b82f6"
247
- }
248
- },
249
- {
250
- name: "dark",
251
- label: "Dark",
252
- icon: "moon",
253
- colors: {
254
- primary: "#60a5fa",
255
- secondary: "#a78bfa",
256
- accent: "#fbbf24",
257
- background: "#0f172a",
258
- surface: "#1e293b",
259
- surfaceElevated: "#334155",
260
- text: "#f8fafc",
261
- textMuted: "#94a3b8",
262
- textInverse: "#0f172a",
263
- border: "#334155",
264
- ring: "#60a5fa",
265
- success: "#34d399",
266
- warning: "#fbbf24",
267
- error: "#f87171",
268
- info: "#60a5fa"
269
- }
270
- },
271
- {
272
- name: "sepia",
273
- label: "Sepia",
274
- icon: "coffee",
275
- colors: {
276
- primary: "#92400e",
277
- secondary: "#78350f",
278
- accent: "#d97706",
279
- background: "#fdf6e3",
280
- surface: "#f5e9c9",
281
- surfaceElevated: "#ede0b5",
282
- text: "#3c2415",
283
- textMuted: "#78583d",
284
- textInverse: "#fdf6e3",
285
- border: "#d6b896",
286
- ring: "#92400e",
287
- success: "#166534",
288
- warning: "#92400e",
289
- error: "#991b1b",
290
- info: "#1e40af"
291
- }
231
+ themes: { default: () => [
232
+ {
233
+ name: "light",
234
+ label: "Light",
235
+ icon: "sun",
236
+ colors: {
237
+ primary: "#3b82f6",
238
+ secondary: "#8b5cf6",
239
+ accent: "#f59e0b",
240
+ background: "#ffffff",
241
+ surface: "#f8fafc",
242
+ surfaceElevated: "#f1f5f9",
243
+ text: "#111827",
244
+ textMuted: "#6b7280",
245
+ textInverse: "#ffffff",
246
+ border: "#e5e7eb",
247
+ ring: "#3b82f6",
248
+ success: "#10b981",
249
+ warning: "#f59e0b",
250
+ error: "#ef4444",
251
+ info: "#3b82f6"
292
252
  }
293
- ]
294
- },
295
- /** Active theme on first render (default: first in `themes`) */
296
- defaultTheme: {
297
- type: String,
298
- default: void 0
299
- },
300
- /** How to stamp the theme on the DOM */
301
- strategy: {
302
- type: String,
303
- default: "attribute"
304
- },
305
- /** Attribute name (default: `data-theme`) */
306
- attribute: {
307
- type: String,
308
- default: "data-theme"
309
- },
310
- /** Class prefix (default: `theme-`) */
311
- classPrefix: {
312
- type: String,
313
- default: "theme-"
314
- },
315
- /** DOM element that receives the attribute / class */
316
- target: {
317
- type: String,
318
- default: "html"
319
- },
320
- /** CSS variable prefix (default: `--vmt-`) */
321
- cssVarPrefix: {
322
- type: String,
323
- default: "--vmt-"
324
- },
325
- /** Inject CSS variables automatically */
326
- injectCssVars: {
327
- type: Boolean,
328
- default: true
329
- },
330
- /** Persist theme in storage */
331
- storage: {
332
- type: String,
333
- default: "localStorage"
334
- },
335
- /** Storage key for persistence */
336
- storageKey: {
337
- type: String,
338
- default: "vmt-theme"
339
- },
340
- /** Respect OS dark/light preference on first load */
341
- respectSystemPreference: {
342
- type: Boolean,
343
- default: false
344
- },
345
- /** Show the built-in toggle button */
346
- showToggle: {
347
- type: Boolean,
348
- default: true
349
- },
350
- /** Show the label next to the icon */
351
- showLabel: {
352
- type: Boolean,
353
- default: false
354
- },
355
- /** Size (px) of the toggle icon */
356
- iconSize: {
357
- type: Number,
358
- default: 20
359
- },
360
- /** Extra CSS classes on the root wrapper */
361
- extraClass: {
362
- type: String,
363
- default: ""
364
- }
253
+ },
254
+ {
255
+ name: "dark",
256
+ label: "Dark",
257
+ icon: "moon",
258
+ colors: {
259
+ primary: "#60a5fa",
260
+ secondary: "#a78bfa",
261
+ accent: "#fbbf24",
262
+ background: "#0f172a",
263
+ surface: "#1e293b",
264
+ surfaceElevated: "#334155",
265
+ text: "#f8fafc",
266
+ textMuted: "#94a3b8",
267
+ textInverse: "#0f172a",
268
+ border: "#334155",
269
+ ring: "#60a5fa",
270
+ success: "#34d399",
271
+ warning: "#fbbf24",
272
+ error: "#f87171",
273
+ info: "#60a5fa"
274
+ }
275
+ },
276
+ {
277
+ name: "sepia",
278
+ label: "Sepia",
279
+ icon: "coffee",
280
+ colors: {
281
+ primary: "#92400e",
282
+ secondary: "#78350f",
283
+ accent: "#d97706",
284
+ background: "#fdf6e3",
285
+ surface: "#f5e9c9",
286
+ surfaceElevated: "#ede0b5",
287
+ text: "#3c2415",
288
+ textMuted: "#78583d",
289
+ textInverse: "#fdf6e3",
290
+ border: "#d6b896",
291
+ ring: "#92400e",
292
+ success: "#166534",
293
+ warning: "#92400e",
294
+ error: "#991b1b",
295
+ info: "#1e40af"
296
+ }
297
+ }
298
+ ] },
299
+ defaultTheme: {},
300
+ strategy: { default: "attribute" },
301
+ attribute: { default: "data-theme" },
302
+ classPrefix: { default: "theme-" },
303
+ target: { default: "html" },
304
+ cssVarPrefix: { default: "--vmt-" },
305
+ injectCssVars: { type: Boolean, default: true },
306
+ storage: { default: "localStorage" },
307
+ storageKey: { default: "vmt-theme" },
308
+ respectSystemPreference: { type: Boolean, default: false },
309
+ showToggle: { type: Boolean, default: true },
310
+ showLabel: { type: Boolean, default: false },
311
+ iconSize: { default: 20 },
312
+ extraClass: { default: "" }
365
313
  },
366
314
  emits: ["change"],
367
- setup(props, { emit }) {
315
+ setup(__props, { emit: __emit }) {
316
+ const _componentCounts = /* @__PURE__ */ new Map();
317
+ const props = __props;
318
+ const emit = __emit;
368
319
  function getInitial() {
369
320
  var _a;
370
321
  if (props.storage !== "none") {
@@ -387,17 +338,17 @@ const _sfc_main = vueDemi.defineComponent({
387
338
  }
388
339
  return ((_a = props.themes[0]) == null ? void 0 : _a.name) ?? "light";
389
340
  }
390
- const currentName = vueDemi.ref(getInitial());
341
+ const currentName = vue.ref(getInitial());
391
342
  let previousName = null;
392
- const currentTheme = vueDemi.computed(
343
+ const currentTheme = vue.computed(
393
344
  () => props.themes.find(
394
345
  (t) => t.name === currentName.value
395
346
  ) ?? props.themes[0]
396
347
  );
397
- const isDark = vueDemi.computed(
348
+ const isDark = vue.computed(
398
349
  () => currentName.value.toLowerCase().includes("dark")
399
350
  );
400
- const currentIconName = vueDemi.computed(
351
+ const currentIconName = vue.computed(
401
352
  () => currentTheme.value.icon ?? "palette"
402
353
  );
403
354
  function applyTheme(name) {
@@ -438,12 +389,16 @@ const _sfc_main = vueDemi.defineComponent({
438
389
  }
439
390
  function toggleTheme() {
440
391
  if (props.themes.length < 2) return;
441
- const next = props.themes.find(
442
- (t) => t.name !== currentName.value
443
- );
444
- if (next) currentName.value = next.name;
392
+ if (props.themes.length === 2) {
393
+ const other = props.themes.find(
394
+ (t) => t.name !== currentName.value
395
+ );
396
+ if (other) currentName.value = other.name;
397
+ } else {
398
+ nextTheme();
399
+ }
445
400
  }
446
- vueDemi.onMounted(() => {
401
+ function ensureStylesAndApply() {
447
402
  if (props.injectCssVars) {
448
403
  const css = buildCssVars(props.themes, {
449
404
  strategy: props.strategy,
@@ -455,82 +410,74 @@ const _sfc_main = vueDemi.defineComponent({
455
410
  injectStyles(css);
456
411
  }
457
412
  applyTheme(currentName.value);
413
+ }
414
+ let appliedInSetup = false;
415
+ if (typeof document !== "undefined") {
416
+ ensureStylesAndApply();
417
+ appliedInSetup = true;
418
+ }
419
+ vue.onMounted(() => {
420
+ if (!appliedInSetup) {
421
+ ensureStylesAndApply();
422
+ }
458
423
  });
459
- vueDemi.watch(currentName, (name) => {
424
+ vue.watch(currentName, (name) => {
460
425
  applyTheme(name);
461
426
  });
462
- vueDemi.onBeforeUnmount(() => {
463
- removeStyles();
427
+ const _key = props.storageKey;
428
+ _componentCounts.set(_key, (_componentCounts.get(_key) ?? 0) + 1);
429
+ vue.onBeforeUnmount(() => {
430
+ const count = (_componentCounts.get(_key) ?? 1) - 1;
431
+ _componentCounts.set(_key, count);
432
+ if (count <= 0) {
433
+ _componentCounts.delete(_key);
434
+ removeStyles();
435
+ }
464
436
  });
465
- return {
466
- currentName,
467
- currentTheme,
468
- isDark,
469
- currentIconName,
470
- setTheme,
471
- nextTheme,
472
- prevTheme,
473
- toggleTheme
437
+ return (_ctx, _cache) => {
438
+ return vue.openBlock(), vue.createElementBlock("div", {
439
+ class: vue.normalizeClass(["vmt-root", [__props.extraClass, isDark.value ? "vmt-dark" : "vmt-light"]])
440
+ }, [
441
+ vue.renderSlot(_ctx.$slots, "default", {
442
+ current: currentTheme.value,
443
+ themes: __props.themes,
444
+ setTheme,
445
+ nextTheme,
446
+ prevTheme,
447
+ toggleTheme,
448
+ isDark: isDark.value
449
+ }, () => [
450
+ __props.showToggle ? (vue.openBlock(), vue.createElementBlock("button", {
451
+ key: 0,
452
+ class: "vmt-toggle",
453
+ "aria-label": "Current theme: " + currentTheme.value.label + ". Click to switch theme.",
454
+ title: currentTheme.value.label,
455
+ onClick: nextTheme
456
+ }, [
457
+ vue.createElementVNode("span", _hoisted_2, [
458
+ vue.renderSlot(_ctx.$slots, "icon", {
459
+ icon: currentIconName.value,
460
+ size: __props.iconSize,
461
+ theme: currentTheme.value
462
+ }, () => [
463
+ vue.createVNode(_sfc_main$1, {
464
+ name: currentIconName.value,
465
+ size: __props.iconSize
466
+ }, null, 8, ["name", "size"])
467
+ ])
468
+ ]),
469
+ __props.showLabel ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_3, vue.toDisplayString(currentTheme.value.label), 1)) : vue.createCommentVNode("", true)
470
+ ], 8, _hoisted_1)) : vue.createCommentVNode("", true)
471
+ ]),
472
+ vue.renderSlot(_ctx.$slots, "picker", {
473
+ current: currentTheme.value,
474
+ themes: __props.themes,
475
+ setTheme
476
+ })
477
+ ], 2);
474
478
  };
475
479
  }
476
480
  });
477
- const _export_sfc = (sfc, props) => {
478
- const target = sfc.__vccOpts || sfc;
479
- for (const [key, val] of props) {
480
- target[key] = val;
481
- }
482
- return target;
483
- };
484
- const _hoisted_1 = ["aria-label", "title"];
485
- const _hoisted_2 = { class: "vmt-icon" };
486
- const _hoisted_3 = {
487
- key: 0,
488
- class: "vmt-label"
489
- };
490
- function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
491
- const _component_VmtIcon = vue.resolveComponent("VmtIcon");
492
- return vue.openBlock(), vue.createElementBlock("div", {
493
- class: vue.normalizeClass(["vmt-root", [_ctx.extraClass, _ctx.isDark ? "vmt-dark" : "vmt-light"]])
494
- }, [
495
- vue.renderSlot(_ctx.$slots, "default", {
496
- current: _ctx.currentTheme,
497
- themes: _ctx.themes,
498
- setTheme: _ctx.setTheme,
499
- nextTheme: _ctx.nextTheme,
500
- prevTheme: _ctx.prevTheme,
501
- toggleTheme: _ctx.toggleTheme,
502
- isDark: _ctx.isDark
503
- }, () => [
504
- _ctx.showToggle ? (vue.openBlock(), vue.createElementBlock("button", {
505
- key: 0,
506
- class: "vmt-toggle",
507
- "aria-label": "Current theme: " + _ctx.currentTheme.label + ". Click to switch theme.",
508
- title: _ctx.currentTheme.label,
509
- onClick: _cache[0] || (_cache[0] = (...args) => _ctx.nextTheme && _ctx.nextTheme(...args))
510
- }, [
511
- vue.createElementVNode("span", _hoisted_2, [
512
- vue.renderSlot(_ctx.$slots, "icon", {
513
- icon: _ctx.currentIconName,
514
- size: _ctx.iconSize,
515
- theme: _ctx.currentTheme
516
- }, () => [
517
- vue.createVNode(_component_VmtIcon, {
518
- name: _ctx.currentIconName,
519
- size: _ctx.iconSize
520
- }, null, 8, ["name", "size"])
521
- ])
522
- ]),
523
- _ctx.showLabel ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_3, vue.toDisplayString(_ctx.currentTheme.label), 1)) : vue.createCommentVNode("", true)
524
- ], 8, _hoisted_1)) : vue.createCommentVNode("", true)
525
- ]),
526
- vue.renderSlot(_ctx.$slots, "picker", {
527
- current: _ctx.currentTheme,
528
- themes: _ctx.themes,
529
- setTheme: _ctx.setTheme
530
- })
531
- ], 2);
532
- }
533
- const VueMultipleThemes = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
534
481
  const VueMultipleThemesPlugin = {
535
482
  install(app, options = { themes: [] }) {
536
483
  var _a, _b;
@@ -552,32 +499,32 @@ const VueMultipleThemesPlugin = {
552
499
  return;
553
500
  }
554
501
  let initialTheme = ((_a = themes[0]) == null ? void 0 : _a.name) ?? "light";
502
+ let fromStorage = false;
555
503
  if (storage !== "none") {
556
504
  const stored = readStorage(storageKey, storage);
557
505
  if (stored && themes.some((t) => t.name === stored)) {
558
506
  initialTheme = stored;
507
+ fromStorage = true;
559
508
  }
560
509
  }
561
- if (respectSystemPreference && initialTheme === ((_b = themes[0]) == null ? void 0 : _b.name)) {
510
+ if (!fromStorage && respectSystemPreference) {
562
511
  const pref = getSystemPreference();
563
512
  const match = themes.find((t) => t.name === pref);
564
513
  if (match) initialTheme = match.name;
565
514
  }
566
- if (defaultTheme && themes.some((t) => t.name === defaultTheme)) {
567
- const stored = storage !== "none" ? readStorage(storageKey, storage) : null;
568
- if (!stored) initialTheme = defaultTheme;
515
+ if (!fromStorage && initialTheme === (((_b = themes[0]) == null ? void 0 : _b.name) ?? "light") && defaultTheme && themes.some((t) => t.name === defaultTheme)) {
516
+ initialTheme = defaultTheme;
569
517
  }
570
518
  if (injectCssVars) {
571
519
  const css = buildCssVars(themes, { strategy, attribute, classPrefix, cssVarPrefix, target });
572
520
  injectStyles(css);
573
521
  }
574
- applyThemeToDom(initialTheme, null, { strategy, attribute, classPrefix, target });
575
- if (vueDemi.isVue2) {
576
- app.component("VueMultipleThemes", VueMultipleThemes);
577
- } else {
578
- app.component("VueMultipleThemes", VueMultipleThemes);
522
+ if (storage !== "none") {
523
+ writeStorage(storageKey, initialTheme, storage);
579
524
  }
580
- if (!vueDemi.isVue2 && typeof app.provide === "function") {
525
+ applyThemeToDom(initialTheme, null, { strategy, attribute, classPrefix, target });
526
+ app.component("VueMultipleThemes", _sfc_main);
527
+ if (typeof app.provide === "function") {
581
528
  app.provide("vmt:options", options);
582
529
  }
583
530
  }
@@ -626,13 +573,13 @@ function useTheme(options) {
626
573
  if (existing) {
627
574
  currentName = existing.current;
628
575
  } else {
629
- currentName = vueDemi.ref(getInitialTheme());
576
+ currentName = vue.ref(getInitialTheme());
630
577
  singletons.set(singletonKey, { current: currentName, options });
631
578
  }
632
- const theme = vueDemi.computed(
579
+ const theme = vue.computed(
633
580
  () => themes.find((t) => t.name === currentName.value) ?? themes[0]
634
581
  );
635
- const isDark = vueDemi.computed(
582
+ const isDark = vue.computed(
636
583
  () => (currentName.value ?? "").toLowerCase().includes("dark")
637
584
  );
638
585
  let previousName = null;
@@ -667,21 +614,34 @@ function useTheme(options) {
667
614
  }
668
615
  function toggleTheme() {
669
616
  if (themes.length < 2) return;
670
- const current = currentName.value;
671
- const next = themes.find((t) => t.name !== current) ?? themes[1];
672
- currentName.value = next.name;
617
+ if (themes.length === 2) {
618
+ const other = themes.find((t) => t.name !== currentName.value) ?? themes[1];
619
+ currentName.value = other.name;
620
+ } else {
621
+ nextTheme();
622
+ }
673
623
  }
674
- vueDemi.onMounted(() => {
624
+ let appliedInSetup = false;
625
+ if (typeof document !== "undefined") {
675
626
  ensureStyles();
676
- if (currentName.value) applyTheme(currentName.value);
627
+ const initialName = currentName.value;
628
+ if (initialName) applyTheme(initialName);
629
+ appliedInSetup = true;
630
+ }
631
+ vue.onMounted(() => {
632
+ if (!appliedInSetup) {
633
+ ensureStyles();
634
+ const mountedName = currentName.value;
635
+ if (mountedName) applyTheme(mountedName);
636
+ }
677
637
  });
678
- vueDemi.watch(currentName, (name) => {
638
+ vue.watch(currentName, (name) => {
679
639
  if (name) applyTheme(name);
680
640
  });
681
641
  let instanceCount = ((_a = singletons.get(singletonKey)) == null ? void 0 : _a._count) ?? 0;
682
642
  instanceCount++;
683
643
  singletons.get(singletonKey)._count = instanceCount;
684
- vueDemi.onBeforeUnmount(() => {
644
+ vue.onBeforeUnmount(() => {
685
645
  const entry = singletons.get(singletonKey);
686
646
  if (entry) {
687
647
  entry._count = (entry._count ?? 1) - 1;
@@ -1234,7 +1194,7 @@ const PRESET_THEMES = [
1234
1194
  exports.LUCIDE_ICONS = LUCIDE_ICONS;
1235
1195
  exports.PRESET_THEMES = PRESET_THEMES;
1236
1196
  exports.VmtIcon = _sfc_main$1;
1237
- exports.VueMultipleThemes = VueMultipleThemes;
1197
+ exports.VueMultipleThemes = _sfc_main;
1238
1198
  exports.VueMultipleThemesPlugin = VueMultipleThemesPlugin;
1239
1199
  exports.analogous = analogous;
1240
1200
  exports.autoContrast = autoContrast;