@fy-/fws-vue 2.3.70 → 2.3.72

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.
@@ -391,7 +391,7 @@ onUnmounted(() => {
391
391
 
392
392
  <style scoped>
393
393
  .data-table-container {
394
- @apply transition-all duration-300;
394
+ @apply transition-shadow duration-300;
395
395
  }
396
396
 
397
397
  /* Responsive styles */
@@ -407,18 +407,19 @@ onUnmounted(() => {
407
407
 
408
408
  /* Loading spinner animation */
409
409
  @keyframes spin {
410
- to {
411
- transform: rotate(360deg);
412
- }
410
+ from { transform: rotate(0deg); }
411
+ to { transform: rotate(360deg); }
413
412
  }
414
413
 
415
414
  .loading-spinner {
416
415
  animation: spin 1s linear infinite;
416
+ will-change: transform;
417
417
  }
418
418
 
419
419
  /* Fade in animation for rows */
420
420
  tbody tr {
421
- animation: fadeIn 0.2s ease-out forwards;
421
+ animation: fadeIn 0.2s ease-out;
422
+ will-change: opacity;
422
423
  }
423
424
 
424
425
  @keyframes fadeIn {
@@ -427,7 +428,10 @@ tbody tr {
427
428
  }
428
429
 
429
430
  /* Improved hover states for better interactivity */
430
- th, td {
431
+ th[scope="col"]:hover {
432
+ @apply transition-colors duration-200;
433
+ }
434
+ tbody tr:hover {
431
435
  @apply transition-colors duration-200;
432
436
  }
433
437
 
@@ -260,7 +260,7 @@ onUnmounted(() => {
260
260
 
261
261
  <style scoped>
262
262
  .filter-data-wrapper {
263
- @apply transition-all duration-300;
263
+ @apply transition-shadow duration-300;
264
264
  }
265
265
 
266
266
  .filter-data-form {
@@ -268,12 +268,19 @@ onUnmounted(() => {
268
268
  }
269
269
 
270
270
  @keyframes fadeIn {
271
- from { opacity: 0; transform: translateY(-10px); }
272
- to { opacity: 1; transform: translateY(0); }
271
+ from {
272
+ opacity: 0;
273
+ transform: translateY(-10px);
274
+ }
275
+ to {
276
+ opacity: 1;
277
+ transform: translateY(0);
278
+ }
273
279
  }
274
280
 
275
281
  .animate-fadeIn {
276
- animation: fadeIn 0.3s ease-out forwards;
282
+ animation: fadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
283
+ will-change: transform, opacity;
277
284
  }
278
285
 
279
286
  /* Responsive styles */
@@ -421,13 +421,13 @@ onMounted(async () => {
421
421
 
422
422
  <style scoped>
423
423
  .fws-login {
424
- @apply transition-all duration-300;
424
+ @apply transition-opacity duration-300;
425
425
  }
426
426
 
427
427
  .fws-login__oauth a,
428
428
  .fws-login__oauth button,
429
429
  .fws-login__form button[type="submit"] {
430
- @apply transition-all duration-200;
430
+ @apply transition-colors duration-200 transition-shadow;
431
431
  }
432
432
 
433
433
  @media (max-width: 640px) {
@@ -927,54 +927,44 @@ onUnmounted(() => {
927
927
  .slide-next-leave-active,
928
928
  .slide-prev-enter-active,
929
929
  .slide-prev-leave-active {
930
- transition:
931
- opacity 0.15s,
932
- transform 0.15s,
933
- filter 0.15s;
930
+ transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1), transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
931
+ will-change: transform, opacity;
934
932
  }
935
933
 
936
934
  /* Next (slide from right) */
937
935
  .slide-next-enter-from {
938
936
  opacity: 0;
939
937
  transform: translateX(30px);
940
- filter: blur(8px);
941
938
  }
942
939
  .slide-next-enter-to {
943
940
  opacity: 1;
944
941
  transform: translateX(0);
945
- filter: blur(0);
946
942
  }
947
943
  .slide-next-leave-from {
948
944
  opacity: 1;
949
945
  transform: translateX(0);
950
- filter: blur(0);
951
946
  }
952
947
  .slide-next-leave-to {
953
948
  opacity: 0;
954
949
  transform: translateX(-30px);
955
- filter: blur(8px);
956
950
  }
957
951
 
958
952
  /* Prev (slide from left) */
959
953
  .slide-prev-enter-from {
960
954
  opacity: 0;
961
955
  transform: translateX(-30px);
962
- filter: blur(8px);
963
956
  }
964
957
  .slide-prev-enter-to {
965
958
  opacity: 1;
966
959
  transform: translateX(0);
967
- filter: blur(0);
968
960
  }
969
961
  .slide-prev-leave-from {
970
962
  opacity: 1;
971
963
  transform: translateX(0);
972
- filter: blur(0);
973
964
  }
974
965
  .slide-prev-leave-to {
975
966
  opacity: 0;
976
967
  transform: translateX(30px);
977
- filter: blur(8px);
978
968
  }
979
969
 
980
970
  /* Grid layouts */
@@ -569,8 +569,11 @@ input[type="range"]:focus::-moz-range-thumb {
569
569
  }
570
570
 
571
571
  /* Add smooth transitions */
572
- input, select, textarea, input[type="range"]::-webkit-slider-thumb, input[type="range"]::-moz-range-thumb {
573
- @apply transition-all duration-200;
572
+ input, select, textarea {
573
+ @apply transition-colors duration-200 transition-shadow;
574
+ }
575
+ input[type="range"]::-webkit-slider-thumb, input[type="range"]::-moz-range-thumb {
576
+ @apply transition-transform duration-200;
574
577
  }
575
578
 
576
579
  /* Placeholder styling */
@@ -342,18 +342,19 @@ onUnmounted(() => {
342
342
  <style scoped>
343
343
  /* Optional: Add animation for notifications */
344
344
  @keyframes slide-in-right {
345
- 0% {
345
+ from {
346
346
  transform: translateX(100%);
347
347
  opacity: 0;
348
348
  }
349
- 100% {
349
+ to {
350
350
  transform: translateX(0);
351
351
  opacity: 1;
352
352
  }
353
353
  }
354
354
 
355
355
  #base-notif {
356
- animation: slide-in-right 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
356
+ animation: slide-in-right 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
357
+ will-change: transform, opacity;
357
358
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
358
359
  }
359
360
 
@@ -97,7 +97,7 @@ const toggleSidebar = useDebounceFn(() => {
97
97
 
98
98
  <style lang="scss" scoped>
99
99
  .fui-sidebar {
100
- @apply w-60 transition-all duration-300 ease-in-out;
100
+ @apply w-60 transition-[width] duration-300 ease-in-out;
101
101
  .fui-sidebar__controller {
102
102
  @apply py-3 flex items-center justify-end pr-3;
103
103
  svg {
@@ -108,7 +108,7 @@ const toggleSidebar = useDebounceFn(() => {
108
108
  @apply relative flex w-full items-center py-3 px-3 font-semibold text-sm border-l-[.4rem] border-l-transparent;
109
109
  @apply text-fv-neutral-600 hover:bg-fv-neutral-200/[.3] focus:bg-fv-neutral-200/[.3] hover:text-fv-primary-600;
110
110
  @apply dark:text-fv-neutral-300 dark:hover:bg-fv-neutral-700/[.3] dark:focus:bg-fv-neutral-700/[.3] dark:hover:text-fv-primary-400;
111
- @apply transition-all duration-300 ease-in-out;
111
+ @apply transition-colors duration-200 ease-in-out;
112
112
  &.fvside-active {
113
113
  @apply border-l-fv-primary-500 bg-fv-neutral-200 hover:text-fv-neutral-600 focus:text-fv-neutral-600;
114
114
  @apply dark:bg-fv-neutral-700 dark:hover:text-fv-neutral-300 dark:text-fv-neutral-300;
@@ -383,24 +383,23 @@ function handleKeyNavigation(e: KeyboardEvent, index: number) {
383
383
  >
384
384
  {{ help }}
385
385
  </span>
386
- <!-- Tag counter when maxTags is set -->
387
386
  </div>
388
- <div v-if="maxTags > 0" class="tag-counter">
389
- <span>{{ model.length }}/{{ maxTags }}</span>
390
- </div>
391
- <!-- Inline error display if needed -->
392
- <p
393
- v-if="$props.error"
394
- :id="`error_tags_${id}`"
395
- class="text-xs text-red-500 mt-1"
396
- aria-live="assertive"
397
- >
398
- {{ $props.error }}
399
- </p>
400
387
 
401
- <!-- Copy button / or any additional actions -->
402
- <div v-if="copyButton" class="copy-button-container">
388
+ <!-- Tag count and copy button container -->
389
+ <div class="flex items-center justify-between mt-2">
390
+ <!-- Tag counter -->
391
+ <div class="tag-counter">
392
+ <span v-if="maxLenghtPerTag > 0">
393
+ {{ model.length }}/{{ maxLenghtPerTag }} tag{{ model.length !== 1 ? 's' : '' }}
394
+ </span>
395
+ <span v-else>
396
+ {{ model.length }} tag{{ model.length !== 1 ? 's' : '' }}
397
+ </span>
398
+ </div>
399
+
400
+ <!-- Copy button -->
403
401
  <button
402
+ v-if="copyButton"
404
403
  class="copy-button"
405
404
  type="button"
406
405
  :disabled="model.length === 0"
@@ -424,6 +423,16 @@ function handleKeyNavigation(e: KeyboardEvent, index: number) {
424
423
  Copy tags
425
424
  </button>
426
425
  </div>
426
+
427
+ <!-- Inline error display if needed -->
428
+ <p
429
+ v-if="$props.error"
430
+ :id="`error_tags_${id}`"
431
+ class="text-xs text-red-500 mt-1"
432
+ aria-live="assertive"
433
+ >
434
+ {{ $props.error }}
435
+ </p>
427
436
  </div>
428
437
  </template>
429
438
 
@@ -436,7 +445,7 @@ function handleKeyNavigation(e: KeyboardEvent, index: number) {
436
445
  dark:bg-fv-neutral-800 dark:border-fv-neutral-600
437
446
  dark:placeholder-fv-neutral-400 dark:text-white p-2
438
447
  dark:focus-within:ring-fv-primary-500 dark:focus-within:border-fv-primary-500
439
- transition-all duration-200 ease-in-out shadow-sm;
448
+ transition-colors duration-200 ease-in-out shadow-sm;
440
449
  cursor: text;
441
450
  min-height: 2.5rem;
442
451
  }
@@ -455,7 +464,7 @@ function handleKeyNavigation(e: KeyboardEvent, index: number) {
455
464
  .tag {
456
465
  @apply inline-flex items-center justify-between
457
466
  text-sm font-medium rounded-full px-3 py-1
458
- dark:text-white transition-all duration-200 ease-in-out;
467
+ dark:text-white transition-colors duration-200 ease-in-out;
459
468
  }
460
469
 
461
470
  .tag-text {
@@ -522,10 +531,6 @@ function handleKeyNavigation(e: KeyboardEvent, index: number) {
522
531
  }
523
532
 
524
533
  /* Copy button styling */
525
- .copy-button-container {
526
- @apply flex justify-end mt-2;
527
- }
528
-
529
534
  .copy-button {
530
535
  @apply inline-flex items-center justify-center
531
536
  bg-fv-neutral-100 hover:bg-fv-neutral-200
@@ -539,7 +544,7 @@ function handleKeyNavigation(e: KeyboardEvent, index: number) {
539
544
 
540
545
  /* Tag counter styling */
541
546
  .tag-counter {
542
- @apply text-xs text-right text-fv-neutral-500 dark:text-fv-neutral-400 mt-1;
547
+ @apply text-sm text-fv-neutral-600 dark:text-fv-neutral-400;
543
548
  }
544
549
 
545
550
  /* Responsive adjustments */
@@ -5,26 +5,20 @@
5
5
  </template>
6
6
 
7
7
  <style scoped>
8
- .collapse-enter-active {
9
- animation: collapse reverse 300ms ease;
10
- }
11
-
8
+ .collapse-enter-active,
12
9
  .collapse-leave-active {
13
- animation: collapse 300ms ease;
10
+ transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1), opacity 300ms cubic-bezier(0.4, 0, 0.2, 1);
11
+ transform-origin: top;
12
+ will-change: transform, opacity;
14
13
  }
15
14
 
16
- @keyframes collapse {
17
- 100% {
18
- max-height: 0px;
19
- opacity: 0;
20
- }
21
-
22
- 50% {
23
- max-height: 400px;
24
- }
15
+ .collapse-enter-from {
16
+ transform: scaleY(0);
17
+ opacity: 0;
18
+ }
25
19
 
26
- 0% {
27
- opacity: 1;
28
- }
20
+ .collapse-leave-to {
21
+ transform: scaleY(0);
22
+ opacity: 0;
29
23
  }
30
24
  </style>
@@ -5,28 +5,20 @@
5
5
  </template>
6
6
 
7
7
  <style scoped>
8
- .expand-enter-active {
9
- animation: expand reverse 300ms ease;
10
- }
11
-
8
+ .expand-enter-active,
12
9
  .expand-leave-active {
13
- animation: expand 300ms ease;
10
+ transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1), opacity 300ms cubic-bezier(0.4, 0, 0.2, 1);
11
+ transform-origin: center;
12
+ will-change: transform, opacity;
14
13
  }
15
14
 
16
- @keyframes expand {
17
- 100% {
18
- max-height: 0px;
19
- opacity: 0;
20
- transform: scale(0.9);
21
- }
22
-
23
- 50% {
24
- max-height: 400px;
25
- }
15
+ .expand-enter-from {
16
+ transform: scale(0.9) scaleY(0);
17
+ opacity: 0;
18
+ }
26
19
 
27
- 0% {
28
- transform: scale(1);
29
- opacity: 1;
30
- }
20
+ .expand-leave-to {
21
+ transform: scale(0.9) scaleY(0);
22
+ opacity: 0;
31
23
  }
32
24
  </style>
@@ -7,7 +7,8 @@
7
7
  <style scoped>
8
8
  .fade-enter-active,
9
9
  .fade-leave-active {
10
- transition: opacity 0.2s ease-in;
10
+ transition: opacity 200ms cubic-bezier(0.4, 0, 1, 1);
11
+ will-change: opacity;
11
12
  }
12
13
 
13
14
  .fade-enter-from,
@@ -6,11 +6,13 @@
6
6
 
7
7
  <style scoped>
8
8
  .scale-enter-active {
9
- transition: all 0.1s ease-out;
9
+ transition: transform 100ms cubic-bezier(0, 0, 0.2, 1), opacity 100ms cubic-bezier(0, 0, 0.2, 1);
10
+ will-change: transform, opacity;
10
11
  }
11
12
 
12
13
  .scale-leave-active {
13
- transition: all 0.075s ease-in;
14
+ transition: transform 75ms cubic-bezier(0.4, 0, 1, 1), opacity 75ms cubic-bezier(0.4, 0, 1, 1);
15
+ will-change: transform, opacity;
14
16
  }
15
17
 
16
18
  .scale-enter-from {
@@ -18,16 +20,6 @@
18
20
  transform: scale(0.95);
19
21
  }
20
22
 
21
- .scale-enter-to {
22
- opacity: 1;
23
- transform: scale(1);
24
- }
25
-
26
- .scale-leave-from {
27
- opacity: 1;
28
- transform: scale(1);
29
- }
30
-
31
23
  .scale-leave-to {
32
24
  opacity: 0;
33
25
  transform: scale(0.95);
@@ -12,12 +12,10 @@ const props = defineProps<{
12
12
 
13
13
  <style scoped>
14
14
  /* slide left */
15
- .slide-left-enter-active {
16
- transition: all 0.2s;
17
- }
18
-
15
+ .slide-left-enter-active,
19
16
  .slide-left-leave-active {
20
- transition: all 0.2s;
17
+ transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1), opacity 200ms cubic-bezier(0.4, 0, 0.2, 1);
18
+ will-change: transform, opacity;
21
19
  }
22
20
 
23
21
  .slide-left-enter-from {
@@ -31,12 +29,10 @@ const props = defineProps<{
31
29
  }
32
30
 
33
31
  /* slide right */
34
- .slide-right-enter-active {
35
- transition: all 0.2s;
36
- }
37
-
32
+ .slide-right-enter-active,
38
33
  .slide-right-leave-active {
39
- transition: all 0.2s;
34
+ transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1), opacity 200ms cubic-bezier(0.4, 0, 0.2, 1);
35
+ will-change: transform, opacity;
40
36
  }
41
37
 
42
38
  .slide-right-enter-from {
@@ -50,12 +46,10 @@ const props = defineProps<{
50
46
  }
51
47
 
52
48
  /* slide up */
53
- .slide-up-enter-active {
54
- transition: all 0.2s;
55
- }
56
-
49
+ .slide-up-enter-active,
57
50
  .slide-up-leave-active {
58
- transition: all 0.2s;
51
+ transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1), opacity 200ms cubic-bezier(0.4, 0, 0.2, 1);
52
+ will-change: transform, opacity;
59
53
  }
60
54
 
61
55
  .slide-up-enter-from {
@@ -69,12 +63,10 @@ const props = defineProps<{
69
63
  }
70
64
 
71
65
  /* slide down */
72
- .slide-down-enter-active {
73
- transition: all 0.2s;
74
- }
75
-
66
+ .slide-down-enter-active,
76
67
  .slide-down-leave-active {
77
- transition: all 0.2s;
68
+ transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1), opacity 200ms cubic-bezier(0.4, 0, 0.2, 1);
69
+ will-change: transform, opacity;
78
70
  }
79
71
 
80
72
  .slide-down-enter-from {
@@ -88,12 +80,10 @@ const props = defineProps<{
88
80
  }
89
81
 
90
82
  /* shelf up */
91
- .shelf-up-enter-active {
92
- transition: all 0.2s;
93
- }
94
-
83
+ .shelf-up-enter-active,
95
84
  .shelf-up-leave-active {
96
- transition: all 0.2s;
85
+ transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1), opacity 200ms cubic-bezier(0.4, 0, 0.2, 1);
86
+ will-change: transform, opacity;
97
87
  }
98
88
 
99
89
  .shelf-up-enter-from {
@@ -107,12 +97,10 @@ const props = defineProps<{
107
97
  }
108
98
 
109
99
  /* shelf down */
110
- .shelf-down-enter-active {
111
- transition: all 0.2s;
112
- }
113
-
100
+ .shelf-down-enter-active,
114
101
  .shelf-down-leave-active {
115
- transition: all 0.2s;
102
+ transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1), opacity 200ms cubic-bezier(0.4, 0, 0.2, 1);
103
+ will-change: transform, opacity;
116
104
  }
117
105
 
118
106
  .shelf-down-enter-from {
@@ -27,8 +27,9 @@ export interface APIResult {
27
27
  status?: number
28
28
  }
29
29
 
30
- // Use WeakMap for caches to allow garbage collection of unused entries
30
+ // Use Map with size limit for better performance than WeakMap in this case
31
31
  const urlParseCache = new Map<string, string>()
32
+ const MAX_URL_CACHE_SIZE = 500
32
33
 
33
34
  // Global request hash cache with size limit to prevent memory leaks
34
35
  const globalHashCache = new Map<string, number>()
@@ -59,6 +60,14 @@ function getUrlForHash(url: string): string {
59
60
  urlForHash = url
60
61
  }
61
62
 
63
+ // Implement LRU-like cache eviction for URL cache
64
+ if (urlParseCache.size >= MAX_URL_CACHE_SIZE) {
65
+ const firstKey = urlParseCache.keys().next().value
66
+ if (firstKey !== undefined) {
67
+ urlParseCache.delete(firstKey)
68
+ }
69
+ }
70
+
62
71
  urlParseCache.set(url, urlForHash)
63
72
  return urlForHash
64
73
  }
@@ -72,9 +81,11 @@ function checkSSRMode(): boolean {
72
81
  return isSSRMode
73
82
  }
74
83
 
75
- // Fast JSON.stringify for params
84
+ // Fast JSON.stringify for params with caching for common cases
85
+ const EMPTY_PARAMS = ''
76
86
  function stringifyParams(params?: RestParams): string {
77
- return params ? JSON.stringify(params) : ''
87
+ if (!params) return EMPTY_PARAMS
88
+ return JSON.stringify(params)
78
89
  }
79
90
 
80
91
  // Compute request hash with global caching and size limit
@@ -138,9 +149,13 @@ params?: RestParams,
138
149
  const emitMainLoading = (value: boolean) => eventBus.emit('main-loading', value)
139
150
  const emitRestError = (result: any) => eventBus.emit('rest-error', result)
140
151
 
152
+ // Pre-bind emit functions to avoid recreation
153
+ const boundEmitMainLoading = emitMainLoading
154
+ const boundEmitRestError = emitRestError
155
+
141
156
  function handleErrorResult<ResultType extends APIResult>(result: ResultType): Promise<ResultType> {
142
- emitMainLoading(false)
143
- emitRestError(result)
157
+ boundEmitMainLoading(false)
158
+ boundEmitRestError(result)
144
159
  return Promise.reject(result)
145
160
  }
146
161
 
@@ -212,8 +227,8 @@ params?: RestParams,
212
227
  serverRouter.addResult(requestHash, restError)
213
228
  }
214
229
 
215
- emitMainLoading(false)
216
- emitRestError(restError)
230
+ boundEmitMainLoading(false)
231
+ boundEmitRestError(restError)
217
232
  return Promise.resolve(restError)
218
233
  }
219
234
  finally {
@@ -27,8 +27,9 @@ export interface LazyHead {
27
27
  twitterCreator?: string
28
28
  }
29
29
 
30
- // Cache for processed image URLs
30
+ // Cache for processed image URLs with size limit
31
31
  const processedImageUrlCache = new Map<string, string>()
32
+ const MAX_IMAGE_URL_CACHE_SIZE = 200
32
33
 
33
34
  // Helper function to process image URLs with caching
34
35
  function processImageUrl(image: string | undefined, imageType: string | undefined): string | undefined {
@@ -48,22 +49,46 @@ function processImageUrl(image: string | undefined, imageType: string | undefine
48
49
  result = image
49
50
  }
50
51
 
52
+ // Implement LRU-like cache eviction
53
+ if (processedImageUrlCache.size >= MAX_IMAGE_URL_CACHE_SIZE) {
54
+ const firstKey = processedImageUrlCache.keys().next().value
55
+ if (firstKey !== undefined) {
56
+ processedImageUrlCache.delete(firstKey)
57
+ }
58
+ }
59
+
51
60
  processedImageUrlCache.set(cacheKey, result)
52
61
  return result
53
62
  }
54
63
 
55
- // Helper function to normalize image type
64
+ // Cache normalized image types
65
+ const normalizedImageTypeCache = new Map<string | undefined, 'image/jpeg' | 'image/gif' | 'image/png' | null>()
66
+
67
+ // Helper function to normalize image type with caching
56
68
  function normalizeImageType(imageType: string | undefined): 'image/jpeg' | 'image/gif' | 'image/png' | null {
57
- if (!imageType) return null
69
+ const cached = normalizedImageTypeCache.get(imageType)
70
+ if (cached !== undefined) return cached
71
+
72
+ let result: 'image/jpeg' | 'image/gif' | 'image/png' | null
58
73
 
59
- const type = imageType.includes('image/') ? imageType : `image/${imageType}`
60
- if (type === 'image/jpeg' || type === 'image/gif' || type === 'image/png') {
61
- return type as 'image/jpeg' | 'image/gif' | 'image/png'
74
+ if (!imageType) {
75
+ result = null
62
76
  }
63
- return 'image/png'
77
+ else {
78
+ const type = imageType.includes('image/') ? imageType : `image/${imageType}`
79
+ if (type === 'image/jpeg' || type === 'image/gif' || type === 'image/png') {
80
+ result = type as 'image/jpeg' | 'image/gif' | 'image/png'
81
+ }
82
+ else {
83
+ result = 'image/png'
84
+ }
85
+ }
86
+
87
+ normalizedImageTypeCache.set(imageType, result)
88
+ return result
64
89
  }
65
90
 
66
- // Precomputed alternate locale URL template
91
+ // Precomputed alternate locale URL template - inline for better JIT
67
92
  function ALTERNATE_LOCALE_TEMPLATE(scheme: string, host: string, locale: string, path: string) {
68
93
  return `${scheme}://${host}/l/${locale}${path}`
69
94
  }
@@ -57,11 +57,13 @@ const dateFormatOptions = {
57
57
  }),
58
58
  }
59
59
 
60
- // Color cache for contrast calculations to avoid repeated calculations
60
+ // Color cache for contrast calculations with size limit
61
61
  const colorContrastCache = new Map<string, string>()
62
+ const MAX_COLOR_CACHE_SIZE = 100
62
63
 
63
64
  // Locale transform cache to avoid repeated replacements
64
65
  const localeTransformCache = new Map<string, string>()
66
+ const MAX_LOCALE_CACHE_SIZE = 50
65
67
 
66
68
  // Default colors for contrast errors
67
69
  const DEFAULT_DARK_COLOR = '#000000'
@@ -100,7 +102,14 @@ function getContrastingTextColor(backgroundColor: string) {
100
102
  const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
101
103
  const result = luminance > 0.5 ? DEFAULT_DARK_COLOR : DEFAULT_LIGHT_COLOR
102
104
 
103
- // Cache the result
105
+ // Cache the result with LRU eviction
106
+ if (colorContrastCache.size >= MAX_COLOR_CACHE_SIZE) {
107
+ const firstKey = colorContrastCache.keys().next().value
108
+ if (firstKey !== undefined) {
109
+ colorContrastCache.delete(firstKey)
110
+ }
111
+ }
112
+
104
113
  colorContrastCache.set(backgroundColor, result)
105
114
  return result
106
115
  }
@@ -109,6 +118,12 @@ function getContrastingTextColor(backgroundColor: string) {
109
118
  }
110
119
  }
111
120
 
121
+ // Cache for power calculations in formatBytes
122
+ const kPowerCache = new Float64Array(9) // Support up to YB
123
+ for (let i = 0; i < 9; i++) {
124
+ kPowerCache[i] = k ** i
125
+ }
126
+
112
127
  function formatBytes(bytes: number, decimals = 2) {
113
128
  if (!+bytes) {
114
129
  return '0 Bytes'
@@ -118,18 +133,40 @@ function formatBytes(bytes: number, decimals = 2) {
118
133
  // Use precomputed logK for better performance
119
134
  const i = Math.floor(Math.log(bytes) / logK)
120
135
 
121
- return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${byteSizes[i]}`
136
+ // Use cached power value instead of recalculating
137
+ const divisor = i < kPowerCache.length ? kPowerCache[i] : k ** i
138
+
139
+ return `${Number.parseFloat((bytes / divisor).toFixed(dm))} ${byteSizes[i]}`
122
140
  }
123
141
 
124
- // Helper to parse date inputs consistently
142
+ // Cache for parsed date strings
143
+ const parsedDateCache = new Map<string, number>()
144
+ const MAX_DATE_CACHE_SIZE = 100
145
+
146
+ // Helper to parse date inputs consistently with caching
125
147
  function parseDateInput(dt: Date | string | number): number {
126
148
  if (dt instanceof Date) {
127
149
  return dt.getTime()
128
150
  }
129
151
 
130
152
  if (typeof dt === 'string') {
153
+ // Check cache first
154
+ const cached = parsedDateCache.get(dt)
155
+ if (cached !== undefined) return cached
156
+
131
157
  const parsed = Date.parse(dt)
132
- return Number.isNaN(parsed) ? Number.parseInt(dt, 10) : parsed
158
+ const result = Number.isNaN(parsed) ? Number.parseInt(dt, 10) : parsed
159
+
160
+ // Cache with LRU eviction
161
+ if (parsedDateCache.size >= MAX_DATE_CACHE_SIZE) {
162
+ const firstKey = parsedDateCache.keys().next().value
163
+ if (firstKey !== undefined) {
164
+ parsedDateCache.delete(firstKey)
165
+ }
166
+ }
167
+
168
+ parsedDateCache.set(dt, result)
169
+ return result
133
170
  }
134
171
 
135
172
  return dt
@@ -170,6 +207,15 @@ function formatTimeago(dt: Date | string | number) {
170
207
  let localeWithUnderscore = localeTransformCache.get(fullLocale)
171
208
  if (!localeWithUnderscore) {
172
209
  localeWithUnderscore = fullLocale.replace('-', '_')
210
+
211
+ // Implement LRU eviction for locale cache
212
+ if (localeTransformCache.size >= MAX_LOCALE_CACHE_SIZE) {
213
+ const firstKey = localeTransformCache.keys().next().value
214
+ if (firstKey !== undefined) {
215
+ localeTransformCache.delete(firstKey)
216
+ }
217
+ }
218
+
173
219
  localeTransformCache.set(fullLocale, localeWithUnderscore)
174
220
  }
175
221
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fy-/fws-vue",
3
- "version": "2.3.70",
3
+ "version": "2.3.72",
4
4
  "author": "Florian 'Fy' Gasquez <m@fy.to>",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/fy-to/FWJS#readme",