@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.
- package/components/fws/DataTable.vue +10 -6
- package/components/fws/FilterData.vue +11 -4
- package/components/fws/UserFlow.vue +2 -2
- package/components/ui/DefaultGallery.vue +2 -12
- package/components/ui/DefaultInput.vue +5 -2
- package/components/ui/DefaultNotif.vue +4 -3
- package/components/ui/DefaultSidebar.vue +2 -2
- package/components/ui/DefaultTagInput.vue +27 -22
- package/components/ui/transitions/CollapseTransition.vue +11 -17
- package/components/ui/transitions/ExpandTransition.vue +11 -19
- package/components/ui/transitions/FadeTransition.vue +2 -1
- package/components/ui/transitions/ScaleTransition.vue +4 -12
- package/components/ui/transitions/SlideTransition.vue +18 -30
- package/composables/rest.ts +22 -7
- package/composables/seo.ts +33 -8
- package/composables/templating.ts +51 -5
- package/package.json +1 -1
|
@@ -391,7 +391,7 @@ onUnmounted(() => {
|
|
|
391
391
|
|
|
392
392
|
<style scoped>
|
|
393
393
|
.data-table-container {
|
|
394
|
-
@apply transition-
|
|
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
|
-
|
|
411
|
-
|
|
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
|
|
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
|
|
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-
|
|
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 {
|
|
272
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
|
573
|
-
@apply transition-
|
|
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
|
-
|
|
345
|
+
from {
|
|
346
346
|
transform: translateX(100%);
|
|
347
347
|
opacity: 0;
|
|
348
348
|
}
|
|
349
|
-
|
|
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)
|
|
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-
|
|
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-
|
|
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
|
-
<!--
|
|
402
|
-
<div
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
50% {
|
|
23
|
-
max-height: 400px;
|
|
24
|
-
}
|
|
15
|
+
.collapse-enter-from {
|
|
16
|
+
transform: scaleY(0);
|
|
17
|
+
opacity: 0;
|
|
18
|
+
}
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
20
|
+
.expand-leave-to {
|
|
21
|
+
transform: scale(0.9) scaleY(0);
|
|
22
|
+
opacity: 0;
|
|
31
23
|
}
|
|
32
24
|
</style>
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
<style scoped>
|
|
8
8
|
.scale-enter-active {
|
|
9
|
-
transition:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 {
|
package/composables/rest.ts
CHANGED
|
@@ -27,8 +27,9 @@ export interface APIResult {
|
|
|
27
27
|
status?: number
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
// Use
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
216
|
-
|
|
230
|
+
boundEmitMainLoading(false)
|
|
231
|
+
boundEmitRestError(restError)
|
|
217
232
|
return Promise.resolve(restError)
|
|
218
233
|
}
|
|
219
234
|
finally {
|
package/composables/seo.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
return type as 'image/jpeg' | 'image/gif' | 'image/png'
|
|
74
|
+
if (!imageType) {
|
|
75
|
+
result = null
|
|
62
76
|
}
|
|
63
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|