feat(frontend): add shared Badge component and dateFormat utility

Consolidation infrastructure:
- Badge.svelte: compact inline badge/chip with variant (success/warning/destructive/info/primary/muted), size, and dot mode
- dateFormat.ts: formatDate, formatDateTime, formatRelativeTime, formatFileSize — replaces 6+ local definitions
- Export Badge from $lib/ui index
This commit is contained in:
2026-06-17 14:05:37 +03:00
parent 149c05bf1c
commit 358f38660a
3 changed files with 124 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
<!-- #region UI.Badge [C:2] [TYPE Component] [SEMANTICS badge,status,label,chip,tag,ui,atom] -->
<!-- @ingroup UI -->
<!-- @BRIEF Compact inline badge/chip for status, severity, and tag labels. -->
<!-- @LAYER UI -->
<!-- @RELATION USED_BY -> [Dashboard.RepositoryGrid] -->
<!-- @RELATION USED_BY -> [TaskRunner] -->
<!-- @RELATION USED_BY -> [ValidationFindingsPanel] -->
<!-- @UX_STATE Default -> Colored pill with text label. -->
<!-- @UX_STATE Dot -> Colored dot + label (for connection/task status). -->
<script lang="ts">
let {
variant = "muted" as "success" | "warning" | "destructive" | "info" | "primary" | "muted",
size = "md" as "sm" | "md",
dot = false,
class: className = "",
children,
} = $props();
const variantClasses: Record<string, string> = {
success: "bg-success-light text-success",
warning: "bg-warning-light text-warning",
destructive: "bg-destructive-light text-destructive",
info: "bg-info-light text-info",
primary: "bg-primary-light text-primary",
muted: "bg-surface-muted text-text-muted",
};
const sizeClasses: Record<string, string> = {
sm: "px-2 py-0.5",
md: "px-2.5 py-1",
};
const dotColorClasses: Record<string, string> = {
success: "bg-success",
warning: "bg-warning",
destructive: "bg-destructive",
info: "bg-info",
primary: "bg-primary-ring",
muted: "bg-text-muted",
};
</script>
<span class="inline-flex items-center gap-1.5 {className}">
{#if dot}
<span class="h-2.5 w-2.5 rounded-full {dotColorClasses[variant] || dotColorClasses.muted}"></span>
{/if}
<span class="rounded-full {sizeClasses[size] || sizeClasses.md} text-xs font-medium {variantClasses[variant] || variantClasses.muted}">
{@render children?.()}
</span>
</span>
<!-- #endregion UI.Badge -->

View File

@@ -16,6 +16,7 @@ export { default as EmptyState } from './EmptyState.svelte';
export { default as LanguageSwitcher } from './LanguageSwitcher.svelte'; export { default as LanguageSwitcher } from './LanguageSwitcher.svelte';
export { default as Icon } from './Icon.svelte'; export { default as Icon } from './Icon.svelte';
export { default as HelpTooltip } from './HelpTooltip.svelte'; export { default as HelpTooltip } from './HelpTooltip.svelte';
export { default as Badge } from './Badge.svelte';
// [/SECTION: EXPORTS] // [/SECTION: EXPORTS]
// #endregion ui:Module // #endregion ui:Module

View File

@@ -0,0 +1,72 @@
// #region Utils.DateFormat [C:2] [TYPE Module] [SEMANTICS date,format,utility,helper]
// @ingroup Utils
// @BRIEF Centralized date/time formatting utilities. Replaces 6+ local formatDate definitions.
// @LAYER Infra
// @RATIONALE 6 files defined independent formatDate functions and 20+ files use inline new Date().toLocaleDateString().
// Consolidating into one module ensures consistent locale-aware formatting and reduces duplication.
// #region formatDate:Function [TYPE Function]
// @BRIEF Format ISO date string to localized short date (e.g. "15 Jan 2025").
export function formatDate(value: string | Date | null | undefined): string {
if (!value) return "";
const d = typeof value === "string" ? new Date(value) : value;
if (isNaN(d.getTime())) return "";
return d.toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
}
// #endregion formatDate:Function
// #region formatDateTime:Function [TYPE Function]
// @BRIEF Format ISO date string to localized date+time (e.g. "15 Jan 2025, 14:30").
export function formatDateTime(value: string | Date | null | undefined): string {
if (!value) return "";
const d = typeof value === "string" ? new Date(value) : value;
if (isNaN(d.getTime())) return "";
return d.toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
// #endregion formatDateTime:Function
// #region formatRelativeTime:Function [TYPE Function]
// @BRIEF Format date to relative time string (e.g. "2 hours ago", "just now").
export function formatRelativeTime(value: string | Date | null | undefined): string {
if (!value) return "";
const d = typeof value === "string" ? new Date(value) : value;
if (isNaN(d.getTime())) return "";
const now = Date.now();
const diffMs = now - d.getTime();
const diffSec = Math.floor(diffMs / 1000);
const diffMin = Math.floor(diffSec / 60);
const diffHour = Math.floor(diffMin / 60);
const diffDay = Math.floor(diffHour / 24);
if (diffSec < 60) return "just now";
if (diffMin < 60) return `${diffMin}m ago`;
if (diffHour < 24) return `${diffHour}h ago`;
if (diffDay < 30) return `${diffDay}d ago`;
return formatDate(d);
}
// #endregion formatRelativeTime:Function
// #region formatFileSize:Function [TYPE Function]
// @BRIEF Format bytes to human-readable size (e.g. "1.5 MB").
export function formatFileSize(bytes: number | null | undefined): string {
if (bytes == null || bytes < 0) return "";
if (bytes === 0) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
const val = bytes / Math.pow(1024, i);
return `${val.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
}
// #endregion formatFileSize:Function
// #endregion Utils.DateFormat