@grundtone/utils 2.0.1
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 +41 -0
- package/dist/index.d.mts +49 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +226 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +193 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @grundtone/utils
|
|
2
|
+
|
|
3
|
+
Formatting and validation utilities for the [Grundtone](https://grundtone.com) design system.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @grundtone/utils
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { formatCurrency, formatDanishDate, isValidCPR } from '@grundtone/utils';
|
|
15
|
+
|
|
16
|
+
formatCurrency(1234.56); // '1.234,56 kr.'
|
|
17
|
+
formatDanishDate(new Date()); // 'DD/MM/YYYY'
|
|
18
|
+
isValidCPR('123456-7890'); // true
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## API
|
|
22
|
+
|
|
23
|
+
### Formatting
|
|
24
|
+
|
|
25
|
+
- `formatCurrency(amount, currency?)` - Format as Danish currency
|
|
26
|
+
- `formatDanishDate(date)` - Format date in Danish locale
|
|
27
|
+
- `formatPhoneNumber(phone)` - Format Danish phone number
|
|
28
|
+
|
|
29
|
+
### Validation
|
|
30
|
+
|
|
31
|
+
- `isValidCPR(cpr)` - Validate Danish CPR number
|
|
32
|
+
- `isValidEmail(email)` - Validate email address
|
|
33
|
+
- `isValidPhoneNumber(phone)` - Validate Danish phone number
|
|
34
|
+
|
|
35
|
+
## Documentation
|
|
36
|
+
|
|
37
|
+
See [grundtone.com](https://grundtone.com) for full documentation.
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
declare const formatCurrency: (amount: number, currency?: string) => string;
|
|
2
|
+
declare const formatDate: (date: Date | string) => string;
|
|
3
|
+
declare const formatPhoneNumber: (phone: string) => string;
|
|
4
|
+
|
|
5
|
+
declare function generateId(prefix?: string): string;
|
|
6
|
+
|
|
7
|
+
declare const isValidCPR: (cpr: string) => boolean;
|
|
8
|
+
declare const isValidEmail: (email: string) => boolean;
|
|
9
|
+
declare const isValidPhoneNumber: (phone: string) => boolean;
|
|
10
|
+
declare const isRequired: (value: string) => boolean;
|
|
11
|
+
declare const isMinLength: (value: string, min: number) => boolean;
|
|
12
|
+
declare const isMaxLength: (value: string, max: number) => boolean;
|
|
13
|
+
declare const isValidURL: (url: string) => boolean;
|
|
14
|
+
declare const isValidCVR: (cvr: string) => boolean;
|
|
15
|
+
declare const isPattern: (value: string, regex: RegExp) => boolean;
|
|
16
|
+
|
|
17
|
+
interface ValidationResult {
|
|
18
|
+
isValid: boolean;
|
|
19
|
+
message?: string;
|
|
20
|
+
}
|
|
21
|
+
type Validator = (value: string) => ValidationResult;
|
|
22
|
+
type Currency = 'DKK' | 'EUR' | 'USD';
|
|
23
|
+
type DateFormat = 'short' | 'medium' | 'long';
|
|
24
|
+
|
|
25
|
+
declare const sleep: (ms: number) => Promise<void>;
|
|
26
|
+
declare const debounce: <T extends (...args: any[]) => any>(fn: T, delay: number) => ((...args: Parameters<T>) => void);
|
|
27
|
+
|
|
28
|
+
declare function required(message?: string): Validator;
|
|
29
|
+
declare function email(message?: string): Validator;
|
|
30
|
+
declare function phone(message?: string): Validator;
|
|
31
|
+
declare function cpr(message?: string): Validator;
|
|
32
|
+
declare function cvr(message?: string): Validator;
|
|
33
|
+
declare function minLength(min: number, message?: string): Validator;
|
|
34
|
+
declare function maxLength(max: number, message?: string): Validator;
|
|
35
|
+
declare function pattern(regex: RegExp, message?: string): Validator;
|
|
36
|
+
declare function url(message?: string): Validator;
|
|
37
|
+
declare function composeValidators(...validators: Validator[]): Validator;
|
|
38
|
+
|
|
39
|
+
type ThemeMode = 'light' | 'dark' | 'auto';
|
|
40
|
+
type ResolvedMode = 'light' | 'dark';
|
|
41
|
+
declare const THEME_STORAGE_KEY = "grundtone-theme-mode";
|
|
42
|
+
declare function resolveThemeMode(mode: ThemeMode, systemIsDark: boolean): ResolvedMode;
|
|
43
|
+
declare function getSystemIsDark(): boolean;
|
|
44
|
+
declare function persistThemeMode(mode: ThemeMode, key?: string): void;
|
|
45
|
+
declare function loadPersistedThemeMode(key?: string): ThemeMode | null;
|
|
46
|
+
declare function getSystemThemeMode(): ResolvedMode;
|
|
47
|
+
declare function camelToKebab(str: string): string;
|
|
48
|
+
|
|
49
|
+
export { type Currency, type DateFormat, type ResolvedMode, THEME_STORAGE_KEY, type ThemeMode, type ValidationResult, type Validator, camelToKebab, composeValidators, cpr, cvr, debounce, email, formatCurrency, formatDate, formatPhoneNumber, generateId, getSystemIsDark, getSystemThemeMode, isMaxLength, isMinLength, isPattern, isRequired, isValidCPR, isValidCVR, isValidEmail, isValidPhoneNumber, isValidURL, loadPersistedThemeMode, maxLength, minLength, pattern, persistThemeMode, phone, required, resolveThemeMode, sleep, url };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
declare const formatCurrency: (amount: number, currency?: string) => string;
|
|
2
|
+
declare const formatDate: (date: Date | string) => string;
|
|
3
|
+
declare const formatPhoneNumber: (phone: string) => string;
|
|
4
|
+
|
|
5
|
+
declare function generateId(prefix?: string): string;
|
|
6
|
+
|
|
7
|
+
declare const isValidCPR: (cpr: string) => boolean;
|
|
8
|
+
declare const isValidEmail: (email: string) => boolean;
|
|
9
|
+
declare const isValidPhoneNumber: (phone: string) => boolean;
|
|
10
|
+
declare const isRequired: (value: string) => boolean;
|
|
11
|
+
declare const isMinLength: (value: string, min: number) => boolean;
|
|
12
|
+
declare const isMaxLength: (value: string, max: number) => boolean;
|
|
13
|
+
declare const isValidURL: (url: string) => boolean;
|
|
14
|
+
declare const isValidCVR: (cvr: string) => boolean;
|
|
15
|
+
declare const isPattern: (value: string, regex: RegExp) => boolean;
|
|
16
|
+
|
|
17
|
+
interface ValidationResult {
|
|
18
|
+
isValid: boolean;
|
|
19
|
+
message?: string;
|
|
20
|
+
}
|
|
21
|
+
type Validator = (value: string) => ValidationResult;
|
|
22
|
+
type Currency = 'DKK' | 'EUR' | 'USD';
|
|
23
|
+
type DateFormat = 'short' | 'medium' | 'long';
|
|
24
|
+
|
|
25
|
+
declare const sleep: (ms: number) => Promise<void>;
|
|
26
|
+
declare const debounce: <T extends (...args: any[]) => any>(fn: T, delay: number) => ((...args: Parameters<T>) => void);
|
|
27
|
+
|
|
28
|
+
declare function required(message?: string): Validator;
|
|
29
|
+
declare function email(message?: string): Validator;
|
|
30
|
+
declare function phone(message?: string): Validator;
|
|
31
|
+
declare function cpr(message?: string): Validator;
|
|
32
|
+
declare function cvr(message?: string): Validator;
|
|
33
|
+
declare function minLength(min: number, message?: string): Validator;
|
|
34
|
+
declare function maxLength(max: number, message?: string): Validator;
|
|
35
|
+
declare function pattern(regex: RegExp, message?: string): Validator;
|
|
36
|
+
declare function url(message?: string): Validator;
|
|
37
|
+
declare function composeValidators(...validators: Validator[]): Validator;
|
|
38
|
+
|
|
39
|
+
type ThemeMode = 'light' | 'dark' | 'auto';
|
|
40
|
+
type ResolvedMode = 'light' | 'dark';
|
|
41
|
+
declare const THEME_STORAGE_KEY = "grundtone-theme-mode";
|
|
42
|
+
declare function resolveThemeMode(mode: ThemeMode, systemIsDark: boolean): ResolvedMode;
|
|
43
|
+
declare function getSystemIsDark(): boolean;
|
|
44
|
+
declare function persistThemeMode(mode: ThemeMode, key?: string): void;
|
|
45
|
+
declare function loadPersistedThemeMode(key?: string): ThemeMode | null;
|
|
46
|
+
declare function getSystemThemeMode(): ResolvedMode;
|
|
47
|
+
declare function camelToKebab(str: string): string;
|
|
48
|
+
|
|
49
|
+
export { type Currency, type DateFormat, type ResolvedMode, THEME_STORAGE_KEY, type ThemeMode, type ValidationResult, type Validator, camelToKebab, composeValidators, cpr, cvr, debounce, email, formatCurrency, formatDate, formatPhoneNumber, generateId, getSystemIsDark, getSystemThemeMode, isMaxLength, isMinLength, isPattern, isRequired, isValidCPR, isValidCVR, isValidEmail, isValidPhoneNumber, isValidURL, loadPersistedThemeMode, maxLength, minLength, pattern, persistThemeMode, phone, required, resolveThemeMode, sleep, url };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/utils/format.ts
|
|
4
|
+
var formatCurrency = (amount, currency = "DKK") => {
|
|
5
|
+
return new Intl.NumberFormat("da-DK", {
|
|
6
|
+
style: "currency",
|
|
7
|
+
currency
|
|
8
|
+
}).format(amount);
|
|
9
|
+
};
|
|
10
|
+
var formatDate = (date) => {
|
|
11
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
12
|
+
return new Intl.DateTimeFormat("da-DK", {
|
|
13
|
+
year: "numeric",
|
|
14
|
+
month: "long",
|
|
15
|
+
day: "numeric"
|
|
16
|
+
}).format(d);
|
|
17
|
+
};
|
|
18
|
+
var formatPhoneNumber = (phone2) => {
|
|
19
|
+
const cleaned = phone2.replace(/\D/g, "");
|
|
20
|
+
const match = cleaned.match(/^(\d{2})(\d{2})(\d{2})(\d{2})$/);
|
|
21
|
+
if (match) {
|
|
22
|
+
return `${match[1]} ${match[2]} ${match[3]} ${match[4]}`;
|
|
23
|
+
}
|
|
24
|
+
return phone2;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// src/utils/id.ts
|
|
28
|
+
var counter = 0;
|
|
29
|
+
function generateId(prefix = "gt") {
|
|
30
|
+
counter += 1;
|
|
31
|
+
return `${prefix}-${counter}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/utils/validation.ts
|
|
35
|
+
var isValidCPR = (cpr2) => {
|
|
36
|
+
const cleaned = cpr2.replace(/\D/g, "");
|
|
37
|
+
if (cleaned.length !== 10) return false;
|
|
38
|
+
const day = parseInt(cleaned.substring(0, 2));
|
|
39
|
+
const month = parseInt(cleaned.substring(2, 4));
|
|
40
|
+
const year = parseInt(cleaned.substring(4, 6));
|
|
41
|
+
const lastFourDigits = cleaned.substring(6, 10);
|
|
42
|
+
if (day < 1 || day > 31 || month < 1 || month > 12) return false;
|
|
43
|
+
let fullYear;
|
|
44
|
+
const firstControlDigit = parseInt(lastFourDigits.charAt(0));
|
|
45
|
+
if (firstControlDigit <= 3) {
|
|
46
|
+
fullYear = 1900 + year;
|
|
47
|
+
} else if (firstControlDigit === 4 || firstControlDigit === 9) {
|
|
48
|
+
if (year <= 36) {
|
|
49
|
+
fullYear = 2e3 + year;
|
|
50
|
+
} else {
|
|
51
|
+
fullYear = 1900 + year;
|
|
52
|
+
}
|
|
53
|
+
} else if (firstControlDigit >= 5 && firstControlDigit <= 8) {
|
|
54
|
+
if (year <= 57) {
|
|
55
|
+
fullYear = 2e3 + year;
|
|
56
|
+
} else {
|
|
57
|
+
fullYear = 1800 + year;
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (fullYear < 2007 || fullYear === 2007 && month === 1 && day === 1) {
|
|
63
|
+
const weights = [4, 3, 2, 7, 6, 5, 4, 3, 2, 1];
|
|
64
|
+
const sum = cleaned.split("").map(Number).reduce((acc, digit, i) => acc + digit * weights[i], 0);
|
|
65
|
+
return sum % 11 === 0;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
};
|
|
69
|
+
var isValidEmail = (email2) => {
|
|
70
|
+
const parts = email2.split("@");
|
|
71
|
+
if (parts.length !== 2) return false;
|
|
72
|
+
const [local, domain] = parts;
|
|
73
|
+
if (!local || !domain) return false;
|
|
74
|
+
if (domain.indexOf(".") === -1) return false;
|
|
75
|
+
return /^[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email2);
|
|
76
|
+
};
|
|
77
|
+
var isValidPhoneNumber = (phone2) => {
|
|
78
|
+
const cleaned = phone2.replace(/\D/g, "");
|
|
79
|
+
return cleaned.length === 8;
|
|
80
|
+
};
|
|
81
|
+
var isRequired = (value) => {
|
|
82
|
+
return value.trim().length > 0;
|
|
83
|
+
};
|
|
84
|
+
var isMinLength = (value, min) => {
|
|
85
|
+
return value.length >= min;
|
|
86
|
+
};
|
|
87
|
+
var isMaxLength = (value, max) => {
|
|
88
|
+
return value.length <= max;
|
|
89
|
+
};
|
|
90
|
+
var isValidURL = (url2) => {
|
|
91
|
+
try {
|
|
92
|
+
const parsed = new URL(url2);
|
|
93
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
var isValidCVR = (cvr2) => {
|
|
99
|
+
const cleaned = cvr2.replace(/\D/g, "");
|
|
100
|
+
if (cleaned.length !== 8) return false;
|
|
101
|
+
const weights = [2, 7, 6, 5, 4, 3, 2, 1];
|
|
102
|
+
const sum = cleaned.split("").map(Number).reduce((acc, digit, i) => acc + digit * weights[i], 0);
|
|
103
|
+
return sum % 11 === 0;
|
|
104
|
+
};
|
|
105
|
+
var isPattern = (value, regex) => {
|
|
106
|
+
return regex.test(value);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// src/helpers/index.ts
|
|
110
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
111
|
+
var debounce = (fn, delay) => {
|
|
112
|
+
let timeoutId;
|
|
113
|
+
return (...args) => {
|
|
114
|
+
clearTimeout(timeoutId);
|
|
115
|
+
timeoutId = setTimeout(() => fn(...args), delay);
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// src/utils/validators.ts
|
|
120
|
+
var ok = { isValid: true };
|
|
121
|
+
var fail = (message) => ({ isValid: false, message });
|
|
122
|
+
function required(message = "This field is required") {
|
|
123
|
+
return (value) => isRequired(value) ? ok : fail(message);
|
|
124
|
+
}
|
|
125
|
+
function email(message = "Invalid email address") {
|
|
126
|
+
return (value) => value === "" || isValidEmail(value) ? ok : fail(message);
|
|
127
|
+
}
|
|
128
|
+
function phone(message = "Invalid phone number") {
|
|
129
|
+
return (value) => value === "" || isValidPhoneNumber(value) ? ok : fail(message);
|
|
130
|
+
}
|
|
131
|
+
function cpr(message = "Invalid CPR number") {
|
|
132
|
+
return (value) => value === "" || isValidCPR(value) ? ok : fail(message);
|
|
133
|
+
}
|
|
134
|
+
function cvr(message = "Invalid CVR number") {
|
|
135
|
+
return (value) => value === "" || isValidCVR(value) ? ok : fail(message);
|
|
136
|
+
}
|
|
137
|
+
function minLength(min, message = `Must be at least ${min} characters`) {
|
|
138
|
+
return (value) => isMinLength(value, min) ? ok : fail(message);
|
|
139
|
+
}
|
|
140
|
+
function maxLength(max, message = `Must be at most ${max} characters`) {
|
|
141
|
+
return (value) => isMaxLength(value, max) ? ok : fail(message);
|
|
142
|
+
}
|
|
143
|
+
function pattern(regex, message = "Invalid format") {
|
|
144
|
+
return (value) => value === "" || isPattern(value, regex) ? ok : fail(message);
|
|
145
|
+
}
|
|
146
|
+
function url(message = "Invalid URL") {
|
|
147
|
+
return (value) => value === "" || isValidURL(value) ? ok : fail(message);
|
|
148
|
+
}
|
|
149
|
+
function composeValidators(...validators) {
|
|
150
|
+
return (value) => {
|
|
151
|
+
for (const validator of validators) {
|
|
152
|
+
const result = validator(value);
|
|
153
|
+
if (!result.isValid) return result;
|
|
154
|
+
}
|
|
155
|
+
return ok;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/utils/theme-mode.ts
|
|
160
|
+
var THEME_STORAGE_KEY = "grundtone-theme-mode";
|
|
161
|
+
function resolveThemeMode(mode, systemIsDark) {
|
|
162
|
+
if (mode === "auto") return systemIsDark ? "dark" : "light";
|
|
163
|
+
return mode;
|
|
164
|
+
}
|
|
165
|
+
function getSystemIsDark() {
|
|
166
|
+
if (typeof window === "undefined") return false;
|
|
167
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
168
|
+
}
|
|
169
|
+
function persistThemeMode(mode, key = THEME_STORAGE_KEY) {
|
|
170
|
+
if (typeof window === "undefined") return;
|
|
171
|
+
try {
|
|
172
|
+
window.localStorage.setItem(key, mode);
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function loadPersistedThemeMode(key = THEME_STORAGE_KEY) {
|
|
177
|
+
if (typeof window === "undefined") return null;
|
|
178
|
+
try {
|
|
179
|
+
const stored = window.localStorage.getItem(key);
|
|
180
|
+
if (stored === "light" || stored === "dark" || stored === "auto")
|
|
181
|
+
return stored;
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
function getSystemThemeMode() {
|
|
187
|
+
return getSystemIsDark() ? "dark" : "light";
|
|
188
|
+
}
|
|
189
|
+
function camelToKebab(str) {
|
|
190
|
+
return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
exports.THEME_STORAGE_KEY = THEME_STORAGE_KEY;
|
|
194
|
+
exports.camelToKebab = camelToKebab;
|
|
195
|
+
exports.composeValidators = composeValidators;
|
|
196
|
+
exports.cpr = cpr;
|
|
197
|
+
exports.cvr = cvr;
|
|
198
|
+
exports.debounce = debounce;
|
|
199
|
+
exports.email = email;
|
|
200
|
+
exports.formatCurrency = formatCurrency;
|
|
201
|
+
exports.formatDate = formatDate;
|
|
202
|
+
exports.formatPhoneNumber = formatPhoneNumber;
|
|
203
|
+
exports.generateId = generateId;
|
|
204
|
+
exports.getSystemIsDark = getSystemIsDark;
|
|
205
|
+
exports.getSystemThemeMode = getSystemThemeMode;
|
|
206
|
+
exports.isMaxLength = isMaxLength;
|
|
207
|
+
exports.isMinLength = isMinLength;
|
|
208
|
+
exports.isPattern = isPattern;
|
|
209
|
+
exports.isRequired = isRequired;
|
|
210
|
+
exports.isValidCPR = isValidCPR;
|
|
211
|
+
exports.isValidCVR = isValidCVR;
|
|
212
|
+
exports.isValidEmail = isValidEmail;
|
|
213
|
+
exports.isValidPhoneNumber = isValidPhoneNumber;
|
|
214
|
+
exports.isValidURL = isValidURL;
|
|
215
|
+
exports.loadPersistedThemeMode = loadPersistedThemeMode;
|
|
216
|
+
exports.maxLength = maxLength;
|
|
217
|
+
exports.minLength = minLength;
|
|
218
|
+
exports.pattern = pattern;
|
|
219
|
+
exports.persistThemeMode = persistThemeMode;
|
|
220
|
+
exports.phone = phone;
|
|
221
|
+
exports.required = required;
|
|
222
|
+
exports.resolveThemeMode = resolveThemeMode;
|
|
223
|
+
exports.sleep = sleep;
|
|
224
|
+
exports.url = url;
|
|
225
|
+
//# sourceMappingURL=index.js.map
|
|
226
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/format.ts","../src/utils/id.ts","../src/utils/validation.ts","../src/helpers/index.ts","../src/utils/validators.ts","../src/utils/theme-mode.ts"],"names":["phone","cpr","email","url","cvr"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Formaterer et tal til en valuta string\n */\nexport const formatCurrency = (amount: number, currency = 'DKK'): string => {\n return new Intl.NumberFormat('da-DK', {\n style: 'currency',\n currency,\n }).format(amount);\n};\n\n/**\n * Formaterer et dato til dansk format\n */\nexport const formatDate = (date: Date | string): string => {\n const d = typeof date === 'string' ? new Date(date) : date;\n return new Intl.DateTimeFormat('da-DK', {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n }).format(d);\n};\n\n/**\n * Formaterer et telefonnummer til dansk format\n */\nexport const formatPhoneNumber = (phone: string): string => {\n const cleaned = phone.replace(/\\D/g, '');\n const match = cleaned.match(/^(\\d{2})(\\d{2})(\\d{2})(\\d{2})$/);\n if (match) {\n return `${match[1]} ${match[2]} ${match[3]} ${match[4]}`;\n }\n return phone;\n};\n","let counter = 0;\n\n/**\n * Generates a unique ID string for associating labels with form inputs.\n * Shared between Vue and React Native components.\n */\nexport function generateId(prefix = 'gt'): string {\n counter += 1;\n return `${prefix}-${counter}`;\n}\n","/**\n * Validerer et dansk CPR-nummer\n *\n * CPR numre før 2007: Bruger modulus 11 check\n * CPR numre fra 2007+: Bruger kun formatvalidering (modulus 11 ikke længere anvendt)\n *\n * @param cpr CPR nummer med eller uden bindestreg (format: DDMMYY-XXXX eller DDMMYYXXXX)\n * @returns true hvis gyldig CPR nummer\n */\nexport const isValidCPR = (cpr: string): boolean => {\n const cleaned = cpr.replace(/\\D/g, '');\n if (cleaned.length !== 10) return false;\n\n // Udtræk fødselsdato og kontrol cifre\n const day = parseInt(cleaned.substring(0, 2));\n const month = parseInt(cleaned.substring(2, 4));\n const year = parseInt(cleaned.substring(4, 6));\n const lastFourDigits = cleaned.substring(6, 10);\n\n // Grundlæggende dato validering\n if (day < 1 || day > 31 || month < 1 || month > 12) return false;\n\n // Bestem fuldt årtal baseret på CPR regler\n let fullYear: number;\n const firstControlDigit = parseInt(lastFourDigits.charAt(0));\n\n if (firstControlDigit <= 3) {\n fullYear = 1900 + year;\n } else if (firstControlDigit === 4 || firstControlDigit === 9) {\n if (year <= 36) {\n fullYear = 2000 + year;\n } else {\n fullYear = 1900 + year;\n }\n } else if (firstControlDigit >= 5 && firstControlDigit <= 8) {\n if (year <= 57) {\n fullYear = 2000 + year;\n } else {\n fullYear = 1800 + year;\n }\n } else {\n return false;\n }\n\n // For CPR numre udstedt før 2007, anvend modulus 11 check\n if (fullYear < 2007 || (fullYear === 2007 && month === 1 && day === 1)) {\n const weights = [4, 3, 2, 7, 6, 5, 4, 3, 2, 1];\n const sum = cleaned\n .split('')\n .map(Number)\n .reduce((acc, digit, i) => acc + digit * weights[i], 0);\n\n return sum % 11 === 0;\n }\n\n // For CPR numre fra 2007+, kun format validering (modulus 11 ikke anvendt)\n return true;\n};\n\n/**\n * Validerer en email adresse\n * Uses a simpler regex to avoid ReDoS vulnerability\n */\nexport const isValidEmail = (email: string): boolean => {\n // Simple validation: one @ symbol, domain with at least one dot\n const parts = email.split('@');\n if (parts.length !== 2) return false;\n\n const [local, domain] = parts;\n if (!local || !domain) return false;\n if (domain.indexOf('.') === -1) return false;\n\n // Basic character validation without complex regex\n return /^[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(email);\n};\n\n/**\n * Validerer et dansk telefonnummer\n */\nexport const isValidPhoneNumber = (phone: string): boolean => {\n const cleaned = phone.replace(/\\D/g, '');\n return cleaned.length === 8;\n};\n\n/**\n * Checks that a value is non-empty after trimming whitespace.\n */\nexport const isRequired = (value: string): boolean => {\n return value.trim().length > 0;\n};\n\n/**\n * Checks that a value meets a minimum length requirement.\n */\nexport const isMinLength = (value: string, min: number): boolean => {\n return value.length >= min;\n};\n\n/**\n * Checks that a value does not exceed a maximum length.\n */\nexport const isMaxLength = (value: string, max: number): boolean => {\n return value.length <= max;\n};\n\n/**\n * Basic URL validation using the URL constructor.\n */\nexport const isValidURL = (url: string): boolean => {\n try {\n const parsed = new URL(url);\n return parsed.protocol === 'http:' || parsed.protocol === 'https:';\n } catch {\n return false;\n }\n};\n\n/**\n * Validates a Danish CVR number (8 digits, modulus 11 check).\n */\nexport const isValidCVR = (cvr: string): boolean => {\n const cleaned = cvr.replace(/\\D/g, '');\n if (cleaned.length !== 8) return false;\n\n const weights = [2, 7, 6, 5, 4, 3, 2, 1];\n const sum = cleaned\n .split('')\n .map(Number)\n .reduce((acc, digit, i) => acc + digit * weights[i], 0);\n\n return sum % 11 === 0;\n};\n\n/**\n * Checks that a value matches a given regular expression.\n */\nexport const isPattern = (value: string, regex: RegExp): boolean => {\n return regex.test(value);\n};\n","export const sleep = (ms: number): Promise<void> =>\n new Promise(resolve => setTimeout(resolve, ms));\n\nexport const debounce = <T extends (...args: any[]) => any>(\n fn: T,\n delay: number,\n): ((...args: Parameters<T>) => void) => {\n let timeoutId: ReturnType<typeof setTimeout>;\n return (...args: Parameters<T>) => {\n clearTimeout(timeoutId);\n timeoutId = setTimeout(() => fn(...args), delay);\n };\n};\n","import type { Validator } from '../types';\nimport {\n isRequired,\n isValidEmail,\n isValidPhoneNumber,\n isValidCPR,\n isValidCVR,\n isMinLength,\n isMaxLength,\n isValidURL,\n isPattern,\n} from './validation';\n\nconst ok = { isValid: true };\nconst fail = (message: string) => ({ isValid: false, message });\n\nexport function required(message = 'This field is required'): Validator {\n return value => (isRequired(value) ? ok : fail(message));\n}\n\nexport function email(message = 'Invalid email address'): Validator {\n return value => (value === '' || isValidEmail(value) ? ok : fail(message));\n}\n\nexport function phone(message = 'Invalid phone number'): Validator {\n return value =>\n value === '' || isValidPhoneNumber(value) ? ok : fail(message);\n}\n\nexport function cpr(message = 'Invalid CPR number'): Validator {\n return value => (value === '' || isValidCPR(value) ? ok : fail(message));\n}\n\nexport function cvr(message = 'Invalid CVR number'): Validator {\n return value => (value === '' || isValidCVR(value) ? ok : fail(message));\n}\n\nexport function minLength(\n min: number,\n message = `Must be at least ${min} characters`,\n): Validator {\n return value => (isMinLength(value, min) ? ok : fail(message));\n}\n\nexport function maxLength(\n max: number,\n message = `Must be at most ${max} characters`,\n): Validator {\n return value => (isMaxLength(value, max) ? ok : fail(message));\n}\n\nexport function pattern(regex: RegExp, message = 'Invalid format'): Validator {\n return value =>\n value === '' || isPattern(value, regex) ? ok : fail(message);\n}\n\nexport function url(message = 'Invalid URL'): Validator {\n return value => (value === '' || isValidURL(value) ? ok : fail(message));\n}\n\n/**\n * Composes multiple validators into a single validator.\n * Returns the first failing result, or a passing result if all pass.\n */\nexport function composeValidators(...validators: Validator[]): Validator {\n return value => {\n for (const validator of validators) {\n const result = validator(value);\n if (!result.isValid) return result;\n }\n return ok;\n };\n}\n","/**\n * Shared theme mode utilities — platform-agnostic.\n *\n * Used by both Vue and React Native to resolve, persist, and detect theme mode.\n */\n\n/**\n * Theme mode — matches @grundtone/core's ThemeMode.\n * Defined locally to avoid circular dependency between utils and core.\n */\nexport type ThemeMode = 'light' | 'dark' | 'auto';\nexport type ResolvedMode = 'light' | 'dark';\n\n/** Default localStorage key for persisted theme mode. */\nexport const THEME_STORAGE_KEY = 'grundtone-theme-mode';\n\n/**\n * Resolve a ThemeMode to a concrete 'light' or 'dark' value.\n * When mode is 'auto', uses the systemIsDark flag to decide.\n */\nexport function resolveThemeMode(\n mode: ThemeMode,\n systemIsDark: boolean,\n): ResolvedMode {\n if (mode === 'auto') return systemIsDark ? 'dark' : 'light';\n return mode;\n}\n\n/**\n * Check if the system prefers dark mode.\n * Web only — uses matchMedia. SSR-safe (returns false on server).\n */\nexport function getSystemIsDark(): boolean {\n if (typeof window === 'undefined') return false;\n return window.matchMedia('(prefers-color-scheme: dark)').matches;\n}\n\n/**\n * Persist theme mode to localStorage.\n * Web only, SSR-safe — no-op on server or when localStorage is unavailable.\n */\nexport function persistThemeMode(\n mode: ThemeMode,\n key = THEME_STORAGE_KEY,\n): void {\n if (typeof window === 'undefined') return;\n try {\n window.localStorage.setItem(key, mode);\n } catch {\n // localStorage unavailable (private browsing, etc.)\n }\n}\n\n/**\n * Load persisted theme mode from localStorage.\n * Returns null if no valid mode is stored. SSR-safe.\n */\nexport function loadPersistedThemeMode(\n key = THEME_STORAGE_KEY,\n): ThemeMode | null {\n if (typeof window === 'undefined') return null;\n try {\n const stored = window.localStorage.getItem(key);\n if (stored === 'light' || stored === 'dark' || stored === 'auto')\n return stored;\n } catch {\n // localStorage unavailable\n }\n return null;\n}\n\n/**\n * Get the system theme mode as a resolved string.\n * Web only, SSR-safe (returns 'light' on server).\n */\nexport function getSystemThemeMode(): ResolvedMode {\n return getSystemIsDark() ? 'dark' : 'light';\n}\n\n/**\n * Convert camelCase to kebab-case.\n * Used for mapping theme object keys to CSS custom property names.\n *\n * @example camelToKebab('primaryLight') → 'primary-light'\n * @example camelToKebab('onPrimary') → 'on-primary'\n */\nexport function camelToKebab(str: string): string {\n return str.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);\n}\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// src/utils/format.ts
|
|
2
|
+
var formatCurrency = (amount, currency = "DKK") => {
|
|
3
|
+
return new Intl.NumberFormat("da-DK", {
|
|
4
|
+
style: "currency",
|
|
5
|
+
currency
|
|
6
|
+
}).format(amount);
|
|
7
|
+
};
|
|
8
|
+
var formatDate = (date) => {
|
|
9
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
10
|
+
return new Intl.DateTimeFormat("da-DK", {
|
|
11
|
+
year: "numeric",
|
|
12
|
+
month: "long",
|
|
13
|
+
day: "numeric"
|
|
14
|
+
}).format(d);
|
|
15
|
+
};
|
|
16
|
+
var formatPhoneNumber = (phone2) => {
|
|
17
|
+
const cleaned = phone2.replace(/\D/g, "");
|
|
18
|
+
const match = cleaned.match(/^(\d{2})(\d{2})(\d{2})(\d{2})$/);
|
|
19
|
+
if (match) {
|
|
20
|
+
return `${match[1]} ${match[2]} ${match[3]} ${match[4]}`;
|
|
21
|
+
}
|
|
22
|
+
return phone2;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// src/utils/id.ts
|
|
26
|
+
var counter = 0;
|
|
27
|
+
function generateId(prefix = "gt") {
|
|
28
|
+
counter += 1;
|
|
29
|
+
return `${prefix}-${counter}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/utils/validation.ts
|
|
33
|
+
var isValidCPR = (cpr2) => {
|
|
34
|
+
const cleaned = cpr2.replace(/\D/g, "");
|
|
35
|
+
if (cleaned.length !== 10) return false;
|
|
36
|
+
const day = parseInt(cleaned.substring(0, 2));
|
|
37
|
+
const month = parseInt(cleaned.substring(2, 4));
|
|
38
|
+
const year = parseInt(cleaned.substring(4, 6));
|
|
39
|
+
const lastFourDigits = cleaned.substring(6, 10);
|
|
40
|
+
if (day < 1 || day > 31 || month < 1 || month > 12) return false;
|
|
41
|
+
let fullYear;
|
|
42
|
+
const firstControlDigit = parseInt(lastFourDigits.charAt(0));
|
|
43
|
+
if (firstControlDigit <= 3) {
|
|
44
|
+
fullYear = 1900 + year;
|
|
45
|
+
} else if (firstControlDigit === 4 || firstControlDigit === 9) {
|
|
46
|
+
if (year <= 36) {
|
|
47
|
+
fullYear = 2e3 + year;
|
|
48
|
+
} else {
|
|
49
|
+
fullYear = 1900 + year;
|
|
50
|
+
}
|
|
51
|
+
} else if (firstControlDigit >= 5 && firstControlDigit <= 8) {
|
|
52
|
+
if (year <= 57) {
|
|
53
|
+
fullYear = 2e3 + year;
|
|
54
|
+
} else {
|
|
55
|
+
fullYear = 1800 + year;
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (fullYear < 2007 || fullYear === 2007 && month === 1 && day === 1) {
|
|
61
|
+
const weights = [4, 3, 2, 7, 6, 5, 4, 3, 2, 1];
|
|
62
|
+
const sum = cleaned.split("").map(Number).reduce((acc, digit, i) => acc + digit * weights[i], 0);
|
|
63
|
+
return sum % 11 === 0;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
};
|
|
67
|
+
var isValidEmail = (email2) => {
|
|
68
|
+
const parts = email2.split("@");
|
|
69
|
+
if (parts.length !== 2) return false;
|
|
70
|
+
const [local, domain] = parts;
|
|
71
|
+
if (!local || !domain) return false;
|
|
72
|
+
if (domain.indexOf(".") === -1) return false;
|
|
73
|
+
return /^[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email2);
|
|
74
|
+
};
|
|
75
|
+
var isValidPhoneNumber = (phone2) => {
|
|
76
|
+
const cleaned = phone2.replace(/\D/g, "");
|
|
77
|
+
return cleaned.length === 8;
|
|
78
|
+
};
|
|
79
|
+
var isRequired = (value) => {
|
|
80
|
+
return value.trim().length > 0;
|
|
81
|
+
};
|
|
82
|
+
var isMinLength = (value, min) => {
|
|
83
|
+
return value.length >= min;
|
|
84
|
+
};
|
|
85
|
+
var isMaxLength = (value, max) => {
|
|
86
|
+
return value.length <= max;
|
|
87
|
+
};
|
|
88
|
+
var isValidURL = (url2) => {
|
|
89
|
+
try {
|
|
90
|
+
const parsed = new URL(url2);
|
|
91
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var isValidCVR = (cvr2) => {
|
|
97
|
+
const cleaned = cvr2.replace(/\D/g, "");
|
|
98
|
+
if (cleaned.length !== 8) return false;
|
|
99
|
+
const weights = [2, 7, 6, 5, 4, 3, 2, 1];
|
|
100
|
+
const sum = cleaned.split("").map(Number).reduce((acc, digit, i) => acc + digit * weights[i], 0);
|
|
101
|
+
return sum % 11 === 0;
|
|
102
|
+
};
|
|
103
|
+
var isPattern = (value, regex) => {
|
|
104
|
+
return regex.test(value);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// src/helpers/index.ts
|
|
108
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
109
|
+
var debounce = (fn, delay) => {
|
|
110
|
+
let timeoutId;
|
|
111
|
+
return (...args) => {
|
|
112
|
+
clearTimeout(timeoutId);
|
|
113
|
+
timeoutId = setTimeout(() => fn(...args), delay);
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// src/utils/validators.ts
|
|
118
|
+
var ok = { isValid: true };
|
|
119
|
+
var fail = (message) => ({ isValid: false, message });
|
|
120
|
+
function required(message = "This field is required") {
|
|
121
|
+
return (value) => isRequired(value) ? ok : fail(message);
|
|
122
|
+
}
|
|
123
|
+
function email(message = "Invalid email address") {
|
|
124
|
+
return (value) => value === "" || isValidEmail(value) ? ok : fail(message);
|
|
125
|
+
}
|
|
126
|
+
function phone(message = "Invalid phone number") {
|
|
127
|
+
return (value) => value === "" || isValidPhoneNumber(value) ? ok : fail(message);
|
|
128
|
+
}
|
|
129
|
+
function cpr(message = "Invalid CPR number") {
|
|
130
|
+
return (value) => value === "" || isValidCPR(value) ? ok : fail(message);
|
|
131
|
+
}
|
|
132
|
+
function cvr(message = "Invalid CVR number") {
|
|
133
|
+
return (value) => value === "" || isValidCVR(value) ? ok : fail(message);
|
|
134
|
+
}
|
|
135
|
+
function minLength(min, message = `Must be at least ${min} characters`) {
|
|
136
|
+
return (value) => isMinLength(value, min) ? ok : fail(message);
|
|
137
|
+
}
|
|
138
|
+
function maxLength(max, message = `Must be at most ${max} characters`) {
|
|
139
|
+
return (value) => isMaxLength(value, max) ? ok : fail(message);
|
|
140
|
+
}
|
|
141
|
+
function pattern(regex, message = "Invalid format") {
|
|
142
|
+
return (value) => value === "" || isPattern(value, regex) ? ok : fail(message);
|
|
143
|
+
}
|
|
144
|
+
function url(message = "Invalid URL") {
|
|
145
|
+
return (value) => value === "" || isValidURL(value) ? ok : fail(message);
|
|
146
|
+
}
|
|
147
|
+
function composeValidators(...validators) {
|
|
148
|
+
return (value) => {
|
|
149
|
+
for (const validator of validators) {
|
|
150
|
+
const result = validator(value);
|
|
151
|
+
if (!result.isValid) return result;
|
|
152
|
+
}
|
|
153
|
+
return ok;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/utils/theme-mode.ts
|
|
158
|
+
var THEME_STORAGE_KEY = "grundtone-theme-mode";
|
|
159
|
+
function resolveThemeMode(mode, systemIsDark) {
|
|
160
|
+
if (mode === "auto") return systemIsDark ? "dark" : "light";
|
|
161
|
+
return mode;
|
|
162
|
+
}
|
|
163
|
+
function getSystemIsDark() {
|
|
164
|
+
if (typeof window === "undefined") return false;
|
|
165
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
166
|
+
}
|
|
167
|
+
function persistThemeMode(mode, key = THEME_STORAGE_KEY) {
|
|
168
|
+
if (typeof window === "undefined") return;
|
|
169
|
+
try {
|
|
170
|
+
window.localStorage.setItem(key, mode);
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function loadPersistedThemeMode(key = THEME_STORAGE_KEY) {
|
|
175
|
+
if (typeof window === "undefined") return null;
|
|
176
|
+
try {
|
|
177
|
+
const stored = window.localStorage.getItem(key);
|
|
178
|
+
if (stored === "light" || stored === "dark" || stored === "auto")
|
|
179
|
+
return stored;
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
function getSystemThemeMode() {
|
|
185
|
+
return getSystemIsDark() ? "dark" : "light";
|
|
186
|
+
}
|
|
187
|
+
function camelToKebab(str) {
|
|
188
|
+
return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export { THEME_STORAGE_KEY, camelToKebab, composeValidators, cpr, cvr, debounce, email, formatCurrency, formatDate, formatPhoneNumber, generateId, getSystemIsDark, getSystemThemeMode, isMaxLength, isMinLength, isPattern, isRequired, isValidCPR, isValidCVR, isValidEmail, isValidPhoneNumber, isValidURL, loadPersistedThemeMode, maxLength, minLength, pattern, persistThemeMode, phone, required, resolveThemeMode, sleep, url };
|
|
192
|
+
//# sourceMappingURL=index.mjs.map
|
|
193
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/format.ts","../src/utils/id.ts","../src/utils/validation.ts","../src/helpers/index.ts","../src/utils/validators.ts","../src/utils/theme-mode.ts"],"names":["phone","cpr","email","url","cvr"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Formaterer et tal til en valuta string\n */\nexport const formatCurrency = (amount: number, currency = 'DKK'): string => {\n return new Intl.NumberFormat('da-DK', {\n style: 'currency',\n currency,\n }).format(amount);\n};\n\n/**\n * Formaterer et dato til dansk format\n */\nexport const formatDate = (date: Date | string): string => {\n const d = typeof date === 'string' ? new Date(date) : date;\n return new Intl.DateTimeFormat('da-DK', {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n }).format(d);\n};\n\n/**\n * Formaterer et telefonnummer til dansk format\n */\nexport const formatPhoneNumber = (phone: string): string => {\n const cleaned = phone.replace(/\\D/g, '');\n const match = cleaned.match(/^(\\d{2})(\\d{2})(\\d{2})(\\d{2})$/);\n if (match) {\n return `${match[1]} ${match[2]} ${match[3]} ${match[4]}`;\n }\n return phone;\n};\n","let counter = 0;\n\n/**\n * Generates a unique ID string for associating labels with form inputs.\n * Shared between Vue and React Native components.\n */\nexport function generateId(prefix = 'gt'): string {\n counter += 1;\n return `${prefix}-${counter}`;\n}\n","/**\n * Validerer et dansk CPR-nummer\n *\n * CPR numre før 2007: Bruger modulus 11 check\n * CPR numre fra 2007+: Bruger kun formatvalidering (modulus 11 ikke længere anvendt)\n *\n * @param cpr CPR nummer med eller uden bindestreg (format: DDMMYY-XXXX eller DDMMYYXXXX)\n * @returns true hvis gyldig CPR nummer\n */\nexport const isValidCPR = (cpr: string): boolean => {\n const cleaned = cpr.replace(/\\D/g, '');\n if (cleaned.length !== 10) return false;\n\n // Udtræk fødselsdato og kontrol cifre\n const day = parseInt(cleaned.substring(0, 2));\n const month = parseInt(cleaned.substring(2, 4));\n const year = parseInt(cleaned.substring(4, 6));\n const lastFourDigits = cleaned.substring(6, 10);\n\n // Grundlæggende dato validering\n if (day < 1 || day > 31 || month < 1 || month > 12) return false;\n\n // Bestem fuldt årtal baseret på CPR regler\n let fullYear: number;\n const firstControlDigit = parseInt(lastFourDigits.charAt(0));\n\n if (firstControlDigit <= 3) {\n fullYear = 1900 + year;\n } else if (firstControlDigit === 4 || firstControlDigit === 9) {\n if (year <= 36) {\n fullYear = 2000 + year;\n } else {\n fullYear = 1900 + year;\n }\n } else if (firstControlDigit >= 5 && firstControlDigit <= 8) {\n if (year <= 57) {\n fullYear = 2000 + year;\n } else {\n fullYear = 1800 + year;\n }\n } else {\n return false;\n }\n\n // For CPR numre udstedt før 2007, anvend modulus 11 check\n if (fullYear < 2007 || (fullYear === 2007 && month === 1 && day === 1)) {\n const weights = [4, 3, 2, 7, 6, 5, 4, 3, 2, 1];\n const sum = cleaned\n .split('')\n .map(Number)\n .reduce((acc, digit, i) => acc + digit * weights[i], 0);\n\n return sum % 11 === 0;\n }\n\n // For CPR numre fra 2007+, kun format validering (modulus 11 ikke anvendt)\n return true;\n};\n\n/**\n * Validerer en email adresse\n * Uses a simpler regex to avoid ReDoS vulnerability\n */\nexport const isValidEmail = (email: string): boolean => {\n // Simple validation: one @ symbol, domain with at least one dot\n const parts = email.split('@');\n if (parts.length !== 2) return false;\n\n const [local, domain] = parts;\n if (!local || !domain) return false;\n if (domain.indexOf('.') === -1) return false;\n\n // Basic character validation without complex regex\n return /^[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(email);\n};\n\n/**\n * Validerer et dansk telefonnummer\n */\nexport const isValidPhoneNumber = (phone: string): boolean => {\n const cleaned = phone.replace(/\\D/g, '');\n return cleaned.length === 8;\n};\n\n/**\n * Checks that a value is non-empty after trimming whitespace.\n */\nexport const isRequired = (value: string): boolean => {\n return value.trim().length > 0;\n};\n\n/**\n * Checks that a value meets a minimum length requirement.\n */\nexport const isMinLength = (value: string, min: number): boolean => {\n return value.length >= min;\n};\n\n/**\n * Checks that a value does not exceed a maximum length.\n */\nexport const isMaxLength = (value: string, max: number): boolean => {\n return value.length <= max;\n};\n\n/**\n * Basic URL validation using the URL constructor.\n */\nexport const isValidURL = (url: string): boolean => {\n try {\n const parsed = new URL(url);\n return parsed.protocol === 'http:' || parsed.protocol === 'https:';\n } catch {\n return false;\n }\n};\n\n/**\n * Validates a Danish CVR number (8 digits, modulus 11 check).\n */\nexport const isValidCVR = (cvr: string): boolean => {\n const cleaned = cvr.replace(/\\D/g, '');\n if (cleaned.length !== 8) return false;\n\n const weights = [2, 7, 6, 5, 4, 3, 2, 1];\n const sum = cleaned\n .split('')\n .map(Number)\n .reduce((acc, digit, i) => acc + digit * weights[i], 0);\n\n return sum % 11 === 0;\n};\n\n/**\n * Checks that a value matches a given regular expression.\n */\nexport const isPattern = (value: string, regex: RegExp): boolean => {\n return regex.test(value);\n};\n","export const sleep = (ms: number): Promise<void> =>\n new Promise(resolve => setTimeout(resolve, ms));\n\nexport const debounce = <T extends (...args: any[]) => any>(\n fn: T,\n delay: number,\n): ((...args: Parameters<T>) => void) => {\n let timeoutId: ReturnType<typeof setTimeout>;\n return (...args: Parameters<T>) => {\n clearTimeout(timeoutId);\n timeoutId = setTimeout(() => fn(...args), delay);\n };\n};\n","import type { Validator } from '../types';\nimport {\n isRequired,\n isValidEmail,\n isValidPhoneNumber,\n isValidCPR,\n isValidCVR,\n isMinLength,\n isMaxLength,\n isValidURL,\n isPattern,\n} from './validation';\n\nconst ok = { isValid: true };\nconst fail = (message: string) => ({ isValid: false, message });\n\nexport function required(message = 'This field is required'): Validator {\n return value => (isRequired(value) ? ok : fail(message));\n}\n\nexport function email(message = 'Invalid email address'): Validator {\n return value => (value === '' || isValidEmail(value) ? ok : fail(message));\n}\n\nexport function phone(message = 'Invalid phone number'): Validator {\n return value =>\n value === '' || isValidPhoneNumber(value) ? ok : fail(message);\n}\n\nexport function cpr(message = 'Invalid CPR number'): Validator {\n return value => (value === '' || isValidCPR(value) ? ok : fail(message));\n}\n\nexport function cvr(message = 'Invalid CVR number'): Validator {\n return value => (value === '' || isValidCVR(value) ? ok : fail(message));\n}\n\nexport function minLength(\n min: number,\n message = `Must be at least ${min} characters`,\n): Validator {\n return value => (isMinLength(value, min) ? ok : fail(message));\n}\n\nexport function maxLength(\n max: number,\n message = `Must be at most ${max} characters`,\n): Validator {\n return value => (isMaxLength(value, max) ? ok : fail(message));\n}\n\nexport function pattern(regex: RegExp, message = 'Invalid format'): Validator {\n return value =>\n value === '' || isPattern(value, regex) ? ok : fail(message);\n}\n\nexport function url(message = 'Invalid URL'): Validator {\n return value => (value === '' || isValidURL(value) ? ok : fail(message));\n}\n\n/**\n * Composes multiple validators into a single validator.\n * Returns the first failing result, or a passing result if all pass.\n */\nexport function composeValidators(...validators: Validator[]): Validator {\n return value => {\n for (const validator of validators) {\n const result = validator(value);\n if (!result.isValid) return result;\n }\n return ok;\n };\n}\n","/**\n * Shared theme mode utilities — platform-agnostic.\n *\n * Used by both Vue and React Native to resolve, persist, and detect theme mode.\n */\n\n/**\n * Theme mode — matches @grundtone/core's ThemeMode.\n * Defined locally to avoid circular dependency between utils and core.\n */\nexport type ThemeMode = 'light' | 'dark' | 'auto';\nexport type ResolvedMode = 'light' | 'dark';\n\n/** Default localStorage key for persisted theme mode. */\nexport const THEME_STORAGE_KEY = 'grundtone-theme-mode';\n\n/**\n * Resolve a ThemeMode to a concrete 'light' or 'dark' value.\n * When mode is 'auto', uses the systemIsDark flag to decide.\n */\nexport function resolveThemeMode(\n mode: ThemeMode,\n systemIsDark: boolean,\n): ResolvedMode {\n if (mode === 'auto') return systemIsDark ? 'dark' : 'light';\n return mode;\n}\n\n/**\n * Check if the system prefers dark mode.\n * Web only — uses matchMedia. SSR-safe (returns false on server).\n */\nexport function getSystemIsDark(): boolean {\n if (typeof window === 'undefined') return false;\n return window.matchMedia('(prefers-color-scheme: dark)').matches;\n}\n\n/**\n * Persist theme mode to localStorage.\n * Web only, SSR-safe — no-op on server or when localStorage is unavailable.\n */\nexport function persistThemeMode(\n mode: ThemeMode,\n key = THEME_STORAGE_KEY,\n): void {\n if (typeof window === 'undefined') return;\n try {\n window.localStorage.setItem(key, mode);\n } catch {\n // localStorage unavailable (private browsing, etc.)\n }\n}\n\n/**\n * Load persisted theme mode from localStorage.\n * Returns null if no valid mode is stored. SSR-safe.\n */\nexport function loadPersistedThemeMode(\n key = THEME_STORAGE_KEY,\n): ThemeMode | null {\n if (typeof window === 'undefined') return null;\n try {\n const stored = window.localStorage.getItem(key);\n if (stored === 'light' || stored === 'dark' || stored === 'auto')\n return stored;\n } catch {\n // localStorage unavailable\n }\n return null;\n}\n\n/**\n * Get the system theme mode as a resolved string.\n * Web only, SSR-safe (returns 'light' on server).\n */\nexport function getSystemThemeMode(): ResolvedMode {\n return getSystemIsDark() ? 'dark' : 'light';\n}\n\n/**\n * Convert camelCase to kebab-case.\n * Used for mapping theme object keys to CSS custom property names.\n *\n * @example camelToKebab('primaryLight') → 'primary-light'\n * @example camelToKebab('onPrimary') → 'on-primary'\n */\nexport function camelToKebab(str: string): string {\n return str.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@grundtone/utils",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Utilities and helpers for Grundtone",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"private": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.mjs",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"sideEffects": false,
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/**"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/grundtone/grundtone.git",
|
|
17
|
+
"directory": "packages/utils"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup",
|
|
24
|
+
"dev": "tsup --watch",
|
|
25
|
+
"lint": "eslint src",
|
|
26
|
+
"test": "vitest --passWithNoTests",
|
|
27
|
+
"clean": "rimraf .turbo node_modules dist",
|
|
28
|
+
"prepublishOnly": "test -d dist || pnpm run build"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@grundtone/core": "workspace:*"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^20.17.45",
|
|
35
|
+
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
36
|
+
"@typescript-eslint/parser": "^6.21.0",
|
|
37
|
+
"eslint": "^8.56.0",
|
|
38
|
+
"rimraf": "^5.0.5",
|
|
39
|
+
"tsup": "^8.0.2"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {}
|
|
42
|
+
}
|