@zap-wunschlachen/wl-shared-components 1.0.87 → 1.0.88

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/App.vue CHANGED
@@ -73,7 +73,7 @@
73
73
  type="button"
74
74
  @click="currentPage = 'validatedinput'"
75
75
  >
76
- ValidatedInput
76
+ Input
77
77
  </button>
78
78
  </nav>
79
79
  </header>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zap-wunschlachen/wl-shared-components",
3
- "version": "1.0.87",
3
+ "version": "1.0.88",
4
4
  "type": "module",
5
5
  "module": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -27,7 +27,6 @@
27
27
  },
28
28
  "devDependencies": {
29
29
  "@axe-core/playwright": "^4.10.2",
30
- "@types/dompurify": "^3.0.5",
31
30
  "@chromatic-com/storybook": "^1.9.0",
32
31
  "@playwright/test": "^1.55.0",
33
32
  "@storybook/addon-actions": "^8.3.5",
@@ -42,6 +41,7 @@
42
41
  "@storybook/vue3-vite": "^8.3.5",
43
42
  "@testing-library/user-event": "^14.6.1",
44
43
  "@testing-library/vue": "^8.1.0",
44
+ "@types/dompurify": "^3.0.5",
45
45
  "@vitest/coverage-v8": "^3.2.4",
46
46
  "@vitest/ui": "^3.2.4",
47
47
  "@vue/test-utils": "^2.4.6",
@@ -51,17 +51,18 @@
51
51
  "playwright": "^1.55.0",
52
52
  "prettier": "^3.3.1",
53
53
  "storybook": "^8.3.5",
54
+ "tsx": "^4.21.0",
54
55
  "typescript": "^5.2.2",
55
56
  "vite": "^5.2.0",
56
57
  "vitest": "^3.2.4",
57
58
  "vue": "^3.5.11",
58
- "wait-on": "^8.0.4",
59
- "tsx": "^4.21.0"
59
+ "wait-on": "^8.0.4"
60
60
  },
