@svadmin/ui 0.53.0 → 0.53.1
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.
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
2
3
|
import { Copy, Check } from '@lucide/svelte';
|
|
3
4
|
import { Badge } from '../ui/badge/index.js';
|
|
4
5
|
import { cn } from '../../utils.js';
|
|
@@ -27,6 +28,12 @@
|
|
|
27
28
|
|
|
28
29
|
let copied = $state(false);
|
|
29
30
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
31
|
+
let destroyed = false;
|
|
32
|
+
|
|
33
|
+
onDestroy(() => {
|
|
34
|
+
destroyed = true;
|
|
35
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
36
|
+
});
|
|
30
37
|
|
|
31
38
|
const formattedCode = $derived.by(() => {
|
|
32
39
|
if (value == null || value === '') return '';
|
|
@@ -39,20 +46,25 @@
|
|
|
39
46
|
}
|
|
40
47
|
});
|
|
41
48
|
|
|
42
|
-
async function handleCopy() {
|
|
49
|
+
async function handleCopy(event: MouseEvent) {
|
|
50
|
+
event.stopPropagation();
|
|
43
51
|
if (!formattedCode || !copyable) return;
|
|
52
|
+
|
|
53
|
+
const writeText = globalThis.navigator?.clipboard?.writeText;
|
|
54
|
+
if (!writeText) return;
|
|
55
|
+
|
|
44
56
|
try {
|
|
45
|
-
|
|
46
|
-
await navigator.clipboard.writeText(formattedCode);
|
|
47
|
-
}
|
|
57
|
+
await writeText.call(globalThis.navigator.clipboard, formattedCode);
|
|
48
58
|
} catch {
|
|
49
|
-
|
|
59
|
+
return;
|
|
50
60
|
}
|
|
51
61
|
|
|
62
|
+
if (destroyed) return;
|
|
52
63
|
copied = true;
|
|
53
64
|
if (timeoutId) clearTimeout(timeoutId);
|
|
54
65
|
timeoutId = setTimeout(() => {
|
|
55
66
|
copied = false;
|
|
67
|
+
timeoutId = null;
|
|
56
68
|
}, 1500);
|
|
57
69
|
}
|
|
58
70
|
</script>
|
|
@@ -25,23 +25,28 @@
|
|
|
25
25
|
class: className = '',
|
|
26
26
|
}: Props = $props();
|
|
27
27
|
|
|
28
|
+
const normalizedPrecision = $derived(
|
|
29
|
+
Math.min(20, Math.max(0, Math.trunc(Number.isFinite(precision) ? precision : 1)))
|
|
30
|
+
);
|
|
31
|
+
|
|
28
32
|
const numericValue = $derived.by(() => {
|
|
29
|
-
if (value == null
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
if (value == null) return null;
|
|
34
|
+
if (typeof value === 'string' && value.trim() === '') return null;
|
|
35
|
+
const parsed = typeof value === 'number' ? value : Number(value);
|
|
36
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
32
37
|
});
|
|
33
38
|
|
|
34
39
|
const percentNumber = $derived.by(() => {
|
|
35
40
|
if (numericValue === null) return null;
|
|
36
|
-
|
|
41
|
+
const scaled = scale === '1' ? numericValue * 100 : numericValue;
|
|
42
|
+
return Number.isFinite(scaled) ? scaled : null;
|
|
37
43
|
});
|
|
38
44
|
|
|
39
|
-
const formatted = $derived
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
});
|
|
45
|
+
const formatted = $derived(
|
|
46
|
+
percentNumber === null ? nullLabel : `${percentNumber.toFixed(normalizedPrecision)}%`
|
|
47
|
+
);
|
|
43
48
|
|
|
44
|
-
const resolvedTone = $derived.by(() => {
|
|
49
|
+
const resolvedTone = $derived.by((): PercentTone => {
|
|
45
50
|
if (tone !== 'auto') return tone;
|
|
46
51
|
if (percentNumber === null) return 'neutral';
|
|
47
52
|
if (percentNumber >= 80) return 'success';
|
|
@@ -67,13 +72,12 @@
|
|
|
67
72
|
neutral: 'bg-primary',
|
|
68
73
|
};
|
|
69
74
|
|
|
70
|
-
const clampedProgress = $derived
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
});
|
|
75
|
+
const clampedProgress = $derived(
|
|
76
|
+
percentNumber === null ? 0 : Math.min(100, Math.max(0, percentNumber))
|
|
77
|
+
);
|
|
74
78
|
</script>
|
|
75
79
|
|
|
76
|
-
{#if
|
|
80
|
+
{#if percentNumber === null}
|
|
77
81
|
<span class={cn('field-percent text-muted-foreground text-sm', className)}>{nullLabel}</span>
|
|
78
82
|
{:else}
|
|
79
83
|
<div class={cn('field-percent inline-flex items-center gap-2 text-sm', className)}>
|
|
@@ -81,7 +85,14 @@
|
|
|
81
85
|
{formatted}
|
|
82
86
|
</span>
|
|
83
87
|
{#if showProgress}
|
|
84
|
-
<div
|
|
88
|
+
<div
|
|
89
|
+
class="h-1.5 w-16 overflow-hidden rounded-full bg-muted"
|
|
90
|
+
role="progressbar"
|
|
91
|
+
aria-label="Percentage"
|
|
92
|
+
aria-valuemin="0"
|
|
93
|
+
aria-valuemax="100"
|
|
94
|
+
aria-valuenow={clampedProgress}
|
|
95
|
+
>
|
|
85
96
|
<div
|
|
86
97
|
class={cn('h-full transition-all duration-300', toneBarClass[resolvedTone])}
|
|
87
98
|
style="width: {clampedProgress}%"
|
|
@@ -24,10 +24,16 @@
|
|
|
24
24
|
class: className = '',
|
|
25
25
|
}: Props = $props();
|
|
26
26
|
|
|
27
|
+
const normalizedMax = $derived(
|
|
28
|
+
Number.isFinite(max) ? Math.min(100, Math.max(1, Math.trunc(max))) : 5
|
|
29
|
+
);
|
|
30
|
+
|
|
27
31
|
const numericValue = $derived.by(() => {
|
|
28
|
-
if (value == null
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
if (value == null) return null;
|
|
33
|
+
if (typeof value === 'string' && value.trim() === '') return null;
|
|
34
|
+
const parsed = typeof value === 'number' ? value : Number(value);
|
|
35
|
+
if (!Number.isFinite(parsed)) return null;
|
|
36
|
+
return Math.max(0, Math.min(normalizedMax, parsed));
|
|
31
37
|
});
|
|
32
38
|
|
|
33
39
|
const sizeClasses: Record<RatingSize, { icon: string; text: string }> = {
|
|
@@ -39,10 +45,10 @@
|
|
|
39
45
|
const stars = $derived.by(() => {
|
|
40
46
|
if (numericValue === null) return [];
|
|
41
47
|
const result: ('full' | 'half' | 'empty')[] = [];
|
|
42
|
-
for (let
|
|
43
|
-
if (numericValue >=
|
|
48
|
+
for (let index = 1; index <= normalizedMax; index += 1) {
|
|
49
|
+
if (numericValue >= index) {
|
|
44
50
|
result.push('full');
|
|
45
|
-
} else if (numericValue >=
|
|
51
|
+
} else if (numericValue >= index - 0.5) {
|
|
46
52
|
result.push('half');
|
|
47
53
|
} else {
|
|
48
54
|
result.push('empty');
|
|
@@ -56,14 +62,18 @@
|
|
|
56
62
|
<span class={cn('field-rating text-muted-foreground text-sm', className)}>{nullLabel}</span>
|
|
57
63
|
{:else}
|
|
58
64
|
<div class={cn('field-rating inline-flex items-center gap-1.5', className)}>
|
|
59
|
-
<div
|
|
65
|
+
<div
|
|
66
|
+
class="inline-flex items-center gap-0.5 text-amber-500"
|
|
67
|
+
role="img"
|
|
68
|
+
aria-label={`${numericValue} out of ${normalizedMax}`}
|
|
69
|
+
>
|
|
60
70
|
{#each stars as starType, idx (idx)}
|
|
61
71
|
{#if starType === 'full'}
|
|
62
|
-
<Star class={cn(sizeClasses[size].icon, 'fill-amber-500 text-amber-500')} />
|
|
72
|
+
<Star aria-hidden="true" class={cn(sizeClasses[size].icon, 'fill-amber-500 text-amber-500')} />
|
|
63
73
|
{:else if starType === 'half'}
|
|
64
|
-
<StarHalf class={cn(sizeClasses[size].icon, 'fill-amber-500 text-amber-500')} />
|
|
74
|
+
<StarHalf aria-hidden="true" class={cn(sizeClasses[size].icon, 'fill-amber-500 text-amber-500')} />
|
|
65
75
|
{:else}
|
|
66
|
-
<Star class={cn(sizeClasses[size].icon, 'text-muted-foreground/30')} />
|
|
76
|
+
<Star aria-hidden="true" class={cn(sizeClasses[size].icon, 'text-muted-foreground/30')} />
|
|
67
77
|
{/if}
|
|
68
78
|
{/each}
|
|
69
79
|
</div>
|