Compare commits

...

1 commit

Author SHA1 Message Date
e4abb59e83 web: Add optional breadcrumb to PageTitle
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-16 02:28:47 +07:00

View file

@ -24,6 +24,11 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
interface BreadcrumbItem {
label: string;
href?: string;
}
interface Props { interface Props {
/** Main section title (e.g. "Zone Editor", "History") */ /** Main section title (e.g. "Zone Editor", "History") */
title: string; title: string;
@ -31,11 +36,13 @@
domain?: string; domain?: string;
/** Optional subtitle or contextual hint below the title */ /** Optional subtitle or contextual hint below the title */
subtitle?: string; subtitle?: string;
/** Breadcrumb navigation trail; last item is the current page */
breadcrumb?: BreadcrumbItem[];
/** Optional slot for status badges, health checks, or summary indicators */ /** Optional slot for status badges, health checks, or summary indicators */
children?: Snippet; children?: Snippet;
} }
let { title, domain, subtitle, children }: Props = $props(); let { title, domain, subtitle, breadcrumb, children }: Props = $props();
</script> </script>
<header class="page-title mb-4"> <header class="page-title mb-4">
@ -48,6 +55,21 @@
{#if subtitle} {#if subtitle}
<p class="page-title-subtitle text-muted">{subtitle}</p> <p class="page-title-subtitle text-muted">{subtitle}</p>
{/if} {/if}
{#if breadcrumb && breadcrumb.length > 0}
<nav aria-label="Breadcrumb">
<ol class="breadcrumb mb-0 mt-2">
{#each breadcrumb as item, i}
<li class="breadcrumb-item" class:active={i === breadcrumb.length - 1}>
{#if item.href && i !== breadcrumb.length - 1}
<a href={item.href}>{item.label}</a>
{:else}
<span>{item.label}</span>
{/if}
</li>
{/each}
</ol>
</nav>
{/if}
</div> </div>
{#if children} {#if children}
<div class="page-title-summary"> <div class="page-title-summary">
@ -63,6 +85,28 @@
border-bottom: 1px solid rgba(0, 0, 0, 0.07); border-bottom: 1px solid rgba(0, 0, 0, 0.07);
} }
/* Breadcrumb */
nav {
--bs-breadcrumb-divider: ">";
font-size: 0.85rem;
}
.breadcrumb-item a {
text-decoration: none;
}
.breadcrumb-item a {
text-decoration: none;
opacity: 0.75;
transition: opacity 0.15s ease;
}
.breadcrumb-item a:hover {
opacity: 1;
text-decoration: underline;
text-underline-offset: 2px;
}
/* Title block */
.page-title-text::before { .page-title-text::before {
content: ""; content: "";
display: block; display: block;