112 lines
2.9 KiB
Svelte
112 lines
2.9 KiB
Svelte
<!-- [DEF:Breadcrumbs:Component] -->
|
|
<script>
|
|
/**
|
|
* @TIER: STANDARD
|
|
* @PURPOSE: Display page hierarchy navigation
|
|
* @LAYER: UI
|
|
* @RELATION: DEPENDS_ON -> page store
|
|
* @INVARIANT: Always shows current page path
|
|
*
|
|
* @UX_STATE: Idle -> Breadcrumbs showing current path
|
|
* @UX_FEEDBACK: Hover on breadcrumb shows clickable state
|
|
* @UX_RECOVERY: Click breadcrumb to navigate
|
|
*/
|
|
|
|
import { page } from "$app/stores";
|
|
import { t, _ } from "$lib/i18n";
|
|
|
|
let { maxVisible = 3 } = $props();
|
|
|
|
// Breadcrumb items derived from current path
|
|
let breadcrumbItems = $derived(
|
|
getBreadcrumbs($page?.url?.pathname || "/", maxVisible),
|
|
);
|
|
|
|
/**
|
|
* Generate breadcrumb items from path
|
|
* @param {string} pathname - Current path
|
|
* @returns {Array} Array of breadcrumb items
|
|
*/
|
|
function getBreadcrumbs(pathname, maxVisible = 3) {
|
|
const segments = pathname.split("/").filter(Boolean);
|
|
const allItems = [{ label: "Home", path: "/" }];
|
|
|
|
let currentPath = "";
|
|
segments.forEach((segment, index) => {
|
|
currentPath += `/${segment}`;
|
|
const label = formatBreadcrumbLabel(segment);
|
|
allItems.push({
|
|
label,
|
|
path: currentPath,
|
|
isLast: index === segments.length - 1,
|
|
});
|
|
});
|
|
|
|
if (allItems.length > maxVisible) {
|
|
const firstItem = allItems[0];
|
|
const itemsToShow = [];
|
|
itemsToShow.push(firstItem);
|
|
itemsToShow.push({ isEllipsis: true });
|
|
|
|
const startFromIndex = allItems.length - (maxVisible - 1);
|
|
for (let i = startFromIndex; i < allItems.length; i++) {
|
|
itemsToShow.push(allItems[i]);
|
|
}
|
|
return itemsToShow;
|
|
}
|
|
|
|
return allItems;
|
|
}
|
|
|
|
/**
|
|
* Format segment to readable label
|
|
* @param {string} segment - URL segment
|
|
* @returns {string} Formatted label
|
|
*/
|
|
function formatBreadcrumbLabel(segment) {
|
|
const specialCases = {
|
|
dashboards: "nav.dashboard",
|
|
datasets: "nav.tools_mapper",
|
|
storage: "nav.tools_storage",
|
|
admin: "nav.admin",
|
|
settings: "nav.settings",
|
|
git: "nav.git",
|
|
};
|
|
|
|
if (specialCases[segment]) {
|
|
return _(specialCases[segment]) || segment;
|
|
}
|
|
|
|
return segment
|
|
.split("-")
|
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(" ");
|
|
}
|
|
</script>
|
|
|
|
<nav
|
|
class="flex items-center space-x-2 text-sm text-gray-600"
|
|
aria-label="Breadcrumb navigation"
|
|
>
|
|
{#each breadcrumbItems as item, index}
|
|
<div class="flex items-center">
|
|
{#if item.isEllipsis}
|
|
<span class="text-gray-400">...</span>
|
|
{:else if item.isLast}
|
|
<span class="text-gray-900 font-medium">{item.label}</span>
|
|
{:else}
|
|
<a
|
|
href={item.path}
|
|
class="hover:text-primary hover:underline cursor-pointer transition-colors"
|
|
>{item.label}</a
|
|
>
|
|
{/if}
|
|
</div>
|
|
{#if index < breadcrumbItems.length - 1}
|
|
<span class="text-gray-400">/</span>
|
|
{/if}
|
|
{/each}
|
|
</nav>
|
|
|
|
<!-- [/DEF:Breadcrumbs:Component] -->
|