fix: miscellaneous UI polish and doc link corrections

- Fix internal doc links to use absolute paths (`#anchor` → `/docs/page#anchor`) in `authoring.mdx` and `registry.mdx` so they resolve correctly from any page
- Clarify `consumesPackages` description wording in the module field reference table
- Switch category badge on module detail page from `outline` to `default` variant for better visual hierarchy
- Nudge `IconCheck` margin, `Label` leading, and adapter-switcher category span font size for tighter alignment
- Normalize `CardTitle` color from `text-foreground/85` → `text-foreground/80` and `text-muted-foreground` across stats cards for consistency; add `size="sm"` to per-category cards and tighten card spacing
- Add `text-sm` to the "Learn more…" docs link on the home page so it inherits the correct size
- Lighten `LastRefreshed` text from `/70` → `/85` opacity for legibility
This commit is contained in:
2026-05-28 11:18:28 -04:00
parent 703a1f76cf
commit 1e4e0269ba
11 changed files with 62 additions and 58 deletions
+13 -13
View File
@@ -68,18 +68,18 @@ export default defineModule({
The whole shape is typed and validated at runtime — `defineModule` throws on the structural rules TypeScript can't express (e.g. `app` install-field overlays on `home: "repo"` modules), and `ModuleSchema` parses the JSON form the CLI fetches.
| Field | Required | What it does |
| -------------------- | -------- | -------------------------------------------------------------------------------------------------------------------- |
| `id` | ✓ | Stable identifier within a category. Used in `stanza add <category> <id>`. |
| `category` | ✓ | One of `KNOWN_CATEGORIES` — see [Categories](/docs/concepts#categories). |
| `label` | ✓ | Display name for the wizard, search results, and the web builder. |
| `description` | ✓ | One-line summary. Surfaced in `stanza search` and module cards. |
| `version` | ✓ | Semver string. Pinned into `stanza.json` at install time so future `update`/`swap` can read it. |
| `peers` | | `Partial<Record<CategoryId, ModuleId[] \| "any">>` — categories this module needs filled. |
| `consumesPackages` | | Dirs of other internal packages this one imports from (see [Cross-package consumption](#cross-package-consumption)). |
| `adapters` | ✓ | At least one. Each carries its own install fields, templates, and codemods. |
| `appKind` | | `"web"` or `"native"`. The runner refuses to install into an incompatible app. |
| `homepage`, `author` | | Surfaced in the web builder; otherwise informational. |
| Field | Required | What it does |
| -------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `id` | ✓ | Stable identifier within a category. Used in `stanza add <category> <id>`. |
| `category` | ✓ | One of `KNOWN_CATEGORIES` — see [Categories](/docs/concepts#categories). |
| `label` | ✓ | Display name for the wizard, search results, and the web builder. |
| `description` | ✓ | One-line summary. Surfaced in `stanza search` and module cards. |
| `version` | ✓ | Semver string. Pinned into `stanza.json` at install time so future `update`/`swap` can read it. |
| `peers` | | `Partial<Record<CategoryId, ModuleId[] \| "any">>` — categories this module needs filled. |
| `consumesPackages` | | Dirs of other internal packages this one imports from (see [Cross-package consumption](/docs/authoring#cross-package-consumption)). |
| `adapters` | ✓ | At least one. Each carries its own install fields, templates, and codemods. |
| `appKind` | | `"web"` or `"native"`. The runner refuses to install into an incompatible app. |
| `homepage`, `author` | | Surfaced in the web builder; otherwise informational. |
Module-level install fields (`dependencies`, `devDependencies`, `env`, `scripts`) are merged into every adapter — adapter-level values override per key, and `env` merges by `name`. Hoist anything that doesn't vary across adapters.
@@ -231,7 +231,7 @@ The runner routes overlay fields into every consuming app's `package.json` (vs t
## Cross-package consumption
If your module's source imports from another internal package (e.g. Better Auth's `auth.ts` reads `db` from the orm package), declare the dependency at the module level:
If your module's source imports from another internal package (e.g. Better Auth's `auth.ts` reads `db` from the ORM package for its database schema), declare the dependency at the module level:
```ts
defineModule({
+1 -1
View File
@@ -3,7 +3,7 @@ title: Registry
description: The roadmap of first-party modules Stanza ships — what's available today and what's planned.
---
Every module fills exactly one **category**. This page is the canonical roadmap of the first-party modules Stanza ships: what's available today, and what's on the way. Third parties can publish their own modules under any `@scope` and have users pull them in alongside these — see the [Authoring manual](/docs/authoring) for the full surface, or jump to [Third-party modules](#third-party-modules) below for a pointer.
Every module fills exactly one **category**. This page is the canonical roadmap of the first-party modules Stanza ships: what's available today, and what's on the way. Third parties can publish their own modules under any `@scope` and have users pull them in alongside these — see the [Authoring manual](/docs/authoring) for the full surface, or jump to [Third-party modules](/docs/registry#third-party-modules) below for a pointer.
## Categories
@@ -202,7 +202,9 @@ const ModuleCard = memo(function ModuleCard({
<TooltipContent sideOffset={8}>View details</TooltipContent>
</Tooltip>
</div>
{selected && <IconCheck className="size-4 shrink-0 text-foreground" aria-hidden />}
{selected && (
<IconCheck className="mr-0.5 size-4 shrink-0 text-foreground" aria-hidden />
)}
</div>
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">{m.description}</p>
</div>
@@ -48,7 +48,10 @@ export function ProjectSetup({
return (
<Card className="gap-0 px-3 py-3.5">
<Label htmlFor={inputId} className="mb-1.5 text-[13px] font-medium text-muted-foreground">
<Label
htmlFor={inputId}
className="mb-2 text-[13px] leading-none font-medium text-muted-foreground"
>
Project name
</Label>
<Field data-invalid={showError || undefined}>
@@ -49,7 +49,7 @@ export function AdapterSwitcher({
const active = resolvedPeers[slot];
return (
<div key={slot} className="flex flex-wrap items-center gap-2">
<span className="w-20 shrink-0 text-xs font-medium text-muted-foreground">
<span className="w-20 shrink-0 text-[13px] font-medium tracking-tight text-muted-foreground">
{categoryLabel(slot)}
</span>
<ToggleGroup
+1 -4
View File
@@ -46,6 +46,7 @@ function listRegistryPaths(): string[] {
{ cause: error },
);
}
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
const index = JSON.parse(raw) as {
categories: Array<{ id: string }>;
modules: Array<{ category: string; id: string }>;
@@ -55,10 +56,6 @@ function listRegistryPaths(): string[] {
return [...categoryPaths, ...modulePaths].toSorted((a, b) => a.localeCompare(b));
}
// OG image routes are deliberately not prerendered — they're rendered at
// request time by takumi-rs (kept warm in the server bundle) and the binary
// outputs collide with sibling subdirs at the filesystem level (e.g. `/og`
// as a file vs `og/registry/...` as a directory).
export function listPrerenderPages() {
const docs = listDocsPaths();
const registry = listRegistryPaths();
+1 -1
View File
@@ -41,7 +41,7 @@ function Page() {
<Link
to="/docs/$"
params={{ _splat: "" }}
className="text-primary underline underline-offset-3 hover:text-primary/80"
className="text-sm text-primary underline underline-offset-3 hover:text-primary/80"
>
Learn more
</Link>
@@ -115,7 +115,7 @@ function ModuleDetailPage() {
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-3">
<h1 className="text-2xl font-medium tracking-tight">{module.label}</h1>
<Badge variant="outline">{categoryLabel(module.category)}</Badge>
<Badge variant="default">{categoryLabel(module.category)}</Badge>
</div>
<p className="mt-1.5 text-sm text-muted-foreground">{module.description}</p>
<div className="mt-3 flex flex-wrap gap-3 text-xs">
@@ -86,7 +86,7 @@ function CategoryLandingPage() {
<Link
to="/registry/$category/$id"
params={{ category: m.category, id: m.id }}
className="flex items-start gap-3 px-3 py-3 transition-colors hover:bg-muted/50"
className="flex items-start gap-3 p-3 transition-colors hover:bg-muted/50"
>
<ModuleLogo logo={m.logo} label={m.label} />
<div className="min-w-0 flex-1">
+27 -31
View File
@@ -6,7 +6,7 @@ import { lazy, Suspense, useEffect, useState } from "react";
import { ModuleLogo } from "@/components/module-logo";
import { BarList } from "@/components/stats/bar-list";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useTimeAgo } from "@/hooks/use-time-ago";
@@ -44,7 +44,7 @@ function formatGeneratedAt(iso: string): string {
function LastRefreshed({ iso }: { iso: string }) {
const ago = useTimeAgo(iso);
return (
<p className="font-mono text-xs leading-4 text-muted-foreground/70">
<p className="font-mono text-xs leading-4 text-muted-foreground/85">
Last refreshed{" "}
<Tooltip>
<TooltipTrigger
@@ -105,9 +105,9 @@ function StatsPage() {
</header>
<section className="mb-4 grid gap-4 sm:grid-cols-2">
<Card>
<Card className="gap-2 pb-2">
<CardHeader>
<CardTitle className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
<CardTitle className="text-xs font-medium tracking-wider text-foreground/80 uppercase">
Projects scaffolded
</CardTitle>
</CardHeader>
@@ -117,9 +117,9 @@ function StatsPage() {
</div>
</CardContent>
</Card>
<Card>
<Card className="gap-2 pb-2">
<CardHeader>
<CardTitle className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
<CardTitle className="text-xs font-medium tracking-wider text-foreground/80 uppercase">
Modules installed
</CardTitle>
</CardHeader>
@@ -132,12 +132,12 @@ function StatsPage() {
</section>
<section className="mb-8">
<Card>
<Card className="gap-2 pb-2">
<CardHeader>
<div className="flex items-baseline justify-between gap-4">
<CardTitle className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
CLI runs &middot; last 30 days
</CardTitle>
<CardTitle className="text-xs font-medium tracking-wider text-foreground/80 uppercase">
CLI runs <span className="text-muted-foreground">&middot; last 30 days</span>
</CardTitle>
<CardAction className="font-mono text-xs text-muted-foreground tabular-nums">
{stats ? (
<span className="font-mono text-xs text-muted-foreground tabular-nums">
{activitySum > 0
@@ -145,9 +145,9 @@ function StatsPage() {
: "No runs yet."}
</span>
) : null}
</div>
</CardAction>
</CardHeader>
<CardContent className="pb-1">
<CardContent>
<Suspense fallback={<div aria-hidden className="h-24 w-full" />}>
<ActivityChart activity30d={stats?.activity30d ?? []} isLoading={stats === null} />
</Suspense>
@@ -162,18 +162,14 @@ function StatsPage() {
const entries = stats?.perCategory[category] ?? [];
const totalInCategory = entries.reduce((acc, entry) => acc + entry.count, 0);
return (
<Card key={category}>
<CardHeader>
<div className="flex items-baseline justify-between gap-3">
<CardTitle className="text-xs font-medium tracking-wider text-foreground/85 uppercase">
{categoryLabel(category)}
</CardTitle>
{totalInCategory > 0 ? (
<span className="font-mono text-xs text-muted-foreground tabular-nums">
{numberFormatter.format(totalInCategory)}
</span>
) : null}
</div>
<Card size="sm" key={category}>
<CardHeader className="pb-1">
<CardTitle className="text-xs! font-medium tracking-wider text-foreground/80 uppercase">
{categoryLabel(category)}
</CardTitle>
<CardAction className="font-mono text-xs text-muted-foreground tabular-nums">
{totalInCategory > 0 ? numberFormatter.format(totalInCategory) : null}
</CardAction>
</CardHeader>
<CardContent>
<BarList
@@ -201,9 +197,9 @@ function StatsPage() {
<section id="telemetry" className="scroll-mt-20">
<h2 className="mb-4 text-lg font-medium tracking-tight">Telemetry policy</h2>
<div className="grid gap-4 sm:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-xs font-medium tracking-wider text-foreground/85 uppercase">
<Card size="sm">
<CardHeader className="pb-1">
<CardTitle className="text-xs! font-medium tracking-wider text-foreground/80 uppercase">
What we save
</CardTitle>
</CardHeader>
@@ -216,9 +212,9 @@ function StatsPage() {
</ul>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-xs font-medium tracking-wider text-foreground/85 uppercase">
<Card size="sm">
<CardHeader className="pb-1">
<CardTitle className="text-xs! font-medium tracking-wider text-foreground/80 uppercase">
What we don&rsquo;t
</CardTitle>
</CardHeader>
+9 -3
View File
@@ -1,3 +1,5 @@
import path from "node:path";
import { LRUCache } from "lru-cache";
import {
createHighlighter,
@@ -27,6 +29,7 @@ const LANGS = [
"shell",
"prisma",
"sql",
"dotenv",
] as const satisfies readonly BundledLanguage[];
const THEMES = ["github-light", "github-dark"] as const satisfies readonly BundledTheme[];
@@ -67,12 +70,15 @@ const EXT_TO_LANG: Record<string, BundledLanguage> = {
bash: "bash",
prisma: "prisma",
sql: "sql",
env: "dotenv",
// anything else returns undefined → highlighter renders as plaintext
};
export function langForPath(path: string): BundledLanguage | undefined {
const ext = path.split(".").pop()?.toLowerCase() ?? "";
return EXT_TO_LANG[ext];
export function langForPath(filePath: string): BundledLanguage | undefined {
const base = path.basename(filePath);
if (base.startsWith(".env")) return "dotenv";
const ext = path.extname(filePath).toLowerCase();
return EXT_TO_LANG[ext.slice(1)];
}
/**