lumen-lib 0.0.0-stage → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -2
- package/assets/bg/blue.mp4 +0 -0
- package/assets/bg/green.mp4 +0 -0
- package/assets/bg/poster.jpg +0 -0
- package/assets/bg/teal.mp4 +0 -0
- package/assets/fonts/auge-bold.otf +0 -0
- package/assets/fonts/auge-regular.otf +0 -0
- package/assets/fonts/auge-semibold.otf +0 -0
- package/assets/fonts/inter-var.ttf +0 -0
- package/assets/fonts/jetbrains-mono-var.ttf +0 -0
- package/assets/fonts/pacifico.ttf +0 -0
- package/assets/fonts/space-grotesk-var.ttf +0 -0
- package/assets/logos/lumen-backup.svg +48 -0
- package/assets/logos/lumen.svg +19 -0
- package/assets/site/backup/Pacifico-Regular-95szj24p.ttf +0 -0
- package/assets/site/backup/auge-bold-vywa1g7b.otf +0 -0
- package/assets/site/backup/auge-regular-hz0spgka.otf +0 -0
- package/assets/site/backup/auge-semibold-td2xkdwf.otf +0 -0
- package/assets/site/backup/copium-meme.png +0 -0
- package/assets/site/backup/engine/controller.api.js +1344 -0
- package/assets/site/backup/engine/controller.inject.js +511 -0
- package/assets/site/backup/engine/controller.sw.js +418 -0
- package/assets/site/backup/engine/corridor.js +13215 -0
- package/assets/site/backup/engine/corridor.wasm +0 -0
- package/assets/site/backup/site-backup.css +1 -0
- package/assets/site/backup/site-backup.js +74 -0
- package/assets/site/backup/sw.js +230 -0
- package/assets/site/index.html +112 -0
- package/assets/site/site.css +226 -0
- package/assets/site/site.js +239 -0
- package/index.cjs +324 -0
- package/index.d.ts +76 -0
- package/package.json +36 -4
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/* lumen-lib site bundle — docs demo with bundled helpers */
|
|
2
|
+
(function () {
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
/* ---- bundled helpers (mirrors index.cjs exports) ---- */
|
|
6
|
+
|
|
7
|
+
function cn() {
|
|
8
|
+
var out = [];
|
|
9
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
10
|
+
var c = arguments[i];
|
|
11
|
+
if (c) out.push(c);
|
|
12
|
+
}
|
|
13
|
+
return out.join(" ");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatBytes(bytes, decimals) {
|
|
17
|
+
decimals = decimals === undefined ? 1 : decimals;
|
|
18
|
+
var n = Number(bytes);
|
|
19
|
+
if (!isFinite(n) || n < 0) return "0 B";
|
|
20
|
+
if (n === 0) return "0 B";
|
|
21
|
+
var units = ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
22
|
+
var i = Math.min(Math.floor(Math.log(n) / Math.log(1024)), units.length - 1);
|
|
23
|
+
var v = n / Math.pow(1024, i);
|
|
24
|
+
return (v >= 100 || i === 0 ? Math.round(v) : v.toFixed(decimals)) + " " + units[i];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatCompact(n) {
|
|
28
|
+
var v = Number(n);
|
|
29
|
+
if (!isFinite(v)) return String(n == null ? "" : n);
|
|
30
|
+
var abs = Math.abs(v);
|
|
31
|
+
var units = [[1e12, "T"], [1e9, "B"], [1e6, "M"], [1e3, "K"]];
|
|
32
|
+
for (var i = 0; i < units.length; i++) {
|
|
33
|
+
if (abs >= units[i][0]) {
|
|
34
|
+
var r = v / units[i][0];
|
|
35
|
+
return (r >= 100 ? Math.round(r) : r.toFixed(1).replace(/\.0$/, "")) + units[i][1];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return String(v);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function timeAgo(date, now) {
|
|
42
|
+
now = now === undefined ? Date.now() : now;
|
|
43
|
+
var then = date instanceof Date ? date.getTime() : new Date(date).getTime();
|
|
44
|
+
if (!isFinite(then)) return "";
|
|
45
|
+
var diff = Math.max(0, now - then);
|
|
46
|
+
var units = [
|
|
47
|
+
["year", 31536000000], ["month", 2592000000], ["week", 604800000],
|
|
48
|
+
["day", 86400000], ["hour", 3600000], ["minute", 60000], ["second", 1000],
|
|
49
|
+
];
|
|
50
|
+
for (var i = 0; i < units.length; i++) {
|
|
51
|
+
var v = Math.floor(diff / units[i][1]);
|
|
52
|
+
if (v >= 1) return v + " " + units[i][0] + (v === 1 ? "" : "s") + " ago";
|
|
53
|
+
}
|
|
54
|
+
return "just now";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function debounce(fn, ms) {
|
|
58
|
+
ms = ms === undefined ? 200 : ms;
|
|
59
|
+
var t = null;
|
|
60
|
+
var d = function () {
|
|
61
|
+
var args = arguments, self = this;
|
|
62
|
+
clearTimeout(t);
|
|
63
|
+
t = setTimeout(function () { fn.apply(self, args); }, ms);
|
|
64
|
+
};
|
|
65
|
+
d.cancel = function () { clearTimeout(t); };
|
|
66
|
+
return d;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
window.lumen-lib = {
|
|
70
|
+
cn: cn,
|
|
71
|
+
formatBytes: formatBytes,
|
|
72
|
+
formatCompact: formatCompact,
|
|
73
|
+
timeAgo: timeAgo,
|
|
74
|
+
debounce: debounce,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/* ---- api reference table ---- */
|
|
78
|
+
|
|
79
|
+
var API = [
|
|
80
|
+
["cn", "cn(...classes)", "Join class names, ignoring falsy values.", "classnames"],
|
|
81
|
+
["capitalize", "capitalize(s)", "Uppercase the first character.", "strings"],
|
|
82
|
+
["kebabCase", "kebabCase(s)", "Convert to kebab-case.", "strings"],
|
|
83
|
+
["camelCase", "camelCase(s)", "Convert to camelCase.", "strings"],
|
|
84
|
+
["truncate", "truncate(s, max, ellipsis)", "Cut a string to length with an ellipsis.", "strings"],
|
|
85
|
+
["stripHtml", "stripHtml(s)", "Remove all HTML tags.", "strings"],
|
|
86
|
+
["formatBytes", "formatBytes(bytes, decimals)", "Human-readable byte sizes: 1.5 MB.", "formatting"],
|
|
87
|
+
["formatNumber", "formatNumber(n, locale)", "Locale-aware number grouping.", "formatting"],
|
|
88
|
+
["formatCompact", "formatCompact(n)", "Compact notation: 1.2K, 3.4M.", "formatting"],
|
|
89
|
+
["timeAgo", "timeAgo(date, now)", "Relative time: \"3 hours ago\".", "formatting"],
|
|
90
|
+
["clamp", "clamp(n, min, max)", "Constrain a number to a range.", "math"],
|
|
91
|
+
["lerp", "lerp(a, b, t)", "Linear interpolation between two numbers.", "math"],
|
|
92
|
+
["randomInt", "randomInt(min, max)", "Random integer in an inclusive range.", "math"],
|
|
93
|
+
["mapRange", "mapRange(n, inMin, inMax, outMin, outMax)", "Remap a value between ranges.", "math"],
|
|
94
|
+
["deepMerge", "deepMerge(target, ...sources)", "Recursive merge for plain objects.", "objects"],
|
|
95
|
+
["omit", "omit(obj, keys)", "Copy an object minus the given keys.", "objects"],
|
|
96
|
+
["pick", "pick(obj, keys)", "Copy an object with only the given keys.", "objects"],
|
|
97
|
+
["chunk", "chunk(arr, size)", "Split an array into fixed-size chunks.", "arrays"],
|
|
98
|
+
["unique", "unique(arr)", "Deduplicate an array.", "arrays"],
|
|
99
|
+
["groupBy", "groupBy(arr, fn)", "Group items by a computed key.", "arrays"],
|
|
100
|
+
["debounce", "debounce(fn, ms)", "Invoke after a quiet period. Cancelable.", "functions"],
|
|
101
|
+
["throttle", "throttle(fn, ms)", "Invoke at most once per interval.", "functions"],
|
|
102
|
+
["once", "once(fn)", "Only the first call runs.", "functions"],
|
|
103
|
+
["sleep", "sleep(ms)", "Promise-based delay.", "functions"],
|
|
104
|
+
["retry", "retry(fn, tries, delay)", "Retry an async fn with backoff.", "functions"],
|
|
105
|
+
["isEmail", "isEmail(s)", "Loose email format check.", "validation"],
|
|
106
|
+
["isUrl", "isUrl(s)", "Loose URL / domain check.", "validation"],
|
|
107
|
+
["isNonEmptyString", "isNonEmptyString(v)", "Type guard for non-empty strings.", "validation"],
|
|
108
|
+
["uid", "uid(length)", "Random id, crypto-backed when available.", "ids"],
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
var GROUP_LABEL = {
|
|
112
|
+
classnames: "classnames",
|
|
113
|
+
strings: "strings",
|
|
114
|
+
formatting: "formatting",
|
|
115
|
+
math: "math",
|
|
116
|
+
objects: "objects & arrays",
|
|
117
|
+
arrays: "objects & arrays",
|
|
118
|
+
functions: "functions",
|
|
119
|
+
validation: "validation",
|
|
120
|
+
ids: "ids",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
function esc(s) {
|
|
124
|
+
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function mount() {
|
|
128
|
+
/* api table */
|
|
129
|
+
var tbody = document.getElementById("api-rows");
|
|
130
|
+
if (tbody) {
|
|
131
|
+
var lastGroup = null;
|
|
132
|
+
API.forEach(function (row) {
|
|
133
|
+
if (GROUP_LABEL[row[3]] !== lastGroup) {
|
|
134
|
+
lastGroup = GROUP_LABEL[row[3]];
|
|
135
|
+
var h = document.createElement("tr");
|
|
136
|
+
var hc = document.createElement("td");
|
|
137
|
+
hc.colSpan = 3;
|
|
138
|
+
hc.className = "group";
|
|
139
|
+
hc.textContent = lastGroup;
|
|
140
|
+
h.appendChild(hc);
|
|
141
|
+
tbody.appendChild(h);
|
|
142
|
+
}
|
|
143
|
+
var tr = document.createElement("tr");
|
|
144
|
+
tr.innerHTML =
|
|
145
|
+
'<td class="name">' + esc(row[0]) + "</td>" +
|
|
146
|
+
'<td class="sig">' + esc(row[1]) + "</td>" +
|
|
147
|
+
'<td class="what">' + esc(row[2]) + "</td>";
|
|
148
|
+
tbody.appendChild(tr);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* demos */
|
|
153
|
+
var set = function (id, text) {
|
|
154
|
+
var el = document.getElementById(id);
|
|
155
|
+
if (el) el.textContent = text;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
set(
|
|
159
|
+
"demo-cn",
|
|
160
|
+
'cn("btn", isActive && "btn--active", className)\n' +
|
|
161
|
+
"=> \"" +
|
|
162
|
+
cn("btn", true && "btn--active", "ml-2") +
|
|
163
|
+
'"'
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
set(
|
|
167
|
+
"demo-fmt",
|
|
168
|
+
"formatBytes(1536) => " + formatBytes(1536) + "\n" +
|
|
169
|
+
"formatBytes(248291048) => " + formatBytes(248291048) + "\n" +
|
|
170
|
+
"formatCompact(3400000) => " + formatCompact(3400000)
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
var now = Date.now();
|
|
174
|
+
set(
|
|
175
|
+
"demo-time",
|
|
176
|
+
"timeAgo(now - 45s) => " + timeAgo(now - 45000, now) + "\n" +
|
|
177
|
+
"timeAgo(now - 3h) => " + timeAgo(now - 3 * 3600000, now) + "\n" +
|
|
178
|
+
"timeAgo(now - 2d) => " + timeAgo(now - 2 * 86400000, now)
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
var calls = 0;
|
|
182
|
+
var debounced = debounce(function () { calls++; }, 300);
|
|
183
|
+
debounced(); debounced(); debounced();
|
|
184
|
+
set(
|
|
185
|
+
"demo-deb",
|
|
186
|
+
"debounced() x3 in one tick\n" +
|
|
187
|
+
"=> fn fires once after 300ms quiet\n" +
|
|
188
|
+
"calls so far: " + calls
|
|
189
|
+
);
|
|
190
|
+
setTimeout(function () {
|
|
191
|
+
var el = document.getElementById("demo-deb");
|
|
192
|
+
if (el)
|
|
193
|
+
el.textContent =
|
|
194
|
+
"debounced() x3 in one tick\n" +
|
|
195
|
+
"=> fn fired exactly once\n" +
|
|
196
|
+
"calls: " + calls;
|
|
197
|
+
}, 450);
|
|
198
|
+
|
|
199
|
+
/* copy install button */
|
|
200
|
+
var btn = document.getElementById("copy-install");
|
|
201
|
+
if (btn) {
|
|
202
|
+
btn.addEventListener("click", function () {
|
|
203
|
+
var text = "npm install lumen-lib";
|
|
204
|
+
var done = function () {
|
|
205
|
+
btn.textContent = "copied";
|
|
206
|
+
btn.classList.add("done");
|
|
207
|
+
setTimeout(function () {
|
|
208
|
+
btn.textContent = "copy";
|
|
209
|
+
btn.classList.remove("done");
|
|
210
|
+
}, 1400);
|
|
211
|
+
};
|
|
212
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
213
|
+
navigator.clipboard.writeText(text).then(done, fallback);
|
|
214
|
+
} else {
|
|
215
|
+
fallback();
|
|
216
|
+
}
|
|
217
|
+
function fallback() {
|
|
218
|
+
var ta = document.createElement("textarea");
|
|
219
|
+
ta.value = text;
|
|
220
|
+
ta.style.position = "fixed";
|
|
221
|
+
ta.style.opacity = "0";
|
|
222
|
+
document.body.appendChild(ta);
|
|
223
|
+
ta.select();
|
|
224
|
+
try {
|
|
225
|
+
document.execCommand("copy");
|
|
226
|
+
} catch (e) {}
|
|
227
|
+
document.body.removeChild(ta);
|
|
228
|
+
done();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (document.readyState === "loading") {
|
|
235
|
+
document.addEventListener("DOMContentLoaded", mount);
|
|
236
|
+
} else {
|
|
237
|
+
mount();
|
|
238
|
+
}
|
|
239
|
+
})();
|
package/index.cjs
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lumen-lib — tiny, dependency-free utilities for modern web apps.
|
|
5
|
+
*
|
|
6
|
+
* Everything is exported as a named ESM export. No runtime dependencies,
|
|
7
|
+
* no polyfills, works in browsers and Node >= 14.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/* ------------------------------------------------------------------ */
|
|
11
|
+
/* classnames */
|
|
12
|
+
/* ------------------------------------------------------------------ */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Join class names, ignoring falsy values.
|
|
16
|
+
* @param {...(string | false | null | undefined)} classes
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
function cn(...classes) {
|
|
20
|
+
return classes.filter(Boolean).join(" ");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* ------------------------------------------------------------------ */
|
|
24
|
+
/* strings */
|
|
25
|
+
/* ------------------------------------------------------------------ */
|
|
26
|
+
|
|
27
|
+
/** Capitalize the first character of a string. */
|
|
28
|
+
function capitalize(s) {
|
|
29
|
+
const str = String(s ?? "");
|
|
30
|
+
return str ? str[0].toUpperCase() + str.slice(1) : "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Convert a string to kebab-case. */
|
|
34
|
+
function kebabCase(s) {
|
|
35
|
+
return String(s ?? "")
|
|
36
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
37
|
+
.replace(/[\s_]+/g, "-")
|
|
38
|
+
.replace(/[^a-zA-Z0-9-]/g, "")
|
|
39
|
+
.toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Convert a string to camelCase. */
|
|
43
|
+
function camelCase(s) {
|
|
44
|
+
return String(s ?? "")
|
|
45
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ""))
|
|
46
|
+
.replace(/^[A-Z]/, (c) => c.toLowerCase());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Truncate a string to `max` chars, appending an ellipsis when cut. */
|
|
50
|
+
function truncate(s, max = 80, ellipsis = "…") {
|
|
51
|
+
const str = String(s ?? "");
|
|
52
|
+
if (str.length <= max) return str;
|
|
53
|
+
return str.slice(0, Math.max(0, max - ellipsis.length)) + ellipsis;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Strip all HTML tags from a string. */
|
|
57
|
+
function stripHtml(s) {
|
|
58
|
+
return String(s ?? "").replace(/<[^>]*>/g, "");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ------------------------------------------------------------------ */
|
|
62
|
+
/* formatting */
|
|
63
|
+
/* ------------------------------------------------------------------ */
|
|
64
|
+
|
|
65
|
+
const BYTE_UNITS = ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
66
|
+
|
|
67
|
+
/** Format a byte count as a human-readable string (e.g. "1.5 MB"). */
|
|
68
|
+
function formatBytes(bytes, decimals = 1) {
|
|
69
|
+
const n = Number(bytes);
|
|
70
|
+
if (!Number.isFinite(n) || n < 0) return "0 B";
|
|
71
|
+
if (n === 0) return "0 B";
|
|
72
|
+
const i = Math.min(Math.floor(Math.log(n) / Math.log(1024)), BYTE_UNITS.length - 1);
|
|
73
|
+
const v = n / 1024 ** i;
|
|
74
|
+
return `${v >= 100 || i === 0 ? Math.round(v) : v.toFixed(decimals)} ${BYTE_UNITS[i]}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Format a number with locale grouping (e.g. 1,234,567). */
|
|
78
|
+
function formatNumber(n, locale = "en-US") {
|
|
79
|
+
const v = Number(n);
|
|
80
|
+
if (!Number.isFinite(v)) return String(n ?? "");
|
|
81
|
+
return v.toLocaleString(locale);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Compact number format: 1200 -> "1.2K", 3400000 -> "3.4M". */
|
|
85
|
+
function formatCompact(n) {
|
|
86
|
+
const v = Number(n);
|
|
87
|
+
if (!Number.isFinite(v)) return String(n ?? "");
|
|
88
|
+
const abs = Math.abs(v);
|
|
89
|
+
const units = [
|
|
90
|
+
[1e12, "T"],
|
|
91
|
+
[1e9, "B"],
|
|
92
|
+
[1e6, "M"],
|
|
93
|
+
[1e3, "K"],
|
|
94
|
+
];
|
|
95
|
+
for (const [size, suffix] of units) {
|
|
96
|
+
if (abs >= size) {
|
|
97
|
+
const r = v / size;
|
|
98
|
+
return `${r >= 100 ? Math.round(r) : r.toFixed(1).replace(/\.0$/, "")}${suffix}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return String(v);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Relative time: timeAgo(date) -> "3 minutes ago". */
|
|
105
|
+
function timeAgo(date, now = Date.now()) {
|
|
106
|
+
const then = date instanceof Date ? date.getTime() : new Date(date).getTime();
|
|
107
|
+
if (!Number.isFinite(then)) return "";
|
|
108
|
+
const diff = Math.max(0, now - then);
|
|
109
|
+
const units = [
|
|
110
|
+
["year", 31536000000],
|
|
111
|
+
["month", 2592000000],
|
|
112
|
+
["week", 604800000],
|
|
113
|
+
["day", 86400000],
|
|
114
|
+
["hour", 3600000],
|
|
115
|
+
["minute", 60000],
|
|
116
|
+
["second", 1000],
|
|
117
|
+
];
|
|
118
|
+
for (const [name, ms] of units) {
|
|
119
|
+
const v = Math.floor(diff / ms);
|
|
120
|
+
if (v >= 1) return `${v} ${name}${v === 1 ? "" : "s"} ago`;
|
|
121
|
+
}
|
|
122
|
+
return "just now";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* ------------------------------------------------------------------ */
|
|
126
|
+
/* math */
|
|
127
|
+
/* ------------------------------------------------------------------ */
|
|
128
|
+
|
|
129
|
+
/** Clamp `n` between `min` and `max`. */
|
|
130
|
+
function clamp(n, min, max) {
|
|
131
|
+
return Math.min(Math.max(n, min), max);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Linear interpolation between `a` and `b` by `t` (0..1). */
|
|
135
|
+
function lerp(a, b, t) {
|
|
136
|
+
return a + (b - a) * t;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Random integer in [min, max] inclusive. */
|
|
140
|
+
function randomInt(min = 0, max = 1) {
|
|
141
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Map `n` from one range to another, optionally clamped. */
|
|
145
|
+
function mapRange(n, inMin, inMax, outMin, outMax, clampResult = true) {
|
|
146
|
+
const t = (n - inMin) / (inMax - inMin);
|
|
147
|
+
const out = outMin + t * (outMax - outMin);
|
|
148
|
+
return clampResult ? clamp(out, Math.min(outMin, outMax), Math.max(outMin, outMax)) : out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* ------------------------------------------------------------------ */
|
|
152
|
+
/* objects & arrays */
|
|
153
|
+
/* ------------------------------------------------------------------ */
|
|
154
|
+
|
|
155
|
+
/** Deep-merge plain objects; later sources win. Arrays are replaced. */
|
|
156
|
+
function deepMerge(target, ...sources) {
|
|
157
|
+
const out = { ...(target && typeof target === "object" ? target : {}) };
|
|
158
|
+
for (const src of sources) {
|
|
159
|
+
if (!src || typeof src !== "object") continue;
|
|
160
|
+
for (const [k, v] of Object.entries(src)) {
|
|
161
|
+
const prev = out[k];
|
|
162
|
+
out[k] =
|
|
163
|
+
prev && v && typeof prev === "object" && typeof v === "object" &&
|
|
164
|
+
!Array.isArray(prev) && !Array.isArray(v)
|
|
165
|
+
? deepMerge(prev, v)
|
|
166
|
+
: v;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Return a copy of `obj` without the listed keys. */
|
|
173
|
+
function omit(obj, keys) {
|
|
174
|
+
const drop = new Set(Array.isArray(keys) ? keys : [keys]);
|
|
175
|
+
const out = {};
|
|
176
|
+
for (const [k, v] of Object.entries(obj ?? {})) if (!drop.has(k)) out[k] = v;
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Return a new object with only the listed keys. */
|
|
181
|
+
function pick(obj, keys) {
|
|
182
|
+
const want = new Set(Array.isArray(keys) ? keys : [keys]);
|
|
183
|
+
const out = {};
|
|
184
|
+
for (const k of want) if (obj && k in obj) out[k] = obj[k];
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Split an array into chunks of `size`. */
|
|
189
|
+
function chunk(arr, size = 1) {
|
|
190
|
+
const list = Array.isArray(arr) ? arr : [];
|
|
191
|
+
const n = Math.max(1, Math.floor(size));
|
|
192
|
+
const out = [];
|
|
193
|
+
for (let i = 0; i < list.length; i += n) out.push(list.slice(i, i + n));
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Deduplicate an array (primitive-safe via SameValueZero). */
|
|
198
|
+
function unique(arr) {
|
|
199
|
+
return [...new Set(Array.isArray(arr) ? arr : [])];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Group array items by the key returned from `fn`. */
|
|
203
|
+
function groupBy(arr, fn) {
|
|
204
|
+
const out = {};
|
|
205
|
+
for (const item of Array.isArray(arr) ? arr : []) {
|
|
206
|
+
const k = fn(item);
|
|
207
|
+
(out[k] ??= []).push(item);
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* ------------------------------------------------------------------ */
|
|
213
|
+
/* functions */
|
|
214
|
+
/* ------------------------------------------------------------------ */
|
|
215
|
+
|
|
216
|
+
/** Debounce: call `fn` after `ms` of quiet. Returns a cancelable fn. */
|
|
217
|
+
function debounce(fn, ms = 200) {
|
|
218
|
+
let t = null;
|
|
219
|
+
const debounced = (...args) => {
|
|
220
|
+
clearTimeout(t);
|
|
221
|
+
t = setTimeout(() => fn(...args), ms);
|
|
222
|
+
};
|
|
223
|
+
debounced.cancel = () => clearTimeout(t);
|
|
224
|
+
return debounced;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Throttle: call `fn` at most once per `ms`. Trailing edge by default. */
|
|
228
|
+
function throttle(fn, ms = 200) {
|
|
229
|
+
let last = 0;
|
|
230
|
+
let timer = null;
|
|
231
|
+
return (...args) => {
|
|
232
|
+
const now = Date.now();
|
|
233
|
+
const run = () => {
|
|
234
|
+
last = Date.now();
|
|
235
|
+
fn(...args);
|
|
236
|
+
};
|
|
237
|
+
if (now - last >= ms) run();
|
|
238
|
+
else if (!timer) timer = setTimeout(() => { timer = null; run(); }, ms - (now - last));
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Wrap `fn` so it only executes once; later calls return the first result. */
|
|
243
|
+
function once(fn) {
|
|
244
|
+
let called = false;
|
|
245
|
+
let result;
|
|
246
|
+
return (...args) => {
|
|
247
|
+
if (!called) {
|
|
248
|
+
called = true;
|
|
249
|
+
result = fn(...args);
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Resolve after `ms` milliseconds. */
|
|
256
|
+
function sleep(ms) {
|
|
257
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Retry an async `fn` up to `tries` times with linear backoff. */
|
|
261
|
+
async function retry(fn, tries = 3, delay = 250) {
|
|
262
|
+
let lastErr;
|
|
263
|
+
for (let i = 0; i < tries; i++) {
|
|
264
|
+
try {
|
|
265
|
+
return await fn(i);
|
|
266
|
+
} catch (e) {
|
|
267
|
+
lastErr = e;
|
|
268
|
+
if (i < tries - 1) await sleep(delay * (i + 1));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
throw lastErr;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* ------------------------------------------------------------------ */
|
|
275
|
+
/* validation */
|
|
276
|
+
/* ------------------------------------------------------------------ */
|
|
277
|
+
|
|
278
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
279
|
+
|
|
280
|
+
/** Loose email format check. */
|
|
281
|
+
function isEmail(s) {
|
|
282
|
+
return EMAIL_RE.test(String(s ?? ""));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Loose URL check: accepts http(s) URLs or bare domains. */
|
|
286
|
+
function isUrl(s) {
|
|
287
|
+
const str = String(s ?? "").trim();
|
|
288
|
+
if (!str) return false;
|
|
289
|
+
try {
|
|
290
|
+
const u = new URL(str.includes("://") ? str : `https://${str}`);
|
|
291
|
+
return !!u.hostname && u.hostname.includes(".");
|
|
292
|
+
} catch {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** True for non-empty strings. */
|
|
298
|
+
function isNonEmptyString(v) {
|
|
299
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/* ------------------------------------------------------------------ */
|
|
303
|
+
/* ids */
|
|
304
|
+
/* ------------------------------------------------------------------ */
|
|
305
|
+
|
|
306
|
+
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
307
|
+
|
|
308
|
+
/** Random id: uid(8) -> "k3j9x2ab". Prefers crypto.randomUUID when available. */
|
|
309
|
+
function uid(length = 8) {
|
|
310
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID && length === 36)
|
|
311
|
+
return crypto.randomUUID();
|
|
312
|
+
let out = "";
|
|
313
|
+
const bytes = new Uint8Array(length);
|
|
314
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
315
|
+
crypto.getRandomValues(bytes);
|
|
316
|
+
for (const b of bytes) out += ID_ALPHABET[b % ID_ALPHABET.length];
|
|
317
|
+
} else {
|
|
318
|
+
for (let i = 0; i < length; i++) out += ID_ALPHABET[randomInt(0, ID_ALPHABET.length - 1)];
|
|
319
|
+
}
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
module.exports = { cn, capitalize, kebabCase, camelCase, truncate, stripHtml, formatBytes, formatNumber, formatCompact, timeAgo, clamp, lerp, randomInt, mapRange, deepMerge, omit, pick, chunk, unique, groupBy, debounce, throttle, once, sleep, retry, isEmail, isUrl, isNonEmptyString, uid };
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export declare function cn(
|
|
2
|
+
...classes: Array<string | false | null | undefined>
|
|
3
|
+
): string;
|
|
4
|
+
|
|
5
|
+
export declare function capitalize(s: unknown): string;
|
|
6
|
+
export declare function kebabCase(s: unknown): string;
|
|
7
|
+
export declare function camelCase(s: unknown): string;
|
|
8
|
+
export declare function truncate(
|
|
9
|
+
s: unknown,
|
|
10
|
+
max?: number,
|
|
11
|
+
ellipsis?: string
|
|
12
|
+
): string;
|
|
13
|
+
export declare function stripHtml(s: unknown): string;
|
|
14
|
+
|
|
15
|
+
export declare function formatBytes(bytes: unknown, decimals?: number): string;
|
|
16
|
+
export declare function formatNumber(n: unknown, locale?: string): string;
|
|
17
|
+
export declare function formatCompact(n: unknown): string;
|
|
18
|
+
export declare function timeAgo(
|
|
19
|
+
date: Date | string | number,
|
|
20
|
+
now?: number
|
|
21
|
+
): string;
|
|
22
|
+
|
|
23
|
+
export declare function clamp(n: number, min: number, max: number): number;
|
|
24
|
+
export declare function lerp(a: number, b: number, t: number): number;
|
|
25
|
+
export declare function randomInt(min?: number, max?: number): number;
|
|
26
|
+
export declare function mapRange(
|
|
27
|
+
n: number,
|
|
28
|
+
inMin: number,
|
|
29
|
+
inMax: number,
|
|
30
|
+
outMin: number,
|
|
31
|
+
outMax: number,
|
|
32
|
+
clampResult?: boolean
|
|
33
|
+
): number;
|
|
34
|
+
|
|
35
|
+
export declare function deepMerge<T extends object>(
|
|
36
|
+
target: T,
|
|
37
|
+
...sources: object[]
|
|
38
|
+
): T;
|
|
39
|
+
export declare function omit<T extends object, K extends keyof T>(
|
|
40
|
+
obj: T,
|
|
41
|
+
keys: K | K[]
|
|
42
|
+
): Partial<T>;
|
|
43
|
+
export declare function pick<T extends object, K extends keyof T>(
|
|
44
|
+
obj: T,
|
|
45
|
+
keys: K | K[]
|
|
46
|
+
): Partial<T>;
|
|
47
|
+
export declare function chunk<T>(arr: T[], size?: number): T[][];
|
|
48
|
+
export declare function unique<T>(arr: T[]): T[];
|
|
49
|
+
export declare function groupBy<T, K extends string | number>(
|
|
50
|
+
arr: T[],
|
|
51
|
+
fn: (item: T) => K
|
|
52
|
+
): Record<K, T[]>;
|
|
53
|
+
|
|
54
|
+
export declare function debounce<T extends (...args: never[]) => void>(
|
|
55
|
+
fn: T,
|
|
56
|
+
ms?: number
|
|
57
|
+
): T & { cancel: () => void };
|
|
58
|
+
export declare function throttle<T extends (...args: never[]) => void>(
|
|
59
|
+
fn: T,
|
|
60
|
+
ms?: number
|
|
61
|
+
): T;
|
|
62
|
+
export declare function once<T extends (...args: never[]) => unknown>(
|
|
63
|
+
fn: T
|
|
64
|
+
): T;
|
|
65
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
66
|
+
export declare function retry<T>(
|
|
67
|
+
fn: (attempt: number) => Promise<T>,
|
|
68
|
+
tries?: number,
|
|
69
|
+
delay?: number
|
|
70
|
+
): Promise<T>;
|
|
71
|
+
|
|
72
|
+
export declare function isEmail(s: unknown): boolean;
|
|
73
|
+
export declare function isUrl(s: unknown): boolean;
|
|
74
|
+
export declare function isNonEmptyString(v: unknown): v is string;
|
|
75
|
+
|
|
76
|
+
export declare function uid(length?: number): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lumen-lib",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Small, dependency-free utilities for modern web apps \u2014 classnames, strings, formatting, math, objects, and async helpers.",
|
|
5
|
+
"main": "index.cjs",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./index.d.ts",
|
|
10
|
+
"import": "./index.cjs",
|
|
11
|
+
"require": "./index.cjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"index.cjs",
|
|
16
|
+
"index.d.ts",
|
|
17
|
+
"assets"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"utility",
|
|
21
|
+
"utilities",
|
|
22
|
+
"helpers",
|
|
23
|
+
"classname",
|
|
24
|
+
"cn",
|
|
25
|
+
"debounce",
|
|
26
|
+
"formatting",
|
|
27
|
+
"zero-dependency"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=14"
|
|
32
|
+
},
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"react": "^19.3.1",
|
|
36
|
+
"react-dom": "^19.3.1"
|
|
37
|
+
}
|
|
38
|
+
}
|