@svadmin/ui 0.55.0 → 0.55.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.
|
@@ -25,6 +25,7 @@ import CodeField from './fields/CodeField.svelte';
|
|
|
25
25
|
import AvatarField from './fields/AvatarField.svelte';
|
|
26
26
|
import CopyField from './fields/CopyField.svelte';
|
|
27
27
|
import FileField from './fields/FileField.svelte';
|
|
28
|
+
import MarkdownField from './fields/MarkdownField.svelte';
|
|
28
29
|
/**
|
|
29
30
|
* Built-in display components for show/detail pages.
|
|
30
31
|
* Keys match FieldDefinition.type values.
|
|
@@ -55,6 +56,7 @@ export const builtinDisplayComponents = {
|
|
|
55
56
|
avatar: AvatarField,
|
|
56
57
|
copy: CopyField,
|
|
57
58
|
file: FileField,
|
|
59
|
+
markdown: MarkdownField,
|
|
58
60
|
};
|
|
59
61
|
// ─── Custom component registry ─────────────────────────────────
|
|
60
62
|
// Users can register custom field display components at runtime
|
|
@@ -1,17 +1,58 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/* eslint-disable svelte/no-at-html-tags */
|
|
3
|
-
import
|
|
4
|
-
interface Props { value: string | null | undefined }
|
|
5
|
-
let { value }: Props = $props();
|
|
3
|
+
import { onMount } from "svelte";
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
interface Props {
|
|
6
|
+
value?: string | null | undefined;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
let { value, class: className = "" }: Props = $props();
|
|
11
|
+
|
|
12
|
+
interface DomPurifyLike {
|
|
13
|
+
sanitize(source: string | Node, config?: Record<string, unknown>): string;
|
|
14
|
+
}
|
|
15
|
+
let DOMPurify: DomPurifyLike | null = $state(null);
|
|
16
|
+
|
|
17
|
+
onMount(() => {
|
|
18
|
+
let cancelled = false;
|
|
19
|
+
import("isomorphic-dompurify")
|
|
20
|
+
.then((pkg) => {
|
|
21
|
+
if (!cancelled) {
|
|
22
|
+
const mod = pkg as unknown as { default?: DomPurifyLike; sanitize?: DomPurifyLike["sanitize"] };
|
|
23
|
+
const loaded: DomPurifyLike | undefined =
|
|
24
|
+
mod.default && typeof mod.default.sanitize === "function"
|
|
25
|
+
? mod.default
|
|
26
|
+
: typeof mod.sanitize === "function"
|
|
27
|
+
? (mod as DomPurifyLike)
|
|
28
|
+
: undefined;
|
|
29
|
+
if (loaded) {
|
|
30
|
+
DOMPurify = loaded;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
.catch(() => {});
|
|
35
|
+
return () => {
|
|
36
|
+
cancelled = true;
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const sanitizedHtml = $derived.by(() => {
|
|
41
|
+
if (!value) return "";
|
|
42
|
+
if (DOMPurify && typeof DOMPurify.sanitize === "function") {
|
|
43
|
+
return DOMPurify.sanitize(value, { USE_PROFILES: { html: true } });
|
|
44
|
+
}
|
|
45
|
+
return String(value)
|
|
46
|
+
.replace(/&/g, "&")
|
|
47
|
+
.replace(/</g, "<")
|
|
48
|
+
.replace(/>/g, ">")
|
|
49
|
+
.replace(/"/g, """)
|
|
50
|
+
.replace(/'/g, "'");
|
|
51
|
+
});
|
|
12
52
|
</script>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
{:else}
|
|
53
|
+
|
|
54
|
+
{#if !value}
|
|
16
55
|
<span class="text-muted-foreground">—</span>
|
|
56
|
+
{:else}
|
|
57
|
+
<div class="prose prose-sm dark:prose-invert max-w-none leading-relaxed [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-[0.875em] [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:rounded-lg [&_pre]:overflow-x-auto [&_a]:text-primary {className}">{@html sanitizedHtml}</div>
|
|
17
58
|
{/if}
|
|
@@ -31,10 +31,16 @@
|
|
|
31
31
|
class: className = '',
|
|
32
32
|
}: Props = $props();
|
|
33
33
|
|
|
34
|
+
const normalizedPrecision = $derived(
|
|
35
|
+
precision != null && Number.isFinite(precision)
|
|
36
|
+
? Math.min(20, Math.max(0, Math.trunc(precision)))
|
|
37
|
+
: undefined
|
|
38
|
+
);
|
|
39
|
+
|
|
34
40
|
const formatted = $derived.by(() => {
|
|
35
41
|
if (value == null || value === '') return nullLabel;
|
|
36
42
|
const num = Number(value);
|
|
37
|
-
if (
|
|
43
|
+
if (!Number.isFinite(num)) return nullLabel;
|
|
38
44
|
|
|
39
45
|
const opts: Intl.NumberFormatOptions = { ...options };
|
|
40
46
|
|
|
@@ -46,9 +52,9 @@
|
|
|
46
52
|
opts.notation = 'compact';
|
|
47
53
|
opts.compactDisplay = 'short';
|
|
48
54
|
}
|
|
49
|
-
if (
|
|
50
|
-
opts.minimumFractionDigits =
|
|
51
|
-
opts.maximumFractionDigits =
|
|
55
|
+
if (normalizedPrecision !== undefined) {
|
|
56
|
+
opts.minimumFractionDigits = normalizedPrecision;
|
|
57
|
+
opts.maximumFractionDigits = normalizedPrecision;
|
|
52
58
|
}
|
|
53
59
|
if (signed && num > 0 && !currency) {
|
|
54
60
|
opts.signDisplay = 'always';
|