vueless 1.5.1-beta.3 → 1.5.1-beta.4

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/index.d.ts CHANGED
@@ -27,7 +27,15 @@ export {
27
27
  } from "./utils/ui";
28
28
  export { addToRequestQueue, removeFromRequestQueue } from "./utils/requestQueue";
29
29
  export { isMac, isPWA, isIOS, isAndroid, isMobileApp, isWindows } from "./utils/platform";
30
- export { getTheme, setTheme, resetTheme, normalizeThemeConfig, cssVar, setRootCSSVariables } from "./utils/theme";
30
+ export {
31
+ getTheme,
32
+ setTheme,
33
+ resetTheme,
34
+ getThemeCookieName,
35
+ normalizeThemeConfig,
36
+ cssVar,
37
+ setRootCSSVariables,
38
+ } from "./utils/theme";
31
39
  export { getArgs, getArgTypes, getSlotNames, getSlotsFragment, getSource, getDocsDescription } from "./utils/storybook";
32
40
  /* adapters */
33
41
  export { default as defaultEnLocale } from "./adapter.locale/locales/en";
package/index.ts CHANGED
@@ -33,7 +33,15 @@ export {
33
33
  } from "./utils/ui";
34
34
  export { addToRequestQueue, removeFromRequestQueue } from "./utils/requestQueue";
35
35
  export { isMac, isPWA, isIOS, isAndroid, isMobileApp, isWindows } from "./utils/platform";
36
- export { getTheme, setTheme, resetTheme, normalizeThemeConfig, cssVar, setRootCSSVariables } from "./utils/theme";
36
+ export {
37
+ getTheme,
38
+ setTheme,
39
+ resetTheme,
40
+ getThemeCookieName,
41
+ normalizeThemeConfig,
42
+ cssVar,
43
+ setRootCSSVariables,
44
+ } from "./utils/theme";
37
45
  export { getArgs, getArgTypes, getSlotNames, getSlotsFragment, getSource, getDocsDescription } from "./utils/storybook";
38
46
  /* adapters */
39
47
  export { default as defaultEnLocale } from "./adapter.locale/locales/en";
package/modules.d.ts CHANGED
@@ -33,3 +33,4 @@ declare module "virtual:vueless/vue-i18n" {
33
33
  }
34
34
 
35
35
  declare const __VUELESS_DEV__: boolean;
36
+ declare const __VUELESS_APP_NAME__: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vueless",
3
- "version": "1.5.1-beta.3",
3
+ "version": "1.5.1-beta.4",
4
4
  "description": "Vue Styleless UI Component Library, powered by Tailwind CSS.",
5
5
  "author": "Johnny Grid <hello@vueless.com> (https://vueless.com)",
6
6
  "homepage": "https://vueless.com",