61
61
  "dependencies": {
62
62
  "@heroicons/vue": "^2.1.5",
63
63
  "@mdi/font": "^7.4.47",
64
64
  "@vitejs/plugin-vue": "^5.1.4",
65
+ "awesome-phonenumber": "^7.8.0",
65
66
  "date-fns": "^4.1.0",
66
67
  "dompurify": "^3.2.4",
67
68
  "flag-icons": "^7.5.0",
@@ -66,6 +66,7 @@
66
66
  import { ref, nextTick, computed, inject, watch } from 'vue';
67
67
  import './Input.css';
68
68
  import { useI18n } from 'vue-i18n';
69
+ import { resolvePreset } from './presets';
69
70
  import { siteColors } from "../../utils/index";
70
71
 
71
72
  // Inject theme colors from ThemeProvider, fallback to global siteColors
@@ -83,6 +84,7 @@ const { t } = useI18n();
83
84
  const emit = defineEmits([
84
85
  'update:modelValue', // Emits when the input value is updated
85
86
  'update:valid', // Emits when the validation state changes
87
+ 'update:formatted', // Emits formatted value when preset has a format function
86
88
  'click:prepend', // Emits when the prepend icon is clicked
87
89
  'click:append', // Emits when the append icon is clicked
88
90
  'click:prepend-inner', // Emits when the prepend inner icon is clicked
@@ -96,8 +98,6 @@ const emit = defineEmits([
96
98
  // Generate unique ID for accessibility - connects input to hint/error message
97
99
  const inputHintId = `input-hint-${Math.random().toString(36).slice(2, 11)}`;
98
100
 
99
- const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
100
-
101
101
  // Define the component props
102
102
  const props = defineProps({
103
103
  /**
@@ -217,12 +217,13 @@ const props = defineProps({
217
217
  },
218
218
 
219
219
  /**
220
- * Activates a built-in validation ruleset.
220
+ * Activates a built-in validation ruleset. Pass a string shorthand ('email', 'phone'),
221
+ * a preset factory result, or a custom { rules, format? } object.
221
222
  */
222
223
  preset: {
223
- type: String,
224
+ type: [String, Object],
224
225
  default: undefined,
225
- validator: (v) => [undefined, 'email'].includes(v),
226
+ validator: (v) => v === undefined || typeof v === 'string' || (typeof v === 'object' && Array.isArray(v.rules)),
226
227
  },
227
228
 
228
229
  /**
@@ -246,12 +247,8 @@ const props = defineProps({
246
247
  // --- Validation logic ---
247
248
  const validationError = ref('');
248
249
 
249
- const presetRules = computed(() => {
250
- if (props.preset === 'email') {
251
- return [(value) => EMAIL_REGEX.test(value) || t('wl.input.email_error', 'Ungültige E-Mail-Adresse')];
252
- }
253
- return [];
254
- });
250
+ const resolvedPreset = computed(() => props.preset ? resolvePreset(props.preset, t) : null);
251
+ const presetRules = computed(() => resolvedPreset.value?.rules ?? []);
255
252
 
256
253
  const allRules = computed(() => [...presetRules.value, ...props.rules]);
257
254
 
@@ -329,6 +326,9 @@ const onAppendInnerClick = () => {
329
326
  // Emit the updated value when the input changes, syncing with v-model
330
327
  const onInput = (value) => {
331
328
  emit('update:modelValue', value);
329
+ if (resolvedPreset.value?.format) {
330
+ emit('update:formatted', resolvedPreset.value.format(value));
331
+ }
332
332
  if (allRules.value.length > 0 && props.validateOn === 'input') {
333
333
  validate(value);
334
334
  }
@@ -0,0 +1,60 @@
1
+ import { parsePhoneNumber } from 'awesome-phonenumber';
2
+
3
+ export interface InputPreset {
4
+ rules: ((value: string) => true | string)[];
5
+ format?: (value: string) => string | undefined;
6
+ }
7
+
8
+ type TranslateFn = (key: string, fallback: string) => string;
9
+
10
+ const defaultT: TranslateFn = (_key, fallback) => fallback;
11
+
12
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
13
+
14
+ export function emailPreset(t: TranslateFn = defaultT): InputPreset {
15
+ return {
16
+ rules: [
17
+ (value) => EMAIL_REGEX.test(value) || t('wl.input.email_error', 'Ungültige E-Mail-Adresse'),
18
+ ],
19
+ };
20
+ }
21
+
22
+ export interface PhonePresetOptions {
23
+ region?: string;
24
+ }
25
+
26
+ export function phonePreset(options?: PhonePresetOptions, t: TranslateFn = defaultT): InputPreset {
27
+ const region = options?.region ?? 'DE';
28
+ return {
29
+ rules: [
30
+ (value) => {
31
+ if (!/^[+\d\s\-()/.]+$/.test(value)) return t('wl.input.phone_error', 'Ungültige Telefonnummer');
32
+ if (!/^[+0]/.test(value.trim())) return t('wl.input.phone_error', 'Ungültige Telefonnummer');
33
+ const pn = parsePhoneNumber(value, { regionCode: region });
34
+ return pn.possible || t('wl.input.phone_error', 'Ungültige Telefonnummer');
35
+ },
36
+ ],
37
+ format: (value) => {
38
+ if (!value) return undefined;
39
+ const pn = parsePhoneNumber(value, { regionCode: region });
40
+ return pn.possible && pn.number?.e164 ? pn.number.e164 : value;
41
+ },
42
+ };
43
+ }
44
+
45
+ const BUILT_IN_PRESETS: Record<string, (t: TranslateFn) => InputPreset> = {
46
+ email: (t) => emailPreset(t),
47
+ phone: (t) => phonePreset(undefined, t),
48
+ };
49
+
50
+ export function resolvePreset(
51
+ preset: string | InputPreset,
52
+ t: TranslateFn = defaultT,
53
+ ): InputPreset {
54
+ if (typeof preset === 'string') {
55
+ const factory = BUILT_IN_PRESETS[preset];
56
+ if (!factory) throw new Error(`Unknown preset: "${preset}"`);
57
+ return factory(t);
58
+ }
59
+ return preset;
60
+ }
@@ -30,6 +30,7 @@ export { default as Dialog } from './Dialog/Dialog.vue';
30
30
  export { default as AnamneseAnswerDialog } from './AnamneseAnswerDialog/AnamneseAnswerDialog.vue';
31
31
  export { default as EditField } from './EditField/EditField.vue';
32
32
  export { default as Input } from './Input/Input.vue';
33
+ export { emailPreset, phonePreset, type InputPreset } from './Input/presets';
33
34
  export { default as Modal } from './Modal/Modal.vue';
34
35
  export { default as NotificationBubble } from './NotificationBubble/NotificationBubble.vue';
35
36
  export { default as OtpInput } from './OtpInput/OtpInput.vue';