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 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
|
||||||
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