Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions docs/src/components/Breadcrumbs.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
---
/**
* Breadcrumbs.astro
*
* Auto-generates ancestor breadcrumb trail from the current URL path.
* Always shows at least the section name (e.g. "Components", "Get Started").
* Use parentGroup for logical parents not reflected in the URL (e.g. component category).
*/

interface Props {
/** Optional logical parent not reflected in the URL (e.g. component category) */
parentGroup?: { label: string; url?: string };
}

const { parentGroup } = Astro.props;
const pathname = Astro.url.pathname.replace(/\/$/, '');
const segments = pathname.split('/').filter(Boolean);

// Show on any page within a section
const showBreadcrumbs = segments.length >= 1;

// Build set of valid page routes from the pages directory
const pageFiles = import.meta.glob('/src/pages/**/*.{astro,md,mdx}', { eager: false });
const validPaths = new Set(
Object.keys(pageFiles).map(file =>
file
.replace('/src/pages', '')
.replace(/\/index\.(astro|md|mdx)$/, '')
.replace(/\.(astro|md|mdx)$/, '')
.replace(/\[.*?\]/g, '*') // dynamic routes aren't valid intermediate targets
)
);

// Known labels for path segments
const SEGMENT_LABELS: Record<string, string> = {
"get-started": "Get Started",
"foundations": "Foundations",
"components": "Components",
"tokens": "Tokens",
"examples": "Examples",
"designers": "Designers",
"developers": "Developers",
};

function segmentToLabel(segment: string): string {
if (SEGMENT_LABELS[segment]) return SEGMENT_LABELS[segment];
// Fallback: kebab-case to sentence case
return segment.split('-').map((w, i) =>
i === 0 ? w.charAt(0).toUpperCase() + w.slice(1) : w
).join(' ');
}

// Build ancestor crumbs: all segments except the last, but always include the section
const crumbs: Array<{ label: string; url: string; hasPage: boolean }> = [];
let path = '';
const crumbCount = Math.max(1, segments.length - 1);
for (let i = 0; i < crumbCount; i++) {
path += '/' + segments[i];
crumbs.push({ label: segmentToLabel(segments[i]), url: path, hasPage: validPaths.has(path) });
}
---

{showBreadcrumbs && (
<nav class="breadcrumbs" aria-label="Breadcrumb">
<ol>
<li>
<a href="/">Design System</a>
<span class="separator" aria-hidden="true">/</span>
</li>
{crumbs.map((crumb, i) => (
<li>
{/* TODO: Swap to <goa-link> once underline control lands (#3504) */}
{crumb.hasPage ? (
<a href={crumb.url}>{crumb.label}</a>
) : (
<span>{crumb.label}</span>
)}
<span class="separator" aria-hidden="true">/</span>
</li>
))}
{parentGroup && (
<li>
{parentGroup.url ? (
<a href={parentGroup.url}>{parentGroup.label}</a>
) : (
<span>{parentGroup.label}</span>
)}
<span class="separator" aria-hidden="true">/</span>
</li>
)}
<li class="sr-only" aria-current="page">
{segmentToLabel(segments[segments.length - 1])}
Comment thread
ArakTaiRoth marked this conversation as resolved.
</li>
</ol>
</nav>
)}

<style>
.breadcrumbs {
margin-bottom: var(--goa-space-l);
}

.breadcrumbs ol {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--goa-space-2xs);
list-style: none;
margin: 0;
padding: 0;
}

.breadcrumbs li {
display: flex;
align-items: center;
gap: var(--goa-space-2xs);
font: var(--goa-typography-body-xs);
color: var(--goa-color-greyscale-400);
}

.breadcrumbs a,
.breadcrumbs span:not(.separator) {
color: var(--goa-color-greyscale-500);
text-decoration: none;
font: var(--goa-typography-body-xs);
}

.breadcrumbs a:hover {
color: var(--goa-color-interactive-default);
text-decoration: underline;
}

