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:
51
frontend/src/lib/ui/Badge.svelte
Normal file
51
frontend/src/lib/ui/Badge.svelte
Normal 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 -->
|
||||
@@ -16,6 +16,7 @@ export { default as EmptyState } from './EmptyState.svelte';
|
||||
export { default as LanguageSwitcher } from './LanguageSwitcher.svelte';
|
||||
export { default as Icon } from './Icon.svelte';
|
||||
export { default as HelpTooltip } from './HelpTooltip.svelte';
|
||||
export { default as Badge } from './Badge.svelte';
|
||||
// [/SECTION: EXPORTS]
|
||||
|
||||
// #endregion ui:Module
|
||||
72
frontend/src/lib/utils/dateFormat.ts
Normal file
72
frontend/src/lib/utils/dateFormat.ts
Normal 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
|
||||
Reference in New Issue
Block a user