wip: DRY up things

This commit is contained in:
2026-01-11 02:16:23 +00:00
parent 9f1af55602
commit d2efcb01ba
23 changed files with 326 additions and 471 deletions

View File

@@ -1,4 +1,5 @@
---
import SectionHeader from "./SectionHeader.astro";
import { config } from "../config";
interface Props {
@@ -13,14 +14,10 @@ const { introduction, summary, license } = Astro.props;
<section id="about" class="py-20 sm:py-28">
<div class="section-container">
<div class="max-w-3xl mx-auto">
<!-- Section header -->
<div class="mb-12 text-center">
<h2 class="text-3xl sm:text-4xl mb-4">About Common-Flow</h2>
<p class="text-lg text-gray-600 dark:text-neutral-400">
A practical git workflow that combines the best of GitHub Flow with
versioned releases
</p>
</div>
<SectionHeader
title="About Common-Flow"
subtitle="A practical git workflow that combines the best of GitHub Flow with versioned releases"
/>
<!-- Introduction -->
<div class="prose-spec mb-12" set:html={introduction} />

View File

@@ -1,4 +1,5 @@
---
import SectionHeader from "./SectionHeader.astro";
import type { FAQItem } from "../utils/parseSpecContent";
interface Props {
@@ -11,13 +12,10 @@ const { items } = Astro.props;
<section id="faq" class="py-20 sm:py-28">
<div class="section-container">
<div class="max-w-3xl mx-auto">
<!-- Section header -->
<div class="mb-12 text-center">
<h2 class="text-3xl sm:text-4xl mb-4">FAQ</h2>
<p class="text-lg text-gray-600 dark:text-neutral-400">
Common questions about Git Common-Flow
</p>
</div>
<SectionHeader
title="FAQ"
subtitle="Common questions about Git Common-Flow"
/>
<!-- FAQ Items -->
<div class="space-y-0">

View File

@@ -1,6 +1,7 @@
---
import VersionSelector from "./VersionSelector.astro";
import ThemeToggle from "./ThemeToggle.astro";
import GitHubIcon from "./icons/GitHubIcon.astro";
import { config } from "../config";
interface Props {
@@ -8,6 +9,12 @@ interface Props {
}
const { version } = Astro.props;
const navItems = [
{ id: "about", label: "About" },
{ id: "spec", label: "Spec" },
{ id: "faq", label: "FAQ" },
];
---
<header
@@ -41,42 +48,22 @@ const { version } = Astro.props;
<!-- Desktop Navigation -->
<nav class="hidden md:flex items-center gap-1">
<a
href="#about"
class="nav-link inline-flex items-center px-4 py-2 text-sm font-medium
rounded-lg transition-colors cursor-pointer
text-gray-600 dark:text-neutral-400
hover:bg-gray-100 hover:text-gray-950
dark:hover:bg-neutral-800 dark:hover:text-neutral-50"
data-nav-link
data-section-id="about"
>
About
</a>
<a
href="#spec"
class="nav-link inline-flex items-center px-4 py-2 text-sm font-medium
rounded-lg transition-colors cursor-pointer
text-gray-600 dark:text-neutral-400
hover:bg-gray-100 hover:text-gray-950
dark:hover:bg-neutral-800 dark:hover:text-neutral-50"
data-nav-link
data-section-id="spec"
>
Spec
</a>
<a
href="#faq"
class="nav-link inline-flex items-center px-4 py-2 text-sm font-medium
rounded-lg transition-colors cursor-pointer
text-gray-600 dark:text-neutral-400
hover:bg-gray-100 hover:text-gray-950
dark:hover:bg-neutral-800 dark:hover:text-neutral-50"
data-nav-link
data-section-id="faq"
>
FAQ
</a>
{
navItems.map((item) => (
<a
href={`#${item.id}`}
class="nav-link inline-flex items-center px-4 py-2 text-sm font-medium
rounded-lg transition-colors cursor-pointer
text-gray-600 dark:text-neutral-400
hover:bg-gray-100 hover:text-gray-950
dark:hover:bg-neutral-800 dark:hover:text-neutral-50"
data-nav-link
data-section-id={item.id}
>
{item.label}
</a>
))
}
</nav>
<!-- Right side: Theme, GitHub -->
@@ -93,12 +80,7 @@ const { version } = Astro.props;
hover:bg-gray-100 dark:hover:bg-neutral-800"
aria-label="View on GitHub"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path
fill-rule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd"></path>
</svg>
<GitHubIcon />
</a>
<!-- Mobile menu button -->
@@ -137,38 +119,26 @@ const { version } = Astro.props;
versions={Array.from(config.versions)}
/>
</div>
<a
href="#about"
class="nav-link block py-2 text-gray-600 dark:text-neutral-400
hover:text-sky-600"
data-nav-link
data-section-id="about"
>
About
</a>
<a
href="#spec"
class="nav-link block py-2 text-gray-600 dark:text-neutral-400
hover:text-sky-600"
data-nav-link
data-section-id="spec"
>
Spec
</a>
<a
href="#faq"
class="nav-link block py-2 text-gray-600 dark:text-neutral-400
hover:text-sky-600"
data-nav-link
data-section-id="faq"
>
FAQ
</a>
{
navItems.map((item) => (
<a
href={`#${item.id}`}
class="nav-link block py-2 text-gray-600 dark:text-neutral-400
hover:text-sky-600"
data-nav-link
data-section-id={item.id}
>
{item.label}
</a>
))
}
</div>
</nav>
</header>
<script>
import { initActiveSectionTracker } from "../scripts/activeSectionTracker";
function initHeader() {
const header = document.getElementById("site-header");
const hero = document.getElementById("hero");
@@ -212,51 +182,10 @@ const { version } = Astro.props;
}
// Active section tracking for nav links
const navLinks = document.querySelectorAll("[data-nav-link]");
const sections: { id: string; element: Element }[] = [];
// Collect unique section IDs
const seenIds = new Set<string>();
navLinks.forEach((link) => {
const id = link.getAttribute("data-section-id");
if (id && !seenIds.has(id)) {
seenIds.add(id);
const section = document.getElementById(id);
if (section) {
sections.push({ id, element: section });
}
}
initActiveSectionTracker({
linkSelector: "[data-nav-link]",
defaultToFirst: false,
});
function updateActiveNavSection() {
const headerOffset = 100;
let activeId: string | null = null;
for (const { id, element } of sections) {
const rect = element.getBoundingClientRect();
if (rect.top <= headerOffset) {
activeId = id;
}
}
navLinks.forEach((link) => {
const linkId = link.getAttribute("data-section-id");
link.classList.toggle("active", linkId === activeId);
});
}
let ticking = false;
window.addEventListener("scroll", () => {
if (!ticking) {
requestAnimationFrame(() => {
updateActiveNavSection();
ticking = false;
});
ticking = true;
}
});
updateActiveNavSection();
}
// Initialize on load

View File

@@ -1,6 +1,7 @@
---
import VersionSelector from "./VersionSelector.astro";
import ThemeToggle from "./ThemeToggle.astro";
import GitHubIcon from "./icons/GitHubIcon.astro";
import { config } from "../config";
interface Props {
@@ -58,12 +59,7 @@ const { version, svgPath } = Astro.props;
hover:bg-white/50 dark:hover:bg-neutral-800/50"
aria-label="View on GitHub"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path
fill-rule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd"></path>
</svg>
<GitHubIcon />
</a>
</div>
</div>

View File

@@ -0,0 +1,16 @@
---
interface Props {
title: string;
subtitle: string;
class?: string;
}
const { title, subtitle, class: className } = Astro.props;
---
<div class:list={["mb-12 text-center", className]}>
<h2 class="text-3xl sm:text-4xl mb-4">{title}</h2>
<p class="text-lg text-gray-600 dark:text-neutral-400">
{subtitle}
</p>
</div>

View File

@@ -1,4 +1,5 @@
---
import SectionHeader from "./SectionHeader.astro";
import SpecSidebar from "./SpecSidebar.astro";
import type { TocItem } from "../utils/parseSpecContent";
@@ -14,13 +15,11 @@ const { terminology, terminologyTitle, specification, tocItems } = Astro.props;
<section id="spec" class="py-20 sm:py-28">
<div class="section-container">
<!-- Section header -->
<div class="max-w-3xl mx-auto mb-12 text-center">
<h2 class="text-3xl sm:text-4xl mb-4">The Specification</h2>
<p class="text-lg text-gray-600 dark:text-neutral-400">
The complete Git Common-Flow specification
</p>
</div>
<SectionHeader
title="The Specification"
subtitle="The complete Git Common-Flow specification"
class="max-w-3xl mx-auto"
/>
<!-- Content with sidebar -->
<div class="lg:flex lg:gap-8">

View File

@@ -1,4 +1,5 @@
---
import TocLink from "./TocLink.astro";
import type { TocItem } from "../utils/parseSpecContent";
interface Props {
@@ -21,30 +22,7 @@ const { items } = Astro.props;
>
Table of Contents
</div>
{
items.map((item) => (
<a
href={`#${item.id}`}
class:list={[
"sidebar-link block py-2 px-4 text-sm rounded-md",
"transition-colors",
"text-gray-500 hover:text-gray-950 hover:bg-gray-100",
"dark:text-neutral-500 dark:hover:text-neutral-50 dark:hover:bg-neutral-800",
item.level === 3 && "pl-6 text-[0.8125rem]",
item.clause && "flex",
]}
data-sidebar-link
data-section-id={item.id}
>
{item.clause && (
<span class="shrink-0 w-6 text-gray-400 dark:text-neutral-600">
{item.clause}
</span>
)}
<span>{item.title}</span>
</a>
))
}
{items.map((item) => <TocLink item={item} trackActive />)}
</nav>
</aside>
@@ -111,83 +89,20 @@ const { items } = Astro.props;
</div>
<nav class="space-y-1">
{
items.map((item) => (
<a
href={`#${item.id}`}
class:list={[
"sidebar-link block py-2 px-4 text-sm rounded-md",
"transition-colors",
"text-gray-500 hover:text-gray-950 hover:bg-gray-100",
"dark:text-neutral-500 dark:hover:text-neutral-50 dark:hover:bg-neutral-800",
item.level === 3 && "pl-6 text-[0.8125rem]",
item.clause && "flex",
]}
data-toc-link
>
{item.clause && (
<span class="shrink-0 w-6 text-gray-400 dark:text-neutral-600">
{item.clause}
</span>
)}
<span>{item.title}</span>
</a>
))
}
{items.map((item) => <TocLink item={item} />)}
</nav>
</div>
</div>
<script>
import { initActiveSectionTracker } from "../scripts/activeSectionTracker";
function initSpecSidebar() {
// Active section tracking
const sidebarLinks = document.querySelectorAll("[data-sidebar-link]");
const sections: { id: string; element: Element }[] = [];
sidebarLinks.forEach((link) => {
const id = link.getAttribute("data-section-id");
if (id) {
const section = document.getElementById(id);
if (section) {
sections.push({ id, element: section });
}
}
// Active section tracking for sidebar links
initActiveSectionTracker({
linkSelector: "[data-sidebar-link]",
});
// Highlight the section whose top is closest to viewport top
function updateActiveSection() {
const headerOffset = 100; // Account for sticky header
let activeId = sections[0]?.id;
for (const { id, element } of sections) {
const rect = element.getBoundingClientRect();
// Find the last section whose top has scrolled past the header
if (rect.top <= headerOffset) {
activeId = id;
}
}
sidebarLinks.forEach((link) => {
const linkId = link.getAttribute("data-section-id");
link.classList.toggle("active", linkId === activeId);
});
}
// Update on scroll with throttling
let ticking = false;
window.addEventListener("scroll", () => {
if (!ticking) {
requestAnimationFrame(() => {
updateActiveSection();
ticking = false;
});
ticking = true;
}
});
// Initial update
updateActiveSection();
// Mobile TOC drawer
const toggleBtn = document.getElementById("spec-toc-toggle");
const drawer = document.getElementById("spec-toc-drawer");

View File

@@ -0,0 +1,34 @@
---
import type { TocItem } from "../utils/parseSpecContent";
interface Props {
item: TocItem;
trackActive?: boolean;
}
const { item, trackActive = false } = Astro.props;
---
<a
href={`#${item.id}`}
class:list={[
"sidebar-link block py-2 px-4 text-sm rounded-md",
"transition-colors",
"text-gray-500 hover:text-gray-950 hover:bg-gray-100",
"dark:text-neutral-500 dark:hover:text-neutral-50 dark:hover:bg-neutral-800",
item.level === 3 && "pl-6 text-[0.8125rem]",
item.clause && "flex",
]}
data-sidebar-link={trackActive ? "" : undefined}
data-section-id={trackActive ? item.id : undefined}
data-toc-link={!trackActive ? "" : undefined}
>
{
item.clause && (
<span class="shrink-0 w-6 text-gray-400 dark:text-neutral-600">
{item.clause}
</span>
)
}
<span>{item.title}</span>
</a>

View File

@@ -0,0 +1,14 @@
---
interface Props {
class?: string;
}
const { class: className = "w-5 h-5" } = Astro.props;
---
<svg class={className} fill="currentColor" viewBox="0 0 24 24">
<path
fill-rule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd"></path>
</svg>

View File

@@ -0,0 +1,64 @@
export interface ActiveSectionTrackerOptions {
linkSelector: string;
sectionIdAttr?: string;
headerOffset?: number;
defaultToFirst?: boolean;
}
export function initActiveSectionTracker(
options: ActiveSectionTrackerOptions,
): void {
const {
linkSelector,
sectionIdAttr = "data-section-id",
headerOffset = 100,
defaultToFirst = true,
} = options;
const links = document.querySelectorAll(linkSelector);
const sections: { id: string; element: Element }[] = [];
// Collect unique section IDs
const seenIds = new Set<string>();
links.forEach((link) => {
const id = link.getAttribute(sectionIdAttr);
if (id && !seenIds.has(id)) {
seenIds.add(id);
const section = document.getElementById(id);
if (section) {
sections.push({ id, element: section });
}
}
});
function updateActiveSection(): void {
let activeId: string | null = defaultToFirst ? sections[0]?.id : null;
for (const { id, element } of sections) {
const rect = element.getBoundingClientRect();
if (rect.top <= headerOffset) {
activeId = id;
}
}
links.forEach((link) => {
const linkId = link.getAttribute(sectionIdAttr);
link.classList.toggle("active", linkId === activeId);
});
}
// Update on scroll with throttling
let ticking = false;
window.addEventListener("scroll", () => {
if (!ticking) {
requestAnimationFrame(() => {
updateActiveSection();
ticking = false;
});
ticking = true;
}
});
// Initial update
updateActiveSection();
}