@mohasinac/appkit 3.5.2 → 3.5.4

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.
Files changed (70) hide show
  1. package/dist/features/homepage/components/CustomCardsSection.js +1 -1
  2. package/dist/features/homepage/components/SectionCarousel.js +1 -1
  3. package/dist/features/seller/components/CategoryInlineSelect.d.ts +1 -1
  4. package/dist/features/seller/components/CategoryInlineSelect.js +1 -1
  5. package/dist/react/hooks/useBulkSelection.js +12 -3
  6. package/dist/react/hooks/useGesture.js +48 -4
  7. package/dist/react/hooks/useLongPress.d.ts +1 -0
  8. package/dist/react/hooks/useLongPress.js +5 -0
  9. package/dist/react/hooks/usePullToRefresh.js +12 -1
  10. package/dist/react/hooks/useRealtimeEvent.js +18 -1
  11. package/dist/react/hooks/useSwipe.js +14 -0
  12. package/dist/seed/addresses-seed-data.js +8 -99
  13. package/dist/seed/categories-seed-data.js +103 -2082
  14. package/dist/seed/homepage-sections-seed-data.js +9 -9
  15. package/dist/seed/products-art-seed-data.d.ts +1 -10
  16. package/dist/seed/products-art-seed-data.js +13 -122
  17. package/dist/seed/products-auctions-seed-data.js +39 -747
  18. package/dist/seed/products-classifieds-seed-data.d.ts +1 -9
  19. package/dist/seed/products-classifieds-seed-data.js +13 -195
  20. package/dist/seed/products-digital-codes-seed-data.d.ts +1 -9
  21. package/dist/seed/products-digital-codes-seed-data.js +13 -202
  22. package/dist/seed/products-live-items-seed-data.d.ts +1 -12
  23. package/dist/seed/products-live-items-seed-data.js +13 -188
  24. package/dist/seed/products-preorders-seed-data.js +18 -149
  25. package/dist/seed/products-prize-draws-seed-data.d.ts +1 -16
  26. package/dist/seed/products-prize-draws-seed-data.js +13 -385
  27. package/dist/seed/products-standard-seed-data.js +152 -1331
  28. package/dist/seed/products-stickers-seed-data.d.ts +1 -10
  29. package/dist/seed/products-stickers-seed-data.js +13 -122
  30. package/dist/seed/site-settings-seed-data.js +106 -108
  31. package/dist/seed/store-addresses-seed-data.js +14 -46
  32. package/dist/seed/stores-seed-data.js +13 -205
  33. package/dist/styles.css +19 -18
  34. package/dist/tailwind-utilities.css +1 -1
  35. package/dist/ui/components/BaseListingCard.d.ts +2 -1
  36. package/dist/ui/components/BaseListingCard.js +4 -5
  37. package/dist/ui/components/BulkActionBar.style.css +4 -4
  38. package/dist/ui/components/Button.js +52 -22
  39. package/dist/ui/components/Button.style.css +14 -13
  40. package/dist/ui/components/DateInput.js +1 -0
  41. package/dist/ui/components/DynamicBgDiv.js +8 -6
  42. package/dist/ui/components/FormField.js +1 -1
  43. package/dist/ui/components/HorizontalScroller.js +35 -16
  44. package/dist/ui/components/Iframe.d.ts +8 -1
  45. package/dist/ui/components/Layout.d.ts +4 -4
  46. package/dist/ui/components/Layout.js +8 -8
  47. package/dist/ui/components/ListingToolbar.js +4 -4
  48. package/dist/ui/components/Motion.js +4 -1
  49. package/dist/ui/components/OtpInput.js +1 -1
  50. package/dist/ui/components/PaginatedSelect.js +29 -3
  51. package/dist/ui/components/Pagination.js +10 -5
  52. package/dist/ui/components/RichTextEditor.js +64 -3
  53. package/dist/ui/components/Select.js +1 -0
  54. package/dist/ui/components/Semantic.d.ts +10 -10
  55. package/dist/ui/components/Semantic.js +25 -25
  56. package/dist/ui/components/SideDrawer.js +13 -11
  57. package/dist/ui/components/SideModal.js +9 -7
  58. package/dist/ui/components/SlottedListingView.d.ts +13 -2
  59. package/dist/ui/components/StickyToolbar.js +9 -5
  60. package/dist/ui/components/TagInput.js +15 -2
  61. package/dist/ui/components/Textarea.js +1 -0
  62. package/dist/ui/components/Toggle.js +8 -1
  63. package/dist/ui/components/UnsavedChangesModal.js +12 -1
  64. package/dist/ui/forms/ColorPickerField.js +7 -0
  65. package/dist/ui/forms/FieldCheckbox.js +1 -1
  66. package/dist/utils/id-generators.d.ts +7 -4
  67. package/dist/utils/id-generators.js +53 -31
  68. package/dist/utils/number.formatter.js +19 -6
  69. package/dist/utils/string.formatter.js +4 -1
  70. package/package.json +1 -1
