@milaboratories/uikit 2.10.45 → 2.11.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.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +19 -19
  2. package/.turbo/turbo-formatter$colon$check.log +2 -2
  3. package/.turbo/turbo-linter$colon$check.log +2 -2
  4. package/.turbo/turbo-types$colon$check.log +1 -1
  5. package/CHANGELOG.md +18 -0
  6. package/dist/components/PlNumberField/PlNumberField.js.map +1 -1
  7. package/dist/components/PlNumberField/PlNumberField.vue.d.ts +45 -75
  8. package/dist/components/PlNumberField/PlNumberField.vue.d.ts.map +1 -1
  9. package/dist/components/PlNumberField/PlNumberField.vue2.js +129 -121
  10. package/dist/components/PlNumberField/PlNumberField.vue2.js.map +1 -1
  11. package/dist/components/PlNumberField/__test__/PlNumberField.spec.d.ts.map +1 -0
  12. package/dist/components/PlNumberField/__test__/parseNumber.spec.d.ts +2 -0
  13. package/dist/components/PlNumberField/__test__/parseNumber.spec.d.ts.map +1 -0
  14. package/dist/components/PlNumberField/parseNumber.d.ts +56 -7
  15. package/dist/components/PlNumberField/parseNumber.d.ts.map +1 -1
  16. package/dist/components/PlNumberField/parseNumber.js +40 -56
  17. package/dist/components/PlNumberField/parseNumber.js.map +1 -1
  18. package/dist/components/PlNumberField/pl-number-field.css +1 -1
  19. package/dist/components/PlSearchField/PlSearchField.js.map +1 -1
  20. package/dist/components/PlSearchField/PlSearchField.style.js.map +1 -1
  21. package/dist/components/PlSearchField/PlSearchField.vue.d.ts +20 -32
  22. package/dist/components/PlSearchField/PlSearchField.vue.d.ts.map +1 -1
  23. package/dist/components/PlSearchField/PlSearchField.vue2.js +4 -2
  24. package/dist/components/PlSearchField/PlSearchField.vue2.js.map +1 -1
  25. package/dist/components/PlTextField/PlTextField.js.map +1 -1
  26. package/dist/components/PlTextField/PlTextField.vue.d.ts +46 -118
  27. package/dist/components/PlTextField/PlTextField.vue.d.ts.map +1 -1
  28. package/dist/components/PlTextField/PlTextField.vue2.js +61 -58
  29. package/dist/components/PlTextField/PlTextField.vue2.js.map +1 -1
  30. package/package.json +5 -5
  31. package/src/components/PlNumberField/PlNumberField.vue +151 -143
  32. package/src/components/PlNumberField/__test__/PlNumberField.spec.ts +296 -0
  33. package/src/components/PlNumberField/__test__/parseNumber.spec.ts +204 -0
  34. package/src/components/PlNumberField/parseNumber.ts +125 -98
  35. package/src/components/PlNumberField/pl-number-field.scss +17 -4
  36. package/src/components/PlSearchField/PlSearchField.vue +8 -4
  37. package/src/components/PlTextField/PlTextField.vue +37 -49
  38. package/src/components/PlTextField/__tests__/TextField.spec.ts +2 -2
  39. package/dist/components/PlNumberField/__tests__/PlNumberField.spec.d.ts.map +0 -1
  40. package/src/components/PlNumberField/__tests__/PlNumberField.spec.ts +0 -182
  41. /package/dist/components/PlNumberField/{__tests__ → __test__}/PlNumberField.spec.d.ts +0 -0