.breadcrumbs a:focus {
outline: none;
}

.breadcrumbs a:focus-visible {
outline: 2px solid var(--goa-color-interactive-default);
outline-offset: 2px;
border-radius: var(--goa-border-radius-xs);
}

.separator {
color: var(--goa-color-greyscale-300);
font: var(--goa-typography-body-xs);
}

.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>
16 changes: 16 additions & 0 deletions docs/src/components/ComponentsGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,22 @@ export function ComponentsGrid({ components }: ComponentsGridProps) {
status: string[];
}>({ category: [], status: [] });

// Read URL params on mount for initial filters (e.g. ?category=inputs-and-actions)
const [urlFiltersApplied, setUrlFiltersApplied] = useState(false);
useEffect(() => {
if (urlFiltersApplied) return;
const params = new URLSearchParams(window.location.search);
const fromUrl = {
category: params.get("category")?.split(",").filter(Boolean) ?? [],
status: params.get("status")?.split(",").filter(Boolean) ?? [],
};
if (fromUrl.category.length || fromUrl.status.length) {
setPendingFilters(fromUrl);
setAppliedFilters(fromUrl);
}
setUrlFiltersApplied(true);
}, [urlFiltersApplied]);

// Hooks
const { sortConfig, setSortConfig, sortByKey, clearSort, handleTableSort } =
useTwoLevelSort();
Expand Down
10 changes: 10 additions & 0 deletions docs/src/layouts/DocumentationPageLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/

import BaseLayout from './BaseLayout.astro';
import Breadcrumbs from '../components/Breadcrumbs.astro';
import { SiteNav } from '../components/SiteNav';
import { MobileHeader } from '../components/MobileHeader';
import { TableOfContents } from '../components/TableOfContents';
Expand Down Expand Up @@ -63,6 +64,9 @@ const navCategories = await getNavCategories();
</div>

<div class="content-card">
<div class="breadcrumb-row">
<Breadcrumbs />
</div>
<div class:list={["content-grid", { "with-toc": showToc }]}>
<!-- Documentation Content -->
<article class="prose-content">
Expand Down Expand Up @@ -133,6 +137,12 @@ const navCategories = await getNavCategories();
grid-template-columns: 1fr 200px;
}

/* Breadcrumb: match content-grid centering */
.breadcrumb-row {
max-width: 960px;
margin: 0 auto var(--goa-space-xs);
}

/* Prose typography */
.prose-content {
max-width: 70ch;
Expand Down
9 changes: 9 additions & 0 deletions docs/src/layouts/TokensLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/

import BaseLayout from './BaseLayout.astro';
import Breadcrumbs from '../components/Breadcrumbs.astro';
import { SiteNav } from '../components/SiteNav';
import { MobileHeader } from '../components/MobileHeader';
import { getNavCategories } from '../lib/nav-categories';
Expand Down Expand Up @@ -47,6 +48,9 @@ const navCategories = await getNavCategories();

<div class="content-with-drawer">
<div class="content-card">
<div class="breadcrumb-row">
<Breadcrumbs />
</div>
<slot />
</div>
</div>
Expand Down Expand Up @@ -112,6 +116,11 @@ const navCategories = await getNavCategories();
min-height: calc(100vh - var(--goa-space-l) * 2);
}

.breadcrumb-row {
max-width: 1408px;
margin: 0 auto var(--goa-space-xs);
}

