mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-28 08:49:38 +02:00
feat: add GitHub star nudges to CLI build view and docs sidebar (#271)
* feat: load star count during context builds * docs: document star prompt opt-out * fix: initialize demo context star count * feat(docs-site): add GitHub star count widget to docs sidebar * test: isolate star-prompt build-view tests from ambient CI env
This commit is contained in:
parent
5232578d44
commit
795a97485a
15 changed files with 928 additions and 12 deletions
|
|
@ -2,10 +2,21 @@ import { source } from "@/lib/source";
|
|||
import { DocsLayout } from "fumadocs-ui/layouts/docs";
|
||||
import type { ReactNode } from "react";
|
||||
import { baseOptions } from "@/app/layout.config";
|
||||
import { GitHubStars } from "@/components/github-stars";
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout tree={source.pageTree} {...baseOptions}>
|
||||
<DocsLayout
|
||||
tree={source.pageTree}
|
||||
{...baseOptions}
|
||||
sidebar={{
|
||||
banner: (
|
||||
<div className="flex">
|
||||
<GitHubStars />
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -869,6 +869,147 @@ body::after {
|
|||
50% { opacity: 0.65; transform: scale(0.9); }
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
GitHub star widget (navbar)
|
||||
Split pill: GitHub mark + "Star" │ gold star + count.
|
||||
═══════════════════════════════════════════ */
|
||||
.ktx-stars {
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
height: 32px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--color-fd-border);
|
||||
background: color-mix(in oklch, var(--color-fd-card) 72%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
font-family: var(--font-display), var(--font-sans), sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
color: var(--color-fd-foreground);
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 2px rgba(27, 27, 24, 0.04);
|
||||
transition:
|
||||
transform 0.3s var(--ktx-ease),
|
||||
box-shadow 0.3s var(--ktx-ease),
|
||||
border-color 0.3s ease;
|
||||
animation: ktx-stars-in 0.5s var(--ktx-ease) both;
|
||||
}
|
||||
|
||||
@keyframes ktx-stars-in {
|
||||
from { opacity: 0; transform: translateY(-4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.ktx-stars:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: color-mix(in oklch, var(--color-fd-primary) 45%, var(--color-fd-border));
|
||||
box-shadow:
|
||||
0 6px 18px -8px rgba(14, 116, 144, 0.28),
|
||||
0 1px 2px rgba(27, 27, 24, 0.05);
|
||||
}
|
||||
|
||||
.ktx-stars:focus-visible {
|
||||
outline: 2px solid var(--color-fd-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.dark .ktx-stars {
|
||||
background: color-mix(in oklch, var(--color-fd-card) 60%, transparent);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.dark .ktx-stars:hover {
|
||||
border-color: rgba(34, 211, 238, 0.4);
|
||||
box-shadow:
|
||||
0 6px 18px -8px rgba(34, 211, 238, 0.3),
|
||||
0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.ktx-stars-seg {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 11px;
|
||||
}
|
||||
|
||||
.ktx-stars-seg--count {
|
||||
border-left: 1px solid var(--color-fd-border);
|
||||
background: color-mix(in oklch, var(--color-fd-primary) 6%, transparent);
|
||||
transition: background 0.3s var(--ktx-ease);
|
||||
}
|
||||
|
||||
.ktx-stars:hover .ktx-stars-seg--count {
|
||||
background: color-mix(in oklch, var(--color-fd-primary) 12%, transparent);
|
||||
}
|
||||
|
||||
.ktx-stars-gh {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.ktx-stars-text {
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.ktx-stars-star {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
fill: #f5b301;
|
||||
transition: transform 0.3s var(--ktx-ease), filter 0.3s var(--ktx-ease);
|
||||
}
|
||||
|
||||
.ktx-stars:hover .ktx-stars-star {
|
||||
transform: scale(1.18) rotate(-8deg);
|
||||
filter: drop-shadow(0 1px 4px rgba(245, 179, 1, 0.55));
|
||||
}
|
||||
|
||||
.ktx-stars-count {
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--color-fd-foreground);
|
||||
}
|
||||
|
||||
/* Skeleton shown only on the rare cold (uncached) fetch */
|
||||
.ktx-stars--skeleton {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.ktx-stars-skeleton-bar {
|
||||
display: inline-block;
|
||||
width: 26px;
|
||||
height: 11px;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--color-fd-muted) 25%,
|
||||
color-mix(in oklch, var(--color-fd-muted-foreground) 28%, var(--color-fd-muted)) 50%,
|
||||
var(--color-fd-muted) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: ktx-stars-shimmer 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes ktx-stars-shimmer {
|
||||
from { background-position: 200% 0; }
|
||||
to { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* Compact on phones: drop the "Star" word, keep mark + count */
|
||||
@media (max-width: 640px) {
|
||||
.ktx-stars-text { display: none; }
|
||||
.ktx-stars-seg { padding: 0 9px; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ktx-stars { animation: none; transition: none; }
|
||||
.ktx-stars:hover { transform: none; }
|
||||
.ktx-stars:hover .ktx-stars-star { transform: none; }
|
||||
.ktx-stars-skeleton-bar { animation: none; }
|
||||
}
|
||||
|
||||
/* Dot grid */
|
||||
.dot-grid {
|
||||
background-image: radial-gradient(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
|
||||
import { GitHubIcon } from "@/components/github-icon";
|
||||
import { Logo } from "@/components/logo";
|
||||
import { SlackIcon } from "@/components/slack-icon";
|
||||
|
||||
|
|
@ -26,14 +25,6 @@ export const baseOptions: BaseLayoutProps = {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "icon",
|
||||
label: "GitHub",
|
||||
icon: <GitHubIcon />,
|
||||
text: "GitHub",
|
||||
url: "https://github.com/kaelio/ktx",
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
type: "icon",
|
||||
label: "Join the ktx Slack community",
|
||||
|
|
|
|||
93
docs-site/components/github-stars.tsx
Normal file
93
docs-site/components/github-stars.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { Suspense } from "react";
|
||||
import { GitHubIcon } from "@/components/github-icon";
|
||||
|
||||
const REPO = "kaelio/ktx";
|
||||
const REPO_URL = `https://github.com/${REPO}`;
|
||||
const API_URL = `https://api.github.com/repos/${REPO}`;
|
||||
|
||||
async function fetchStarCount(): Promise<number | null> {
|
||||
try {
|
||||
const res = await fetch(API_URL, {
|
||||
headers: { Accept: "application/vnd.github+json" },
|
||||
// Revalidate hourly. GitHub's unauthenticated REST limit is 60 req/h per
|
||||
// IP, so a single cached server-side fetch keeps the count fresh while
|
||||
// never exposing visitors to rate limits or layout shift.
|
||||
next: { revalidate: 3600 },
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = (await res.json()) as { stargazers_count?: unknown };
|
||||
return typeof data.stargazers_count === "number"
|
||||
? data.stargazers_count
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Compact, GitHub-style count: 847 → "847", 1234 → "1.2k", 12345 → "12.3k". */
|
||||
function formatStars(count: number): string {
|
||||
if (count < 1000) return count.toLocaleString("en-US");
|
||||
const thousands = count / 1000;
|
||||
const rounded =
|
||||
thousands >= 100 ? Math.round(thousands) : Math.round(thousands * 10) / 10;
|
||||
return `${rounded}k`;
|
||||
}
|
||||
|
||||
function StarGlyph() {
|
||||
return (
|
||||
<svg className="ktx-stars-star" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M12 2.6l2.9 5.88 6.49.95-4.7 4.57 1.11 6.46L12 17.4l-5.8 3.06 1.11-6.46-4.7-4.57 6.49-.95z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
async function StarsContent() {
|
||||
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">
|
||||
<StarGlyph />
|
||||
<span className="ktx-stars-count">{formatStars(count)}</span>
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export function GitHubStars() {
|
||||
return (
|
||||
<Suspense fallback={<StarsSkeleton />}>
|
||||
<StarsContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
@ -112,6 +112,18 @@ pnpm add -g @kaelio/ktx
|
|||
yarn global add @kaelio/ktx
|
||||
```
|
||||
|
||||
## Build-view star prompt
|
||||
|
||||
During an interactive context build, `ktx setup` and `ktx ingest` can show a dim
|
||||
GitHub star reminder above the `Ctrl+C to stop` hint. **ktx** skips this prompt
|
||||
for CI, non-TTY output, and `DO_NOT_TRACK=1`.
|
||||
|
||||
To suppress only this prompt while keeping other notices enabled, set:
|
||||
|
||||
```bash
|
||||
KTX_NO_STAR=1
|
||||
```
|
||||
|
||||
## Project resolution
|
||||
|
||||
Most commands are project-aware. Pass `--project-dir <path>` when scripting or
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue