@svadmin/ui 0.51.0 → 0.53.0
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/dist/components/BatchActionBar.svelte +119 -0
- package/dist/components/BatchActionBar.svelte.d.ts +15 -0
- package/dist/components/FilterDropdown.svelte +252 -0
- package/dist/components/FilterDropdown.svelte.d.ts +24 -0
- package/dist/components/content/AuditTimeline.svelte +86 -0
- package/dist/components/content/AuditTimeline.svelte.d.ts +20 -0
- package/dist/components/content/index.d.ts +2 -0
- package/dist/components/content/index.js +1 -0
- package/dist/components/fields/AvatarField.svelte +123 -0
- package/dist/components/fields/AvatarField.svelte.d.ts +16 -0
- package/dist/components/fields/BooleanField.svelte +90 -8
- package/dist/components/fields/BooleanField.svelte.d.ts +8 -0
- package/dist/components/fields/CodeField.svelte +96 -0
- package/dist/components/fields/CodeField.svelte.d.ts +13 -0
- package/dist/components/fields/CopyField.svelte +102 -0
- package/dist/components/fields/CopyField.svelte.d.ts +15 -0
- package/dist/components/fields/DateField.svelte +74 -4
- package/dist/components/fields/DateField.svelte.d.ts +4 -0
- package/dist/components/fields/DateRangeField.svelte +98 -0
- package/dist/components/fields/DateRangeField.svelte.d.ts +22 -0
- package/dist/components/fields/NumberField.svelte +72 -4
- package/dist/components/fields/NumberField.svelte.d.ts +9 -0
- package/dist/components/fields/PercentField.svelte +92 -0
- package/dist/components/fields/PercentField.svelte.d.ts +13 -0
- package/dist/components/fields/RatingField.svelte +76 -0
- package/dist/components/fields/RatingField.svelte.d.ts +12 -0
- package/dist/components/fields/TagField.svelte +90 -6
- package/dist/components/fields/TagField.svelte.d.ts +9 -0
- package/dist/components/fields/UrlField.svelte +43 -7
- package/dist/components/fields/UrlField.svelte.d.ts +5 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +9 -0
- package/package.json +1 -1
|
@@ -1,13 +1,95 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type BooleanTone = 'success' | 'warning' | 'danger' | 'info' | 'neutral';
|
|
3
|
+
</script>
|
|
4
|
+
|
|
1
5
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
6
|
+
import { Badge } from '../ui/badge/index.js';
|
|
7
|
+
import { cn } from '../../utils.js';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
value: boolean | null | undefined;
|
|
11
|
+
mode?: 'icon' | 'badge' | 'tag' | 'text';
|
|
12
|
+
trueIcon?: string;
|
|
13
|
+
falseIcon?: string;
|
|
14
|
+
trueLabel?: string;
|
|
15
|
+
falseLabel?: string;
|
|
16
|
+
nullLabel?: string;
|
|
17
|
+
trueTone?: BooleanTone;
|
|
18
|
+
falseTone?: BooleanTone;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
value,
|
|
24
|
+
mode = 'icon',
|
|
25
|
+
trueIcon = '✓',
|
|
26
|
+
falseIcon = '✗',
|
|
27
|
+
trueLabel = 'Yes',
|
|
28
|
+
falseLabel = 'No',
|
|
29
|
+
nullLabel = '—',
|
|
30
|
+
trueTone = 'success',
|
|
31
|
+
falseTone = 'danger',
|
|
32
|
+
class: className = '',
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
|
|
35
|
+
const toneClass: Record<BooleanTone, string> = {
|
|
36
|
+
success: 'border-success/30 bg-success/10 text-success',
|
|
37
|
+
warning: 'border-warning/30 bg-warning/10 text-warning-foreground',
|
|
38
|
+
danger: 'border-destructive/30 bg-destructive/10 text-destructive',
|
|
39
|
+
info: 'border-primary/30 bg-primary/10 text-primary',
|
|
40
|
+
neutral: 'border-border bg-muted text-muted-foreground',
|
|
41
|
+
};
|
|
4
42
|
</script>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
43
|
+
|
|
44
|
+
{#if value == null}
|
|
45
|
+
<span class={cn('text-muted-foreground', className)}>{nullLabel}</span>
|
|
46
|
+
{:else if mode === 'badge' || mode === 'tag'}
|
|
47
|
+
<Badge
|
|
48
|
+
variant="outline"
|
|
49
|
+
class={cn(
|
|
50
|
+
'font-medium text-xs',
|
|
51
|
+
value ? toneClass[trueTone] : toneClass[falseTone],
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{value ? trueLabel : falseLabel}
|
|
56
|
+
</Badge>
|
|
57
|
+
{:else if mode === 'text'}
|
|
58
|
+
<span
|
|
59
|
+
class={cn(
|
|
60
|
+
'font-medium text-sm',
|
|
61
|
+
value
|
|
62
|
+
? trueTone === 'success'
|
|
63
|
+
? 'text-success'
|
|
64
|
+
: 'text-foreground'
|
|
65
|
+
: falseTone === 'danger'
|
|
66
|
+
? 'text-destructive'
|
|
67
|
+
: 'text-muted-foreground',
|
|
68
|
+
className
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
{value ? trueLabel : falseLabel}
|
|
8
72
|
</span>
|
|
9
73
|
{:else}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
74
|
+
{#if value}
|
|
75
|
+
<span
|
|
76
|
+
class={cn(
|
|
77
|
+
'inline-flex items-center justify-center w-5 h-5 rounded-full text-xs font-semibold',
|
|
78
|
+
toneClass[trueTone],
|
|
79
|
+
className
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{trueIcon}
|
|
83
|
+
</span>
|
|
84
|
+
{:else}
|
|
85
|
+
<span
|
|
86
|
+
class={cn(
|
|
87
|
+
'inline-flex items-center justify-center w-5 h-5 rounded-full text-xs font-semibold',
|
|
88
|
+
toneClass[falseTone],
|
|
89
|
+
className
|
|
90
|
+
)}
|
|
91
|
+
>
|
|
92
|
+
{falseIcon}
|
|
93
|
+
</span>
|
|
94
|
+
{/if}
|
|
13
95
|
{/if}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
export type BooleanTone = 'success' | 'warning' | 'danger' | 'info' | 'neutral';
|
|
1
2
|
interface Props {
|
|
2
3
|
value: boolean | null | undefined;
|
|
4
|
+
mode?: 'icon' | 'badge' | 'tag' | 'text';
|
|
3
5
|
trueIcon?: string;
|
|
4
6
|
falseIcon?: string;
|
|
7
|
+
trueLabel?: string;
|
|
8
|
+
falseLabel?: string;
|
|
9
|
+
nullLabel?: string;
|
|
10
|
+
trueTone?: BooleanTone;
|
|
11
|
+
falseTone?: BooleanTone;
|
|
12
|
+
class?: string;
|
|
5
13
|
}
|
|
6
14
|
declare const BooleanField: import("svelte").Component<Props, {}, "">;
|
|
7
15
|
type BooleanField = ReturnType<typeof BooleanField>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Copy, Check } from '@lucide/svelte';
|
|
3
|
+
import { Badge } from '../ui/badge/index.js';
|
|
4
|
+
import { cn } from '../../utils.js';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
value?: string | number | Record<string, unknown> | unknown[] | null | undefined;
|
|
8
|
+
language?: string;
|
|
9
|
+
copyable?: boolean;
|
|
10
|
+
title?: string;
|
|
11
|
+
copiedTitle?: string;
|
|
12
|
+
nullLabel?: string;
|
|
13
|
+
maxHeight?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
value,
|
|
19
|
+
language,
|
|
20
|
+
copyable = true,
|
|
21
|
+
title = 'Copy code',
|
|
22
|
+
copiedTitle = 'Copied!',
|
|
23
|
+
nullLabel = '—',
|
|
24
|
+
maxHeight = 'max-h-48',
|
|
25
|
+
class: className = '',
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
let copied = $state(false);
|
|
29
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
30
|
+
|
|
31
|
+
const formattedCode = $derived.by(() => {
|
|
32
|
+
if (value == null || value === '') return '';
|
|
33
|
+
if (typeof value === 'string') return value;
|
|
34
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
35
|
+
try {
|
|
36
|
+
return JSON.stringify(value, null, 2);
|
|
37
|
+
} catch {
|
|
38
|
+
return String(value);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
async function handleCopy() {
|
|
43
|
+
if (!formattedCode || !copyable) return;
|
|
44
|
+
try {
|
|
45
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
46
|
+
await navigator.clipboard.writeText(formattedCode);
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// Fallback
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
copied = true;
|
|
53
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
54
|
+
timeoutId = setTimeout(() => {
|
|
55
|
+
copied = false;
|
|
56
|
+
}, 1500);
|
|
57
|
+
}
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
{#if !formattedCode}
|
|
61
|
+
<span class={cn('field-code text-muted-foreground text-sm', className)}>{nullLabel}</span>
|
|
62
|
+
{:else}
|
|
63
|
+
<div class={cn('field-code relative group rounded-md border border-border/80 bg-muted/40 text-xs font-mono', className)}>
|
|
64
|
+
{#if language || copyable}
|
|
65
|
+
<div class="flex items-center justify-between border-b border-border/60 bg-muted/60 px-2.5 py-1 text-[11px] text-muted-foreground">
|
|
66
|
+
{#if language}
|
|
67
|
+
<Badge variant="outline" class="h-4 px-1 text-[10px] font-mono">
|
|
68
|
+
{language.toUpperCase()}
|
|
69
|
+
</Badge>
|
|
70
|
+
{:else}
|
|
71
|
+
<span></span>
|
|
72
|
+
{/if}
|
|
73
|
+
|
|
74
|
+
{#if copyable}
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-muted-foreground hover:text-foreground hover:bg-background/80 transition-colors focus:outline-none focus:ring-1 focus:ring-ring"
|
|
78
|
+
onclick={handleCopy}
|
|
79
|
+
title={copied ? copiedTitle : title}
|
|
80
|
+
aria-label={copied ? copiedTitle : title}
|
|
81
|
+
>
|
|
82
|
+
{#if copied}
|
|
83
|
+
<Check class="h-3 w-3 text-success animate-in zoom-in-50" />
|
|
84
|
+
<span class="text-[10px] text-success">{copiedTitle}</span>
|
|
85
|
+
{:else}
|
|
86
|
+
<Copy class="h-3 w-3" />
|
|
87
|
+
<span class="text-[10px]">{title}</span>
|
|
88
|
+
{/if}
|
|
89
|
+
</button>
|
|
90
|
+
{/if}
|
|
91
|
+
</div>
|
|
92
|
+
{/if}
|
|
93
|
+
|
|
94
|
+
<pre class={cn('p-2.5 overflow-x-auto overflow-y-auto leading-relaxed select-all', maxHeight)}><code>{formattedCode}</code></pre>
|
|
95
|
+
</div>
|
|
96
|
+
{/if}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
value?: string | number | Record<string, unknown> | unknown[] | null | undefined;
|
|
3
|
+
language?: string;
|
|
4
|
+
copyable?: boolean;
|
|
5
|
+
title?: string;
|
|
6
|
+
copiedTitle?: string;
|
|
7
|
+
nullLabel?: string;
|
|
8
|
+
maxHeight?: string;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
declare const CodeField: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type CodeField = ReturnType<typeof CodeField>;
|
|
13
|
+
export default CodeField;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
3
|
+
import { Copy, Check } from '@lucide/svelte';
|
|
4
|
+
import { cn } from '../../utils.js';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
value?: string | number | null | undefined;
|
|
8
|
+
displayValue?: string;
|
|
9
|
+
masked?: boolean;
|
|
10
|
+
monospace?: boolean;
|
|
11
|
+
copyable?: boolean;
|
|
12
|
+
title?: string;
|
|
13
|
+
copiedTitle?: string;
|
|
14
|
+
nullLabel?: string;
|
|
15
|
+
oncopy?: (value: string) => void;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
value,
|
|
21
|
+
displayValue,
|
|
22
|
+
masked = false,
|
|
23
|
+
monospace = true,
|
|
24
|
+
copyable = true,
|
|
25
|
+
title = 'Copy',
|
|
26
|
+
copiedTitle = 'Copied!',
|
|
27
|
+
nullLabel = '—',
|
|
28
|
+
oncopy,
|
|
29
|
+
class: className = '',
|
|
30
|
+
}: Props = $props();
|
|
31
|
+
|
|
32
|
+
let copied = $state(false);
|
|
33
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
34
|
+
let destroyed = false;
|
|
35
|
+
|
|
36
|
+
onDestroy(() => {
|
|
37
|
+
destroyed = true;
|
|
38
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function maskText(str: string): string {
|
|
42
|
+
if (str.length <= 8) return '****';
|
|
43
|
+
return `${str.slice(0, 4)}...${str.slice(-4)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const rawString = $derived(value != null && value !== '' ? String(value) : '');
|
|
47
|
+
const displayText = $derived.by(() => {
|
|
48
|
+
if (!rawString) return nullLabel;
|
|
49
|
+
if (displayValue) return displayValue;
|
|
50
|
+
if (masked) return maskText(rawString);
|
|
51
|
+
return rawString;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
async function handleCopy(event: MouseEvent) {
|
|
55
|
+
event.stopPropagation();
|
|
56
|
+
if (!rawString || !copyable) return;
|
|
57
|
+
|
|
58
|
+
const writeText = globalThis.navigator?.clipboard?.writeText;
|
|
59
|
+
if (!writeText) return;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await writeText.call(globalThis.navigator.clipboard, rawString);
|
|
63
|
+
} catch {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (destroyed) return;
|
|
68
|
+
copied = true;
|
|
69
|
+
oncopy?.(rawString);
|
|
70
|
+
|
|
71
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
72
|
+
timeoutId = setTimeout(() => {
|
|
73
|
+
copied = false;
|
|
74
|
+
timeoutId = null;
|
|
75
|
+
}, 1500);
|
|
76
|
+
}
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
{#if !rawString}
|
|
80
|
+
<span class={cn('field-copy text-muted-foreground text-sm', className)}>{nullLabel}</span>
|
|
81
|
+
{:else}
|
|
82
|
+
<span class={cn('field-copy inline-flex items-center gap-1.5 text-sm group', className)}>
|
|
83
|
+
<span class={cn(monospace ? 'font-mono text-xs' : '', 'truncate select-all')}>
|
|
84
|
+
{displayText}
|
|
85
|
+
</span>
|
|
86
|
+
{#if copyable}
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
class="inline-flex items-center justify-center h-5 w-5 rounded text-muted-foreground hover:text-foreground hover:bg-muted/60 transition-colors focus:outline-none focus:ring-1 focus:ring-ring"
|
|
90
|
+
onclick={handleCopy}
|
|
91
|
+
title={copied ? copiedTitle : title}
|
|
92
|
+
aria-label={copied ? copiedTitle : title}
|
|
93
|
+
>
|
|
94
|
+
{#if copied}
|
|
95
|
+
<Check class="h-3 w-3 text-success animate-in zoom-in-50 duration-150" />
|
|
96
|
+
{:else}
|
|
97
|
+
<Copy class="h-3 w-3 opacity-70 group-hover:opacity-100 transition-opacity" />
|
|
98
|
+
{/if}
|
|
99
|
+
</button>
|
|
100
|
+
{/if}
|
|
101
|
+
</span>
|
|
102
|
+
{/if}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
value?: string | number | null | undefined;
|
|
3
|
+
displayValue?: string;
|
|
4
|
+
masked?: boolean;
|
|
5
|
+
monospace?: boolean;
|
|
6
|
+
copyable?: boolean;
|
|
7
|
+
title?: string;
|
|
8
|
+
copiedTitle?: string;
|
|
9
|
+
nullLabel?: string;
|
|
10
|
+
oncopy?: (value: string) => void;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
declare const CopyField: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type CopyField = ReturnType<typeof CopyField>;
|
|
15
|
+
export default CopyField;
|
|
@@ -1,6 +1,76 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type DateFieldFormat = 'date' | 'time' | 'datetime' | 'relative' | 'iso';
|
|
3
|
+
</script>
|
|
4
|
+
|
|
1
5
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
6
|
+
import { cn } from '../../utils.js';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
value: string | number | Date | null | undefined;
|
|
10
|
+
locale?: string;
|
|
11
|
+
format?: DateFieldFormat;
|
|
12
|
+
options?: Intl.DateTimeFormatOptions;
|
|
13
|
+
nullLabel?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
value,
|
|
19
|
+
locale = 'en-US',
|
|
20
|
+
format = 'date',
|
|
21
|
+
options,
|
|
22
|
+
nullLabel = '—',
|
|
23
|
+
class: className = '',
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
|
|
26
|
+
function formatRelative(date: Date, loc: string): string {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const diffMs = date.getTime() - now;
|
|
29
|
+
const diffSec = Math.round(diffMs / 1000);
|
|
30
|
+
const diffMin = Math.round(diffSec / 60);
|
|
31
|
+
const diffHour = Math.round(diffMin / 60);
|
|
32
|
+
const diffDay = Math.round(diffHour / 24);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const rtf = new Intl.RelativeTimeFormat(loc, { numeric: 'auto' });
|
|
36
|
+
if (Math.abs(diffSec) < 60) return rtf.format(diffSec, 'second');
|
|
37
|
+
if (Math.abs(diffMin) < 60) return rtf.format(diffMin, 'minute');
|
|
38
|
+
if (Math.abs(diffHour) < 24) return rtf.format(diffHour, 'hour');
|
|
39
|
+
if (Math.abs(diffDay) < 30) return rtf.format(diffDay, 'day');
|
|
40
|
+
} catch {
|
|
41
|
+
// Fallback
|
|
42
|
+
}
|
|
43
|
+
return new Intl.DateTimeFormat(loc, { dateStyle: 'medium' }).format(date);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const formatted = $derived.by(() => {
|
|
47
|
+
if (value == null || value === '') return nullLabel;
|
|
48
|
+
const d = value instanceof Date ? value : new Date(value);
|
|
49
|
+
if (isNaN(d.getTime())) return nullLabel;
|
|
50
|
+
|
|
51
|
+
if (format === 'iso') return d.toISOString();
|
|
52
|
+
if (format === 'relative') return formatRelative(d, locale);
|
|
53
|
+
|
|
54
|
+
const defaultOptions: Intl.DateTimeFormatOptions =
|
|
55
|
+
options ??
|
|
56
|
+
(format === 'datetime'
|
|
57
|
+
? { dateStyle: 'medium', timeStyle: 'short' }
|
|
58
|
+
: format === 'time'
|
|
59
|
+
? { timeStyle: 'medium' }
|
|
60
|
+
: { dateStyle: 'medium' });
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
return new Intl.DateTimeFormat(locale, defaultOptions).format(d);
|
|
64
|
+
} catch {
|
|
65
|
+
return d.toLocaleDateString();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const fullIsoTitle = $derived.by(() => {
|
|
70
|
+
if (value == null || value === '') return undefined;
|
|
71
|
+
const d = value instanceof Date ? value : new Date(value);
|
|
72
|
+
return isNaN(d.getTime()) ? undefined : d.toISOString();
|
|
73
|
+
});
|
|
5
74
|
</script>
|
|
6
|
-
|
|
75
|
+
|
|
76
|
+
<span class={cn('field-date text-sm', className)} title={fullIsoTitle}>{formatted}</span>
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
export type DateFieldFormat = 'date' | 'time' | 'datetime' | 'relative' | 'iso';
|
|
1
2
|
interface Props {
|
|
2
3
|
value: string | number | Date | null | undefined;
|
|
3
4
|
locale?: string;
|
|
5
|
+
format?: DateFieldFormat;
|
|
4
6
|
options?: Intl.DateTimeFormatOptions;
|
|
7
|
+
nullLabel?: string;
|
|
8
|
+
class?: string;
|
|
5
9
|
}
|
|
6
10
|
declare const DateField: import("svelte").Component<Props, {}, "">;
|
|
7
11
|
type DateField = ReturnType<typeof DateField>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type DateRangeFormat = 'date' | 'time' | 'datetime' | 'iso';
|
|
3
|
+
export type DateRangeValue =
|
|
4
|
+
| [string | number | Date | null | undefined, string | number | Date | null | undefined]
|
|
5
|
+
| { start?: string | number | Date | null; end?: string | number | Date | null; from?: string | number | Date | null; to?: string | number | Date | null }
|
|
6
|
+
| null
|
|
7
|
+
| undefined;
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<script lang="ts">
|
|
11
|
+
import { Calendar } from '@lucide/svelte';
|
|
12
|
+
import { cn } from '../../utils.js';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
value?: DateRangeValue;
|
|
16
|
+
startDate?: string | number | Date | null | undefined;
|
|
17
|
+
endDate?: string | number | Date | null | undefined;
|
|
18
|
+
separator?: string;
|
|
19
|
+
locale?: string;
|
|
20
|
+
format?: DateRangeFormat;
|
|
21
|
+
options?: Intl.DateTimeFormatOptions;
|
|
22
|
+
nullLabel?: string;
|
|
23
|
+
showIcon?: boolean;
|
|
24
|
+
class?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
value,
|
|
29
|
+
startDate,
|
|
30
|
+
endDate,
|
|
31
|
+
separator = '~',
|
|
32
|
+
locale = 'en-US',
|
|
33
|
+
format = 'date',
|
|
34
|
+
options,
|
|
35
|
+
nullLabel = '—',
|
|
36
|
+
showIcon = false,
|
|
37
|
+
class: className = '',
|
|
38
|
+
}: Props = $props();
|
|
39
|
+
|
|
40
|
+
const start = $derived.by(() => {
|
|
41
|
+
if (startDate !== undefined) return startDate;
|
|
42
|
+
if (Array.isArray(value)) return value[0];
|
|
43
|
+
if (value && typeof value === 'object') {
|
|
44
|
+
const obj = value as Record<string, unknown>;
|
|
45
|
+
return (obj.start ?? obj.from) as string | number | Date | null | undefined;
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const end = $derived.by(() => {
|
|
51
|
+
if (endDate !== undefined) return endDate;
|
|
52
|
+
if (Array.isArray(value)) return value[1];
|
|
53
|
+
if (value && typeof value === 'object') {
|
|
54
|
+
const obj = value as Record<string, unknown>;
|
|
55
|
+
return (obj.end ?? obj.to) as string | number | Date | null | undefined;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function formatDate(val: string | number | Date | null | undefined): string | null {
|
|
61
|
+
if (val == null || val === '') return null;
|
|
62
|
+
const d = val instanceof Date ? val : new Date(val);
|
|
63
|
+
if (isNaN(d.getTime())) return null;
|
|
64
|
+
|
|
65
|
+
if (format === 'iso') return d.toISOString();
|
|
66
|
+
|
|
67
|
+
const defaultOptions: Intl.DateTimeFormatOptions =
|
|
68
|
+
options ??
|
|
69
|
+
(format === 'datetime'
|
|
70
|
+
? { dateStyle: 'medium', timeStyle: 'short' }
|
|
71
|
+
: format === 'time'
|
|
72
|
+
? { timeStyle: 'medium' }
|
|
73
|
+
: { dateStyle: 'medium' });
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
return new Intl.DateTimeFormat(locale, defaultOptions).format(d);
|
|
77
|
+
} catch {
|
|
78
|
+
return d.toLocaleDateString();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const startFormatted = $derived(formatDate(start));
|
|
83
|
+
const endFormatted = $derived(formatDate(end));
|
|
84
|
+
|
|
85
|
+
const display = $derived.by(() => {
|
|
86
|
+
if (!startFormatted && !endFormatted) return nullLabel;
|
|
87
|
+
if (startFormatted && endFormatted) return `${startFormatted} ${separator} ${endFormatted}`;
|
|
88
|
+
if (startFormatted) return `${startFormatted} ${separator} ${nullLabel}`;
|
|
89
|
+
return `${nullLabel} ${separator} ${endFormatted}`;
|
|
90
|
+
});
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<span class={cn('field-date-range inline-flex items-center gap-1.5 text-sm', className)}>
|
|
94
|
+
{#if showIcon}
|
|
95
|
+
<Calendar class="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
96
|
+
{/if}
|
|
97
|
+
<span>{display}</span>
|
|
98
|
+
</span>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type DateRangeFormat = 'date' | 'time' | 'datetime' | 'iso';
|
|
2
|
+
export type DateRangeValue = [string | number | Date | null | undefined, string | number | Date | null | undefined] | {
|
|
3
|
+
start?: string | number | Date | null;
|
|
4
|
+
end?: string | number | Date | null;
|
|
5
|
+
from?: string | number | Date | null;
|
|
6
|
+
to?: string | number | Date | null;
|
|
7
|
+
} | null | undefined;
|
|
8
|
+
interface Props {
|
|
9
|
+
value?: DateRangeValue;
|
|
10
|
+
startDate?: string | number | Date | null | undefined;
|
|
11
|
+
endDate?: string | number | Date | null | undefined;
|
|
12
|
+
separator?: string;
|
|
13
|
+
locale?: string;
|
|
14
|
+
format?: DateRangeFormat;
|
|
15
|
+
options?: Intl.DateTimeFormatOptions;
|
|
16
|
+
nullLabel?: string;
|
|
17
|
+
showIcon?: boolean;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
declare const DateRangeField: import("svelte").Component<Props, {}, "">;
|
|
21
|
+
type DateRangeField = ReturnType<typeof DateRangeField>;
|
|
22
|
+
export default DateRangeField;
|
|
@@ -1,6 +1,74 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { cn } from '../../utils.js';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
value: number | string | null | undefined;
|
|
6
|
+
locale?: string;
|
|
7
|
+
currency?: string;
|
|
8
|
+
unit?: string;
|
|
9
|
+
prefix?: string;
|
|
10
|
+
precision?: number;
|
|
11
|
+
compact?: boolean;
|
|
12
|
+
signed?: boolean;
|
|
13
|
+
tabular?: boolean;
|
|
14
|
+
options?: Intl.NumberFormatOptions;
|
|
15
|
+
nullLabel?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
value,
|
|
21
|
+
locale = 'en-US',
|
|
22
|
+
currency,
|
|
23
|
+
unit,
|
|
24
|
+
prefix,
|
|
25
|
+
precision,
|
|
26
|
+
compact = false,
|
|
27
|
+
signed = false,
|
|
28
|
+
tabular = true,
|
|
29
|
+
options = {},
|
|
30
|
+
nullLabel = '—',
|
|
31
|
+
class: className = '',
|
|
32
|
+
}: Props = $props();
|
|
33
|
+
|
|
34
|
+
const formatted = $derived.by(() => {
|
|
35
|
+
if (value == null || value === '') return nullLabel;
|
|
36
|
+
const num = Number(value);
|
|
37
|
+
if (isNaN(num)) return nullLabel;
|
|
38
|
+
|
|
39
|
+
const opts: Intl.NumberFormatOptions = { ...options };
|
|
40
|
+
|
|
41
|
+
if (currency) {
|
|
42
|
+
opts.style = 'currency';
|
|
43
|
+
opts.currency = currency;
|
|
44
|
+
}
|
|
45
|
+
if (compact) {
|
|
46
|
+
opts.notation = 'compact';
|
|
47
|
+
opts.compactDisplay = 'short';
|
|
48
|
+
}
|
|
49
|
+
if (precision != null) {
|
|
50
|
+
opts.minimumFractionDigits = precision;
|
|
51
|
+
opts.maximumFractionDigits = precision;
|
|
52
|
+
}
|
|
53
|
+
if (signed && num > 0 && !currency) {
|
|
54
|
+
opts.signDisplay = 'always';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
let result = new Intl.NumberFormat(locale, opts).format(num);
|
|
59
|
+
if (prefix && !currency) {
|
|
60
|
+
result = `${prefix}${result}`;
|
|
61
|
+
}
|
|
62
|
+
if (unit) {
|
|
63
|
+
result = `${result} ${unit}`;
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
} catch {
|
|
67
|
+
return String(num);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
5
70
|
</script>
|
|
6
|
-
|
|
71
|
+
|
|
72
|
+
<span class={cn('field-number text-sm', tabular && 'font-mono tabular-nums', className)}>
|
|
73
|
+
{formatted}
|
|
74
|
+
</span>
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
interface Props {
|
|
2
2
|
value: number | string | null | undefined;
|
|
3
3
|
locale?: string;
|
|
4
|
+
currency?: string;
|
|
5
|
+
unit?: string;
|
|
6
|
+
prefix?: string;
|
|
7
|
+
precision?: number;
|
|
8
|
+
compact?: boolean;
|
|
9
|
+
signed?: boolean;
|
|
10
|
+
tabular?: boolean;
|
|
4
11
|
options?: Intl.NumberFormatOptions;
|
|
12
|
+
nullLabel?: string;
|
|
13
|
+
class?: string;
|
|
5
14
|
}
|
|
6
15
|
declare const NumberField: import("svelte").Component<Props, {}, "">;
|
|
7
16
|
type NumberField = ReturnType<typeof NumberField>;
|