webspresso 0.0.110 → 0.0.111

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.
@@ -5,6 +5,22 @@
5
5
  * Zero-dependency, lightweight helpers to avoid duplicate logic across client modules.
6
6
  */
7
7
 
8
+ /**
9
+ * Check if a value is an Array.
10
+ * @param {unknown} val
11
+ * @returns {val is any[]}
12
+ */
13
+ const isArray = Array.isArray;
14
+
15
+ /**
16
+ * Check if a value is a finite number (`typeof val === 'number' && Number.isFinite(val)`).
17
+ * @param {unknown} val
18
+ * @returns {val is number}
19
+ */
20
+ function isNumber(val) {
21
+ return typeof val === 'number' && Number.isFinite(val);
22
+ }
23
+
8
24
  /**
9
25
  * Check if a value is a non-null object.
10
26
  * @param {unknown} val
@@ -20,7 +36,7 @@ function isObject(val) {
20
36
  * @returns {boolean}
21
37
  */
22
38
  function isPlainObject(val) {
23
- return isObject(val) && !Array.isArray(val) && Object.prototype.toString.call(val) === '[object Object]';
39
+ return isObject(val) && !isArray(val) && Object.prototype.toString.call(val) === '[object Object]';
24
40
  }
25
41
 
26
42
  /**
@@ -169,7 +185,7 @@ function parseDataValue(val) {
169
185
  if (val === 'undefined') return undefined;
170
186
  if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(val)) {
171
187
  const num = Number(val);
172
- if (!Number.isNaN(num)) return num;
188
+ if (isNumber(num)) return num;
173
189
  }
174
190
  if ((val.startsWith('{') && val.endsWith('}')) || (val.startsWith('[') && val.endsWith(']'))) {
175
191
  try {
@@ -265,7 +281,172 @@ function decorateElement(el) {
265
281
  return el;
266
282
  }
267
283
 
284
+ /**
285
+ * Parse a duration value (number in ms, numeric string, or human duration string like '500ms', '10s', '2m', '1h')
286
+ * into milliseconds. Returns `defaultMs` (0) when false/null/undefined/invalid.
287
+ *
288
+ * @param {unknown} val
289
+ * @param {number} [defaultMs=0]
290
+ * @returns {number}
291
+ */
292
+ function parseDurationMs(val, defaultMs = 0) {
293
+ if (val === false || val === null || val === undefined || val === '' || val === 'false') {
294
+ return 0;
295
+ }
296
+ if (isNumber(val)) {
297
+ return val > 0 ? Math.floor(val) : 0;
298
+ }
299
+ if (typeof val === 'string') {
300
+ const trimmed = val.trim().toLowerCase();
301
+ if (!trimmed || trimmed === 'false' || trimmed === '0') return 0;
302
+ const match = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h)?$/.exec(trimmed);
303
+ if (!match) return defaultMs;
304
+ const num = parseFloat(match[1]);
305
+ if (!isNumber(num) || num <= 0) return 0;
306
+ const unit = match[2] || 'ms';
307
+ if (unit === 's') return Math.floor(num * 1000);
308
+ if (unit === 'm') return Math.floor(num * 60 * 1000);
309
+ if (unit === 'h') return Math.floor(num * 60 * 60 * 1000);
310
+ return Math.floor(num);
311
+ }
312
+ return defaultMs;
313
+ }
314
+
315
+ /**
316
+ * Normalize a React Query-style refetch policy (`true`, `false`, `'always'`, `'true'`, `'false'`).
317
+ *
318
+ * @param {unknown} val
319
+ * @param {boolean|'always'} [defaultMode=false]
320
+ * @returns {boolean|'always'}
321
+ */
322
+ function parseRefetchMode(val, defaultMode = false) {
323
+ if (val === undefined || val === null || val === '') {
324
+ return defaultMode;
325
+ }
326
+ if (val === 'always') return 'always';
327
+ if (val === true || val === 'true' || val === '1') return true;
328
+ if (val === false || val === 'false' || val === '0') return false;
329
+ return Boolean(val);
330
+ }
331
+
332
+ /**
333
+ * Generate a short base-36 random token (`Math.random().toString(36).slice(2, 2 + len)`).
334
+ *
335
+ * @param {number} [len=5]
336
+ * @returns {string}
337
+ */
338
+ function randomId(len = 5) {
339
+ return Math.random().toString(36).slice(2, 2 + len);
340
+ }
341
+
342
+ /**
343
+ * Generate a timestamped unique ID with an optional prefix (e.g. `uid('evt')` -> `'evt_1730000000_a1b2c'`).
344
+ *
345
+ * @param {string} [prefix='id']
346
+ * @param {number} [len=5]
347
+ * @returns {string}
348
+ */
349
+ function uid(prefix = 'id', len = 5) {
350
+ return `${prefix}_${Date.now()}_${randomId(len)}`;
351
+ }
352
+
353
+ /**
354
+ * Apply or restore automatic busy/loading state (`data-w-busy`, `aria-busy`, `disabled`,
355
+ * `data-w-loading-text` / `data-w-disable-with`, and `data-w-loading-class`) on an interactive element.
356
+ *
357
+ * @param {Element|null|undefined} el
358
+ * @param {boolean} isBusy
359
+ */
360
+ function setElementBusyState(el, isBusy) {
361
+ if (!el || typeof el.setAttribute !== 'function') return;
362
+
363
+ if (isBusy) {
364
+ el.__wBusyCount = (el.__wBusyCount || 0) + 1;
365
+ if (el.__wBusyCount > 1) return;
366
+
367
+ el.__wActionBusy = true;
368
+ el.setAttribute('data-w-busy', 'true');
369
+ el.setAttribute('aria-busy', 'true');
370
+
371
+ const tag = el.tagName || '';
372
+ const isInteractive =
373
+ tag === 'BUTTON' ||
374
+ tag === 'INPUT' ||
375
+ tag === 'SELECT' ||
376
+ tag === 'TEXTAREA' ||
377
+ 'disabled' in el ||
378
+ el.hasAttribute('data-w-loading-text') ||
379
+ el.hasAttribute('data-w-disable-with');
380
+
381
+ if (isInteractive && !el.hasAttribute('data-w-disabled') && !el.hasAttribute('data-w-no-disable')) {
382
+ el.__wWasDisabled = Boolean(el.disabled);
383
+ el.__wHadDisabledAttr = el.hasAttribute('disabled');
384
+ el.disabled = true;
385
+ el.setAttribute('disabled', '');
386
+ el.setAttribute('aria-disabled', 'true');
387
+ }
388
+
389
+ const loadingText = el.getAttribute('data-w-loading-text') ?? el.getAttribute('data-w-disable-with');
390
+ if (loadingText !== null) {
391
+ if (tag === 'INPUT' && (el.type === 'submit' || el.type === 'button')) {
392
+ el.__wOrigValue = el.value;
393
+ el.value = loadingText;
394
+ } else {
395
+ el.__wOrigText = el.textContent;
396
+ el.textContent = loadingText;
397
+ }
398
+ }
399
+
400
+ const loadingClass = safeTrim(el.getAttribute('data-w-loading-class'));
401
+ if (loadingClass && el.classList) {
402
+ const classes = loadingClass.split(/\s+/).filter(Boolean);
403
+ el.__wAddedClasses = classes;
404
+ for (let i = 0; i < classes.length; i++) {
405
+ el.classList.add(classes[i]);
406
+ }
407
+ }
408
+ } else {
409
+ if (!el.__wBusyCount) return;
410
+ el.__wBusyCount -= 1;
411
+ if (el.__wBusyCount > 0) return;
412
+
413
+ el.__wActionBusy = false;
414
+ el.removeAttribute('data-w-busy');
415
+ el.removeAttribute('aria-busy');
416
+
417
+ if (!el.hasAttribute('data-w-disabled') && !el.hasAttribute('data-w-no-disable')) {
418
+ if (!el.__wWasDisabled) {
419
+ el.disabled = false;
420
+ if (!el.__wHadDisabledAttr) {
421
+ el.removeAttribute('disabled');
422
+ }
423
+ el.removeAttribute('aria-disabled');
424
+ }
425
+ delete el.__wWasDisabled;
426
+ delete el.__wHadDisabledAttr;
427
+ }
428
+
429
+ if (el.__wOrigValue !== undefined) {
430
+ el.value = el.__wOrigValue;
431
+ delete el.__wOrigValue;
432
+ }
433
+ if (el.__wOrigText !== undefined) {
434
+ el.textContent = el.__wOrigText;
435
+ delete el.__wOrigText;
436
+ }
437
+
438
+ if (el.__wAddedClasses && el.classList) {
439
+ for (let i = 0; i < el.__wAddedClasses.length; i++) {
440
+ el.classList.remove(el.__wAddedClasses[i]);
441
+ }
442
+ delete el.__wAddedClasses;
443
+ }
444
+ }
445
+ }
446
+
268
447
  module.exports = {
448
+ isArray,
449
+ isNumber,
269
450
  isObject,
270
451
  isPlainObject,
271
452
  isPromise,
@@ -277,5 +458,10 @@ module.exports = {
277
458
  toKebabCase,
278
459
  parseDataValue,
279
460
  decorateElement,
461
+ parseDurationMs,
462
+ parseRefetchMode,
463
+ randomId,
464
+ uid,
465
+ setElementBusyState,
280
466
  };
281
467