/* Tablet: modal drawer mode (push drawer switches at 1023px) */
@media (max-width: 1023px) {
.content-card {
Expand Down
9 changes: 6 additions & 3 deletions docs/src/pages/components/[slug].astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
import { getCollection, render } from 'astro:content';
import ComponentPageLayout from '../../layouts/ComponentPageLayout.astro';
import Breadcrumbs from '../../components/Breadcrumbs.astro';
import PropsTable from '../../components/PropsTable.astro';
import GuidanceGrid from '../../components/GuidanceGrid.astro';
import ExampleDisplay from '../../components/ExampleDisplay.astro';
Expand All @@ -26,6 +27,8 @@ export async function getStaticPaths() {

const { component } = Astro.props;
const { slug } = Astro.params;
const categoryLabel = component.data.category
.split('-').map((w: string, i: number) => i === 0 ? w.charAt(0).toUpperCase() + w.slice(1) : w).join(' ');

// 1. Render the component MDX content
const { Content } = await render(component);
Expand Down Expand Up @@ -60,11 +63,11 @@ const v1DocsUrl = `https://v1.design.alberta.ca/components/${slug}`;
category={component.data.category}
>
<div class="component-page">
<Breadcrumbs
parentGroup={{ label: categoryLabel, url: `/components?category=${component.data.category}` }}
/>
<!-- Component Header -->
<header class="component-header">
<!-- Category Badge -->
<goa-badge version="2" type="default" emphasis="subtle" icon="false" content={component.data.category.replace(/-/g, ' ')}></goa-badge>

<!-- Title -->
<h1 class="component-title">{component.data.name}</h1>

Expand Down
2 changes: 2 additions & 0 deletions docs/src/pages/components/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/
import { getCollection } from 'astro:content';
import ComponentPageLayout from '../../layouts/ComponentPageLayout.astro';
import Breadcrumbs from '../../components/Breadcrumbs.astro';
import { ComponentsGrid } from '../../components/ComponentsGrid';

// Get all visible components (filter out hidden)
Expand All @@ -34,6 +35,7 @@ const components = sortedComponents.map(component => ({
description="Browse all GoA Design System components"
>
<div class="components-page">
<Breadcrumbs />
<!-- Header -->
<header class="page-header">
<h1>All Components</h1>
Expand Down
12 changes: 2 additions & 10 deletions docs/src/pages/examples/[slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/
import { getCollection, render } from 'astro:content';
import ExamplesPageLayout from '../../layouts/ExamplesPageLayout.astro';
import Breadcrumbs from '../../components/Breadcrumbs.astro';
import ExampleDisplay from '../../components/ExampleDisplay.astro';
import { TableOfContents } from '../../components/TableOfContents';
import { getExampleCode } from '../../lib/example-code';
Expand Down Expand Up @@ -79,14 +80,9 @@ const v1DocsUrl = `https://v1.design.alberta.ca/examples/${slug}`;
currentSlug={slug}
>
<article class="example-page">
<Breadcrumbs />
<!-- Header -->
<header class="example-header">
<!-- Back button -->
<goa-link version="2" leadingicon="arrow-back" size="small">
<a href="/examples">Back to all examples</a>
</goa-link>

<!-- Badges -->
<div class="badges">
{data.categories.map((cat: string) => (
<goa-badge version="2" type="information" emphasis="subtle" icon="false" content={formatCategory(cat)}></goa-badge>
Expand Down Expand Up @@ -166,10 +162,6 @@ const v1DocsUrl = `https://v1.design.alberta.ca/examples/${slug}`;
margin-bottom: var(--goa-space-l, 1.5rem);
}

.example-header goa-link {
margin-bottom: var(--goa-space-m, 1rem);
}

/* Badges */
.badges {
display: flex;
Expand Down
2 changes: 2 additions & 0 deletions docs/src/pages/examples/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/
import { getCollection } from 'astro:content';
import ExamplesPageLayout from '../../layouts/ExamplesPageLayout.astro';
import Breadcrumbs from '../../components/Breadcrumbs.astro';
import { ExamplesGrid } from '../../components/ExamplesGrid';

// Get all examples
Expand All @@ -35,6 +36,7 @@ const examples = sortedExamples.map(example => ({
description="Code examples showing GoA Design System components in context"
>
<div class="examples-page">
<Breadcrumbs />
<!-- Header -->
<header class="page-header">
<h1>Examples</h1>
Expand Down
Loading