@svadmin/ui 0.29.0 → 0.29.2
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/package.json +20 -9
- package/src/README.md +0 -18
- package/src/actions.ts +0 -110
- package/src/app.css +0 -519
- package/src/component-registry.svelte.ts +0 -60
- package/src/components/AICommandBar.svelte +0 -180
- package/src/components/AboutSettings.svelte +0 -106
- package/src/components/AdminApp.svelte +0 -199
- package/src/components/AnimatedCounter.svelte +0 -46
- package/src/components/AnomalyBadge.svelte +0 -65
- package/src/components/AppearanceSettings.svelte +0 -183
- package/src/components/AuditLogDrawer.svelte +0 -93
- package/src/components/AuditLogViewer.svelte +0 -226
- package/src/components/Authenticated.svelte +0 -51
- package/src/components/AutoForm.svelte +0 -242
- package/src/components/AutoSaveIndicator.svelte +0 -35
- package/src/components/AutoTable.svelte +0 -839
- package/src/components/Breadcrumbs.svelte +0 -58
- package/src/components/CanAccess.svelte +0 -23
- package/src/components/CatchAllNavigate.svelte +0 -21
- package/src/components/ChatDialog.svelte +0 -535
- package/src/components/ComboboxField.svelte +0 -129
- package/src/components/CommandPalette.svelte +0 -135
- package/src/components/ConfigErrorScreen.svelte +0 -126
- package/src/components/ConfirmDialog.svelte +0 -53
- package/src/components/CopilotPanel.svelte +0 -163
- package/src/components/CreatePage.svelte +0 -39
- package/src/components/DevTools.svelte +0 -149
- package/src/components/DraggableGrid.svelte +0 -86
- package/src/components/DraggableHeader.svelte +0 -112
- package/src/components/DrawerForm.svelte +0 -35
- package/src/components/EditPage.svelte +0 -52
- package/src/components/EmptyState.svelte +0 -31
- package/src/components/ErrorBoundary.svelte +0 -49
- package/src/components/ErrorComponent.svelte +0 -36
- package/src/components/FieldRenderer.svelte +0 -357
- package/src/components/ForgotPasswordPage.svelte +0 -112
- package/src/components/Header.svelte +0 -94
- package/src/components/InferencerPanel.svelte +0 -192
- package/src/components/InfiniteList.svelte +0 -93
- package/src/components/InlineEdit.svelte +0 -88
- package/src/components/InsightCard.svelte +0 -98
- package/src/components/KeyboardShortcuts.svelte +0 -61
- package/src/components/Layout.svelte +0 -167
- package/src/components/ListPage.svelte +0 -71
- package/src/components/LiveIndicator.svelte +0 -58
- package/src/components/LoginPage.svelte +0 -163
- package/src/components/MarkdownRenderer.svelte +0 -127
- package/src/components/ModalForm.svelte +0 -38
- package/src/components/NavigateToResource.svelte +0 -20
- package/src/components/PageHeader.svelte +0 -39
- package/src/components/PageSkeleton.svelte +0 -71
- package/src/components/PasswordInput.svelte +0 -96
- package/src/components/PermissionMatrix.svelte +0 -185
- package/src/components/ProfilePage.svelte +0 -284
- package/src/components/RegisterPage.svelte +0 -112
- package/src/components/RolesSettings.svelte +0 -146
- package/src/components/SettingsPage.svelte +0 -113
- package/src/components/ShowPage.svelte +0 -73
- package/src/components/Sidebar.svelte +0 -417
- package/src/components/SidebarItem.svelte +0 -129
- package/src/components/SmartSuggest.svelte +0 -158
- package/src/components/StatsCard.svelte +0 -79
- package/src/components/StepsForm.svelte +0 -192
- package/src/components/TaskQueueDrawer.svelte +0 -158
- package/src/components/TenantSwitcher.svelte +0 -84
- package/src/components/ThemedTitle.svelte +0 -32
- package/src/components/Toast.svelte +0 -46
- package/src/components/TooltipButton.svelte +0 -42
- package/src/components/UndoableNotification.svelte +0 -69
- package/src/components/UpdatePasswordPage.svelte +0 -89
- package/src/components/VirtualTable.svelte +0 -76
- package/src/components/VoiceInput.svelte +0 -120
- package/src/components/buttons/CloneButton.svelte +0 -30
- package/src/components/buttons/CreateButton.svelte +0 -29
- package/src/components/buttons/DeleteButton.svelte +0 -59
- package/src/components/buttons/EditButton.svelte +0 -30
- package/src/components/buttons/ExportButton.svelte +0 -30
- package/src/components/buttons/ImportButton.svelte +0 -56
- package/src/components/buttons/ListButton.svelte +0 -23
- package/src/components/buttons/RefreshButton.svelte +0 -36
- package/src/components/buttons/SaveButton.svelte +0 -27
- package/src/components/buttons/ShowButton.svelte +0 -31
- package/src/components/buttons/index.ts +0 -10
- package/src/components/charts/BarChart.svelte +0 -69
- package/src/components/charts/LineChart.svelte +0 -115
- package/src/components/charts/PieChart.svelte +0 -95
- package/src/components/charts/index.ts +0 -3
- package/src/components/fieldComponentMap.ts +0 -79
- package/src/components/fields/ArrayField.svelte +0 -86
- package/src/components/fields/BooleanField.svelte +0 -13
- package/src/components/fields/DateField.svelte +0 -6
- package/src/components/fields/EmailField.svelte +0 -9
- package/src/components/fields/FileField.svelte +0 -14
- package/src/components/fields/ImageField.svelte +0 -9
- package/src/components/fields/JsonField.svelte +0 -72
- package/src/components/fields/MarkdownField.svelte +0 -9
- package/src/components/fields/MultiSelectField.svelte +0 -21
- package/src/components/fields/NumberField.svelte +0 -6
- package/src/components/fields/RelationField.svelte +0 -24
- package/src/components/fields/RichTextField.svelte +0 -35
- package/src/components/fields/SelectField.svelte +0 -14
- package/src/components/fields/TagField.svelte +0 -12
- package/src/components/fields/TextField.svelte +0 -11
- package/src/components/fields/UrlField.svelte +0 -14
- package/src/components/ui/alert/alert-description.svelte +0 -17
- package/src/components/ui/alert/alert-title.svelte +0 -17
- package/src/components/ui/alert/alert.svelte +0 -44
- package/src/components/ui/alert/index.ts +0 -15
- package/src/components/ui/alert-dialog/alert-dialog-content.svelte +0 -29
- package/src/components/ui/alert-dialog/alert-dialog-description.svelte +0 -16
- package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +0 -11
- package/src/components/ui/alert-dialog/alert-dialog-header.svelte +0 -11
- package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +0 -16
- package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +0 -6
- package/src/components/ui/alert-dialog/alert-dialog-title.svelte +0 -16
- package/src/components/ui/alert-dialog/index.ts +0 -20
- package/src/components/ui/avatar/avatar.svelte +0 -63
- package/src/components/ui/avatar/index.ts +0 -2
- package/src/components/ui/badge/badge.svelte +0 -49
- package/src/components/ui/badge/index.ts +0 -3
- package/src/components/ui/breadcrumb/breadcrumb-ellipsis.svelte +0 -18
- package/src/components/ui/breadcrumb/breadcrumb-item.svelte +0 -15
- package/src/components/ui/breadcrumb/breadcrumb-link.svelte +0 -15
- package/src/components/ui/breadcrumb/breadcrumb-list.svelte +0 -15
- package/src/components/ui/breadcrumb/breadcrumb-page.svelte +0 -18
- package/src/components/ui/breadcrumb/breadcrumb-separator.svelte +0 -22
- package/src/components/ui/breadcrumb/breadcrumb.svelte +0 -11
- package/src/components/ui/breadcrumb/index.ts +0 -7
- package/src/components/ui/button/button.svelte +0 -90
- package/src/components/ui/button/index.ts +0 -18
- package/src/components/ui/card/card-action.svelte +0 -23
- package/src/components/ui/card/card-content.svelte +0 -20
- package/src/components/ui/card/card-description.svelte +0 -20
- package/src/components/ui/card/card-footer.svelte +0 -20
- package/src/components/ui/card/card-header.svelte +0 -23
- package/src/components/ui/card/card-title.svelte +0 -15
- package/src/components/ui/card/card.svelte +0 -22
- package/src/components/ui/card/index.ts +0 -25
- package/src/components/ui/checkbox/checkbox.svelte +0 -39
- package/src/components/ui/checkbox/index.ts +0 -6
- package/src/components/ui/collapsible/index.ts +0 -7
- package/src/components/ui/command/command-dialog.svelte +0 -31
- package/src/components/ui/command/index.ts +0 -2
- package/src/components/ui/context-menu/index.ts +0 -11
- package/src/components/ui/dialog/dialog-close.svelte +0 -11
- package/src/components/ui/dialog/dialog-content.svelte +0 -48
- package/src/components/ui/dialog/dialog-description.svelte +0 -17
- package/src/components/ui/dialog/dialog-footer.svelte +0 -32
- package/src/components/ui/dialog/dialog-header.svelte +0 -20
- package/src/components/ui/dialog/dialog-overlay.svelte +0 -17
- package/src/components/ui/dialog/dialog-portal.svelte +0 -7
- package/src/components/ui/dialog/dialog-title.svelte +0 -17
- package/src/components/ui/dialog/dialog-trigger.svelte +0 -11
- package/src/components/ui/dialog/dialog.svelte +0 -7
- package/src/components/ui/dialog/index.ts +0 -34
- package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +0 -43
- package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +0 -35
- package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +0 -36
- package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +0 -10
- package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +0 -27
- package/src/components/ui/dropdown-menu/dropdown-menu.svelte +0 -45
- package/src/components/ui/dropdown-menu/index.ts +0 -21
- package/src/components/ui/input/index.ts +0 -7
- package/src/components/ui/input/input.svelte +0 -48
- package/src/components/ui/label/index.ts +0 -1
- package/src/components/ui/label/label.svelte +0 -18
- package/src/components/ui/pagination/index.ts +0 -7
- package/src/components/ui/pagination/pagination-content.svelte +0 -11
- package/src/components/ui/pagination/pagination-ellipsis.svelte +0 -12
- package/src/components/ui/pagination/pagination-item.svelte +0 -11
- package/src/components/ui/pagination/pagination-link.svelte +0 -26
- package/src/components/ui/pagination/pagination-next.svelte +0 -22
- package/src/components/ui/pagination/pagination-previous.svelte +0 -22
- package/src/components/ui/pagination/pagination.svelte +0 -11
- package/src/components/ui/popover/index.ts +0 -12
- package/src/components/ui/popover/popover-content.svelte +0 -25
- package/src/components/ui/progress/index.ts +0 -1
- package/src/components/ui/progress/progress.svelte +0 -22
- package/src/components/ui/scroll-area/index.ts +0 -1
- package/src/components/ui/scroll-area/scroll-area.svelte +0 -20
- package/src/components/ui/select/index.ts +0 -2
- package/src/components/ui/select/select.svelte +0 -36
- package/src/components/ui/separator/index.ts +0 -7
- package/src/components/ui/separator/separator.svelte +0 -23
- package/src/components/ui/sheet/index.ts +0 -16
- package/src/components/ui/sheet/sheet-content.svelte +0 -28
- package/src/components/ui/sheet/sheet-description.svelte +0 -22
- package/src/components/ui/sheet/sheet-footer.svelte +0 -22
- package/src/components/ui/sheet/sheet-header.svelte +0 -22
- package/src/components/ui/sheet/sheet-title.svelte +0 -22
- package/src/components/ui/sheet/sheet.svelte +0 -78
- package/src/components/ui/skeleton/index.ts +0 -2
- package/src/components/ui/skeleton/skeleton.svelte +0 -21
- package/src/components/ui/switch/index.ts +0 -2
- package/src/components/ui/switch/switch.svelte +0 -49
- package/src/components/ui/table/index.ts +0 -28
- package/src/components/ui/table/table-body.svelte +0 -15
- package/src/components/ui/table/table-caption.svelte +0 -20
- package/src/components/ui/table/table-cell.svelte +0 -15
- package/src/components/ui/table/table-footer.svelte +0 -20
- package/src/components/ui/table/table-head.svelte +0 -15
- package/src/components/ui/table/table-header.svelte +0 -20
- package/src/components/ui/table/table-row.svelte +0 -15
- package/src/components/ui/table/table.svelte +0 -17
- package/src/components/ui/tabs/index.ts +0 -15
- package/src/components/ui/tabs/tabs-content.svelte +0 -30
- package/src/components/ui/tabs/tabs-list.svelte +0 -26
- package/src/components/ui/tabs/tabs-trigger.svelte +0 -37
- package/src/components/ui/tabs/tabs.svelte +0 -27
- package/src/components/ui/textarea/index.ts +0 -2
- package/src/components/ui/textarea/textarea.svelte +0 -24
- package/src/components/ui/tooltip/index.ts +0 -19
- package/src/components/ui/tooltip/tooltip-content.svelte +0 -52
- package/src/components/ui/tooltip/tooltip-portal.svelte +0 -7
- package/src/components/ui/tooltip/tooltip-provider.svelte +0 -7
- package/src/components/ui/tooltip/tooltip-trigger.svelte +0 -7
- package/src/components/ui/tooltip/tooltip.svelte +0 -10
- package/src/editor-config.svelte.ts +0 -23
- package/src/index.ts +0 -155
- package/src/markdown-deps.d.ts +0 -21
- package/src/router-state.svelte.ts +0 -65
- package/src/types.ts +0 -33
- package/src/utils.ts +0 -28
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { useImport, useCan, t, type UseImportOptions } from '@svadmin/core';
|
|
3
|
-
import { Button } from '../ui/button/index.js';
|
|
4
|
-
import { Upload } from '@lucide/svelte';
|
|
5
|
-
|
|
6
|
-
let { resource, hideText = false, onFinish, accessControl = { enabled: true, hideIfUnauthorized: true }, class: className = '' } = $props<{
|
|
7
|
-
resource: string;
|
|
8
|
-
hideText?: boolean;
|
|
9
|
-
onFinish?: (result: { succeeded: unknown[]; errored: { request: unknown; error: unknown }[] }) => void;
|
|
10
|
-
accessControl?: { enabled?: boolean; hideIfUnauthorized?: boolean };
|
|
11
|
-
class?: string;
|
|
12
|
-
}>();
|
|
13
|
-
|
|
14
|
-
let fileInput: HTMLInputElement | undefined = $state();
|
|
15
|
-
|
|
16
|
-
const importHook = useImport({
|
|
17
|
-
get resource() { return resource; },
|
|
18
|
-
onFinish: (result) => onFinish?.(result),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
function triggerImport() {
|
|
22
|
-
fileInput?.click();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function handleFileChange(e: Event) {
|
|
26
|
-
const input = e.target as HTMLInputElement;
|
|
27
|
-
const file = input.files?.[0];
|
|
28
|
-
if (file) {
|
|
29
|
-
importHook.handleChange({ file });
|
|
30
|
-
input.value = ''; // reset for re-import
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const can = useCan(() => ({ resource, action: 'import', queryOptions: { enabled: accessControl?.enabled ?? true } }));
|
|
35
|
-
const hidden = $derived(accessControl?.hideIfUnauthorized && !can.allowed);
|
|
36
|
-
</script>
|
|
37
|
-
|
|
38
|
-
{#if !hidden}
|
|
39
|
-
<input
|
|
40
|
-
bind:this={fileInput}
|
|
41
|
-
type="file"
|
|
42
|
-
accept=".csv"
|
|
43
|
-
style="display:none"
|
|
44
|
-
onchange={handleFileChange}
|
|
45
|
-
/>
|
|
46
|
-
<Button
|
|
47
|
-
variant="outline"
|
|
48
|
-
size={hideText ? 'icon' : 'sm'}
|
|
49
|
-
class={className}
|
|
50
|
-
disabled={importHook.isLoading || !can.allowed}
|
|
51
|
-
onclick={triggerImport}
|
|
52
|
-
>
|
|
53
|
-
<Upload class="h-4 w-4" />
|
|
54
|
-
{#if !hideText}<span class="ml-1">{t('common.import')}</span>{/if}
|
|
55
|
-
</Button>
|
|
56
|
-
{/if}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { useNavigation, t } from '@svadmin/core';
|
|
3
|
-
import { Button } from '../ui/button/index.js';
|
|
4
|
-
import { List } from '@lucide/svelte';
|
|
5
|
-
|
|
6
|
-
let { resource, hideText = false, class: className = '' } = $props<{
|
|
7
|
-
resource: string;
|
|
8
|
-
hideText?: boolean;
|
|
9
|
-
class?: string;
|
|
10
|
-
}>();
|
|
11
|
-
|
|
12
|
-
const nav = useNavigation();
|
|
13
|
-
</script>
|
|
14
|
-
|
|
15
|
-
<Button
|
|
16
|
-
variant="outline"
|
|
17
|
-
size={hideText ? 'icon' : 'sm'}
|
|
18
|
-
class={className}
|
|
19
|
-
onclick={() => nav.list(resource)}
|
|
20
|
-
>
|
|
21
|
-
<List class="h-4 w-4" />
|
|
22
|
-
{#if !hideText}<span class="ml-1">{resource}</span>{/if}
|
|
23
|
-
</Button>
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { useInvalidate, t } from '@svadmin/core';
|
|
3
|
-
import { Button } from '../ui/button/index.js';
|
|
4
|
-
import { RefreshCw } from '@lucide/svelte';
|
|
5
|
-
|
|
6
|
-
let { resource, hideText = false, class: className = '' } = $props<{
|
|
7
|
-
resource: string;
|
|
8
|
-
hideText?: boolean;
|
|
9
|
-
class?: string;
|
|
10
|
-
}>();
|
|
11
|
-
|
|
12
|
-
const invalidate = useInvalidate();
|
|
13
|
-
let spinning = $state(false);
|
|
14
|
-
let spinTimer: ReturnType<typeof setTimeout> | undefined;
|
|
15
|
-
|
|
16
|
-
$effect(() => {
|
|
17
|
-
return () => { if (spinTimer) clearTimeout(spinTimer); };
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
function refresh() {
|
|
21
|
-
spinning = true;
|
|
22
|
-
invalidate({ resource, invalidates: ['list', 'many'] });
|
|
23
|
-
if (spinTimer) clearTimeout(spinTimer);
|
|
24
|
-
spinTimer = setTimeout(() => { spinning = false; }, 600);
|
|
25
|
-
}
|
|
26
|
-
</script>
|
|
27
|
-
|
|
28
|
-
<Button
|
|
29
|
-
variant="ghost"
|
|
30
|
-
size={hideText ? 'icon' : 'sm'}
|
|
31
|
-
class={className}
|
|
32
|
-
onclick={refresh}
|
|
33
|
-
>
|
|
34
|
-
<RefreshCw class="h-4 w-4 {spinning ? 'animate-spin' : ''}" />
|
|
35
|
-
{#if !hideText}<span class="ml-1">{t('common.retry')}</span>{/if}
|
|
36
|
-
</Button>
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { t } from '@svadmin/core';
|
|
3
|
-
import { Button } from '../ui/button/index.js';
|
|
4
|
-
import { Save, Loader2 } from '@lucide/svelte';
|
|
5
|
-
|
|
6
|
-
let { loading = false, hideText = false, type = 'submit', class: className = '' } = $props<{
|
|
7
|
-
loading?: boolean;
|
|
8
|
-
hideText?: boolean;
|
|
9
|
-
type?: 'submit' | 'button';
|
|
10
|
-
class?: string;
|
|
11
|
-
}>();
|
|
12
|
-
</script>
|
|
13
|
-
|
|
14
|
-
<Button
|
|
15
|
-
{type}
|
|
16
|
-
variant="default"
|
|
17
|
-
size={hideText ? 'icon' : 'default'}
|
|
18
|
-
class={className}
|
|
19
|
-
disabled={loading}
|
|
20
|
-
>
|
|
21
|
-
{#if loading}
|
|
22
|
-
<Loader2 class="h-4 w-4 animate-spin" />
|
|
23
|
-
{:else}
|
|
24
|
-
<Save class="h-4 w-4" />
|
|
25
|
-
{/if}
|
|
26
|
-
{#if !hideText}<span class="ml-1">{t('common.save')}</span>{/if}
|
|
27
|
-
</Button>
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { useNavigation, useCan, t } from '@svadmin/core';
|
|
3
|
-
import { Button } from '../ui/button/index.js';
|
|
4
|
-
import { Eye } from '@lucide/svelte';
|
|
5
|
-
|
|
6
|
-
let { resource, recordItemId, hideText = false, accessControl = { enabled: true, hideIfUnauthorized: true }, class: className = '' } = $props<{
|
|
7
|
-
resource: string;
|
|
8
|
-
recordItemId: string | number;
|
|
9
|
-
hideText?: boolean;
|
|
10
|
-
accessControl?: { enabled?: boolean; hideIfUnauthorized?: boolean };
|
|
11
|
-
class?: string;
|
|
12
|
-
}>();
|
|
13
|
-
|
|
14
|
-
const nav = useNavigation();
|
|
15
|
-
const can = useCan(() => ({ resource, action: 'show', queryOptions: { enabled: accessControl?.enabled ?? true } }));
|
|
16
|
-
const hidden = $derived(accessControl?.hideIfUnauthorized && !can.allowed);
|
|
17
|
-
</script>
|
|
18
|
-
|
|
19
|
-
{#if !hidden}
|
|
20
|
-
|
|
21
|
-
<Button
|
|
22
|
-
variant="ghost"
|
|
23
|
-
size={hideText ? 'icon' : 'sm'}
|
|
24
|
-
class={className}
|
|
25
|
-
disabled={!can.allowed}
|
|
26
|
-
onclick={() => nav.show(resource, recordItemId)}
|
|
27
|
-
>
|
|
28
|
-
<Eye class="h-4 w-4" />
|
|
29
|
-
{#if !hideText}<span class="ml-1">{t('common.detail')}</span>{/if}
|
|
30
|
-
</Button>
|
|
31
|
-
{/if}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export { default as CreateButton } from './CreateButton.svelte';
|
|
2
|
-
export { default as EditButton } from './EditButton.svelte';
|
|
3
|
-
export { default as DeleteButton } from './DeleteButton.svelte';
|
|
4
|
-
export { default as ShowButton } from './ShowButton.svelte';
|
|
5
|
-
export { default as ListButton } from './ListButton.svelte';
|
|
6
|
-
export { default as RefreshButton } from './RefreshButton.svelte';
|
|
7
|
-
export { default as ExportButton } from './ExportButton.svelte';
|
|
8
|
-
export { default as ImportButton } from './ImportButton.svelte';
|
|
9
|
-
export { default as SaveButton } from './SaveButton.svelte';
|
|
10
|
-
export { default as CloneButton } from './CloneButton.svelte';
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
interface DataPoint {
|
|
3
|
-
label: string;
|
|
4
|
-
value: number;
|
|
5
|
-
color?: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
data: DataPoint[];
|
|
10
|
-
height?: number;
|
|
11
|
-
/** Show value labels on top of bars */
|
|
12
|
-
showValues?: boolean;
|
|
13
|
-
class?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
let { data, height = 200, showValues = true, class: className = '' }: Props = $props();
|
|
17
|
-
|
|
18
|
-
const maxValue = $derived(Math.max(...data.map(d => d.value), 1));
|
|
19
|
-
const barWidth = $derived(Math.max(20, Math.min(60, 400 / data.length)));
|
|
20
|
-
const gap = $derived(Math.max(4, barWidth * 0.3));
|
|
21
|
-
const totalWidth = $derived(data.length * (barWidth + gap) - gap + 40);
|
|
22
|
-
</script>
|
|
23
|
-
|
|
24
|
-
<div class="w-full overflow-x-auto {className}">
|
|
25
|
-
<svg viewBox="0 0 {totalWidth} {height + 30}" class="w-full" style="min-width: {Math.min(totalWidth, 300)}px;">
|
|
26
|
-
<!-- Bars -->
|
|
27
|
-
{#each data as point, i}
|
|
28
|
-
{@const barHeight = (point.value / maxValue) * (height - 20)}
|
|
29
|
-
{@const x = 20 + i * (barWidth + gap)}
|
|
30
|
-
{@const y = height - barHeight}
|
|
31
|
-
|
|
32
|
-
<!-- Bar with animation -->
|
|
33
|
-
<rect
|
|
34
|
-
{x}
|
|
35
|
-
{y}
|
|
36
|
-
width={barWidth}
|
|
37
|
-
height={barHeight}
|
|
38
|
-
rx="4"
|
|
39
|
-
fill={point.color || 'var(--primary)'}
|
|
40
|
-
opacity="0.85"
|
|
41
|
-
class="transition-all duration-500 hover:opacity-100"
|
|
42
|
-
>
|
|
43
|
-
<animate attributeName="height" from="0" to={barHeight} dur="0.6s" fill="freeze" calcMode="spline" keySplines="0.16 1 0.3 1" />
|
|
44
|
-
<animate attributeName="y" from={height} to={y} dur="0.6s" fill="freeze" calcMode="spline" keySplines="0.16 1 0.3 1" />
|
|
45
|
-
</rect>
|
|
46
|
-
|
|
47
|
-
<!-- Value label -->
|
|
48
|
-
{#if showValues}
|
|
49
|
-
<text
|
|
50
|
-
x={x + barWidth / 2}
|
|
51
|
-
y={y - 6}
|
|
52
|
-
text-anchor="middle"
|
|
53
|
-
fill="var(--muted-foreground)"
|
|
54
|
-
font-size="11"
|
|
55
|
-
font-weight="500"
|
|
56
|
-
>{point.value}</text>
|
|
57
|
-
{/if}
|
|
58
|
-
|
|
59
|
-
<!-- X-axis label -->
|
|
60
|
-
<text
|
|
61
|
-
x={x + barWidth / 2}
|
|
62
|
-
y={height + 16}
|
|
63
|
-
text-anchor="middle"
|
|
64
|
-
fill="var(--muted-foreground)"
|
|
65
|
-
font-size="10"
|
|
66
|
-
>{point.label}</text>
|
|
67
|
-
{/each}
|
|
68
|
-
</svg>
|
|
69
|
-
</div>
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
interface DataPoint {
|
|
3
|
-
label: string;
|
|
4
|
-
value: number;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
interface Props {
|
|
8
|
-
data: DataPoint[];
|
|
9
|
-
height?: number;
|
|
10
|
-
color?: string;
|
|
11
|
-
/** Show dots at data points */
|
|
12
|
-
showDots?: boolean;
|
|
13
|
-
/** Fill area under the line */
|
|
14
|
-
fill?: boolean;
|
|
15
|
-
class?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
let { data, height = 200, color = 'var(--primary)', showDots = true, fill = true, class: className = '' }: Props = $props();
|
|
19
|
-
|
|
20
|
-
const padding = { top: 20, right: 20, bottom: 30, left: 40 };
|
|
21
|
-
const chartWidth = $derived(Math.max(data.length * 60, 300));
|
|
22
|
-
const chartHeight = $derived(height - padding.top - padding.bottom);
|
|
23
|
-
|
|
24
|
-
const maxValue = $derived(Math.max(...data.map(d => d.value), 1));
|
|
25
|
-
const minValue = $derived(Math.min(...data.map(d => d.value), 0));
|
|
26
|
-
const range = $derived(maxValue - minValue || 1);
|
|
27
|
-
|
|
28
|
-
function getX(i: number): number {
|
|
29
|
-
if (data.length <= 1) return padding.left + chartWidth / 2;
|
|
30
|
-
return padding.left + (i / (data.length - 1)) * (chartWidth - padding.left - padding.right);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function getY(value: number): number {
|
|
34
|
-
return padding.top + chartHeight - ((value - minValue) / range) * chartHeight;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const linePath = $derived(
|
|
38
|
-
data.map((d, i) => `${i === 0 ? 'M' : 'L'} ${getX(i)} ${getY(d.value)}`).join(' ')
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
const areaPath = $derived(
|
|
42
|
-
linePath + ` L ${getX(data.length - 1)} ${padding.top + chartHeight} L ${getX(0)} ${padding.top + chartHeight} Z`
|
|
43
|
-
);
|
|
44
|
-
</script>
|
|
45
|
-
|
|
46
|
-
<div class="w-full overflow-x-auto {className}">
|
|
47
|
-
<svg viewBox="0 0 {chartWidth} {height}" class="w-full" style="min-width: {Math.min(chartWidth, 300)}px;">
|
|
48
|
-
<!-- Grid lines -->
|
|
49
|
-
{#each [0, 0.25, 0.5, 0.75, 1] as ratio}
|
|
50
|
-
{@const y = padding.top + chartHeight * (1 - ratio)}
|
|
51
|
-
<line
|
|
52
|
-
x1={padding.left}
|
|
53
|
-
y1={y}
|
|
54
|
-
x2={chartWidth - padding.right}
|
|
55
|
-
y2={y}
|
|
56
|
-
stroke="var(--border)"
|
|
57
|
-
stroke-width="1"
|
|
58
|
-
stroke-dasharray={ratio === 0 ? 'none' : '4 4'}
|
|
59
|
-
opacity="0.5"
|
|
60
|
-
/>
|
|
61
|
-
<text x={padding.left - 8} y={y + 4} text-anchor="end" fill="var(--muted-foreground)" font-size="10">
|
|
62
|
-
{Math.round(minValue + range * ratio)}
|
|
63
|
-
</text>
|
|
64
|
-
{/each}
|
|
65
|
-
|
|
66
|
-
<!-- Area fill -->
|
|
67
|
-
{#if fill && data.length > 1}
|
|
68
|
-
<path
|
|
69
|
-
d={areaPath}
|
|
70
|
-
fill={color}
|
|
71
|
-
opacity="0.08"
|
|
72
|
-
/>
|
|
73
|
-
{/if}
|
|
74
|
-
|
|
75
|
-
<!-- Line -->
|
|
76
|
-
{#if data.length > 1}
|
|
77
|
-
<path
|
|
78
|
-
d={linePath}
|
|
79
|
-
fill="none"
|
|
80
|
-
stroke={color}
|
|
81
|
-
stroke-width="2"
|
|
82
|
-
stroke-linecap="round"
|
|
83
|
-
stroke-linejoin="round"
|
|
84
|
-
>
|
|
85
|
-
<animate attributeName="stroke-dashoffset" from="2000" to="0" dur="1s" fill="freeze" />
|
|
86
|
-
</path>
|
|
87
|
-
{/if}
|
|
88
|
-
|
|
89
|
-
<!-- Dots -->
|
|
90
|
-
{#if showDots}
|
|
91
|
-
{#each data as point, i}
|
|
92
|
-
<circle
|
|
93
|
-
cx={getX(i)}
|
|
94
|
-
cy={getY(point.value)}
|
|
95
|
-
r="4"
|
|
96
|
-
fill="var(--background)"
|
|
97
|
-
stroke={color}
|
|
98
|
-
stroke-width="2"
|
|
99
|
-
class="transition-all duration-200 hover:r-[6]"
|
|
100
|
-
/>
|
|
101
|
-
{/each}
|
|
102
|
-
{/if}
|
|
103
|
-
|
|
104
|
-
<!-- X-axis labels -->
|
|
105
|
-
{#each data as point, i}
|
|
106
|
-
<text
|
|
107
|
-
x={getX(i)}
|
|
108
|
-
y={height - 4}
|
|
109
|
-
text-anchor="middle"
|
|
110
|
-
fill="var(--muted-foreground)"
|
|
111
|
-
font-size="10"
|
|
112
|
-
>{point.label}</text>
|
|
113
|
-
{/each}
|
|
114
|
-
</svg>
|
|
115
|
-
</div>
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
interface Slice {
|
|
3
|
-
label: string;
|
|
4
|
-
value: number;
|
|
5
|
-
color: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
data: Slice[];
|
|
10
|
-
size?: number;
|
|
11
|
-
/** Donut hole ratio (0 = pie, 0.6 = donut) */
|
|
12
|
-
donut?: number;
|
|
13
|
-
class?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
let { data, size = 200, donut = 0.6, class: className = '' }: Props = $props();
|
|
17
|
-
|
|
18
|
-
const total = $derived(data.reduce((sum, d) => sum + d.value, 0) || 1);
|
|
19
|
-
const center = $derived(size / 2);
|
|
20
|
-
const radius = $derived(size / 2 - 8);
|
|
21
|
-
const innerRadius = $derived(radius * donut);
|
|
22
|
-
|
|
23
|
-
function getArcPath(startAngle: number, endAngle: number, outer: number, inner: number): string {
|
|
24
|
-
const startOuter = polarToCartesian(center, center, outer, endAngle);
|
|
25
|
-
const endOuter = polarToCartesian(center, center, outer, startAngle);
|
|
26
|
-
const startInner = polarToCartesian(center, center, inner, startAngle);
|
|
27
|
-
const endInner = polarToCartesian(center, center, inner, endAngle);
|
|
28
|
-
const largeArc = endAngle - startAngle > 180 ? 1 : 0;
|
|
29
|
-
|
|
30
|
-
return [
|
|
31
|
-
`M ${startOuter.x} ${startOuter.y}`,
|
|
32
|
-
`A ${outer} ${outer} 0 ${largeArc} 0 ${endOuter.x} ${endOuter.y}`,
|
|
33
|
-
inner > 0 ? `L ${startInner.x} ${startInner.y}` : `L ${center} ${center}`,
|
|
34
|
-
inner > 0 ? `A ${inner} ${inner} 0 ${largeArc} 1 ${endInner.x} ${endInner.y}` : '',
|
|
35
|
-
'Z',
|
|
36
|
-
].join(' ');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function polarToCartesian(cx: number, cy: number, r: number, angleDeg: number) {
|
|
40
|
-
const rad = ((angleDeg - 90) * Math.PI) / 180;
|
|
41
|
-
return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const slices = $derived.by(() => {
|
|
45
|
-
let currentAngle = 0;
|
|
46
|
-
return data.map(d => {
|
|
47
|
-
const angle = (d.value / total) * 360;
|
|
48
|
-
const slice = {
|
|
49
|
-
...d,
|
|
50
|
-
startAngle: currentAngle,
|
|
51
|
-
endAngle: currentAngle + angle,
|
|
52
|
-
percentage: ((d.value / total) * 100).toFixed(1),
|
|
53
|
-
};
|
|
54
|
-
currentAngle += angle;
|
|
55
|
-
return slice;
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
</script>
|
|
59
|
-
|
|
60
|
-
<div class="inline-flex flex-col items-center gap-3 {className}">
|
|
61
|
-
<svg viewBox="0 0 {size} {size}" width={size} height={size}>
|
|
62
|
-
{#each slices as slice, i}
|
|
63
|
-
<path
|
|
64
|
-
d={getArcPath(slice.startAngle, slice.endAngle, radius, innerRadius)}
|
|
65
|
-
fill={slice.color}
|
|
66
|
-
class="transition-all duration-200 hover:opacity-80"
|
|
67
|
-
stroke="var(--background)"
|
|
68
|
-
stroke-width="2"
|
|
69
|
-
>
|
|
70
|
-
<animate attributeName="opacity" from="0" to="1" dur="0.4s" begin="{i * 0.1}s" fill="freeze" />
|
|
71
|
-
</path>
|
|
72
|
-
{/each}
|
|
73
|
-
|
|
74
|
-
<!-- Center label (donut only) -->
|
|
75
|
-
{#if donut > 0}
|
|
76
|
-
<text x={center} y={center - 6} text-anchor="middle" fill="var(--foreground)" font-size="20" font-weight="600">
|
|
77
|
-
{total.toLocaleString()}
|
|
78
|
-
</text>
|
|
79
|
-
<text x={center} y={center + 12} text-anchor="middle" fill="var(--muted-foreground)" font-size="11">
|
|
80
|
-
Total
|
|
81
|
-
</text>
|
|
82
|
-
{/if}
|
|
83
|
-
</svg>
|
|
84
|
-
|
|
85
|
-
<!-- Legend -->
|
|
86
|
-
<div class="flex flex-wrap justify-center gap-x-4 gap-y-1">
|
|
87
|
-
{#each slices as slice}
|
|
88
|
-
<div class="flex items-center gap-1.5 text-xs">
|
|
89
|
-
<span class="h-2.5 w-2.5 rounded-full" style="background-color: {slice.color};"></span>
|
|
90
|
-
<span class="text-muted-foreground">{slice.label}</span>
|
|
91
|
-
<span class="font-medium text-foreground">{slice.percentage}%</span>
|
|
92
|
-
</div>
|
|
93
|
-
{/each}
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
// Shared field type → display component mapping
|
|
2
|
-
// Used by both ShowPage and FieldRenderer to avoid duplicate if/else chains.
|
|
3
|
-
// Components are lazily resolved by field type at runtime.
|
|
4
|
-
|
|
5
|
-
import type { Component } from 'svelte';
|
|
6
|
-
|
|
7
|
-
export interface FieldComponentMap {
|
|
8
|
-
[fieldType: string]: Component<any>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// Default display component map (read-only field display)
|
|
12
|
-
// Dynamically imports components only when first used
|
|
13
|
-
const displayCache = new Map<string, Component<any>>();
|
|
14
|
-
|
|
15
|
-
// Pre-import all field display components (tree-shaken by bundler)
|
|
16
|
-
import BooleanField from './fields/BooleanField.svelte';
|
|
17
|
-
import ImageField from './fields/ImageField.svelte';
|
|
18
|
-
import TagField from './fields/TagField.svelte';
|
|
19
|
-
import DateField from './fields/DateField.svelte';
|
|
20
|
-
import EmailField from './fields/EmailField.svelte';
|
|
21
|
-
import UrlField from './fields/UrlField.svelte';
|
|
22
|
-
import SelectField from './fields/SelectField.svelte';
|
|
23
|
-
import MultiSelectField from './fields/MultiSelectField.svelte';
|
|
24
|
-
import RelationField from './fields/RelationField.svelte';
|
|
25
|
-
import JsonField from './fields/JsonField.svelte';
|
|
26
|
-
import RichTextField from './fields/RichTextField.svelte';
|
|
27
|
-
import NumberField from './fields/NumberField.svelte';
|
|
28
|
-
import FileField from './fields/FileField.svelte';
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Built-in display components for show/detail pages.
|
|
32
|
-
* Keys match FieldDefinition.type values.
|
|
33
|
-
*/
|
|
34
|
-
export const builtinDisplayComponents: FieldComponentMap = {
|
|
35
|
-
boolean: BooleanField,
|
|
36
|
-
image: ImageField,
|
|
37
|
-
images: ImageField,
|
|
38
|
-
tags: TagField,
|
|
39
|
-
date: DateField,
|
|
40
|
-
email: EmailField,
|
|
41
|
-
url: UrlField,
|
|
42
|
-
select: SelectField,
|
|
43
|
-
multiselect: MultiSelectField,
|
|
44
|
-
relation: RelationField,
|
|
45
|
-
json: JsonField,
|
|
46
|
-
richtext: RichTextField,
|
|
47
|
-
textarea: RichTextField,
|
|
48
|
-
number: NumberField,
|
|
49
|
-
file: FileField,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
// ─── Custom component registry ─────────────────────────────────
|
|
53
|
-
// Users can register custom field display components at runtime
|
|
54
|
-
|
|
55
|
-
const customDisplayComponents = new Map<string, Component<any>>();
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Register a custom field display component for a given field type.
|
|
59
|
-
* The component will be used by ShowPage and any other display contexts.
|
|
60
|
-
*/
|
|
61
|
-
export function registerDisplayComponent(fieldType: string, component: Component<any>): void {
|
|
62
|
-
customDisplayComponents.set(fieldType, component);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Get the display component for a field type.
|
|
67
|
-
* Custom components take precedence over built-in ones.
|
|
68
|
-
* Returns undefined if no component is registered for the type.
|
|
69
|
-
*/
|
|
70
|
-
export function getDisplayComponent(fieldType: string): Component<any> | undefined {
|
|
71
|
-
return customDisplayComponents.get(fieldType) ?? builtinDisplayComponents[fieldType];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Check if a field type has a display component (custom or built-in).
|
|
76
|
-
*/
|
|
77
|
-
export function hasDisplayComponent(fieldType: string): boolean {
|
|
78
|
-
return customDisplayComponents.has(fieldType) || fieldType in builtinDisplayComponents;
|
|
79
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { FieldDefinition } from '@svadmin/core';
|
|
3
|
-
import { t } from '@svadmin/core/i18n';
|
|
4
|
-
import { Button } from '../ui/button/index.js';
|
|
5
|
-
import { Plus, Trash2 } from '@lucide/svelte';
|
|
6
|
-
import TooltipButton from '../TooltipButton.svelte';
|
|
7
|
-
import FieldRenderer from '../FieldRenderer.svelte';
|
|
8
|
-
|
|
9
|
-
let { field, value, onchange } = $props<{
|
|
10
|
-
field: FieldDefinition;
|
|
11
|
-
value: unknown;
|
|
12
|
-
onchange: (val: unknown) => void;
|
|
13
|
-
}>();
|
|
14
|
-
|
|
15
|
-
const arrayVal = $derived(Array.isArray(value) ? value : []);
|
|
16
|
-
const subFields = $derived(field.subFields || []);
|
|
17
|
-
|
|
18
|
-
function getDefaultForType(type: string): unknown {
|
|
19
|
-
switch (type) {
|
|
20
|
-
case 'text': case 'textarea': case 'richtext': case 'image': return '';
|
|
21
|
-
case 'number': return 0;
|
|
22
|
-
case 'boolean': return false;
|
|
23
|
-
case 'tags': case 'images': case 'multiselect': case 'array': return [];
|
|
24
|
-
case 'json': return {};
|
|
25
|
-
default: return '';
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function handleAdd() {
|
|
30
|
-
const newItem: Record<string, unknown> = {};
|
|
31
|
-
for (const f of subFields) {
|
|
32
|
-
newItem[f.key] = f.defaultValue ?? getDefaultForType(f.type);
|
|
33
|
-
}
|
|
34
|
-
onchange([...arrayVal, newItem]);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function handleRemove(index: number) {
|
|
38
|
-
onchange(arrayVal.filter((_, i) => i !== index));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function handleChange(index: number, key: string, val: unknown) {
|
|
42
|
-
const next = [...arrayVal];
|
|
43
|
-
next[index] = { ...next[index], [key]: val };
|
|
44
|
-
onchange(next);
|
|
45
|
-
}
|
|
46
|
-
</script>
|
|
47
|
-
|
|
48
|
-
<div class="space-y-4 rounded-lg border border-dashed p-4">
|
|
49
|
-
<div class="flex items-center justify-between">
|
|
50
|
-
<div class="text-sm font-medium text-foreground">
|
|
51
|
-
{field.label}
|
|
52
|
-
{#if field.required}
|
|
53
|
-
<span class="text-destructive">*</span>
|
|
54
|
-
{/if}
|
|
55
|
-
</div>
|
|
56
|
-
<Button variant="outline" size="sm" type="button" onclick={handleAdd}>
|
|
57
|
-
<Plus class="h-3.5 w-3.5 mr-1" /> {t('common.add') || 'Add'}
|
|
58
|
-
</Button>
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
<div class="space-y-4 pt-2">
|
|
62
|
-
{#each arrayVal as item, i}
|
|
63
|
-
<div class="relative rounded-md border bg-card p-4 pt-6 shadow-sm">
|
|
64
|
-
<div class="absolute right-2 top-2">
|
|
65
|
-
<TooltipButton tooltip={t('common.remove') || 'Remove'} variant="ghost" size="icon" class="h-8 w-8 text-muted-foreground hover:text-destructive transition-colors" onclick={() => handleRemove(i)}>
|
|
66
|
-
<Trash2 class="h-4 w-4" />
|
|
67
|
-
</TooltipButton>
|
|
68
|
-
</div>
|
|
69
|
-
<div class="grid gap-5">
|
|
70
|
-
{#each subFields as subField (subField.key)}
|
|
71
|
-
<FieldRenderer
|
|
72
|
-
field={subField}
|
|
73
|
-
value={item[subField.key]}
|
|
74
|
-
onchange={(val) => handleChange(i, subField.key, val)}
|
|
75
|
-
/>
|
|
76
|
-
{/each}
|
|
77
|
-
</div>
|
|
78
|
-
</div>
|
|
79
|
-
{/each}
|
|
80
|
-
{#if arrayVal.length === 0}
|
|
81
|
-
<div class="flex flex-col items-center justify-center py-6 text-sm text-muted-foreground bg-muted/10 rounded-md">
|
|
82
|
-
<span>{t('common.noData') || 'No items added yet.'}</span>
|
|
83
|
-
</div>
|
|
84
|
-
{/if}
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
interface Props { value: boolean | null | undefined; trueIcon?: string; falseIcon?: string }
|
|
3
|
-
let { value, trueIcon = '✓', falseIcon = '✗' }: Props = $props();
|
|
4
|
-
</script>
|
|
5
|
-
{#if value}
|
|
6
|
-
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full text-sm bg-success/10 text-success">
|
|
7
|
-
{trueIcon}
|
|
8
|
-
</span>
|
|
9
|
-
{:else}
|
|
10
|
-
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full text-sm bg-destructive/10 text-destructive">
|
|
11
|
-
{falseIcon}
|
|
12
|
-
</span>
|
|
13
|
-
{/if}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
interface Props { value: string | number | Date | null | undefined; locale?: string; options?: Intl.DateTimeFormatOptions }
|
|
3
|
-
let { value, locale = 'en-US', options = { dateStyle: 'medium' } }: Props = $props();
|
|
4
|
-
const formatted = $derived(value != null ? new Intl.DateTimeFormat(locale, options).format(new Date(value as string | number | Date)) : '—');
|
|
5
|
-
</script>
|
|
6
|
-
<span class="field-date">{formatted}</span>
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
interface Props { value: string | null | undefined }
|
|
3
|
-
let { value }: Props = $props();
|
|
4
|
-
</script>
|
|
5
|
-
{#if value}
|
|
6
|
-
<a href="mailto:{value}" class="text-primary hover:underline no-underline">{value}</a>
|
|
7
|
-
{:else}
|
|
8
|
-
<span class="text-muted-foreground">—</span>
|
|
9
|
-
{/if}
|