vueless 1.2.10 → 1.2.11

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
@@ -26,7 +26,7 @@ Vueless is simple enough for everyday use and powerful enough for advanced scena
26
26
  - ♿️ Accessibility (a11y)
27
27
  - ⚙️ Server-side rendering (SSR) friendly
28
28
  - 🖼️ 1000+ built-in SVG icons
29
- - 🧪️ 1200+ unit tests ensuring consistent logic
29
+ - 🧪️ 1300+ unit tests ensuring consistent logic
30
30
  - 🛡️ Full TypeScript support with type safety
31
31
 
32
32
  ### Advanced Features
@@ -14,15 +14,16 @@ import { COMPONENTS, VUELESS_PACKAGE_DIR, VUELESS_USER_COMPONENTS_DIR } from "..
14
14
  /**
15
15
  * Copies an existing Vueless component to a new location with a new name.
16
16
  * This includes duplicating the source files and updating the component's internal references as needed.
17
+ * If the target name is omitted or matches the source name, the component will override the original.
17
18
  *
18
- * @param {Array<string>} options - An array containing two elements:
19
+ * @param {Array<string>} options - An array containing one or two elements:
19
20
  * – The name of the component to be copied.
20
- * – The desired name for the copied component.
21
+ * – (Optional) The desired name for the copied component. If omitted, uses the source component name.
21
22
  * @return {Promise<void>} A promise that resolves when the component has been successfully copied.
22
23
  * If an error occurs, no value is returned, and the operation exits early with a logged message.
23
24
  */
