mirror of
https://github.com/jimeh/commonflow.org.git
synced 2026-02-19 05:46:40 +00:00
wip: unifi layouts and improve footer
This commit is contained in:
@@ -3,23 +3,16 @@ import { config } from "../config";
|
||||
---
|
||||
|
||||
<footer
|
||||
class="pt-12 pb-6 my-28 text-center text-sm
|
||||
class="pt-12 pb-6 my-28 text-sm
|
||||
text-gray-500 dark:text-neutral-500
|
||||
border-t border-gray-200 dark:border-neutral-800"
|
||||
>
|
||||
<div class="section-container">
|
||||
<div
|
||||
class="section-container flex flex-col sm:flex-row
|
||||
sm:justify-between sm:items-center gap-2"
|
||||
>
|
||||
<p>
|
||||
{config.title} is authored by
|
||||
<a
|
||||
href={config.authorUrl}
|
||||
class="hover:text-sky-600"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{config.author}
|
||||
</a>
|
||||
</p>
|
||||
<p class="mt-2">
|
||||
License:
|
||||
<a
|
||||
href={config.license.url}
|
||||
class="hover:text-sky-600"
|
||||
@@ -29,5 +22,16 @@ import { config } from "../config";
|
||||
{config.license.name}
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
{config.title} by
|
||||
<a
|
||||
href={config.authorUrl}
|
||||
class="hover:text-sky-600"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{config.author}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { CollectionEntry } from "astro:content";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
import SinglePage from "../layouts/SinglePage.astro";
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import Header from "./Header.astro";
|
||||
import Hero from "./Hero.astro";
|
||||
import AboutSection from "./AboutSection.astro";
|
||||
@@ -30,7 +30,7 @@ const markdown = content.replace(/^---[\s\S]*?---\n/, "");
|
||||
const parsed = await parseSpecContent(markdown, version);
|
||||
---
|
||||
|
||||
<SinglePage title={spec.data.title} version={version}>
|
||||
<BaseLayout title={spec.data.title}>
|
||||
<Header version={version} />
|
||||
|
||||
<main>
|
||||
@@ -53,4 +53,4 @@ const parsed = await parseSpecContent(markdown, version);
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</SinglePage>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
---
|
||||
// Theme toggle component - sun/moon icon to switch between light and dark mode
|
||||
// Theme toggle component - cycles through light, dark, and auto (system) modes
|
||||
---
|
||||
|
||||
<button
|
||||
data-theme-toggle
|
||||
type="button"
|
||||
class="p-2 rounded-lg transition-colors duration-200
|
||||
text-gray-500 dark:text-neutral-500
|
||||
hover:text-gray-950 dark:hover:text-neutral-50
|
||||
hover:bg-gray-100 dark:hover:bg-neutral-800"
|
||||
aria-label="Toggle dark mode"
|
||||
>
|
||||
<!-- Sun icon (shown in dark mode) -->
|
||||
<div class="relative group">
|
||||
<button
|
||||
data-theme-toggle
|
||||
type="button"
|
||||
class="p-2 rounded-lg cursor-pointer transition-colors duration-200
|
||||
text-gray-500 dark:text-neutral-500
|
||||
hover:text-gray-950 dark:hover:text-neutral-50
|
||||
hover:bg-gray-100 dark:hover:bg-neutral-800"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
<!-- Sun icon (shown when theme is light) -->
|
||||
<svg
|
||||
data-sun-icon
|
||||
data-theme-icon="light"
|
||||
class="hidden w-5 h-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -27,10 +28,10 @@
|
||||
6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16
|
||||
12a4 4 0 11-8 0 4 4 0 018 0z"></path>
|
||||
</svg>
|
||||
<!-- Moon icon (shown in light mode) -->
|
||||
<!-- Moon icon (shown when theme is dark) -->
|
||||
<svg
|
||||
data-moon-icon
|
||||
class="w-5 h-5"
|
||||
data-theme-icon="dark"
|
||||
class="hidden w-5 h-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -42,43 +43,102 @@
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003
|
||||
9.003 0 008.354-5.646z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Auto/System icon (shown when theme is auto) -->
|
||||
<svg
|
||||
data-theme-icon="auto"
|
||||
class="hidden w-5 h-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2
|
||||
2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Tooltip -->
|
||||
<div
|
||||
class="absolute left-1/2 -translate-x-1/2 top-full mt-2
|
||||
px-2 py-1 text-xs font-medium whitespace-nowrap rounded-md shadow-sm
|
||||
bg-gray-900 text-white dark:bg-white dark:text-gray-900
|
||||
opacity-0 group-hover:opacity-100
|
||||
transition-opacity duration-200 pointer-events-none"
|
||||
>
|
||||
<span data-tooltip-text="light" class="hidden">Light</span>
|
||||
<span data-tooltip-text="dark" class="hidden">Dark</span>
|
||||
<span data-tooltip-text="auto" class="hidden">System</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
type ThemeMode = "light" | "dark" | "auto";
|
||||
|
||||
function initTheme() {
|
||||
const toggles = document.querySelectorAll(
|
||||
"[data-theme-toggle]"
|
||||
) as NodeListOf<HTMLButtonElement>;
|
||||
|
||||
function getTheme(): "dark" | "light" {
|
||||
function getStoredMode(): ThemeMode {
|
||||
const stored = localStorage.getItem("theme");
|
||||
if (stored === "dark" || stored === "light") {
|
||||
if (stored === "dark" || stored === "light" || stored === "auto") {
|
||||
return stored;
|
||||
}
|
||||
return "auto";
|
||||
}
|
||||
|
||||
function getSystemTheme(): "dark" | "light" {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
function updateAllIcons(isDark: boolean) {
|
||||
// Update all sun/moon icons on the page
|
||||
document.querySelectorAll("[data-sun-icon]").forEach((icon) => {
|
||||
icon.classList.toggle("hidden", !isDark);
|
||||
function getEffectiveTheme(mode: ThemeMode): "dark" | "light" {
|
||||
if (mode === "auto") {
|
||||
return getSystemTheme();
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
function updateAllIcons(mode: ThemeMode) {
|
||||
document.querySelectorAll("[data-theme-icon]").forEach((icon) => {
|
||||
const iconMode = (icon as HTMLElement).dataset.themeIcon;
|
||||
icon.classList.toggle("hidden", iconMode !== mode);
|
||||
});
|
||||
document.querySelectorAll("[data-moon-icon]").forEach((icon) => {
|
||||
icon.classList.toggle("hidden", isDark);
|
||||
document.querySelectorAll("[data-tooltip-text]").forEach((text) => {
|
||||
const textMode = (text as HTMLElement).dataset.tooltipText;
|
||||
text.classList.toggle("hidden", textMode !== mode);
|
||||
});
|
||||
}
|
||||
|
||||
function setTheme(theme: "dark" | "light") {
|
||||
localStorage.setItem("theme", theme);
|
||||
document.documentElement.classList.toggle("dark", theme === "dark");
|
||||
updateAllIcons(theme === "dark");
|
||||
function applyTheme(mode: ThemeMode) {
|
||||
const effective = getEffectiveTheme(mode);
|
||||
if (effective === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
updateAllIcons(mode);
|
||||
}
|
||||
|
||||
function setMode(mode: ThemeMode) {
|
||||
localStorage.setItem("theme", mode);
|
||||
applyTheme(mode);
|
||||
}
|
||||
|
||||
function cycleMode(): ThemeMode {
|
||||
const current = getStoredMode();
|
||||
// Cycle: light → dark → auto → light
|
||||
if (current === "light") return "dark";
|
||||
if (current === "dark") return "auto";
|
||||
return "light";
|
||||
}
|
||||
|
||||
// Initialize theme
|
||||
const currentTheme = getTheme();
|
||||
setTheme(currentTheme);
|
||||
const currentMode = getStoredMode();
|
||||
applyTheme(currentMode);
|
||||
|
||||
// Attach click handlers to all toggle buttons
|
||||
toggles.forEach((toggle) => {
|
||||
@@ -87,17 +147,16 @@
|
||||
toggle.dataset.initialized = "true";
|
||||
|
||||
toggle.addEventListener("click", () => {
|
||||
const isDark = document.documentElement.classList.contains("dark");
|
||||
setTheme(isDark ? "light" : "dark");
|
||||
setMode(cycleMode());
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for system preference changes
|
||||
// Listen for system preference changes (only affects auto mode)
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", (e) => {
|
||||
if (!localStorage.getItem("theme")) {
|
||||
setTheme(e.matches ? "dark" : "light");
|
||||
.addEventListener("change", () => {
|
||||
if (getStoredMode() === "auto") {
|
||||
applyTheme("auto");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
---
|
||||
import "../styles/global.css";
|
||||
import { config } from "../config";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
const { title, description, version } = Astro.props;
|
||||
|
||||
const defaultDescription =
|
||||
"An attempt to gather a sensible selection of the most common usage " +
|
||||
"patterns of git into a single and concise specification.";
|
||||
const { title, description = config.description } = Astro.props;
|
||||
const fullTitle = title === config.title ? title : `${title} | ${config.title}`;
|
||||
const canonicalUrl = Astro.url.href;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
@@ -19,29 +17,24 @@ const defaultDescription =
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content={description || defaultDescription} />
|
||||
<meta name="author" content="Jim Myhrberg" />
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
|
||||
<title>{fullTitle}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta name="author" content={config.author} />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content={title} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={description || defaultDescription}
|
||||
/>
|
||||
<meta property="og:title" content={fullTitle} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={`https://commonflow.org/${version}`} />
|
||||
<meta property="og:url" content={canonicalUrl} />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content={description || defaultDescription}
|
||||
/>
|
||||
<meta name="twitter:title" content={fullTitle} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
|
||||
<title>{title}</title>
|
||||
|
||||
<!-- Fonts - distinctive choices -->
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
@@ -54,13 +47,15 @@ const defaultDescription =
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
|
||||
<!-- Theme initialization - prevent flash -->
|
||||
<!-- Prevent flash of wrong theme -->
|
||||
<script is:inline>
|
||||
(function () {
|
||||
const theme = localStorage.getItem("theme");
|
||||
const mode = localStorage.getItem("theme");
|
||||
const prefersDark =
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
if (
|
||||
theme === "dark" ||
|
||||
(!theme && window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
mode === "dark" ||
|
||||
(mode !== "light" && prefersDark)
|
||||
) {
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
@@ -73,12 +68,10 @@ const defaultDescription =
|
||||
<!-- Re-init theme on Astro page transitions -->
|
||||
<script>
|
||||
document.addEventListener("astro:after-swap", () => {
|
||||
const theme = localStorage.getItem("theme");
|
||||
if (
|
||||
theme === "dark" ||
|
||||
(!theme &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
) {
|
||||
const mode = localStorage.getItem("theme");
|
||||
const prefersDark =
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
if (mode === "dark" || (mode !== "light" && prefersDark)) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
import "../styles/global.css";
|
||||
import { config } from "../config";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const { title, description = config.description } = Astro.props;
|
||||
const fullTitle = title === config.title ? title : `${title} | ${config.title}`;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="canonical" href={Astro.url} />
|
||||
|
||||
<title>{fullTitle}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta name="author" content={config.author} />
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,500;12..96,600;12..96,700;12..96,800&family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Prevent flash of wrong theme -->
|
||||
<script is:inline>
|
||||
(function () {
|
||||
const theme = localStorage.getItem("theme");
|
||||
if (
|
||||
theme === "dark" ||
|
||||
(!theme &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
) {
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body class="min-h-screen flex flex-col items-center justify-center p-8">
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,30 +1,32 @@
|
||||
---
|
||||
import Default from "../layouts/Default.astro";
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
---
|
||||
|
||||
<Default title="Page Not Found">
|
||||
<div class="text-center">
|
||||
<h1
|
||||
class="text-[8rem] sm:text-[12rem] font-display font-bold leading-none
|
||||
text-gray-300 dark:text-neutral-700"
|
||||
>
|
||||
404
|
||||
</h1>
|
||||
<p class="text-xl mb-2 text-gray-600 dark:text-neutral-400">
|
||||
Page not found
|
||||
</p>
|
||||
<p class="text-gray-500 dark:text-neutral-500">
|
||||
The page you're looking for doesn't exist.
|
||||
</p>
|
||||
<a
|
||||
href="/"
|
||||
class="inline-flex items-center justify-center gap-2 mt-8
|
||||
px-5 py-2.5 text-sm font-medium rounded-lg
|
||||
transition-all cursor-pointer
|
||||
bg-sky-600 text-white
|
||||
hover:bg-sky-500 hover:-translate-y-0.5 hover:shadow-md"
|
||||
>
|
||||
Go to homepage
|
||||
</a>
|
||||
<BaseLayout title="Page Not Found">
|
||||
<div class="flex flex-col items-center justify-center min-h-screen p-8">
|
||||
<div class="text-center">
|
||||
<h1
|
||||
class="text-[8rem] sm:text-[12rem] font-display font-bold leading-none
|
||||
text-gray-300 dark:text-neutral-700"
|
||||
>
|
||||
404
|
||||
</h1>
|
||||
<p class="text-xl mb-2 text-gray-600 dark:text-neutral-400">
|
||||
Page not found
|
||||
</p>
|
||||
<p class="text-gray-500 dark:text-neutral-500">
|
||||
The page you're looking for doesn't exist.
|
||||
</p>
|
||||
<a
|
||||
href="/"
|
||||
class="inline-flex items-center justify-center gap-2 mt-8
|
||||
px-5 py-2.5 text-sm font-medium rounded-lg
|
||||
transition-all cursor-pointer
|
||||
bg-sky-600 text-white
|
||||
hover:bg-sky-500 hover:-translate-y-0.5 hover:shadow-md"
|
||||
>
|
||||
Go to homepage
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Default>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -414,7 +414,7 @@ html {
|
||||
width: 2rem;
|
||||
text-align: right;
|
||||
font-weight: 500;
|
||||
color: theme(colors.slate.500);
|
||||
color: theme(colors.slate.400);
|
||||
}
|
||||
|
||||
.dark .prose-spec ol>li::before {
|
||||
|
||||
Reference in New Issue
Block a user