@@ -1,124 +1,151 @@
1
- type ParseResult = {
2
- error?: Error;
3
- value?: number;
4
- cleanInput: string;
5
- };
6
-
7
- const NUMBER_REGEX = /^[-−–+]?(\d+)?[.,]?(\d+)?$/; // parseFloat works without errors on strings with multiple dots, or letters in value
8
-
9
- function isPartial(v: string) {
10
- return v === "." || v === "," || v === "-";
11
- }
12
-
13
- function clearNumericValue(v: string) {
14
- v = v.trim();
15
- v = v.replace(",", ".");
16
- v = v.replace("−", "-"); // minus, replacing for the case of input the whole copied value
17
- v = v.replace("–", "-"); // dash, replacing for the case of input the whole copied value
18
- v = v.replace("+", "");
19
- return v;
1
+ /**
2
+ * Strict number parser. No locale guessing, no normalization.
3
+ * Non-canonical forms (leading zeros, trailing dots, etc.) are accepted —
4
+ * formatting to canonical form happens on blur/enter in the component.
5
+ *
6
+ * Input/output table (covers ~90% of real cases):
7
+ *
8
+ * | Input | Result | Reason |
9
+ * |---------------------|---------------------------------|---------------------------------|
10
+ * | "" | {} | empty |
11
+ * | "-" | {} | partial | apply blur/enter format
12
+ * | "." | {} | partial | apply blur/enter format
13
+ * | "-." | {} | partial | apply blur/enter format
14
+ * | "123." | { value: 123 } | trailing dot | apply blur/enter format
15
+ * | "1e" | { value: 1 } | partial exp → 1e+0 | apply blur/enter format
16
+ * | "1e-" | { value: 1 } | partial exp 1e-0 | apply blur/enter format
17
+ * | "1e+" | { value: 1 } | partial exp 1e+0 | apply blur/enter format
18
+ * | "123" | { value: 123 } | exact match |
19
+ * | "-5" | { value: -5 } | exact match |
20
+ * | "0.5" | { value: 0.5 } | exact match |
21
+ * | "0.0000000001" | { value: 1e-10 } | decimal form matches |
22
+ * | "1e-5" | { value: 0.00001 } | exponential notation | apply blur/enter format
23
+ * | "2e+10" | { value: 2e10 } | exponential notation | apply blur/enter format
24
+ * | ".5" | { value: 0.5 } | not canonical | apply blur/enter format
25
+ * | "01" | { value: 1 } | leading zero | apply blur/enter format
26
+ * | "1.0" | { value: 1 } | trailing zero | apply blur/enter format
27
+ * | "1.10" | { value: 1.1 } | trailing zero | apply blur/enter format
28
+ * | "+5" | { value: 5 } | unnecessary plus | apply blur/enter format
29
+ * | "1,5" | { error: "...separator..." } | comma instead of dot |
30
+ * | "1.232,111" | { error: "...separator..." } | EU locale format |
31
+ * | "1.237.62" | { error: "...separator..." } | multiple dots (EU thousands) |
32
+ * | "555.555.555,100" | { error: "...separator..." } | EU locale format |
33
+ * | "1,222,333.05" | { error: "...separator..." } | US locale format |
34
+ * | "abc" | { error: "not a number" } | letters |
35
+ * | "12abc" | { error: "not a number" } | letters mixed in |
36
+ * | "1.237.asdf62" | { error: "not a number" } | letters mixed in |
37
+ * | "9007199254740993" | { error: "precision..." } | integer exceeds safe range |
38
+ * | "0.1234567890123456789" | { error: "precision..." } | too many digits for float64 |
39
+ */
40
+
41
+ export type ParseResult =
42
+ | { value: number; error?: undefined }
43
+ | { value?: undefined; error: string }
44
+ | { value?: undefined; error?: undefined };
45
+
46
+ const EXP_RE = /^-?\d+(\.\d+)?e[+-]?\d+$/i;
47
+ const EXP_PARTIAL_RE = /^-?\d+(\.\d+)?e[+-]?$/i;
48
+
49
+ /** "-", ".", "-." — NaN for Number() but clearly in-progress typing */
50
+ function isPartialInput(str: string): boolean {
51
+ return str === "-" || str === "." || str === "-.";
20
52
  }
21
53
 
22
- function stringToNumber(v: string) {
23
- return parseFloat(clearNumericValue(v));
24
- }
25
-
26
- function clearInput(v: string): string {
27
- v = v.trim();
28
-
29
- if (isPartial(v)) {
30
- return v;
54
+ /**
55
+ * Normalize a decimal string by removing cosmetic differences:
56
+ * leading +, leading zeros, trailing zeros after decimal, trailing dot.
57
+ * Used to compare user input with canonical float representation.
58
+ */
59
+ function normalizeDecimalString(s: string): string {
60
+ let sign = "";
61
+ if (s.startsWith("-")) {
62
+ sign = "-";
63
+ s = s.slice(1);
64
+ } else if (s.startsWith("+")) {
65
+ s = s.slice(1);
31
66
  }
32
67
 
33
- if (/^-[^0-9.]/.test(v)) {
34
- return "-";
35
- }
68
+ // Remove leading zeros (keep one before decimal point)
69
+ s = s.replace(/^0+(?=\d)/, "");
70
+ if (s.startsWith(".")) s = "0" + s;
36
71
 
37
- const match = v.match(/^(.*)[.,][^0-9].*$/);
38
- if (match) {
39
- return match[1] + ".";
72
+ // Remove trailing zeros after decimal point, then trailing dot
73
+ if (s.includes(".")) {
74
+ s = s.replace(/0+$/, "").replace(/\.$/, "");
40
75
  }
41
76
 
42
- if (v.match(NUMBER_REGEX)) {
43
- return clearNumericValue(v);
44
- }
77
+ if (s === "" || s === "0") return "0";
45
78
 
46
- const n = stringToNumber(v);
79
+ return sign + s;
80
+ }
47
81
 
48
- return isNaN(n) ? "" : String(+n);
82
+ /** Complete partial exponential: "1e" "1e+0", "1e-" → "1e-0", "1e+" → "1e+0" */
83
+ function completeExponential(str: string): string {
84
+ if (/e$/i.test(str)) return str + "+0";
85
+ if (/e[+-]$/i.test(str)) return str + "0";
86
+ return str;
49
87
  }
50
88
 
51
- export function parseNumber(
52
- props: {
53
- minValue?: number;
54
- maxValue?: number;
55
- validate?: (v: number) => string | undefined;
56
- },
57
- str: string,
58
- ): ParseResult {
89
+ export function tryParseNumber(str: string): ParseResult {
59
90
  str = str.trim();
60
-
61
- const cleanInput = clearInput(str);
62
-
63
- if (str === "") {
64
- return {
65
- value: undefined,
66
- cleanInput,
67
- };
91
+ if (str === "") return {};
92
+ if (isPartialInput(str)) return {};
93
+
94
+ // Exponential notation (full or partial)
95
+ if (EXP_RE.test(str) || EXP_PARTIAL_RE.test(str)) {
96
+ const completed = completeExponential(str);
97
+ const n = Number(completed);
98
+ if (!Number.isFinite(n)) return { error: "Value is not a number" };
99
+ return { value: n };
68
100
  }
69
101
 
70
- if (!str.match(NUMBER_REGEX)) {
71
- return {
72
- error: Error("Value is not a number"),
73
- cleanInput,
74
- };
102
+ const n = Number(str);
103
+ if (!Number.isFinite(n)) {
104
+ // Only digits, dots, commas, sign, spaces → likely a locale/format issue
105
+ if (/^[-+]?[\d.,\s]+$/.test(str)) {
106
+ return { error: "Use dot as decimal separator, e.g. 3.14" };
107
+ }
108
+ return { error: "Value is not a number" };
75
109
  }
76
110
 
77
- if (isPartial(str)) {
78
- return {
79
- error: Error("Enter a number"),
80
- cleanInput,
81
- };
111
+ // Precision loss: input has more precision than float64 can represent
112
+ const canonical = numberToDecimalString(n);
113
+ const normalized = normalizeDecimalString(str);
114
+ if (normalized !== canonical) {
115
+ return { error: `Precision exceeded, actual value: ${canonical}` };
82
116
  }
83
117
 
84
- const value = stringToNumber(str);
118
+ return { value: n };
119
+ }
85
120
 
86
- if (isNaN(value)) {
87
- return {
88
- error: Error("Value is not a number"),
89
- cleanInput,
90
- };
121
+ /**
122
+ * Converts a number to a plain decimal string (no exponential notation).
123
+ * E.g. 1e-7 "0.0000001", 2e+21 → "2000000000000000000000"
124
+ */
125
+ export function numberToDecimalString(n: number | undefined): string {
126
+ if (n === undefined) return "";
127
+ const s = String(n);
128
+ if (!s.includes("e") && !s.includes("E")) return s;
129
+ try {
130
+ return n.toFixed(20).replace(/\.?0+$/, "");
131
+ } catch {
132
+ return s;
91
133
  }
134
+ }
92
135
 
136
+ export function validateNumber(
137
+ value: number,
138
+ props: {
139
+ minValue?: number;
140
+ maxValue?: number;
141
+ validate?: (v: number) => string | undefined;
142
+ },
143
+ ): string | undefined {
93
144
  if (props.minValue !== undefined && value < props.minValue) {
94
- return {
95
- error: Error(`Value must be higher than ${props.minValue}`),
96
- value,
97
- cleanInput,
98
- };
145
+ return `Value must be higher than ${props.minValue}`;
99
146
  }
100
-
101
147
  if (props.maxValue !== undefined && value > props.maxValue) {
102
- return {
103
- error: Error(`Value must be less than ${props.maxValue}`),
104
- value,
105
- cleanInput,
106
- };
148
+ return `Value must be less than ${props.maxValue}`;
107
149
  }
108
-
109
- if (props.validate) {
110
- const error = props.validate(value);
111
- if (error) {
112
- return {
113
- error: Error(error),
114
- value,
115
- cleanInput,
116
- };
117
- }
118
- }
119
-
120
- return {
121
- value,
122
- cleanInput,
123
- };
150
+ return props.validate?.(value);
124
151
  }
@@ -7,6 +7,7 @@
7
7
  --label-offset-right-x: 8px;
8
8
  --label-color: var(--txt-01);
9
9
  --color-hint: #9d9eae;
10
+ --show-clearable: none;
10
11
 
11
12
  // overflow: hidden;
12
13
 
@@ -22,12 +23,10 @@
22
23
 
23
24
  &__wrapper {
24
25
  padding-left: 12px;
25
- // background-color: rgb(111, 94, 94);
26
+ padding-right: 8px;
27
+ gap: 8px;
26
28
  border-radius: 6px;
27
29
  }
28
- &__wrapper.withoutArrows {
29
- padding-right: 12px;
30
- }
31
30
 
32
31
  &__icons {
33
32
  // background-color: green;
@@ -60,6 +59,12 @@
60
59
  border-bottom: 1px solid var(--contour-color);
61
60
  }
62
61
 
62
+ &__clearable {
63
+ display: var(--show-clearable) !important;
64
+ --icon-color: var(--ic-02) !important;
65
+ cursor: pointer;
66
+ }
67
+
63
68
  &__hint {
64
69
  margin-top: 3px;
65
70
  color: var(--color-hint);
@@ -90,6 +95,14 @@
90
95
  transition: all 0.3s;
91
96
  }
92
97
 
98
+ &:hover {
99
+ --show-clearable: inline-block;
100
+ }
101
+
102
+ &:focus-within {
103
+ --show-clearable: inline-block;
104
+ }
105
+
93
106
  &:focus-within:not(.error) {
94
107
  --label-color: var(--txt-focus);
95
108
  --contour-color: var(--border-color-focus);
@@ -1,14 +1,14 @@
1
- <script lang="ts" setup>
1
+ <script lang="ts" setup generic="V extends undefined | string, C extends V">
2
2
  import { PlIcon16 } from "../PlIcon16";
3
3
  import { PlIcon24 } from "../PlIcon24";
4
4
  import { computed } from "vue";
5
5
  import PlTooltip from "../PlTooltip/PlTooltip.vue";
6
6
 
7
- const model = defineModel<string>({ required: true });
7
+ const model = defineModel<V>({ required: true });
8
8
 
9
9
  const props = defineProps<{
10
10
  modelValue?: string;
11
- clearable?: boolean;
11
+ clearable?: boolean | (() => C);
12
12
  placeholder?: string;
13
13
  disabled?: boolean;
14
14
  helper?: string;
@@ -20,7 +20,11 @@ const slots = defineSlots<{
20
20
  const nonEmpty = computed(() => model.value != null && model.value.length > 0);
21
21
  const hasHelper = computed(() => props.helper != null || slots.helper != null);
22
22
 
23
- const clear = () => (model.value = "");
23
+ const clear = () => {
24
+ if (props.clearable) {
25
+ model.value = (typeof props.clearable === "function" ? props.clearable() : "") as V;
26
+ }
27
+ };
24
28
  </script>
25
29
 
26
30
  <template>
@@ -7,13 +7,20 @@ export default {
7
7
  };
8
8
  </script>
9
9
 
10
- <script lang="ts" setup>
10
+ <script
11
+ lang="ts"
12
+ setup
13
+ generic="
14
+ R extends true | false,
15
+ V extends undefined | string,
16
+ C extends Exclude<V, R extends true ? undefined : never>
17
+ "
18
+ >
11
19
  import { computed, ref, useSlots } from "vue";
12
20
  import SvgRequired from "../../assets/images/required.svg?raw";
13
21
  import { getErrorMessage } from "../../helpers/error.ts";
14
22
  import DoubleContour from "../../utils/DoubleContour.vue";
15
23
  import { useLabelNotch } from "../../utils/useLabelNotch";
16
- import { useValidation } from "../../utils/useValidation";
17
24
  import { PlIcon16 } from "../PlIcon16";
18
25
  import { PlIcon24 } from "../PlIcon24";
19
26
  import { PlSvg } from "../PlSvg";
@@ -25,58 +32,37 @@ const slots = useSlots();
25
32
  /**
26
33
  * The current value of the input field.
27
34
  */
28
- const model = defineModel<string>({
29
- default: "",
35
+ const model = defineModel<V>({
36
+ required: true,
30
37
  });
31
38
 
32
39
  const props = defineProps<{
33
- /**
34
- * The label to display above the input field.
35
- */
40
+ /** The label to display above the input field. */
36
41
  label?: string;
37
42
  /**
38
43
  * If `true`, a clear icon will appear in the input field to clear the value (set it to empty string).
44
+ * If a function, calls it to get the reset value.
39
45
  */
40
- clearable?: boolean;
41
- /**
42
- * If `true`, the input field is marked as required.
43
- */
44
- required?: boolean;
45
- /**
46
- * An error message to display below the input field.
47
- */
46
+ clearable?: (R extends true ? never : boolean) | (() => C);
47
+ /** If `true`, the input field is marked as required and will show an error if left empty. */
48
+ required?: R;
49
+ /** An error message to display below the input field. */
48
50
  error?: unknown;
49
- /**
50
- * A helper text to display below the input field when there are no errors.
51
- */
51
+ /** A helper text to display below the input field when there are no errors. */
52
52
  helper?: string;
53
- /**
54
- * A placeholder text to display inside the input field when it is empty.
55
- */
53
+ /** A placeholder text to display inside the input field when it is empty. */
56
54
  placeholder?: string;
57
- /**
58
- * If `true`, the input field is disabled and cannot be interacted with.
59
- */
55
+ /** If `true`, the input field is disabled and cannot be interacted with. */
60
56
  disabled?: boolean;
61
- /**
62
- * If `true`, the input field has a dashed border.
63
- */
57
+ /** If `true`, the input field has a dashed border. */
64
58
  dashed?: boolean;
65
- /**
66
- * A prefix text to display inside the input field before the value.
67
- */
59
+ /** A prefix text to display inside the input field before the value. */
68
60
  prefix?: string;
69
- /**
70
- * An array of validation rules to apply to the input field. Each rule is a function that takes the current value and returns `true` if valid or an error message if invalid.
71
- */
72
- rules?: ((v: string) => boolean | string)[];
73
- /**
74
- * The string specifies whether the field should be a password or not, value could be "password" or undefined.
75
- */
61
+ /** Additional validity check for input value that must return an error text if failed */
62
+ validate?: (v: V) => string | undefined;
63
+ /** The string specifies whether the field should be a password or not, value could be "password" or undefined. */
76
64
  type?: "password";
77
- /**
78
- * Makes some of corners not rounded
79
- * */
65
+ /** Makes some of corners not rounded */
80
66
  groupPosition?:
81
67
  | "top"
82
68
  | "bottom"
@@ -107,24 +93,26 @@ const passwordIcon = computed(() => (showPassword.value ? "view-show" : "view-hi
107
93
 
108
94
  const clear = () => {
109
95
  if (props.clearable) {
110
- model.value = "";
96
+ model.value = (typeof props.clearable === "function" ? props.clearable() : "") as V;
111
97
  }
112
98
  };
113
99
 
114
- const validationData = useValidation(model, props.rules || []);
115
-
116
100
  const isEmpty = computed(() => model.value === "");
117
101
 
118
- const nonEmpty = computed(() => !isEmpty.value);
119
-
120
102
  const displayErrors = computed(() => {
121
103
  const errors: string[] = [];
122
104
  const propsError = getErrorMessage(props.error);
123
105
  if (propsError) {
124
106
  errors.push(propsError);
125
107
  }
126
- if (!validationData.value.isValid) {
127
- errors.push(...validationData.value.errors);
108
+ if (props.validate) {
109
+ const error = props.validate(model.value as V);
110
+ if (error) {
111
+ errors.push(error);
112
+ }
113
+ }
114
+ if (props.required && isEmpty.value) {
115
+ errors.push("Value is required");
128
116
  }
129
117
  return errors;
130
118
  });
@@ -132,7 +120,7 @@ const displayErrors = computed(() => {
132
120
  const hasErrors = computed(() => displayErrors.value.length > 0);
133
121
 
134
122
  const canShowClearable = computed(
135
- () => props.clearable && nonEmpty.value && props.type !== "password" && !props.disabled,
123
+ () => props.clearable && !isEmpty.value && props.type !== "password" && !props.disabled,
136
124
  );
137
125
 
138
126
  const togglePasswordVisibility = () => (showPassword.value = !showPassword.value);
@@ -151,7 +139,7 @@ useLabelNotch(rootRef);
151
139
  error: hasErrors,
152
140
  disabled,
153
141
  dashed,
154
- nonEmpty,
142
+ nonEmpty: !isEmpty,
155
143
  }"
156
144
  >
157
145
  <label v-if="label" ref="label">
@@ -18,7 +18,7 @@ describe("TextField", () => {
18
18
  const wrapper = mount(PlTextField, {
19
19
  props: {
20
20
  modelValue: "initialText",
21
- "onUpdate:modelValue": (e: string) => wrapper.setProps({ modelValue: e }),
21
+ "onUpdate:modelValue": (e) => wrapper.setProps({ modelValue: e }),
22
22
  },
23
23
  });
24
24
 
@@ -31,7 +31,7 @@ describe("TextField", () => {
31
31
  props: {
32
32
  modelValue: "initialText" as string | undefined,
33
33
  clearable: true,
34
- "onUpdate:modelValue": (e) => wrapper.setProps({ modelValue: e }),
34
+ "onUpdate:modelValue": (e: string | undefined) => wrapper.setProps({ modelValue: e }),
35
35
  },
36
36
  });
37
37
 
@@ -1 +0,0 @@
1
- {"version":3,"file":"PlNumberField.spec.d.ts","sourceRoot":"","sources":["../../../../src/components/PlNumberField/__tests__/PlNumberField.spec.ts"],"names":[],"mappings":""}