mirror of
https://github.com/Kaelio/ktx.git
synced 2026-07-01 08:59:39 +02:00
docs(site): relocate GitHub stars to sidebar footer, add light/dark switcher (#294)
Move the live GitHub stars widget into the sidebar footer pill as a type:"icon" link, sitting opposite the Slack mark (space-between) and beside the theme switcher. Render it as inner content (GitHub mark + star + compact count) so fumadocs supplies the anchor. Replace the default fumadocs theme switcher with a custom two-icon control where each icon selects its own theme. The built-in "light-dark" mode is a single blind toggle that flips on any click, so clicking the sun while already in light mode jumps to dark. useTheme is sourced from fumadocs-ui/provider/base and the icons are inlined to avoid bare next-themes / lucide-react imports.
This commit is contained in:
parent
0689d709d2
commit
e1067bf734
5 changed files with 173 additions and 131 deletions
|
|
@ -2,7 +2,7 @@ import { Suspense } from "react";
|
|||
import { GitHubIcon } from "@/components/github-icon";
|
||||
|
||||
const REPO = "kaelio/ktx";
|
||||
const REPO_URL = `https://github.com/${REPO}`;
|
||||
export const GITHUB_REPO_URL = `https://github.com/${REPO}`;
|
||||
const API_URL = `https://api.github.com/repos/${REPO}`;
|
||||
|
||||
async function fetchStarCount(): Promise<number | null> {
|
||||
|
|
@ -41,53 +41,42 @@ function StarGlyph() {
|
|||
);
|
||||
}
|
||||
|
||||
async function StarsContent() {
|
||||
async function StarsInner() {
|
||||
const count = await fetchStarCount();
|
||||
const label =
|
||||
count === null
|
||||
? "Star ktx on GitHub"
|
||||
: `Star ktx on GitHub — ${count.toLocaleString("en-US")} stars`;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={REPO_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={label}
|
||||
className="ktx-stars"
|
||||
>
|
||||
<span className="ktx-stars-seg ktx-stars-seg--label">
|
||||
<GitHubIcon className="ktx-stars-gh" />
|
||||
<span className="ktx-stars-text">Star</span>
|
||||
</span>
|
||||
{count !== null && (
|
||||
<span className="ktx-stars-seg ktx-stars-seg--count">
|
||||
<span className="ktx-stars">
|
||||
<GitHubIcon className="ktx-stars-gh" />
|
||||
{count !== null ? (
|
||||
<span className="ktx-stars-count-wrap">
|
||||
<StarGlyph />
|
||||
<span className="ktx-stars-count">{formatStars(count)}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="ktx-stars-count">Star</span>
|
||||
)}
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function StarsSkeleton() {
|
||||
return (
|
||||
<span className="ktx-stars ktx-stars--skeleton" aria-hidden="true">
|
||||
<span className="ktx-stars-seg ktx-stars-seg--label">
|
||||
<GitHubIcon className="ktx-stars-gh" />
|
||||
<span className="ktx-stars-text">Star</span>
|
||||
</span>
|
||||
<span className="ktx-stars-seg ktx-stars-seg--count">
|
||||
<span className="ktx-stars-skeleton-bar" />
|
||||
</span>
|
||||
<span className="ktx-stars" aria-hidden="true">
|
||||
<GitHubIcon className="ktx-stars-gh" />
|
||||
<span className="ktx-stars-skeleton-bar" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Footer star widget — GitHub mark + live count. Rendered as the `icon` of a
|
||||
* fumadocs `type: "icon"` link, so it lands in the sidebar footer pill beside
|
||||
* the Slack icon and the theme toggle. fumadocs supplies the surrounding <a>
|
||||
* (href + aria-label), so this renders inner content only — no anchor.
|
||||
*/
|
||||
export function GitHubStars() {
|
||||
return (
|
||||
<Suspense fallback={<StarsSkeleton />}>
|
||||
<StarsContent />
|
||||
<StarsInner />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
97
docs-site/components/theme-toggle.tsx
Normal file
97
docs-site/components/theme-toggle.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState, type ComponentProps, type SVGProps } from "react";
|
||||
import { useTheme } from "fumadocs-ui/provider/base";
|
||||
|
||||
/**
|
||||
* Two-icon theme switcher (light / dark), each icon selecting its own theme —
|
||||
* unlike fumadocs' default "light-dark" switcher, which is a single blind
|
||||
* toggle that flips on any click. Dropped into the sidebar footer pill via
|
||||
* `slots.themeSwitch`, so fumadocs passes the container className (left
|
||||
* divider, `ms-auto`, rounded inner buttons); we merge it onto our own base.
|
||||
*
|
||||
* Icons are inlined (the project doesn't depend on `lucide-react` directly);
|
||||
* `useTheme` is re-exported by fumadocs so we avoid a bare `next-themes` import.
|
||||
*/
|
||||
function SunIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2" />
|
||||
<path d="M12 20v2" />
|
||||
<path d="m4.93 4.93 1.41 1.41" />
|
||||
<path d="m17.66 17.66 1.41 1.41" />
|
||||
<path d="M2 12h2" />
|
||||
<path d="M20 12h2" />
|
||||
<path d="m6.34 17.66-1.41 1.41" />
|
||||
<path d="m19.07 4.93-1.41 1.41" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MoonIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const OPTIONS = [
|
||||
["light", SunIcon],
|
||||
["dark", MoonIcon],
|
||||
] as const;
|
||||
|
||||
function cx(...classes: (string | false | undefined)[]): string {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
export function ThemeToggle({ className, ...props }: ComponentProps<"div">) {
|
||||
const { setTheme, resolvedTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => setMounted(true), []);
|
||||
const active = mounted ? resolvedTheme : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx("inline-flex items-center overflow-hidden border", className)}
|
||||
data-theme-toggle=""
|
||||
{...props}
|
||||
>
|
||||
{OPTIONS.map(([key, Icon]) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
aria-label={key}
|
||||
onClick={() => setTheme(key)}
|
||||
className={cx(
|
||||
"size-6.5 p-1.5 transition-colors",
|
||||
active === key
|
||||
? "bg-fd-accent text-fd-accent-foreground"
|
||||
: "text-fd-muted-foreground hover:text-fd-accent-foreground",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-full" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue