@mikespa/kit 0.1.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/LICENSE +21 -0
- package/README.md +104 -0
- package/dist/format/index.d.ts +34 -0
- package/dist/format/index.js +108 -0
- package/dist/format/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +150 -0
- package/dist/index.js.map +1 -0
- package/dist/math/index.d.ts +10 -0
- package/dist/math/index.js +23 -0
- package/dist/math/index.js.map +1 -0
- package/dist/string/index.d.ts +14 -0
- package/dist/string/index.js +23 -0
- package/dist/string/index.js.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mikespa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# @mikespa/kit
|
|
2
|
+
|
|
3
|
+
A small, framework-agnostic personal toolkit. Built to stop copy-pasting the
|
|
4
|
+
same `formatCHF` / `slugify` / `clamp` functions between Next.js projects.
|
|
5
|
+
|
|
6
|
+
No runtime dependencies. Tree-shakeable via subpath exports.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @mikespa/kit
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
Import only what you need via subpaths — this keeps bundles small and is
|
|
17
|
+
self-documenting at the call site.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { formatDate, formatCHF, formatSecs, formatHours } from '@mikespa/kit/format';
|
|
21
|
+
import { slugify, truncate } from '@mikespa/kit/string';
|
|
22
|
+
import { clamp, calcTimeHours } from '@mikespa/kit/math';
|
|
23
|
+
|
|
24
|
+
formatDate('2026-06-19'); // "19.06.2026"
|
|
25
|
+
formatCHF(1234.5); // "1'234.50 CHF"
|
|
26
|
+
formatSecs(9000); // "2h30"
|
|
27
|
+
formatHours(2.5); // "2h30"
|
|
28
|
+
slugify('Hello, World!'); // "hello-world"
|
|
29
|
+
clamp(15, 0, 10); // 10
|
|
30
|
+
calcTimeHours('08:00', '10:30'); // 2.5
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or import everything from the root if you don't care about tree-shaking:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { formatCHF, slugify, clamp } from '@mikespa/kit';
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Modules
|
|
40
|
+
|
|
41
|
+
### `./format`
|
|
42
|
+
|
|
43
|
+
| Function | Description | Default output |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| `formatDate(input)` | Date as DD.MM.YYYY | "19.06.2026" |
|
|
46
|
+
| `formatTime(input)` | Time (24h) | "15:30" |
|
|
47
|
+
| `formatDateTime(input)` | Date + time | "19 juin 2026, 15:30" |
|
|
48
|
+
| `formatRelative(input)` | Relative to now | "il y a 3 heures" |
|
|
49
|
+
| `formatNumber(value)` | Locale-aware thousands | "1 234 567" |
|
|
50
|
+
| `formatCHF(amount)` | Swiss CHF, apostrophe sep. | "1'234.50 CHF" |
|
|
51
|
+
| `formatCurrency(value)` | Intl currency (CHF default) | "1 234.50 CHF" |
|
|
52
|
+
| `formatCompact(value)` | Compact notation | "1,2 k" |
|
|
53
|
+
| `formatPercent(value)` | Percentage | "45,6%" |
|
|
54
|
+
| `formatBytes(bytes)` | Human-readable file size | "1.5 KB" |
|
|
55
|
+
| `formatSecs(seconds)` | Duration from seconds | "2h30" / "45min" |
|
|
56
|
+
| `formatHours(hours)` | Duration from decimal hours | "2h30" / "45min" |
|
|
57
|
+
|
|
58
|
+
All `format*` functions that wrap `Intl` accept an optional `locale` parameter
|
|
59
|
+
(defaults to `'fr-CH'`) so consuming projects can override per-call.
|
|
60
|
+
|
|
61
|
+
### `./string`
|
|
62
|
+
|
|
63
|
+
| Function | Description |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `slugify(input)` | URL-friendly slug |
|
|
66
|
+
| `truncate(input, maxLength)` | Truncate with suffix |
|
|
67
|
+
| `capitalize(input)` | First letter uppercase |
|
|
68
|
+
| `pluralize(count, singular)` | Simple pluralizer |
|
|
69
|
+
|
|
70
|
+
### `./math`
|
|
71
|
+
|
|
72
|
+
| Function | Description |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `clamp(value, min, max)` | Clamp to range |
|
|
75
|
+
| `lerp(start, end, t)` | Linear interpolate |
|
|
76
|
+
| `round(value, decimals)` | Round to N decimals |
|
|
77
|
+
| `calcTimeHours(start, end)` | Hours between "HH:MM" strings |
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm install
|
|
83
|
+
npm run dev # build in watch mode
|
|
84
|
+
npm run build # build once (output -> dist/)
|
|
85
|
+
npm test # run tests
|
|
86
|
+
npm run test:watch
|
|
87
|
+
npm run typecheck
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Publishing
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm version patch # or minor / major
|
|
94
|
+
git push --follow-tags
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The `publish` workflow runs automatically on any `v*.*.*` tag push.
|
|
98
|
+
`prepublishOnly` runs the build, so `dist/` is always fresh.
|
|
99
|
+
|
|
100
|
+
You need an `NPM_TOKEN` secret set in the GitHub repo settings.
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Format a number with locale-aware thousand separators. e.g. 1234567 -> "1 234 567" (fr-CH) */
|
|
2
|
+
declare function formatNumber(value: number, locale?: string, options?: Intl.NumberFormatOptions): string;
|
|
3
|
+
/** Format a number in compact/abbreviated notation. e.g. 1200 -> "1,2 k" (fr-CH) */
|
|
4
|
+
declare function formatCompact(value: number, locale?: string): string;
|
|
5
|
+
/** Format a number as currency. e.g. (1234.5) -> "1 234.50 CHF" (fr-CH) */
|
|
6
|
+
declare function formatCurrency(value: number, currency?: string, locale?: string): string;
|
|
7
|
+
/** Format an amount in CHF with apostrophe thousands separator. e.g. 1234.5 -> "1'234.50 CHF" */
|
|
8
|
+
declare function formatCHF(amount: number): string;
|
|
9
|
+
/** Format a number as a percentage. e.g. 0.456 -> "45,6%" (fr-CH) */
|
|
10
|
+
declare function formatPercent(value: number, locale?: string, fractionDigits?: number): string;
|
|
11
|
+
/** Format a byte count into a human-readable file size. e.g. 1536 -> "1.5 KB" */
|
|
12
|
+
declare function formatBytes(bytes: number, decimals?: number): string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Thin, opinionated wrappers around `Intl.DateTimeFormat` and `Intl.RelativeTimeFormat`.
|
|
16
|
+
*/
|
|
17
|
+
type DateInput = Date | string | number;
|
|
18
|
+
/** Format a date as DD.MM.YYYY. e.g. "19.06.2026" */
|
|
19
|
+
declare function formatDate(input: DateInput, locale?: string, options?: Intl.DateTimeFormatOptions): string;
|
|
20
|
+
/** Format just the time portion. e.g. "15:30" (24h in fr-CH) */
|
|
21
|
+
declare function formatTime(input: DateInput, locale?: string, options?: Intl.DateTimeFormatOptions): string;
|
|
22
|
+
/** Format date and time together. e.g. "19 juin 2026, 15:30" */
|
|
23
|
+
declare function formatDateTime(input: DateInput, locale?: string, options?: Intl.DateTimeFormatOptions): string;
|
|
24
|
+
/** Format a date relative to now. e.g. "3 hours ago", "in 2 days" */
|
|
25
|
+
declare function formatRelative(input: DateInput, locale?: string, now?: DateInput): string;
|
|
26
|
+
|
|
27
|
+
/** Formats a duration in seconds: "2h30" or "45min" */
|
|
28
|
+
declare function formatSecs(seconds: number): string;
|
|
29
|
+
/** Formats a duration in seconds: "2h30" or "45min" */
|
|
30
|
+
declare function formatMinutes(minutes: number): string;
|
|
31
|
+
/** Formats a decimal number of hours: "2h30" or "45min" */
|
|
32
|
+
declare function formatHours(hours: number): string;
|
|
33
|
+
|
|
34
|
+
export { formatBytes, formatCHF, formatCompact, formatCurrency, formatDate, formatDateTime, formatHours, formatMinutes, formatNumber, formatPercent, formatRelative, formatSecs, formatTime };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// src/format/number.ts
|
|
2
|
+
var DEFAULT_LOCALE = "fr-CH";
|
|
3
|
+
function formatNumber(value, locale = DEFAULT_LOCALE, options) {
|
|
4
|
+
return new Intl.NumberFormat(locale, options).format(value);
|
|
5
|
+
}
|
|
6
|
+
function formatCompact(value, locale = DEFAULT_LOCALE) {
|
|
7
|
+
return new Intl.NumberFormat(locale, { notation: "compact" }).format(value);
|
|
8
|
+
}
|
|
9
|
+
function formatCurrency(value, currency = "CHF", locale = DEFAULT_LOCALE) {
|
|
10
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency }).format(value);
|
|
11
|
+
}
|
|
12
|
+
function formatCHF(amount) {
|
|
13
|
+
const [int, dec] = amount.toFixed(2).split(".");
|
|
14
|
+
const intFormatted = int.replace(/\B(?=(\d{3})+(?!\d))/g, "'");
|
|
15
|
+
return `${intFormatted}.${dec} CHF`;
|
|
16
|
+
}
|
|
17
|
+
function formatPercent(value, locale = DEFAULT_LOCALE, fractionDigits = 1) {
|
|
18
|
+
return new Intl.NumberFormat(locale, {
|
|
19
|
+
style: "percent",
|
|
20
|
+
minimumFractionDigits: 0,
|
|
21
|
+
maximumFractionDigits: fractionDigits
|
|
22
|
+
}).format(value);
|
|
23
|
+
}
|
|
24
|
+
function formatBytes(bytes, decimals = 1) {
|
|
25
|
+
if (bytes === 0) return "0 B";
|
|
26
|
+
const k = 1024;
|
|
27
|
+
const sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
28
|
+
const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k));
|
|
29
|
+
const value = bytes / Math.pow(k, i);
|
|
30
|
+
return `${value.toFixed(decimals)} ${sizes[i]}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/format/date.ts
|
|
34
|
+
var DEFAULT_LOCALE2 = "fr-CH";
|
|
35
|
+
function toDate(input) {
|
|
36
|
+
return input instanceof Date ? input : new Date(input);
|
|
37
|
+
}
|
|
38
|
+
function formatDate(input, locale = DEFAULT_LOCALE2, options = {}) {
|
|
39
|
+
return new Intl.DateTimeFormat(locale, options).format(toDate(input));
|
|
40
|
+
}
|
|
41
|
+
function formatTime(input, locale = DEFAULT_LOCALE2, options = { timeStyle: "short" }) {
|
|
42
|
+
return new Intl.DateTimeFormat(locale, options).format(toDate(input));
|
|
43
|
+
}
|
|
44
|
+
function formatDateTime(input, locale = DEFAULT_LOCALE2, options = { dateStyle: "medium", timeStyle: "short" }) {
|
|
45
|
+
return new Intl.DateTimeFormat(locale, options).format(toDate(input));
|
|
46
|
+
}
|
|
47
|
+
function formatRelative(input, locale = DEFAULT_LOCALE2, now = /* @__PURE__ */ new Date()) {
|
|
48
|
+
const diffSeconds = (toDate(input).getTime() - toDate(now).getTime()) / 1e3;
|
|
49
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
|
50
|
+
const units = [
|
|
51
|
+
["year", 31536e3],
|
|
52
|
+
["month", 2592e3],
|
|
53
|
+
["week", 604800],
|
|
54
|
+
["day", 86400],
|
|
55
|
+
["hour", 3600],
|
|
56
|
+
["minute", 60],
|
|
57
|
+
["second", 1]
|
|
58
|
+
];
|
|
59
|
+
for (const [unit, secondsInUnit] of units) {
|
|
60
|
+
const delta = diffSeconds / secondsInUnit;
|
|
61
|
+
if (Math.abs(delta) >= 1 || unit === "second") {
|
|
62
|
+
return rtf.format(Math.round(delta), unit);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return rtf.format(0, "second");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/format/duration.ts
|
|
69
|
+
function formatSecs(seconds) {
|
|
70
|
+
const h = Math.floor(seconds / 3600);
|
|
71
|
+
const m = Math.floor(seconds % 3600 / 60);
|
|
72
|
+
if (h === 0) return `${m}min`;
|
|
73
|
+
if (m === 0) return `${h}h`;
|
|
74
|
+
return `${h}h${String(m).padStart(2, "0")}`;
|
|
75
|
+
}
|
|
76
|
+
function formatMinutes(minutes) {
|
|
77
|
+
if (minutes === 0) return "0min";
|
|
78
|
+
else if (minutes < 1) return "<1min";
|
|
79
|
+
else {
|
|
80
|
+
const h = Math.floor(minutes / 60);
|
|
81
|
+
const m = minutes % 60;
|
|
82
|
+
if (h === 0) return `${m}min`;
|
|
83
|
+
return `${h}h${String(m).padStart(2, "0")}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function formatHours(hours) {
|
|
87
|
+
const h = Math.floor(hours);
|
|
88
|
+
const m = Math.round((hours - h) * 60);
|
|
89
|
+
if (h === 0) return `${m}min`;
|
|
90
|
+
if (m === 0) return `${h}h`;
|
|
91
|
+
return `${h}h${String(m).padStart(2, "0")}`;
|
|
92
|
+
}
|
|
93
|
+
export {
|
|
94
|
+
formatBytes,
|
|
95
|
+
formatCHF,
|
|
96
|
+
formatCompact,
|
|
97
|
+
formatCurrency,
|
|
98
|
+
formatDate,
|
|
99
|
+
formatDateTime,
|
|
100
|
+
formatHours,
|
|
101
|
+
formatMinutes,
|
|
102
|
+
formatNumber,
|
|
103
|
+
formatPercent,
|
|
104
|
+
formatRelative,
|
|
105
|
+
formatSecs,
|
|
106
|
+
formatTime
|
|
107
|
+
};
|
|
108
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/format/number.ts","../../src/format/date.ts","../../src/format/duration.ts"],"sourcesContent":["const DEFAULT_LOCALE = 'fr-CH';\n\n/** Format a number with locale-aware thousand separators. e.g. 1234567 -> \"1 234 567\" (fr-CH) */\nexport function formatNumber(\n value: number,\n locale: string = DEFAULT_LOCALE,\n options?: Intl.NumberFormatOptions\n): string {\n return new Intl.NumberFormat(locale, options).format(value);\n}\n\n/** Format a number in compact/abbreviated notation. e.g. 1200 -> \"1,2 k\" (fr-CH) */\nexport function formatCompact(\n value: number,\n locale: string = DEFAULT_LOCALE\n): string {\n return new Intl.NumberFormat(locale, { notation: 'compact' }).format(value);\n}\n\n/** Format a number as currency. e.g. (1234.5) -> \"1 234.50 CHF\" (fr-CH) */\nexport function formatCurrency(\n value: number,\n currency: string = 'CHF',\n locale: string = DEFAULT_LOCALE\n): string {\n return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(value);\n}\n\n/** Format an amount in CHF with apostrophe thousands separator. e.g. 1234.5 -> \"1'234.50 CHF\" */\nexport function formatCHF(amount: number): string {\n const [int, dec] = amount.toFixed(2).split('.') as [string, string];\n const intFormatted = int.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \"'\");\n return `${intFormatted}.${dec} CHF`;\n}\n\n/** Format a number as a percentage. e.g. 0.456 -> \"45,6%\" (fr-CH) */\nexport function formatPercent(\n value: number,\n locale: string = DEFAULT_LOCALE,\n fractionDigits = 1\n): string {\n return new Intl.NumberFormat(locale, {\n style: 'percent',\n minimumFractionDigits: 0,\n maximumFractionDigits: fractionDigits,\n }).format(value);\n}\n\n/** Format a byte count into a human-readable file size. e.g. 1536 -> \"1.5 KB\" */\nexport function formatBytes(bytes: number, decimals = 1): string {\n if (bytes === 0) return '0 B';\n const k = 1024;\n const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];\n const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k));\n const value = bytes / Math.pow(k, i);\n return `${value.toFixed(decimals)} ${sizes[i]!}`;\n}\n","/**\n * Thin, opinionated wrappers around `Intl.DateTimeFormat` and `Intl.RelativeTimeFormat`.\n */\n\nconst DEFAULT_LOCALE = 'fr-CH';\n\ntype DateInput = Date | string | number;\n\nfunction toDate(input: DateInput): Date {\n return input instanceof Date ? input : new Date(input);\n}\n\n/** Format a date as DD.MM.YYYY. e.g. \"19.06.2026\" */\nexport function formatDate(\n input: DateInput,\n locale: string = DEFAULT_LOCALE,\n options: Intl.DateTimeFormatOptions = {}\n): string {\n return new Intl.DateTimeFormat(locale, options).format(toDate(input));\n}\n\n/** Format just the time portion. e.g. \"15:30\" (24h in fr-CH) */\nexport function formatTime(\n input: DateInput,\n locale: string = DEFAULT_LOCALE,\n options: Intl.DateTimeFormatOptions = { timeStyle: 'short' }\n): string {\n return new Intl.DateTimeFormat(locale, options).format(toDate(input));\n}\n\n/** Format date and time together. e.g. \"19 juin 2026, 15:30\" */\nexport function formatDateTime(\n input: DateInput,\n locale: string = DEFAULT_LOCALE,\n options: Intl.DateTimeFormatOptions = { dateStyle: 'medium', timeStyle: 'short' }\n): string {\n return new Intl.DateTimeFormat(locale, options).format(toDate(input));\n}\n\n/** Format a date relative to now. e.g. \"3 hours ago\", \"in 2 days\" */\nexport function formatRelative(\n input: DateInput,\n locale: string = DEFAULT_LOCALE,\n now: DateInput = new Date()\n): string {\n const diffSeconds = (toDate(input).getTime() - toDate(now).getTime()) / 1000;\n const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });\n\n const units: [Intl.RelativeTimeFormatUnit, number][] = [\n ['year', 31536000],\n ['month', 2592000],\n ['week', 604800],\n ['day', 86400],\n ['hour', 3600],\n ['minute', 60],\n ['second', 1],\n ];\n\n for (const [unit, secondsInUnit] of units) {\n const delta = diffSeconds / secondsInUnit;\n if (Math.abs(delta) >= 1 || unit === 'second') {\n return rtf.format(Math.round(delta), unit);\n }\n }\n\n return rtf.format(0, 'second');\n}\n","/** Formats a duration in seconds: \"2h30\" or \"45min\" */\nexport function formatSecs(seconds: number): string {\n const h = Math.floor(seconds / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n if (h === 0) return `${m}min`;\n if (m === 0) return `${h}h`;\n return `${h}h${String(m).padStart(2, '0')}`;\n}\n\n/** Formats a duration in seconds: \"2h30\" or \"45min\" */\nexport function formatMinutes(minutes: number): string {\n if (minutes === 0) return '0min';\n else if (minutes < 1) return '<1min';\n else {\n const h = Math.floor(minutes / 60);\n const m = minutes % 60;\n if (h === 0) return `${m}min`;\n return `${h}h${String(m).padStart(2, '0')}`;\n }\n}\n\n/** Formats a decimal number of hours: \"2h30\" or \"45min\" */\nexport function formatHours(hours: number): string {\n const h = Math.floor(hours);\n const m = Math.round((hours - h) * 60);\n if (h === 0) return `${m}min`;\n if (m === 0) return `${h}h`;\n return `${h}h${String(m).padStart(2, '0')}`;\n}\n"],"mappings":"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","names":["DEFAULT_LOCALE"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { formatBytes, formatCHF, formatCompact, formatCurrency, formatDate, formatDateTime, formatHours, formatMinutes, formatNumber, formatPercent, formatRelative, formatSecs, formatTime } from './format/index.js';
|
|
2
|
+
export { capitalize, pluralize, slugify, truncate } from './string/index.js';
|
|
3
|
+
export { calcTimeHours, clamp, lerp, round } from './math/index.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// src/format/number.ts
|
|
2
|
+
var DEFAULT_LOCALE = "fr-CH";
|
|
3
|
+
function formatNumber(value, locale = DEFAULT_LOCALE, options) {
|
|
4
|
+
return new Intl.NumberFormat(locale, options).format(value);
|
|
5
|
+
}
|
|
6
|
+
function formatCompact(value, locale = DEFAULT_LOCALE) {
|
|
7
|
+
return new Intl.NumberFormat(locale, { notation: "compact" }).format(value);
|
|
8
|
+
}
|
|
9
|
+
function formatCurrency(value, currency = "CHF", locale = DEFAULT_LOCALE) {
|
|
10
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency }).format(value);
|
|
11
|
+
}
|
|
12
|
+
function formatCHF(amount) {
|
|
13
|
+
const [int, dec] = amount.toFixed(2).split(".");
|
|
14
|
+
const intFormatted = int.replace(/\B(?=(\d{3})+(?!\d))/g, "'");
|
|
15
|
+
return `${intFormatted}.${dec} CHF`;
|
|
16
|
+
}
|
|
17
|
+
function formatPercent(value, locale = DEFAULT_LOCALE, fractionDigits = 1) {
|
|
18
|
+
return new Intl.NumberFormat(locale, {
|
|
19
|
+
style: "percent",
|
|
20
|
+
minimumFractionDigits: 0,
|
|
21
|
+
maximumFractionDigits: fractionDigits
|
|
22
|
+
}).format(value);
|
|
23
|
+
}
|
|
24
|
+
function formatBytes(bytes, decimals = 1) {
|
|
25
|
+
if (bytes === 0) return "0 B";
|
|
26
|
+
const k = 1024;
|
|
27
|
+
const sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
28
|
+
const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k));
|
|
29
|
+
const value = bytes / Math.pow(k, i);
|
|
30
|
+
return `${value.toFixed(decimals)} ${sizes[i]}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/format/date.ts
|
|
34
|
+
var DEFAULT_LOCALE2 = "fr-CH";
|
|
35
|
+
function toDate(input) {
|
|
36
|
+
return input instanceof Date ? input : new Date(input);
|
|
37
|
+
}
|
|
38
|
+
function formatDate(input, locale = DEFAULT_LOCALE2, options = {}) {
|
|
39
|
+
return new Intl.DateTimeFormat(locale, options).format(toDate(input));
|
|
40
|
+
}
|
|
41
|
+
function formatTime(input, locale = DEFAULT_LOCALE2, options = { timeStyle: "short" }) {
|
|
42
|
+
return new Intl.DateTimeFormat(locale, options).format(toDate(input));
|
|
43
|
+
}
|
|
44
|
+
function formatDateTime(input, locale = DEFAULT_LOCALE2, options = { dateStyle: "medium", timeStyle: "short" }) {
|
|
45
|
+
return new Intl.DateTimeFormat(locale, options).format(toDate(input));
|
|
46
|
+
}
|
|
47
|
+
function formatRelative(input, locale = DEFAULT_LOCALE2, now = /* @__PURE__ */ new Date()) {
|
|
48
|
+
const diffSeconds = (toDate(input).getTime() - toDate(now).getTime()) / 1e3;
|
|
49
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
|
50
|
+
const units = [
|
|
51
|
+
["year", 31536e3],
|
|
52
|
+
["month", 2592e3],
|
|
53
|
+
["week", 604800],
|
|
54
|
+
["day", 86400],
|
|
55
|
+
["hour", 3600],
|
|
56
|
+
["minute", 60],
|
|
57
|
+
["second", 1]
|
|
58
|
+
];
|
|
59
|
+
for (const [unit, secondsInUnit] of units) {
|
|
60
|
+
const delta = diffSeconds / secondsInUnit;
|
|
61
|
+
if (Math.abs(delta) >= 1 || unit === "second") {
|
|
62
|
+
return rtf.format(Math.round(delta), unit);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return rtf.format(0, "second");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/format/duration.ts
|
|
69
|
+
function formatSecs(seconds) {
|
|
70
|
+
const h = Math.floor(seconds / 3600);
|
|
71
|
+
const m = Math.floor(seconds % 3600 / 60);
|
|
72
|
+
if (h === 0) return `${m}min`;
|
|
73
|
+
if (m === 0) return `${h}h`;
|
|
74
|
+
return `${h}h${String(m).padStart(2, "0")}`;
|
|
75
|
+
}
|
|
76
|
+
function formatMinutes(minutes) {
|
|
77
|
+
if (minutes === 0) return "0min";
|
|
78
|
+
else if (minutes < 1) return "<1min";
|
|
79
|
+
else {
|
|
80
|
+
const h = Math.floor(minutes / 60);
|
|
81
|
+
const m = minutes % 60;
|
|
82
|
+
if (h === 0) return `${m}min`;
|
|
83
|
+
return `${h}h${String(m).padStart(2, "0")}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function formatHours(hours) {
|
|
87
|
+
const h = Math.floor(hours);
|
|
88
|
+
const m = Math.round((hours - h) * 60);
|
|
89
|
+
if (h === 0) return `${m}min`;
|
|
90
|
+
if (m === 0) return `${h}h`;
|
|
91
|
+
return `${h}h${String(m).padStart(2, "0")}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/string/index.ts
|
|
95
|
+
function slugify(input) {
|
|
96
|
+
return input.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
97
|
+
}
|
|
98
|
+
function truncate(input, maxLength, suffix = "\u2026") {
|
|
99
|
+
if (input.length <= maxLength) return input;
|
|
100
|
+
return input.slice(0, Math.max(0, maxLength - suffix.length)) + suffix;
|
|
101
|
+
}
|
|
102
|
+
function capitalize(input) {
|
|
103
|
+
if (!input) return input;
|
|
104
|
+
return input[0].toUpperCase() + input.slice(1);
|
|
105
|
+
}
|
|
106
|
+
function pluralize(count, singular, plural) {
|
|
107
|
+
const word = count === 1 ? singular : plural ?? `${singular}s`;
|
|
108
|
+
return `${count} ${word}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/math/index.ts
|
|
112
|
+
function clamp(value, min, max) {
|
|
113
|
+
return Math.min(Math.max(value, min), max);
|
|
114
|
+
}
|
|
115
|
+
function lerp(start, end, t) {
|
|
116
|
+
return start + (end - start) * t;
|
|
117
|
+
}
|
|
118
|
+
function round(value, decimals = 0) {
|
|
119
|
+
const factor = 10 ** decimals;
|
|
120
|
+
return Math.round(value * factor) / factor;
|
|
121
|
+
}
|
|
122
|
+
function calcTimeHours(start, end) {
|
|
123
|
+
const [sh, sm] = start.split(":").map(Number);
|
|
124
|
+
const [eh, em] = end.split(":").map(Number);
|
|
125
|
+
return Math.max(0, (eh * 60 + em - (sh * 60 + sm)) / 60);
|
|
126
|
+
}
|
|
127
|
+
export {
|
|
128
|
+
calcTimeHours,
|
|
129
|
+
capitalize,
|
|
130
|
+
clamp,
|
|
131
|
+
formatBytes,
|
|
132
|
+
formatCHF,
|
|
133
|
+
formatCompact,
|
|
134
|
+
formatCurrency,
|
|
135
|
+
formatDate,
|
|
136
|
+
formatDateTime,
|
|
137
|
+
formatHours,
|
|
138
|
+
formatMinutes,
|
|
139
|
+
formatNumber,
|
|
140
|
+
formatPercent,
|
|
141
|
+
formatRelative,
|
|
142
|
+
formatSecs,
|
|
143
|
+
formatTime,
|
|
144
|
+
lerp,
|
|
145
|
+
pluralize,
|
|
146
|
+
round,
|
|
147
|
+
slugify,
|
|
148
|
+
truncate
|
|
149
|
+
};
|
|
150
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/format/number.ts","../src/format/date.ts","../src/format/duration.ts","../src/string/index.ts","../src/math/index.ts"],"sourcesContent":["const DEFAULT_LOCALE = 'fr-CH';\n\n/** Format a number with locale-aware thousand separators. e.g. 1234567 -> \"1 234 567\" (fr-CH) */\nexport function formatNumber(\n value: number,\n locale: string = DEFAULT_LOCALE,\n options?: Intl.NumberFormatOptions\n): string {\n return new Intl.NumberFormat(locale, options).format(value);\n}\n\n/** Format a number in compact/abbreviated notation. e.g. 1200 -> \"1,2 k\" (fr-CH) */\nexport function formatCompact(\n value: number,\n locale: string = DEFAULT_LOCALE\n): string {\n return new Intl.NumberFormat(locale, { notation: 'compact' }).format(value);\n}\n\n/** Format a number as currency. e.g. (1234.5) -> \"1 234.50 CHF\" (fr-CH) */\nexport function formatCurrency(\n value: number,\n currency: string = 'CHF',\n locale: string = DEFAULT_LOCALE\n): string {\n return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(value);\n}\n\n/** Format an amount in CHF with apostrophe thousands separator. e.g. 1234.5 -> \"1'234.50 CHF\" */\nexport function formatCHF(amount: number): string {\n const [int, dec] = amount.toFixed(2).split('.') as [string, string];\n const intFormatted = int.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \"'\");\n return `${intFormatted}.${dec} CHF`;\n}\n\n/** Format a number as a percentage. e.g. 0.456 -> \"45,6%\" (fr-CH) */\nexport function formatPercent(\n value: number,\n locale: string = DEFAULT_LOCALE,\n fractionDigits = 1\n): string {\n return new Intl.NumberFormat(locale, {\n style: 'percent',\n minimumFractionDigits: 0,\n maximumFractionDigits: fractionDigits,\n }).format(value);\n}\n\n/** Format a byte count into a human-readable file size. e.g. 1536 -> \"1.5 KB\" */\nexport function formatBytes(bytes: number, decimals = 1): string {\n if (bytes === 0) return '0 B';\n const k = 1024;\n const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];\n const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k));\n const value = bytes / Math.pow(k, i);\n return `${value.toFixed(decimals)} ${sizes[i]!}`;\n}\n","/**\n * Thin, opinionated wrappers around `Intl.DateTimeFormat` and `Intl.RelativeTimeFormat`.\n */\n\nconst DEFAULT_LOCALE = 'fr-CH';\n\ntype DateInput = Date | string | number;\n\nfunction toDate(input: DateInput): Date {\n return input instanceof Date ? input : new Date(input);\n}\n\n/** Format a date as DD.MM.YYYY. e.g. \"19.06.2026\" */\nexport function formatDate(\n input: DateInput,\n locale: string = DEFAULT_LOCALE,\n options: Intl.DateTimeFormatOptions = {}\n): string {\n return new Intl.DateTimeFormat(locale, options).format(toDate(input));\n}\n\n/** Format just the time portion. e.g. \"15:30\" (24h in fr-CH) */\nexport function formatTime(\n input: DateInput,\n locale: string = DEFAULT_LOCALE,\n options: Intl.DateTimeFormatOptions = { timeStyle: 'short' }\n): string {\n return new Intl.DateTimeFormat(locale, options).format(toDate(input));\n}\n\n/** Format date and time together. e.g. \"19 juin 2026, 15:30\" */\nexport function formatDateTime(\n input: DateInput,\n locale: string = DEFAULT_LOCALE,\n options: Intl.DateTimeFormatOptions = { dateStyle: 'medium', timeStyle: 'short' }\n): string {\n return new Intl.DateTimeFormat(locale, options).format(toDate(input));\n}\n\n/** Format a date relative to now. e.g. \"3 hours ago\", \"in 2 days\" */\nexport function formatRelative(\n input: DateInput,\n locale: string = DEFAULT_LOCALE,\n now: DateInput = new Date()\n): string {\n const diffSeconds = (toDate(input).getTime() - toDate(now).getTime()) / 1000;\n const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });\n\n const units: [Intl.RelativeTimeFormatUnit, number][] = [\n ['year', 31536000],\n ['month', 2592000],\n ['week', 604800],\n ['day', 86400],\n ['hour', 3600],\n ['minute', 60],\n ['second', 1],\n ];\n\n for (const [unit, secondsInUnit] of units) {\n const delta = diffSeconds / secondsInUnit;\n if (Math.abs(delta) >= 1 || unit === 'second') {\n return rtf.format(Math.round(delta), unit);\n }\n }\n\n return rtf.format(0, 'second');\n}\n","/** Formats a duration in seconds: \"2h30\" or \"45min\" */\nexport function formatSecs(seconds: number): string {\n const h = Math.floor(seconds / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n if (h === 0) return `${m}min`;\n if (m === 0) return `${h}h`;\n return `${h}h${String(m).padStart(2, '0')}`;\n}\n\n/** Formats a duration in seconds: \"2h30\" or \"45min\" */\nexport function formatMinutes(minutes: number): string {\n if (minutes === 0) return '0min';\n else if (minutes < 1) return '<1min';\n else {\n const h = Math.floor(minutes / 60);\n const m = minutes % 60;\n if (h === 0) return `${m}min`;\n return `${h}h${String(m).padStart(2, '0')}`;\n }\n}\n\n/** Formats a decimal number of hours: \"2h30\" or \"45min\" */\nexport function formatHours(hours: number): string {\n const h = Math.floor(hours);\n const m = Math.round((hours - h) * 60);\n if (h === 0) return `${m}min`;\n if (m === 0) return `${h}h`;\n return `${h}h${String(m).padStart(2, '0')}`;\n}\n","/** Convert a string into a URL-friendly slug. e.g. \"Hello, World!\" -> \"hello-world\" */\nexport function slugify(input: string): string {\n return input\n .normalize('NFKD')\n .replace(/[\\u0300-\\u036f]/g, '') // strip accents\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n/** Truncate a string to a max length, appending a suffix if it was cut. */\nexport function truncate(input: string, maxLength: number, suffix = '…'): string {\n if (input.length <= maxLength) return input;\n return input.slice(0, Math.max(0, maxLength - suffix.length)) + suffix;\n}\n\n/** Capitalize the first letter of a string. */\nexport function capitalize(input: string): string {\n if (!input) return input;\n return input[0]!.toUpperCase() + input.slice(1);\n}\n\n/**\n * Naive English pluralizer for simple cases. For anything locale-sensitive,\n * prefer `Intl.PluralRules` directly.\n * e.g. pluralize(1, 'item') -> \"1 item\", pluralize(2, 'item') -> \"2 items\"\n */\nexport function pluralize(count: number, singular: string, plural?: string): string {\n const word = count === 1 ? singular : plural ?? `${singular}s`;\n return `${count} ${word}`;\n}\n","/** Clamp a number between a min and max (inclusive). */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\n/** Linearly interpolate between two numbers by t (0-1). */\nexport function lerp(start: number, end: number, t: number): number {\n return start + (end - start) * t;\n}\n\n/** Round a number to a given number of decimal places. */\nexport function round(value: number, decimals = 0): number {\n const factor = 10 ** decimals;\n return Math.round(value * factor) / factor;\n}\n\n/** Returns the hours between two \"HH:MM\" time strings (0 if end <= start). */\nexport function calcTimeHours(start: string, end: string): number {\n const [sh, sm] = start.split(':').map(Number) as [number, number];\n const [eh, em] = end.split(':').map(Number) as [number, number];\n return Math.max(0, (eh * 60 + em - (sh * 60 + sm)) / 60);\n}\n"],"mappings":";AAAA,IAAM,iBAAiB;AAGhB,SAAS,aACd,OACA,SAAiB,gBACjB,SACQ;AACR,SAAO,IAAI,KAAK,aAAa,QAAQ,OAAO,EAAE,OAAO,KAAK;AAC5D;AAGO,SAAS,cACd,OACA,SAAiB,gBACT;AACR,SAAO,IAAI,KAAK,aAAa,QAAQ,EAAE,UAAU,UAAU,CAAC,EAAE,OAAO,KAAK;AAC5E;AAGO,SAAS,eACd,OACA,WAAmB,OACnB,SAAiB,gBACT;AACR,SAAO,IAAI,KAAK,aAAa,QAAQ,EAAE,OAAO,YAAY,SAAS,CAAC,EAAE,OAAO,KAAK;AACpF;AAGO,SAAS,UAAU,QAAwB;AAChD,QAAM,CAAC,KAAK,GAAG,IAAI,OAAO,QAAQ,CAAC,EAAE,MAAM,GAAG;AAC9C,QAAM,eAAe,IAAI,QAAQ,yBAAyB,GAAG;AAC7D,SAAO,GAAG,YAAY,IAAI,GAAG;AAC/B;AAGO,SAAS,cACd,OACA,SAAiB,gBACjB,iBAAiB,GACT;AACR,SAAO,IAAI,KAAK,aAAa,QAAQ;AAAA,IACnC,OAAO;AAAA,IACP,uBAAuB;AAAA,IACvB,uBAAuB;AAAA,EACzB,CAAC,EAAE,OAAO,KAAK;AACjB;AAGO,SAAS,YAAY,OAAe,WAAW,GAAW;AAC/D,MAAI,UAAU,EAAG,QAAO;AACxB,QAAM,IAAI;AACV,QAAM,QAAQ,CAAC,KAAK,MAAM,MAAM,MAAM,MAAM,IAAI;AAChD,QAAM,IAAI,KAAK,MAAM,KAAK,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AAC5D,QAAM,QAAQ,QAAQ,KAAK,IAAI,GAAG,CAAC;AACnC,SAAO,GAAG,MAAM,QAAQ,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAE;AAChD;;;ACpDA,IAAMA,kBAAiB;AAIvB,SAAS,OAAO,OAAwB;AACtC,SAAO,iBAAiB,OAAO,QAAQ,IAAI,KAAK,KAAK;AACvD;AAGO,SAAS,WACd,OACA,SAAiBA,iBACjB,UAAsC,CAAC,GAC/B;AACR,SAAO,IAAI,KAAK,eAAe,QAAQ,OAAO,EAAE,OAAO,OAAO,KAAK,CAAC;AACtE;AAGO,SAAS,WACd,OACA,SAAiBA,iBACjB,UAAsC,EAAE,WAAW,QAAQ,GACnD;AACR,SAAO,IAAI,KAAK,eAAe,QAAQ,OAAO,EAAE,OAAO,OAAO,KAAK,CAAC;AACtE;AAGO,SAAS,eACd,OACA,SAAiBA,iBACjB,UAAsC,EAAE,WAAW,UAAU,WAAW,QAAQ,GACxE;AACR,SAAO,IAAI,KAAK,eAAe,QAAQ,OAAO,EAAE,OAAO,OAAO,KAAK,CAAC;AACtE;AAGO,SAAS,eACd,OACA,SAAiBA,iBACjB,MAAiB,oBAAI,KAAK,GAClB;AACR,QAAM,eAAe,OAAO,KAAK,EAAE,QAAQ,IAAI,OAAO,GAAG,EAAE,QAAQ,KAAK;AACxE,QAAM,MAAM,IAAI,KAAK,mBAAmB,QAAQ,EAAE,SAAS,OAAO,CAAC;AAEnE,QAAM,QAAiD;AAAA,IACrD,CAAC,QAAQ,OAAQ;AAAA,IACjB,CAAC,SAAS,MAAO;AAAA,IACjB,CAAC,QAAQ,MAAM;AAAA,IACf,CAAC,OAAO,KAAK;AAAA,IACb,CAAC,QAAQ,IAAI;AAAA,IACb,CAAC,UAAU,EAAE;AAAA,IACb,CAAC,UAAU,CAAC;AAAA,EACd;AAEA,aAAW,CAAC,MAAM,aAAa,KAAK,OAAO;AACzC,UAAM,QAAQ,cAAc;AAC5B,QAAI,KAAK,IAAI,KAAK,KAAK,KAAK,SAAS,UAAU;AAC7C,aAAO,IAAI,OAAO,KAAK,MAAM,KAAK,GAAG,IAAI;AAAA,IAC3C;AAAA,EACF;AAEA,SAAO,IAAI,OAAO,GAAG,QAAQ;AAC/B;;;ACjEO,SAAS,WAAW,SAAyB;AAClD,QAAM,IAAI,KAAK,MAAM,UAAU,IAAI;AACnC,QAAM,IAAI,KAAK,MAAO,UAAU,OAAQ,EAAE;AAC1C,MAAI,MAAM,EAAG,QAAO,GAAG,CAAC;AACxB,MAAI,MAAM,EAAG,QAAO,GAAG,CAAC;AACxB,SAAO,GAAG,CAAC,IAAI,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC;AAC3C;AAGO,SAAS,cAAc,SAAyB;AACrD,MAAI,YAAY,EAAG,QAAO;AAAA,WACjB,UAAU,EAAG,QAAO;AAAA,OACxB;AACH,UAAM,IAAI,KAAK,MAAM,UAAU,EAAE;AACjC,UAAM,IAAI,UAAU;AACpB,QAAI,MAAM,EAAG,QAAO,GAAG,CAAC;AACxB,WAAO,GAAG,CAAC,IAAI,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC;AAAA,EAC3C;AACF;AAGO,SAAS,YAAY,OAAuB;AACjD,QAAM,IAAI,KAAK,MAAM,KAAK;AAC1B,QAAM,IAAI,KAAK,OAAO,QAAQ,KAAK,EAAE;AACrC,MAAI,MAAM,EAAG,QAAO,GAAG,CAAC;AACxB,MAAI,MAAM,EAAG,QAAO,GAAG,CAAC;AACxB,SAAO,GAAG,CAAC,IAAI,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC;AAC3C;;;AC3BO,SAAS,QAAQ,OAAuB;AAC7C,SAAO,MACJ,UAAU,MAAM,EAChB,QAAQ,oBAAoB,EAAE,EAC9B,YAAY,EACZ,KAAK,EACL,QAAQ,eAAe,GAAG,EAC1B,QAAQ,YAAY,EAAE;AAC3B;AAGO,SAAS,SAAS,OAAe,WAAmB,SAAS,UAAa;AAC/E,MAAI,MAAM,UAAU,UAAW,QAAO;AACtC,SAAO,MAAM,MAAM,GAAG,KAAK,IAAI,GAAG,YAAY,OAAO,MAAM,CAAC,IAAI;AAClE;AAGO,SAAS,WAAW,OAAuB;AAChD,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,MAAM,CAAC,EAAG,YAAY,IAAI,MAAM,MAAM,CAAC;AAChD;AAOO,SAAS,UAAU,OAAe,UAAkB,QAAyB;AAClF,QAAM,OAAO,UAAU,IAAI,WAAW,UAAU,GAAG,QAAQ;AAC3D,SAAO,GAAG,KAAK,IAAI,IAAI;AACzB;;;AC9BO,SAAS,MAAM,OAAe,KAAa,KAAqB;AACrE,SAAO,KAAK,IAAI,KAAK,IAAI,OAAO,GAAG,GAAG,GAAG;AAC3C;AAGO,SAAS,KAAK,OAAe,KAAa,GAAmB;AAClE,SAAO,SAAS,MAAM,SAAS;AACjC;AAGO,SAAS,MAAM,OAAe,WAAW,GAAW;AACzD,QAAM,SAAS,MAAM;AACrB,SAAO,KAAK,MAAM,QAAQ,MAAM,IAAI;AACtC;AAGO,SAAS,cAAc,OAAe,KAAqB;AAChE,QAAM,CAAC,IAAI,EAAE,IAAI,MAAM,MAAM,GAAG,EAAE,IAAI,MAAM;AAC5C,QAAM,CAAC,IAAI,EAAE,IAAI,IAAI,MAAM,GAAG,EAAE,IAAI,MAAM;AAC1C,SAAO,KAAK,IAAI,IAAI,KAAK,KAAK,MAAM,KAAK,KAAK,OAAO,EAAE;AACzD;","names":["DEFAULT_LOCALE"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Clamp a number between a min and max (inclusive). */
|
|
2
|
+
declare function clamp(value: number, min: number, max: number): number;
|
|
3
|
+
/** Linearly interpolate between two numbers by t (0-1). */
|
|
4
|
+
declare function lerp(start: number, end: number, t: number): number;
|
|
5
|
+
/** Round a number to a given number of decimal places. */
|
|
6
|
+
declare function round(value: number, decimals?: number): number;
|
|
7
|
+
/** Returns the hours between two "HH:MM" time strings (0 if end <= start). */
|
|
8
|
+
declare function calcTimeHours(start: string, end: string): number;
|
|
9
|
+
|
|
10
|
+
export { calcTimeHours, clamp, lerp, round };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/math/index.ts
|
|
2
|
+
function clamp(value, min, max) {
|
|
3
|
+
return Math.min(Math.max(value, min), max);
|
|
4
|
+
}
|
|
5
|
+
function lerp(start, end, t) {
|
|
6
|
+
return start + (end - start) * t;
|
|
7
|
+
}
|
|
8
|
+
function round(value, decimals = 0) {
|
|
9
|
+
const factor = 10 ** decimals;
|
|
10
|
+
return Math.round(value * factor) / factor;
|
|
11
|
+
}
|
|
12
|
+
function calcTimeHours(start, end) {
|
|
13
|
+
const [sh, sm] = start.split(":").map(Number);
|
|
14
|
+
const [eh, em] = end.split(":").map(Number);
|
|
15
|
+
return Math.max(0, (eh * 60 + em - (sh * 60 + sm)) / 60);
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
calcTimeHours,
|
|
19
|
+
clamp,
|
|
20
|
+
lerp,
|
|
21
|
+
round
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/math/index.ts"],"sourcesContent":["/** Clamp a number between a min and max (inclusive). */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\n/** Linearly interpolate between two numbers by t (0-1). */\nexport function lerp(start: number, end: number, t: number): number {\n return start + (end - start) * t;\n}\n\n/** Round a number to a given number of decimal places. */\nexport function round(value: number, decimals = 0): number {\n const factor = 10 ** decimals;\n return Math.round(value * factor) / factor;\n}\n\n/** Returns the hours between two \"HH:MM\" time strings (0 if end <= start). */\nexport function calcTimeHours(start: string, end: string): number {\n const [sh, sm] = start.split(':').map(Number) as [number, number];\n const [eh, em] = end.split(':').map(Number) as [number, number];\n return Math.max(0, (eh * 60 + em - (sh * 60 + sm)) / 60);\n}\n"],"mappings":";AACO,SAAS,MAAM,OAAe,KAAa,KAAqB;AACrE,SAAO,KAAK,IAAI,KAAK,IAAI,OAAO,GAAG,GAAG,GAAG;AAC3C;AAGO,SAAS,KAAK,OAAe,KAAa,GAAmB;AAClE,SAAO,SAAS,MAAM,SAAS;AACjC;AAGO,SAAS,MAAM,OAAe,WAAW,GAAW;AACzD,QAAM,SAAS,MAAM;AACrB,SAAO,KAAK,MAAM,QAAQ,MAAM,IAAI;AACtC;AAGO,SAAS,cAAc,OAAe,KAAqB;AAChE,QAAM,CAAC,IAAI,EAAE,IAAI,MAAM,MAAM,GAAG,EAAE,IAAI,MAAM;AAC5C,QAAM,CAAC,IAAI,EAAE,IAAI,IAAI,MAAM,GAAG,EAAE,IAAI,MAAM;AAC1C,SAAO,KAAK,IAAI,IAAI,KAAK,KAAK,MAAM,KAAK,KAAK,OAAO,EAAE;AACzD;","names":[]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Convert a string into a URL-friendly slug. e.g. "Hello, World!" -> "hello-world" */
|
|
2
|
+
declare function slugify(input: string): string;
|
|
3
|
+
/** Truncate a string to a max length, appending a suffix if it was cut. */
|
|
4
|
+
declare function truncate(input: string, maxLength: number, suffix?: string): string;
|
|
5
|
+
/** Capitalize the first letter of a string. */
|
|
6
|
+
declare function capitalize(input: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Naive English pluralizer for simple cases. For anything locale-sensitive,
|
|
9
|
+
* prefer `Intl.PluralRules` directly.
|
|
10
|
+
* e.g. pluralize(1, 'item') -> "1 item", pluralize(2, 'item') -> "2 items"
|
|
11
|
+
*/
|
|
12
|
+
declare function pluralize(count: number, singular: string, plural?: string): string;
|
|
13
|
+
|
|
14
|
+
export { capitalize, pluralize, slugify, truncate };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/string/index.ts
|
|
2
|
+
function slugify(input) {
|
|
3
|
+
return input.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
4
|
+
}
|
|
5
|
+
function truncate(input, maxLength, suffix = "\u2026") {
|
|
6
|
+
if (input.length <= maxLength) return input;
|
|
7
|
+
return input.slice(0, Math.max(0, maxLength - suffix.length)) + suffix;
|
|
8
|
+
}
|
|
9
|
+
function capitalize(input) {
|
|
10
|
+
if (!input) return input;
|
|
11
|
+
return input[0].toUpperCase() + input.slice(1);
|
|
12
|
+
}
|
|
13
|
+
function pluralize(count, singular, plural) {
|
|
14
|
+
const word = count === 1 ? singular : plural ?? `${singular}s`;
|
|
15
|
+
return `${count} ${word}`;
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
capitalize,
|
|
19
|
+
pluralize,
|
|
20
|
+
slugify,
|
|
21
|
+
truncate
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/string/index.ts"],"sourcesContent":["/** Convert a string into a URL-friendly slug. e.g. \"Hello, World!\" -> \"hello-world\" */\nexport function slugify(input: string): string {\n return input\n .normalize('NFKD')\n .replace(/[\\u0300-\\u036f]/g, '') // strip accents\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n/** Truncate a string to a max length, appending a suffix if it was cut. */\nexport function truncate(input: string, maxLength: number, suffix = '…'): string {\n if (input.length <= maxLength) return input;\n return input.slice(0, Math.max(0, maxLength - suffix.length)) + suffix;\n}\n\n/** Capitalize the first letter of a string. */\nexport function capitalize(input: string): string {\n if (!input) return input;\n return input[0]!.toUpperCase() + input.slice(1);\n}\n\n/**\n * Naive English pluralizer for simple cases. For anything locale-sensitive,\n * prefer `Intl.PluralRules` directly.\n * e.g. pluralize(1, 'item') -> \"1 item\", pluralize(2, 'item') -> \"2 items\"\n */\nexport function pluralize(count: number, singular: string, plural?: string): string {\n const word = count === 1 ? singular : plural ?? `${singular}s`;\n return `${count} ${word}`;\n}\n"],"mappings":";AACO,SAAS,QAAQ,OAAuB;AAC7C,SAAO,MACJ,UAAU,MAAM,EAChB,QAAQ,oBAAoB,EAAE,EAC9B,YAAY,EACZ,KAAK,EACL,QAAQ,eAAe,GAAG,EAC1B,QAAQ,YAAY,EAAE;AAC3B;AAGO,SAAS,SAAS,OAAe,WAAmB,SAAS,UAAa;AAC/E,MAAI,MAAM,UAAU,UAAW,QAAO;AACtC,SAAO,MAAM,MAAM,GAAG,KAAK,IAAI,GAAG,YAAY,OAAO,MAAM,CAAC,IAAI;AAClE;AAGO,SAAS,WAAW,OAAuB;AAChD,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,MAAM,CAAC,EAAG,YAAY,IAAI,MAAM,MAAM,CAAC;AAChD;AAOO,SAAS,UAAU,OAAe,UAAkB,QAAyB;AAClF,QAAM,OAAO,UAAU,IAAI,WAAW,UAAU,GAAG,QAAQ;AAC3D,SAAO,GAAG,KAAK,IAAI,IAAI;AACzB;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mikespa/kit",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Personal toolkit of small, framework-agnostic utilities (formatting, strings, math).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"sideEffects": false,
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./format": {
|
|
20
|
+
"types": "./dist/format/index.d.ts",
|
|
21
|
+
"import": "./dist/format/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./string": {
|
|
24
|
+
"types": "./dist/string/index.d.ts",
|
|
25
|
+
"import": "./dist/string/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./math": {
|
|
28
|
+
"types": "./dist/math/index.d.ts",
|
|
29
|
+
"import": "./dist/math/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup",
|
|
34
|
+
"dev": "tsup --watch",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest",
|
|
38
|
+
"prepublishOnly": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"tsup": "^8.3.5",
|
|
42
|
+
"typescript": "^5.7.2",
|
|
43
|
+
"vitest": "^2.1.8"
|
|
44
|
+
}
|
|
45
|
+
}
|