24
25
  export async function copyVuelessComponent(options) {
25
- const [componentName, newComponentName] = options;
26
+ const [componentName, newComponentName = ""] = options;
26
27
 
27
28
  if (!componentName) {
28
29
  console.log(styleText("red", "Component name is required."));
@@ -36,21 +37,14 @@ export async function copyVuelessComponent(options) {
36
37
  return;
37
38
  }
38
39
 
39
- if (newComponentName in COMPONENTS) {
40
- console.log(
41
- styleText("red", `Component with name '${newComponentName}' already exists in Vueless UI.`),
42
- );
43
-
44
- return;
45
- }
46
-
47
- if (!newComponentName.startsWith("U")) {
40
+ if (newComponentName && !newComponentName.startsWith("U")) {
48
41
  console.log(styleText("red", `Component should have 'U' prefix (ex. 'UButtonCustom').`));
49
42
 
50
43
  return;
51
44
  }
52
45
 
53
46
  const absoluteSourcePath = path.join(cwd(), VUELESS_PACKAGE_DIR, COMPONENTS[componentName]);
47
+
54
48
  const absoluteDestPath = path.join(cwd(), VUELESS_USER_COMPONENTS_DIR, newComponentName);
55
49
 
56
50
  if (existsSync(absoluteDestPath)) {
@@ -62,10 +56,14 @@ export async function copyVuelessComponent(options) {
62
56
  await cp(absoluteSourcePath, absoluteDestPath, { recursive: true });
63
57
  await modifyCreatedComponent(absoluteDestPath, componentName, newComponentName);
64
58
 
65
- console.log(
66
- // eslint-disable-next-line vue/max-len, prettier/prettier
67
- styleText("green", `The '${componentName}' was successfully copied into the '${VUELESS_USER_COMPONENTS_DIR}/${newComponentName}' directory.`)
68
- );
59
+ const isOverridingOriginal = componentName === newComponentName;
60
+ const destDir = `${VUELESS_USER_COMPONENTS_DIR}/${newComponentName}`;
61
+
62
+ const successMessage = isOverridingOriginal
63
+ ? `The '${componentName}' was successfully copied and will override the original component.`
64
+ : `The '${componentName}' was successfully copied into the '${destDir}' directory.`;
65
+
66
+ console.log(styleText("green", successMessage));
69
67
  }
70
68
 
71
69
  /**
@@ -1,4 +1,5 @@
1
1
  import { readonly, ref } from "vue";
2
+ import { isCSR } from "../utils/helper";
2
3
 
3
4
  import type { Ref } from "vue";
4
5
 
@@ -34,8 +35,10 @@ function onRemoveFromRequestQueue(event: CustomEvent<{ request: string }>) {
34
35
  removeFromRequestQueue(event.detail.request);
35
36
  }
36
37
 
37
- window.addEventListener("addToRequestQueue", onAddToRequestQueue as EventListener);
38
- window.addEventListener("removeFromRequestQueue", onRemoveFromRequestQueue as EventListener);
38
+ if (isCSR) {
39
+ window.addEventListener("addToRequestQueue", onAddToRequestQueue as EventListener);
40
+ window.addEventListener("removeFromRequestQueue", onRemoveFromRequestQueue as EventListener);
41
+ }
39
42
 
40
43
  export function useRequestQueue() {
41
44
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vueless",
3
- "version": "1.2.10",
3
+ "version": "1.2.11",
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
@@ -20,6 +20,7 @@ import { createTailwindSafelist, clearTailwindSafelist } from "./utils/node/tail
20
20
  import { componentResolver, directiveResolver } from "./utils/node/vuelessResolver.js";
21
21
  import { setCustomPropTypes, removeCustomPropTypes } from "./utils/node/dynamicProps.js";
22
22
  import { buildWebTypes } from "./utils/node/webTypes.js";
23
+ import { overrideComponents, restoreComponents } from "./utils/node/componentOverride.js";
23
24
  import {
24
25
  getNuxtDirs,
25
26
  getVueDirs,
@@ -84,6 +85,7 @@ export const Vueless = function (options = {}) {
84
85
  process.on("SIGINT", async () => {
85
86
  if (isInternalEnv || isStorybookEnv) {
86
87
  await removeCustomPropTypes(vuelessSrcDir);
88
+ await restoreComponents(vuelessSrcDir);
87
89
  }
88
90
 
89
91
  /* remove cached icons */
@@ -129,6 +131,9 @@ export const Vueless = function (options = {}) {
129
131
  await cacheMergedConfigs({ vuelessSrcDir, basePath });
130
132
  }
131
133
 
134
+ /* override components with custom ones from .vueless/components */
135
+ await overrideComponents({ vuelessSrcDir });
136
+
132
137
  /* set custom prop types */
133
138
  await setCustomPropTypes({ vuelessSrcDir, basePath });
134
139
 
@@ -69,6 +69,13 @@ const prefixedHref = computed(() => {
69
69
  });
70
70
 
71
71
  function onClick(event: MouseEvent) {
72
+ if (props.disabled) {
73
+ event.preventDefault();
74
+ event.stopPropagation();
75
+
76
+ return;
77
+ }
78
+
72
79
  emit("click", event);
73
80
  }
74
81
 
@@ -91,7 +91,7 @@ const { monthViewAttrs, monthAttrs, currentMonthAttrs, selectedMonthAttrs, activ
91
91
  size="md"
92
92
  v-bind="selectedMonthAttrs"
93
93
  :disabled="dateIsOutOfRange(month, minDate, maxDate, locale, dateFormat)"
94
- :label="formatDate(month, 'F', props.locale)"
94
+ :label="formatDate(month, 'M', props.locale)"
95
95
  @click="onClickMonth(month)"
96
96
  @mousedown.prevent.capture
97
97
  />
@@ -102,7 +102,7 @@ const { monthViewAttrs, monthAttrs, currentMonthAttrs, selectedMonthAttrs, activ
102
102
  color="primary"
103
103
  v-bind="currentMonthAttrs"
104
104
  :disabled="dateIsOutOfRange(month, minDate, maxDate, locale, dateFormat)"
105
- :label="formatDate(month, 'F', props.locale)"
105
+ :label="formatDate(month, 'M', props.locale)"
106
106
  @click="onClickMonth(month)"
107
107
  @mousedown.prevent.capture
108
108
  />
@@ -114,7 +114,7 @@ const { monthViewAttrs, monthAttrs, currentMonthAttrs, selectedMonthAttrs, activ
114
114
  size="md"
115
115
  v-bind="activeMonthAttrs"
116
116
  :disabled="dateIsOutOfRange(month, minDate, maxDate, locale, dateFormat)"
117
- :label="formatDate(month, 'F', props.locale)"
117
+ :label="formatDate(month, 'M', props.locale)"
118
118
  @click="onClickMonth(month)"
119
119
  @mousedown.prevent.capture
120
120
  />
@@ -126,7 +126,7 @@ const { monthViewAttrs, monthAttrs, currentMonthAttrs, selectedMonthAttrs, activ
126
126
  size="md"
127
127
  v-bind="monthAttrs"
128
128
  :disabled="dateIsOutOfRange(month, minDate, maxDate, locale, dateFormat)"
129
- :label="formatDate(month, 'F', props.locale)"
129
+ :label="formatDate(month, 'M', props.locale)"
130
130
  @click="onClickMonth(month)"
131
131
  @mousedown.prevent.capture
132
132
  />
@@ -585,10 +585,10 @@ describe("UCalendar.vue", () => {
585
585
  });
586
586
 
587
587
  it("Arrow Key Navigation – moves focus correctly in month view", async () => {
588
- const expectedMonthAfterRight = "February";
589
- const expectedMonthAfterLeft = "January";
590
- const expectedMonthAfterDown = "April";
591
- const expectedMonthAfterUp = "January";
588
+ const expectedMonthAfterRight = "Feb";
589
+ const expectedMonthAfterLeft = "Jan";
590
+ const expectedMonthAfterDown = "Apr";
591
+ const expectedMonthAfterUp = "Jan";
592
592
 
593
593
  const component = mount(UCalendar, {
594
594
  props: {
@@ -205,7 +205,7 @@ describe("UCalendarMonthView.vue", () => {
205
205
 
206
206
  const allButtons = component.findAllComponents(UButton);
207
207
  const mayButton = allButtons.find((button) => button.props("label") === "May");
208
- const decemberButton = allButtons.find((button) => button.props("label") === "December");
208
+ const decemberButton = allButtons.find((button) => button.props("label") === "Dec");
209
209
 
210
210
  expect(mayButton?.props("disabled")).toBe(true);
211
211
  expect(decemberButton?.props("disabled")).toBe(true);
@@ -27,26 +27,15 @@ const inputRangeToError = defineModel<string>("inputRangeToError", { required: t
27
27
  const rangeStart = defineModel<string>("rangeStart", { required: true });
28
28
  const rangeEnd = defineModel<string>("rangeEnd", { required: true });
29
29
 
30
- function isGraterThanTo(value: string) {
31
- if (!value) return false;
30
+ function isFromGreaterThanTo(fromValue: string, toValue: string) {
31
+ if (!fromValue || !toValue) return false;
32
32
 
33
- const parsedValue = parseDate(value, INPUT_RANGE_FORMAT, props.locale);
34
- const parsedTo = parseDate(localValue.value.to, props.dateFormat, props.locale);
33
+ const parsedFrom = parseDate(fromValue, INPUT_RANGE_FORMAT, props.locale);
34
+ const parsedTo = parseDate(toValue, INPUT_RANGE_FORMAT, props.locale);
35
35
 
36
- if (!parsedValue || !parsedTo) return false;
36
+ if (!parsedFrom || !parsedTo) return false;
37
37
 
38
- return parsedValue > parsedTo && !isSameDay(parsedValue, parsedTo);
39
- }
40
-
41
- function isSmallerThanFrom(value: string) {
42
- if (!value) return false;
43
-
44
- const parsedValue = parseDate(value, INPUT_RANGE_FORMAT, props.locale);
45
- const parsedFrom = parseDate(localValue.value.from, props.dateFormat, props.locale);
46
-
47
- if (!parsedValue || !parsedFrom) return false;
48
-
49
- return parsedValue < parsedFrom && !isSameDay(parsedValue, parsedFrom);
38
+ return parsedFrom > parsedTo && !isSameDay(parsedFrom, parsedTo);
50
39
  }
51
40
 
52
41
  function validateInput(value: string, type: `${InputRangeType}`) {
@@ -60,18 +49,30 @@ function validateInput(value: string, type: `${InputRangeType}`) {
60
49
  error = props.locale.notCorrectMonthNumber;
61
50
  } else if (isWrongDayNumber(value) && value) {
62
51
  error = props.locale.notCorrectDayNumber;
63
- } else if (isGraterThanTo(value) && type === InputRangeType.Start) {
52
+ } else if (type === InputRangeType.Start && isFromGreaterThanTo(value, rangeEnd.value)) {
64
53
  error = props.locale.fromDateGraterThanSecond;
65
- } else if (isSmallerThanFrom(value) && type === InputRangeType.End) {
54
+ } else if (type === InputRangeType.End && isFromGreaterThanTo(rangeStart.value, value)) {
66
55
  error = props.locale.toDateSmallerThanFirst;
67
56
  }
68
57
 
69
58
  if (type === InputRangeType.Start) {
70
59
  inputRangeFromError.value = error;
60
+
61
+ if (!error && rangeEnd.value) {
62
+ inputRangeToError.value = isFromGreaterThanTo(value, rangeEnd.value)
63
+ ? props.locale.toDateSmallerThanFirst
64
+ : "";
65
+ }
71
66
  }
72
67
 
73
68
  if (type === InputRangeType.End) {
74
69
  inputRangeToError.value = error;
70
+
71
+ if (!error && rangeStart.value) {
72
+ inputRangeFromError.value = isFromGreaterThanTo(rangeStart.value, value)
73
+ ? props.locale.fromDateGraterThanSecond
74
+ : "";
75
+ }
75
76
  }
76
77
  }
77
78
 
@@ -126,7 +126,6 @@ const {
126
126
  :label="item.label"
127
127
  :value="item.value"
128
128
  :disabled="item.disabled"
129
- :size="size"
130
129
  v-bind="tabAttrs"
131
130
  :data-test="getDataTest(`item-${index}`)"
132
131
  />
@@ -0,0 +1,2 @@
1
+ export function overrideComponents({ vuelessSrcDir }?: {}): Promise<void>;
2
+ export function restoreComponents(vuelessSrcDir: any): Promise<void>;
@@ -0,0 +1,139 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
4
+ import { cwd } from "node:process";
5
+
6
+ import { CACHE_DIR, COMPONENTS, VUELESS_USER_COMPONENTS_DIR } from "../../constants.js";
7
+
8
+ async function isDirNotEmpty(dirPath) {
9
+ if (!existsSync(dirPath)) {
10
+ return false;
11
+ }
12
+
13
+ try {
14
+ const files = await fs.readdir(dirPath);
15
+
16
+ return files.length > 0;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ async function cacheOriginalComponent(componentDir, componentName) {
23
+ const cacheDir = path.join(componentDir, CACHE_DIR, componentName);
24
+
25
+ if (existsSync(cacheDir)) {
26
+ return;
27
+ }
28
+
29
+ if (!existsSync(path.join(componentDir, CACHE_DIR))) {
30
+ await fs.mkdir(path.join(componentDir, CACHE_DIR), { recursive: true });
31
+ }
32
+
33
+ await fs.mkdir(cacheDir, { recursive: true });
34
+
35
+ const files = await fs.readdir(componentDir);
36
+
37
+ for (const file of files) {
38
+ const filePath = path.join(componentDir, file);
39
+ const stat = await fs.stat(filePath);
40
+
41
+ if (stat.isFile()) {
42
+ const destPath = path.join(cacheDir, file);
43
+
44
+ await fs.copyFile(filePath, destPath);
45
+ } else if (stat.isDirectory() && file !== CACHE_DIR) {
46
+ const destPath = path.join(cacheDir, file);
47
+
48
+ await fs.cp(filePath, destPath, { recursive: true });
49
+ }
50
+ }
51
+ }
52
+
53
+ async function copyCustomComponent(customComponentDir, targetComponentDir) {
54
+ const files = await fs.readdir(customComponentDir);
55
+
56
+ for (const file of files) {
57
+ const sourcePath = path.join(customComponentDir, file);
58
+ const destPath = path.join(targetComponentDir, file);
59
+ const stat = await fs.stat(sourcePath);
60
+
61
+ if (stat.isFile()) {
62
+ await fs.copyFile(sourcePath, destPath);
63
+ } else if (stat.isDirectory()) {
64
+ await fs.cp(sourcePath, destPath, { recursive: true, force: true });
65
+ }
66
+ }
67
+ }
68
+
69
+ export async function overrideComponents({ vuelessSrcDir } = {}) {
70
+ const customComponentsDir = path.join(cwd(), VUELESS_USER_COMPONENTS_DIR);
71
+
72
+ if (!existsSync(customComponentsDir)) {
73
+ return;
74
+ }
75
+
76
+ for await (const [componentName, componentDir] of Object.entries(COMPONENTS)) {
77
+ const customComponentPath = path.join(customComponentsDir, componentName);
78
+ const isCustomComponentExists = await isDirNotEmpty(customComponentPath);
79
+
80
+ if (!isCustomComponentExists) {
81
+ continue;
82
+ }
83
+
84
+ const targetComponentDir = path.join(vuelessSrcDir, componentDir);
85
+
86
+ await cacheOriginalComponent(targetComponentDir, componentName);
87
+ await copyCustomComponent(customComponentPath, targetComponentDir);
88
+ }
89
+ }
90
+
91
+ export async function restoreComponents(vuelessSrcDir) {
92
+ for await (const componentDir of Object.values(COMPONENTS)) {
93
+ const componentPath = path.join(vuelessSrcDir, componentDir);
94
+ const cachePath = path.join(componentPath, CACHE_DIR);
95
+
96
+ if (!existsSync(cachePath)) {
97
+ continue;
98
+ }
99
+
100
+ const cachedDirs = await fs.readdir(cachePath);
101
+
102
+ for (const cachedDir of cachedDirs) {
103
+ const cachedComponentPath = path.join(cachePath, cachedDir);
104
+ const stat = await fs.stat(cachedComponentPath);
105
+
106
+ if (!stat.isDirectory()) {
107
+ continue;
108
+ }
109
+
110
+ const files = await fs.readdir(cachedComponentPath);
111
+
112
+ for (const file of files) {
113
+ const sourcePath = path.join(cachedComponentPath, file);
114
+ const destPath = path.join(componentPath, file);
115
+ const fileStat = await fs.stat(sourcePath);
116
+
117
+ if (fileStat.isFile()) {
118
+ await fs.copyFile(sourcePath, destPath);
119
+ } else if (fileStat.isDirectory()) {
120
+ const destExists = existsSync(destPath);
121
+
122
+ if (destExists) {
123
+ await fs.rm(destPath, { recursive: true, force: true });
124
+ }
125
+
126
+ await fs.cp(sourcePath, destPath, { recursive: true });
127
+ }
128
+ }
129
+
130
+ await fs.rm(cachedComponentPath, { recursive: true, force: true });
131
+ }
132
+
133
+ const remainingFiles = await fs.readdir(cachePath);
134
+
135
+ if (remainingFiles.length === 0) {
136
+ await fs.rmdir(cachePath);
137
+ }
138
+ }
139
+ }
@@ -22,6 +22,7 @@ const OPTIONAL_MARK = "?";
22
22
  const CLOSING_BRACKET = "}";
23
23
  const IGNORE_PROP = "@ignore";
24
24
  const CUSTOM_PROP = "@custom";
25
+ const DEFAULT_PROP_TYPE = "string";
25
26
 
26
27
  /* regular expressions */
27
28
  const PROPS_INTERFACE_REG_EXP = /export\s+interface\s+Props(?:<[^>]+>)?\s*{([^}]*)}/s;
@@ -243,7 +244,7 @@ async function modifyComponentTypes(filePath, props) {
243
244
  const lines = propsInterface.split("\n");
244
245
 
245
246
  for (const name in props) {
246
- const { type = "string", values = [], description, required, ignore } = props[name];
247
+ const { type, values = [], description, required, ignore } = props[name];
247
248
 
248
249
  /* Find line with prop. */
249
250
  const propRegex = new RegExp(`^\\s*${name}[?:]?\\s*:`);
@@ -269,7 +270,7 @@ async function modifyComponentTypes(filePath, props) {
269
270
  .includes("@extendOnly");
270
271
 
271
272
  const propDescription = description?.replaceAll(/[\n\s]+/g, " ").trim() || "–"; // removes new lines and double spaces.
272
- const propType = unionType.length ? unionType : type;
273
+ const propType = unionType.length ? unionType : type || DEFAULT_PROP_TYPE;
273
274
 
274
275
  /* Add ignore JSDoc property. */
275
276
  if (ignore) {
@@ -284,7 +285,6 @@ async function modifyComponentTypes(filePath, props) {
284
285
  if (unionType.length && (isAssignableValue || !isExtendOnly)) {
285
286
  // Remove multiline union types;
286
287
  lines.splice(propIndex + 1, propEndIndex);
287
-
288
288
  lines.splice(propIndex, 1, ` ${name}${defaultOptionalMark}: ${propType};`);
289
289
  }
290
290
 
@@ -311,7 +311,7 @@ async function modifyComponentTypes(filePath, props) {
311
311
  ` * ${propDescription}`,
312
312
  ` * ${CUSTOM_PROP}`,
313
313
  ` */`,
314
- ` ${name}${optionalMark}: ${type};`,
314
+ ` ${name}${optionalMark}: ${type || DEFAULT_PROP_TYPE};`,
315
315
  ];
316
316
 
317
317
  lines.splice(closingBracketIndex, 0, ...propDefinition);
@@ -1,4 +1,5 @@
1
1
  import { unref } from "vue";
2
+ import { isSSR } from "../utils/helper";
2
3
 
3
4
  import type { MaybeRef } from "vue";
4
5
  import type {
@@ -42,6 +43,8 @@ function clickOutside(
42
43
  handler(event);
43
44
  }
44
45
 
46
+ if (isSSR) return () => {};
47
+
45
48
  window.addEventListener("click", onClick, { passive: true, capture });
46
49
  window.addEventListener("pointerdown", onClick, { passive: true });
47
50