@@ -74,7 +74,8 @@ export function generateOrderId(input) {
74
74
  export function generateFAQId(input) {
75
75
  if (input.customId?.trim())
76
76
  return input.customId.trim();
77
- return `faq-${slugify(input.category)}-${slugify(input.question).substring(0, 50)}`;
77
+ const questionSlug = slugify(input.question).substring(0, 50).replace(/-+$/, "");
78
+ return `faq-${slugify(input.category)}-${questionSlug}`;
78
79
  }
79
80
  // ─── Coupon ───────────────────────────────────────────────────────────────────
80
81
  export function generateCouponId(code, customId) {
@@ -427,53 +428,74 @@ export function generateMediaFilename(ctx) {
427
428
  }
428
429
  }
429
430
  /**
430
- * W1-51 — validate that a filename matches the canonical pattern produced
431
- * by {@link generateMediaFilename}. Called from /api/media/sign before a
431
+ * W1-51 (regex fixed 2026-08-15 see bug notes below) validate that a
432
+ * filename matches the canonical pattern produced by
433
+ * {@link generateMediaFilename}. Called from /api/media/sign before a
432
434
  * signed-URL is issued so malformed names can never enter storage.
433
435
  *
434
- * Pattern: `<prefix>-<slug parts>-<YYYYMMDD>(-<N>)?(-<rand>)?\.<ext>` where
435
- * the prefix is one of the known media type tokens.
436
+ * The generator functions above put the type word (image/video/logo/
437
+ * banner/avatar) at the END of the slug portion, e.g.
438
+ * `product-{name}-{category}-{store}-image-{n}.ext`, NOT as a literal
439
+ * compound prefix like `product-image-{...}`. The previous version of this
440
+ * validator anchored on the compound-prefix shape and rejected real output
441
+ * from every generator except invoice/payout-doc/shipping-proof/
442
+ * refund-proof/catalogue-image — causing /api/media/sign to 500 for
443
+ * product/review/auction/preorder/store/brand/blog/event/category/user/
444
+ * carousel/rich-text uploads. Each pattern below mirrors one generator
445
+ * function's actual output shape. blog-cover/blog-content-image/
446
+ * blog-additional-image all dispatch to the same generator as blog-image
447
+ * (and the four event-* variants likewise share one shape), so one pattern
448
+ * validates all sub-types of that family.
436
449
  *
437
450
  * Returns true when the filename is acceptable.
438
451
  */
439
- const MEDIA_FILENAME_PREFIXES = [
440
- "product-image", "product-video",
441
- "review-image", "review-video",
442
- "auction-image", "preorder-image",
443
- "store-logo", "store-banner",
444
- "brand-logo", "brand-banner",
445
- "blog-image", "blog-cover", "blog-content-image", "blog-additional-image",
446
- "event-image", "event-cover", "event-winner-image", "event-additional-image",
447
- "rich-text-image",
448
- "category-image",
449
- "user-avatar",
450
- "carousel-image",
451
- "catalogue-image",
452
- "invoice", "payout-doc",
453
- "shipping-proof", "refund-proof",
452
+ const SLUG_PART = "[a-z0-9]+(?:-[a-z0-9]+)*";
453
+ const EXT_PART = "[a-z0-9]+";
454
+ const DATE_PART = "\\d{8}";
455
+ const MEDIA_FILENAME_PATTERNS = [
456
+ { context: "product-image", pattern: new RegExp(`^product-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
457
+ { context: "product-video", pattern: new RegExp(`^product-${SLUG_PART}-video-\\d+\\.${EXT_PART}$`) },
458
+ { context: "review-image", pattern: new RegExp(`^review-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
459
+ { context: "review-video", pattern: new RegExp(`^review-${SLUG_PART}-video-1\\.${EXT_PART}$`) },
460
+ { context: "auction-image", pattern: new RegExp(`^auction-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
461
+ { context: "preorder-image", pattern: new RegExp(`^preorder-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
462
+ { context: "store-logo", pattern: new RegExp(`^store-${SLUG_PART}-logo\\.${EXT_PART}$`) },
463
+ { context: "store-banner", pattern: new RegExp(`^store-${SLUG_PART}-banner\\.${EXT_PART}$`) },
464
+ { context: "brand-logo", pattern: new RegExp(`^brand-${SLUG_PART}-logo\\.${EXT_PART}$`) },
465
+ { context: "brand-banner", pattern: new RegExp(`^brand-${SLUG_PART}-banner\\.${EXT_PART}$`) },
466
+ { context: "blog-image", pattern: new RegExp(`^blog-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
467
+ { context: "event-image", pattern: new RegExp(`^event-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
468
+ { context: "rich-text-image", pattern: new RegExp(`^rich-text-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
469
+ { context: "category-image", pattern: new RegExp(`^category-${SLUG_PART}-image\\.${EXT_PART}$`) },
470
+ { context: "user-avatar", pattern: new RegExp(`^user-${SLUG_PART}-avatar\\.${EXT_PART}$`) },
471
+ { context: "carousel-image", pattern: new RegExp(`^carousel-${SLUG_PART}-image\\.${EXT_PART}$`) },
472
+ { context: "catalogue-image", pattern: new RegExp(`^catalogue-image-${SLUG_PART}-\\d+\\.${EXT_PART}$`) },
473
+ { context: "invoice", pattern: new RegExp(`^invoice-${SLUG_PART}-${DATE_PART}\\.pdf$`) },
474
+ { context: "payout-doc", pattern: new RegExp(`^payout-doc-${SLUG_PART}-${DATE_PART}\\.pdf$`) },
475
+ { context: "shipping-proof", pattern: new RegExp(`^shipping-proof-${SLUG_PART}-${DATE_PART}\\.${EXT_PART}$`) },
476
+ { context: "refund-proof", pattern: new RegExp(`^refund-proof-${SLUG_PART}-${SLUG_PART}-${DATE_PART}\\.${EXT_PART}$`) },
477
+ { context: "payment-proof", pattern: new RegExp(`^payment-proof-${SLUG_PART}-${SLUG_PART}-${DATE_PART}\\.${EXT_PART}$`) },
454
478
  ];
455
- const MEDIA_FILENAME_REGEX = new RegExp(`^(?:${MEDIA_FILENAME_PREFIXES.join("|")})-[a-z0-9]([a-z0-9-]*[a-z0-9])?(?:-\\d+)?\\.[a-z0-9]+$`);
456
479
  export function validateMediaFilename(filename) {
457
480
  if (!filename || filename.length > 256)
458
481
  return false;
459
482
  if (filename.includes("..") || filename.includes("/") || filename.includes("\\"))
460
483
  return false;
461
- return MEDIA_FILENAME_REGEX.test(filename);
484
+ return MEDIA_FILENAME_PATTERNS.some(({ pattern }) => pattern.test(filename));
462
485
  }
463
486
  /**
464
- * Derives the `MediaFilenameContext["type"]` a filename was generated under
465
- * (e.g. "catalogue-image") by matching against the same prefix list
466
- * {@link validateMediaFilename} uses the longest matching prefix wins so
467
- * "blog-content-image" isn't mis-derived as "blog-cover"'s sibling "blog-".
487
+ * Derives the `MediaFilenameContext["type"]` family a filename was
488
+ * generated under (e.g. "catalogue-image") by matching against the same
489
+ * shape patterns {@link validateMediaFilename} uses. Sibling context types
490
+ * that share one generator (e.g. blog-cover/blog-content-image/
491
+ * blog-additional-image all produce the same shape as blog-image) resolve
492
+ * to that shared family name — the dispatcher itself can't distinguish them
493
+ * from the filename alone, since it never encoded the sub-type into the name.
468
494
  * Used at /api/media/finalize time to stamp `MediaAssetDocument.contextType`
469
495
  * without changing the finalize request contract.
470
496
  */
471
497
  export function deriveContextTypeFromFilename(filename) {
472
- const stem = filename.replace(/\.[^.]+$/, "");
473
- const matches = MEDIA_FILENAME_PREFIXES.filter((prefix) => stem.startsWith(`${prefix}-`) || stem === prefix);
474
- if (matches.length === 0)
475
- return undefined;
476
- return matches.reduce((longest, candidate) => (candidate.length > longest.length ? candidate : longest));
498
+ return MEDIA_FILENAME_PATTERNS.find(({ pattern }) => pattern.test(filename))?.context;
477
499
  }
478
500
  /**
479
501
  * Generates a scannable barcode identifier for a product listing.
@@ -40,10 +40,20 @@ export function formatFileSize(bytes) {
40
40
  export function formatCompactNumber(num) {
41
41
  if (num < 1000)
42
42
  return num.toString();
43
- if (num < 1000000)
44
- return `${(num / 1000).toFixed(1)}K`;
45
- if (num < 1000000000)
46
- return `${(num / 1000000).toFixed(1)}M`;
43
+ if (num < 1000000) {
44
+ const kValue = (num / 1000).toFixed(1);
45
+ // Rounding can push a value right up to the next unit's boundary
46
+ // (e.g. 999950 -> "1000.0K") — escalate instead of displaying "1000.0K".
47
+ if (kValue === "1000.0")
48
+ return `${(num / 1000000).toFixed(1)}M`;
49
+ return `${kValue}K`;
50
+ }
51
+ if (num < 1000000000) {
52
+ const mValue = (num / 1000000).toFixed(1);
53
+ if (mValue === "1000.0")
54
+ return `${(num / 1000000000).toFixed(1)}B`;
55
+ return `${mValue}M`;
56
+ }
47
57
  return `${(num / 1000000000).toFixed(1)}B`;
48
58
  }
49
59
  export function formatDecimal(num, decimals = 2) {
@@ -73,7 +83,10 @@ export function parseFormattedNumber(str) {
73
83
  }
74
84
  else if (lastDotIndex > -1 && lastCommaIndex === -1) {
75
85
  const afterDot = cleaned.substring(lastDotIndex + 1);
76
- const isThousands = dotCount > 1 || afterDot.length > 3;
86
+ // A single separator followed by exactly 3 digits is the standard
87
+ // thousands-grouping width (e.g. "3.500" -> 3500), not a decimal — only
88
+ // 1-2 trailing digits are treated as a genuine decimal fraction.
89
+ const isThousands = dotCount > 1 || afterDot.length >= 3;
77
90
  if (isThousands)
78
91
  cleaned = cleaned.replace(/\./g, "");
79
92
  decimalSeparator = ".";
@@ -81,7 +94,7 @@ export function parseFormattedNumber(str) {
81
94
  }
82
95
  else if (lastCommaIndex > -1 && lastDotIndex === -1) {
83
96
  const afterComma = cleaned.substring(lastCommaIndex + 1);
84
- const commaIsThousands = commaCount > 1 || afterComma.length > 3;
97
+ const commaIsThousands = commaCount > 1 || afterComma.length >= 3;
85
98
  if (commaIsThousands) {
86
99
  cleaned = cleaned.replace(/,/g, "");
87
100
  decimalSeparator = ".";
@@ -76,7 +76,10 @@ function applyMark(text, mark) {
76
76
  }
77
77
  if (mark.type === "link") {
78
78
  const rawHref = String(mark.attrs?.href ?? "#").trim();
79
- const safe = /^(https?:\/\/|mailto:|\/|#)/i.test(rawHref) ? rawHref : "#";
79
+ // `\/(?!\/)` allows a single leading slash (same-site relative path) but
80
+ // rejects `//host/...` protocol-relative URLs, which browsers resolve to
81
+ // an arbitrary external origin despite "looking" like a local path.
82
+ const safe = /^(https?:\/\/|mailto:|\/(?!\/)|#)/i.test(rawHref) ? rawHref : "#";
80
83
  const href = safe.replace(/[&"<>]/g, (c) => ({ "&": "&amp;", '"': "&quot;", "<": "&lt;", ">": "&gt;" })[c] ?? c);
81
84
  return `<a href="${href}" rel="noopener noreferrer">${text}</a>`;
82
85
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "3.5.2",
3
+ "version": "3.5.4",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"