package/plugin-vite.js CHANGED
@@ -24,6 +24,7 @@ import { overrideComponents, restoreComponents } from "./utils/node/componentOve
24
24
  import {
25
25
  getNuxtDirs,
26
26
  getVueDirs,
27
+ getAppName,
27
28
  getVuelessAppDirs,
28
29
  cacheMergedConfigs,
29
30
  autoImportUserConfigs,
@@ -74,6 +75,9 @@ export const Vueless = function (options = {}) {
74
75
 
75
76
  const vuelessSrcDir = isInternalEnv ? VUELESS_LOCAL_DIR : VUELESS_PACKAGE_DIR;
76
77
 
78
+ /* app name for theme cookie prefixing */
79
+ const appName = getAppName();
80
+
77
81
  // Cache to store previous icon content for change detection
78
82
  const iconContentCache = new Map();
79
83
 
@@ -109,6 +113,7 @@ export const Vueless = function (options = {}) {
109
113
  define: {
110
114
  "process.env": {},
111
115
  __VUELESS_DEV__: process.argv.includes("--watch") || process.argv.includes("-w"),
116
+ __VUELESS_APP_NAME__: JSON.stringify(appName),
112
117
  },
113
118
  optimizeDeps: {
114
119
  include: isInternalEnv
package/types.ts CHANGED
@@ -183,6 +183,11 @@ export interface Config extends ThemeConfig {
183
183
  */
184
184
  directives?: Partial<Directives>;
185
185
 
186
+ /**
187
+ * Theme cookie name prefix (defaults to the app `package.json` name, `""` disables prefixing).
188
+ */
189
+ cookiePrefix?: string;
190
+
186
191
  /**
187
192
  * Tailwind CSS theme config.
188
193
  * https://tailwindcss.com/docs/theme
@@ -13,6 +13,7 @@ export function cacheMergedConfigs({ vuelessSrcDir, basePath }?: {
13
13
  }): Promise<void>;
14
14
  export function buildTSFile(entryPath: string, configOutFile: string): Promise<void>;
15
15
  export function removeFolderIfEmpty(dirPath: string): Promise<void>;
16
+ export function getAppName(): string;
16
17
  export function detectTypeScript(): Promise<boolean>;
17
18
  export function autoImportUserConfigs(basePath?: string): Promise<void>;
18
19
  export function generateConfigIndexContent(imports?: string[], componentEntries?: string[]): Promise<string>;
@@ -2,7 +2,7 @@ import { build as rolldownBuild } from "rolldown";
2
2
  import path from "node:path";
3
3
  import { cwd } from "node:process";
4
4
  import { pathToFileURL } from "node:url";
5
- import { existsSync, statSync } from "node:fs";
5
+ import { existsSync, statSync, readFileSync } from "node:fs";
6
6
  import { mkdir, readdir, rmdir, readFile, writeFile } from "node:fs/promises";
7
7
 
8
8
  import { getMergedConfig, getVuelessConfig } from "./vuelessConfig.js";
@@ -226,6 +226,18 @@ export async function removeFolderIfEmpty(dirPath) {
226
226
  }
227
227
  }
228
228
 
229
+ /**
230
+ * Reads the app name from the project's package.json (used to prefix theme cookies).
231
+ * @returns {string} The package name, or an empty string if unavailable.
232
+ */
233
+ export function getAppName() {
234
+ try {
235
+ return JSON.parse(readFileSync(path.join(cwd(), "package.json"), "utf-8")).name || "";
236
+ } catch {
237
+ return "";
238
+ }
239
+ }
240
+
229
241
  /**
230
242
  * Detects if TypeScript is a dependency in the project's package.json
231
243
  * @returns {Promise<boolean>} True if TypeScript is found in dependencies or devDependencies
@@ -0,0 +1,124 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ import { AUTO_MODE_KEY, COLOR_MODE_KEY } from "../../constants.js";
4
+ import { ColorMode } from "../../types";
5
+ import type { Config } from "../../types";
6
+
7
+ const mockVuelessConfig: Config = {};
8
+
9
+ vi.mock("../ui", () => ({ vuelessConfig: mockVuelessConfig }));
10
+
11
+ /* jsdom doesn't implement matchMedia, which theme.ts evaluates on import. */
12
+ const prefersDark = { matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn() };
13
+
14
+ window.matchMedia = vi.fn(() => prefersDark) as unknown as typeof window.matchMedia;
15
+
16
+ const { getTheme, getThemeCookieName, resetTheme } = await import("../theme");
17
+
18
+ function setConfig(config: Config) {
19
+ Object.keys(mockVuelessConfig).forEach((key) => delete mockVuelessConfig[key as keyof Config]);
20
+ Object.assign(mockVuelessConfig, config);
21
+ }
22
+
23
+ function clearCookies() {
24
+ document.cookie.split("; ").forEach((cookie) => {
25
+ const name = cookie.split("=")[0];
26
+
27
+ if (name) document.cookie = `${name}=; max-age=-1; path=/`;
28
+ });
29
+ }
30
+
31
+ beforeEach(() => {
32
+ localStorage.clear();
33
+ clearCookies();
34
+ setConfig({});
35
+ });
36
+
37
+ describe("getThemeCookieName", () => {
38
+ it("prefixes a key with the app name from package.json", () => {
39
+ expect(getThemeCookieName(COLOR_MODE_KEY)).toBe(`vueless-${COLOR_MODE_KEY}`);
40
+ });
41
+
42
+ it("makes the prefix cookie-safe", () => {
43
+ setConfig({ cookiePrefix: "@acme/Back_Office!" });
44
+
45
+ expect(getThemeCookieName(COLOR_MODE_KEY)).toBe(`acme-back_office-${COLOR_MODE_KEY}`);
46
+ });
47
+
48
+ it("lets cookiePrefix override the app name", () => {
49
+ setConfig({ cookiePrefix: "app-a" });
50
+
51
+ expect(getThemeCookieName(COLOR_MODE_KEY)).toBe(`app-a-${COLOR_MODE_KEY}`);
52
+ });
53
+
54
+ it("returns the legacy name when cookiePrefix is an empty string", () => {
55
+ setConfig({ cookiePrefix: "" });
56
+
57
+ expect(getThemeCookieName(COLOR_MODE_KEY)).toBe(COLOR_MODE_KEY);
58
+ });
59
+ });
60
+
61
+ describe("theme value storage", () => {
62
+ it("writes the cookie prefixed and the localStorage key raw", () => {
63
+ setConfig({ cookiePrefix: "app-a" });
64
+
65
+ getTheme({ colorMode: ColorMode.Dark });
66
+
67
+ expect(localStorage.getItem(COLOR_MODE_KEY)).toBe(ColorMode.Dark);
68
+ expect(document.cookie).toContain(`app-a-${COLOR_MODE_KEY}=${ColorMode.Dark}`);
69
+ expect(document.cookie.split("; ")).not.toContain(`${COLOR_MODE_KEY}=${ColorMode.Dark}`);
70
+ });
71
+
72
+ it("deletes the prefixed cookies on resetTheme", () => {
73
+ setConfig({ cookiePrefix: "app-a" });
74
+
75
+ getTheme({ colorMode: ColorMode.Dark });
76
+ resetTheme();
77
+
78
+ expect(localStorage.getItem(COLOR_MODE_KEY)).toBeNull();
79
+ expect(document.cookie).not.toContain(`app-a-${COLOR_MODE_KEY}=${ColorMode.Dark}`);
80
+ });
81
+ });
82
+
83
+ describe("CSR color mode precedence", () => {
84
+ it("prefers the explicit mode over everything else", () => {
85
+ localStorage.setItem(COLOR_MODE_KEY, ColorMode.Light);
86
+ setConfig({ colorMode: ColorMode.Light });
87
+
88
+ expect(getTheme({ colorMode: ColorMode.Dark }).colorMode).toBe(ColorMode.Dark);
89
+ });
90
+
91
+ it("falls back to localStorage when no mode is passed", () => {
92
+ localStorage.setItem(COLOR_MODE_KEY, ColorMode.Dark);
93
+ setConfig({ colorMode: ColorMode.Light });
94
+
95
+ expect(getTheme().colorMode).toBe(ColorMode.Dark);
96
+ });
97
+
98
+ it("ignores cookies when localStorage is empty", () => {
99
+ document.cookie = `${COLOR_MODE_KEY}=${ColorMode.Dark}; path=/`;
100
+ document.cookie = `vueless-${COLOR_MODE_KEY}=${ColorMode.Dark}; path=/`;
101
+ setConfig({ colorMode: ColorMode.Light });
102
+
103
+ expect(getTheme().colorMode).toBe(ColorMode.Light);
104
+ });
105
+
106
+ it("falls back to vuelessConfig.colorMode", () => {
107
+ setConfig({ colorMode: ColorMode.Dark });
108
+
109
+ expect(getTheme().colorMode).toBe(ColorMode.Dark);
110
+ });
111
+
112
+ it("falls back to light when nothing is set", () => {
113
+ expect(getTheme().colorMode).toBe(ColorMode.Light);
114
+ });
115
+ });
116
+
117
+ describe("auto color mode", () => {
118
+ it("reports auto mode from the cached localStorage flag", () => {
119
+ localStorage.setItem(AUTO_MODE_KEY, "1");
120
+ localStorage.setItem(COLOR_MODE_KEY, ColorMode.Dark);
121
+
122
+ expect(getTheme().isColorModeAuto).toBe(true);
123
+ });
124
+ });
package/utils/theme.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { cloneDeep, merge } from "lodash-es";
2
2
 
3
3
  import { vuelessConfig } from "./ui";
4
- import { isCSR, getStored, getCookie, setCookie, deleteCookie, toNumber } from "./helper";
4
+ import { isCSR, getStored, setCookie, deleteCookie, toNumber } from "./helper";
5
5
 
6
6
  import {
7
7
  PX_IN_REM,
@@ -63,14 +63,33 @@ declare interface SetColorMode {
63
63
  /* Creates a media query that checks if the user's system color scheme is set to the dark. */
64
64
  const prefersColorSchemeDark = isCSR && window.matchMedia("(prefers-color-scheme: dark)");
65
65
 
66
+ /* Prefixes theme cookie names, so apps sharing a host (cookies ignore the port) don't collide. */
67
+ export function getThemeCookieName(key: string) {
68
+ const appName = typeof __VUELESS_APP_NAME__ === "undefined" ? "" : __VUELESS_APP_NAME__;
69
+ const prefix = (vuelessConfig.cookiePrefix ?? appName)
70
+ .toLowerCase()
71
+ .replace(/[^a-z0-9_-]/g, "-")
72
+ .replace(/^-+|-+$/g, "");
73
+
74
+ return prefix ? `${prefix}-${key}` : key;
75
+ }
76
+
77
+ /* Saves a theme value under the raw key in local storage (client) and the prefixed cookie (SSR). */
78
+ function storeThemeValue(key: string, value: string) {
79
+ if (!isCSR) return;
80
+
81
+ localStorage.setItem(key, value);
82
+ setCookie(getThemeCookieName(key), value);
83
+ }
84
+
66
85
  function toggleColorModeClass() {
67
86
  if (!prefersColorSchemeDark) return;
68
87
 
69
88
  const isDarkMode = prefersColorSchemeDark.matches;
70
89
  const colorMode = isDarkMode ? ColorMode.Dark : ColorMode.Light;
71
90
 
72
- setCookie(COLOR_MODE_KEY, colorMode);
73
- setCookie(AUTO_MODE_KEY, String(Number(true)));
91
+ storeThemeValue(COLOR_MODE_KEY, colorMode);
92
+ storeThemeValue(AUTO_MODE_KEY, String(Number(true)));
74
93
 
75
94
  document.documentElement.classList.toggle(DARK_MODE_CLASS, isDarkMode);
76
95
  document.documentElement.classList.toggle(LIGHT_MODE_CLASS, !isDarkMode);
@@ -90,8 +109,9 @@ function toggleColorModeClass() {
90
109
  * - `isColorModeAuto` {boolean}: Indicates whether the color mode is set to auto.
91
110
  */
92
111
  function setCSRColorMode(mode: `${ColorMode}`): SetColorMode {
93
- const colorMode = mode || getCookie(COLOR_MODE_KEY) || vuelessConfig.colorMode || ColorMode.Light;
94
- const isCachedAutoMode = !!Number(getCookie(AUTO_MODE_KEY) ?? 0);
112
+ const colorMode = mode || getStored(COLOR_MODE_KEY) || vuelessConfig.colorMode || ColorMode.Light;
113
+ const storedAutoMode = getStored(AUTO_MODE_KEY);
114
+ const isCachedAutoMode = !!Number(storedAutoMode ?? 0);
95
115
 
96
116
  const isAutoMode = colorMode === ColorMode.Auto;
97
117
  const isSystemDarkMode = isAutoMode && prefersColorSchemeDark && prefersColorSchemeDark?.matches;
@@ -114,17 +134,15 @@ function setCSRColorMode(mode: `${ColorMode}`): SetColorMode {
114
134
  /* Dispatching custom event for the useDarkMode composable. */
115
135
  window.dispatchEvent(new CustomEvent("darkModeChange", { detail: isDarkMode }));
116
136
 
117
- /* Saving color mode value into cookies (server) and local storage (client). */
118
137
  let currentColorMode = colorMode;
119
138
 
120
139
  if (isAutoMode) {
121
140
  currentColorMode = isDarkMode ? ColorMode.Dark : ColorMode.Light;
122
141
  }
123
142
 
124
- /* Define color mode cookies to be used in both CSR and SSR */
125
- if (mode || getCookie(AUTO_MODE_KEY) === undefined) {
126
- setCookie(COLOR_MODE_KEY, currentColorMode);
127
- setCookie(AUTO_MODE_KEY, String(Number(isAutoMode)));
143
+ if (mode || storedAutoMode === undefined) {
144
+ storeThemeValue(COLOR_MODE_KEY, currentColorMode);
145
+ storeThemeValue(AUTO_MODE_KEY, String(Number(isAutoMode)));
128
146
 
129
147
  if (mode !== ColorMode.Auto && prefersColorSchemeDark) {
130
148
  prefersColorSchemeDark.removeEventListener("change", toggleColorModeClass);
@@ -196,7 +214,7 @@ export function resetTheme() {
196
214
 
197
215
  themeKeys.forEach((key) => {
198
216
  localStorage.removeItem(key);
199
- deleteCookie(key);
217
+ deleteCookie(getThemeCookieName(key));
200
218
  });
201
219
  }
202
220
 
@@ -436,8 +454,7 @@ function getPrimaryColor(primary?: PrimaryColors) {
436
454
  validateColorShades(primaryColor, PRIMARY_COLOR);
437
455
 
438
456
  if (isCSR && primary) {
439
- setCookie(storageKey, JSON.stringify(primaryColor));
440
- localStorage.setItem(storageKey, JSON.stringify(primaryColor));
457
+ storeThemeValue(storageKey, JSON.stringify(primaryColor));
441
458
  }
442
459
 
443
460
  return primaryColor;
@@ -454,8 +471,7 @@ function getPrimaryColor(primary?: PrimaryColors) {
454
471
  }
455
472
 
456
473
  if (isCSR && primary) {
457
- setCookie(storageKey, String(primaryColor));
458
- localStorage.setItem(storageKey, String(primaryColor));
474
+ storeThemeValue(storageKey, String(primaryColor));
459
475
  }
460
476
 
461
477
  return primaryColor;
@@ -486,8 +502,7 @@ function getNeutralColor(neutral?: NeutralColors) {
486
502
  validateColorShades(neutralColor, NEUTRAL_COLOR);
487
503
 
488
504
  if (isCSR && neutral) {
489
- setCookie(storageKey, JSON.stringify(neutralColor));
490
- localStorage.setItem(storageKey, JSON.stringify(neutralColor));
505
+ storeThemeValue(storageKey, JSON.stringify(neutralColor));
491
506
  }
492
507
 
493
508
  return neutralColor;
@@ -503,8 +518,7 @@ function getNeutralColor(neutral?: NeutralColors) {
503
518
  }
504
519
 
505
520
  if (isCSR && neutral) {
506
- setCookie(storageKey, String(neutralColor));
507
- localStorage.setItem(storageKey, String(neutralColor));
521
+ storeThemeValue(storageKey, String(neutralColor));
508
522
  }
509
523
 
510
524
  return neutralColor;
@@ -553,15 +567,10 @@ function getText(text?: ThemeConfig["text"]) {
553
567
  /* eslint-enable prettier/prettier,vue/max-len */
554
568
 
555
569
  if (isCSR && text !== undefined) {
556
- setCookie(storageKey.xs, String(mergedText.xs));
557
- setCookie(storageKey.sm, String(mergedText.sm));
558
- setCookie(storageKey.md, String(mergedText.md));
559
- setCookie(storageKey.lg, String(mergedText.lg));
560
-
561
- localStorage.setItem(storageKey.xs, String(mergedText.xs));
562
- localStorage.setItem(storageKey.sm, String(mergedText.sm));
563
- localStorage.setItem(storageKey.md, String(mergedText.md));
564
- localStorage.setItem(storageKey.lg, String(mergedText.lg));
570
+ storeThemeValue(storageKey.xs, String(mergedText.xs));
571
+ storeThemeValue(storageKey.sm, String(mergedText.sm));
572
+ storeThemeValue(storageKey.md, String(mergedText.md));
573
+ storeThemeValue(storageKey.lg, String(mergedText.lg));
565
574
  }
566
575
 
567
576
  return mergedText;
@@ -609,13 +618,9 @@ function getOutlines(outline?: ThemeConfig["outline"]) {
609
618
  /* eslint-enable prettier/prettier,vue/max-len */
610
619
 
611
620
  if (isCSR && outline !== undefined) {
612
- setCookie(storageKey.sm, String(mergedOutline.sm));
613
- setCookie(storageKey.md, String(mergedOutline.md));
614
- setCookie(storageKey.lg, String(mergedOutline.lg));
615
-
616
- localStorage.setItem(storageKey.sm, String(mergedOutline.sm));
617
- localStorage.setItem(storageKey.md, String(mergedOutline.md));
618
- localStorage.setItem(storageKey.lg, String(mergedOutline.lg));
621
+ storeThemeValue(storageKey.sm, String(mergedOutline.sm));
622
+ storeThemeValue(storageKey.md, String(mergedOutline.md));
623
+ storeThemeValue(storageKey.lg, String(mergedOutline.lg));
619
624
  }
620
625
 
621
626
  return mergedOutline;
@@ -672,13 +677,9 @@ function getRoundings(rounding?: ThemeConfig["rounding"]) {
672
677
  /* eslint-enable prettier/prettier,vue/max-len */
673
678
 
674
679
  if (isCSR && rounding !== undefined) {
675
- setCookie(storageKey.sm, String(mergedRounding.sm));
676
- setCookie(storageKey.md, String(mergedRounding.md));
677
- setCookie(storageKey.lg, String(mergedRounding.lg));
678
-
679
- localStorage.setItem(storageKey.sm, String(mergedRounding.sm));
680
- localStorage.setItem(storageKey.md, String(mergedRounding.md));
681
- localStorage.setItem(storageKey.lg, String(mergedRounding.lg));
680
+ storeThemeValue(storageKey.sm, String(mergedRounding.sm));
681
+ storeThemeValue(storageKey.md, String(mergedRounding.md));
682
+ storeThemeValue(storageKey.lg, String(mergedRounding.lg));
682
683
  }
683
684
 
684
685
  return mergedRounding;
@@ -695,8 +696,7 @@ function getSpacing(spacing?: ThemeConfig["spacing"]) {
695
696
  const mergedSpacing = Number(storedSpacing ?? DEFAULT_SPACING);
696
697
 
697
698
  if (isCSR && spacing !== undefined) {
698
- setCookie(storageKey, String(mergedSpacing));
699
- localStorage.setItem(storageKey, String(mergedSpacing));
699
+ storeThemeValue(storageKey, String(mergedSpacing));
700
700
  }
701
701
 
702
702
  return mergedSpacing;
@@ -713,8 +713,7 @@ function getLetterSpacing(letterSpacing?: ThemeConfig["letterSpacing"]) {
713
713
  const mergedSpacing = Number(spacing ?? DEFAULT_LETTER_SPACING);
714
714
 
715
715
  if (isCSR && letterSpacing !== undefined) {
716
- setCookie(storageKey, String(mergedSpacing));
717
- localStorage.setItem(storageKey, String(mergedSpacing));
716
+ storeThemeValue(storageKey, String(mergedSpacing));
718
717
  }
719
718
 
720
719
  return mergedSpacing;
@@ -731,8 +730,7 @@ function getDisabledOpacity(disabledOpacity?: ThemeConfig["disabledOpacity"]) {
731
730
  const mergedOpacity = Math.max(0, Number(opacity ?? DEFAULT_DISABLED_OPACITY));
732
731
 
733
732
  if (isCSR && disabledOpacity !== undefined) {
734
- setCookie(storageKey, String(mergedOpacity));
735
- localStorage.setItem(storageKey, String(mergedOpacity));
733
+ storeThemeValue(storageKey, String(mergedOpacity));
736
734
  }
737
735
 
738
736
  return mergedOpacity;