Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/(site)/(catalogs)/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ToolCatalogSidebar } from "@/app/ui/catalogs/tool-catalog-sidebar";
import { cardArt } from "@/lib/card-art";
import { cardIcon } from "@/lib/card-icons";
import { getPublishedToolCatalog } from "@/lib/tool-catalogs";
import { getBreadcrumbs } from "@/lib/breadcrumbs";
import { catalogContent } from "./content";

export default async function ToolCatalogPage({ params }: { params: Promise<{ slug: string }> }) {
Expand All @@ -31,7 +32,7 @@ export default async function ToolCatalogPage({ params }: { params: Promise<{ sl
sidebarLabel="Tool catalogs sidebar"
>
<EntryHero art={cardArt(catalog.card_art_key)}>
<PageHeader>
<PageHeader breadcrumbs={await getBreadcrumbs(slug)}>
<h1>
<PageTitleIcon><Icon strokeWidth={2.2} /></PageTitleIcon>{" "}
<DirectoryPageTitle title={catalog.title} />
Expand Down
2 changes: 2 additions & 0 deletions app/(site)/(components-ui)/components-ui/[section]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { MediaAndArtExamples } from "../examples/media-and-art";
import { NavigationExamples } from "../examples/navigation";
import { NumbersAndSlidersExamples } from "../examples/numbers-and-sliders";
import { PageLayoutExamples } from "../examples/page-layout";
import { ProseExamples } from "../examples/prose";
import { ResultsExamples } from "../examples/results";
import { TablesExamples } from "../examples/tables";
import { TextAndChoicesExamples } from "../examples/text-and-choices";
Expand All @@ -27,6 +28,7 @@ const examples: Record<string, ComponentType> = {
tables: TablesExamples,
cards: CardsExamples,
content: ContentExamples,
prose: ProseExamples,
"media-and-art": MediaAndArtExamples,
"feedback-and-dialogs": FeedbackAndDialogsExamples,
navigation: NavigationExamples,
Expand Down
18 changes: 6 additions & 12 deletions app/(site)/(components-ui)/components-ui/components-ui.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@

/* Example layouts */

/* Prose demos stop at the reading width, as they do on pages. */
.reading {
max-width: var(--measure-prose);
min-width: 0;
}

.stack {
display: grid;
gap: var(--space-8);
Expand Down Expand Up @@ -153,18 +159,6 @@
position: relative;
}

.cardLogo {
border: 1px solid var(--color-border-strong);
border-radius: var(--radius);
color: var(--color-text);
display: grid;
font-family: var(--font-display);
font-weight: var(--weight-bold);
height: 48px;
place-items: center;
width: 48px;
}

.markerExample {
max-width: 320px;
}
Expand Down
58 changes: 13 additions & 45 deletions app/(site)/(components-ui)/components-ui/examples/cards.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,16 @@
"use client";

import Link from "next/link";
import { CalendarDays, Code2, HardDrive, MemoryStick, WalletCards } from "lucide-react";
import { CatalogCard } from "@/app/ui/cards/catalog-card";
import { HardDrive } from "lucide-react";
import { DirectoryCard } from "@/app/ui/cards/directory-card";
import { PageCard, PageCardCounts } from "@/app/ui/cards/page-card";
import { ToolCard } from "@/app/ui/cards/tool-card";
import { backup321Art } from "@/app/ui/art/backup-321";
import { AppFactCardContent, appFactCardClassName } from "@/app/ui/directories/app-fact-card";
import { DirectoryAppCard } from "@/app/ui/directories/directory-app-card";
import { HardwareFactCardContent, hardwareFactCardClassName } from "@/app/ui/directories/hardware-fact-card";
import type { DirectoryAppCardData } from "@/lib/apps-card";
import type { ToolCatalogSummary } from "@/lib/tool-catalogs";
import { Example } from "../gallery";
import styles from "../components-ui.module.css";

// CatalogCard reads only the card fields, the page slug, and the tool count.
const exampleCatalog = {
card_name: "Storage tools",
card_description: "Plan pools, parity, backups, and growth before you buy drives.",
card_icon_key: null,
card_art_key: "storage-tools",
page: { slug: "storage-tools" },
tool_count: 12,
} as unknown as ToolCatalogSummary;

const exampleApp: DirectoryAppCardData = {
slug: "example-photo-library",
name: "Example photo library",
Expand Down Expand Up @@ -56,49 +42,31 @@ const exampleApp: DirectoryAppCardData = {
export function CardsExamples() {
return (
<>
<Example components={["PageCard", "PageCardCounts"]} note="The shared linked card. The tool size fills grids. The feature size leads a page and can show counts. Without art, the icon shows large." source="app/ui/cards/page-card.tsx" title="Page card">
<Example components={["ToolCard", "PageCard"]} note="PageCard at tool size, for tools in a catalog. ToolCard fills it from a tool's card fields: name, description, icon key, and art key." source="app/ui/cards/tool-card.tsx, app/ui/cards/page-card.tsx" title="Tool card">
<div className={styles.cardGrid}>
<PageCard art={backup321Art} description="Estimate local and off-site backup capacity from your data and retention plan." href="/backup-321-calculator" icon={HardDrive} name="3-2-1 backup calculator" size="tool" />
<PageCard
art={null}
count={<PageCardCounts counts={[{ label: "apps", value: 1240 }, { label: "categories", value: 18 }]} label="Example directory stats" />}
description="Without approved art, the card shows its icon large."
href="/self-host-apps"
icon={HardDrive}
name="Example directory"
/>
<ToolCard tool={{ slug: "dns-lookup", card_name: "DNS lookup", card_description: "Look up any DNS record from several public resolvers at once.", card_icon_key: "file-search", card_art_key: "dns-lookup" }} />
</div>
</Example>

<Example components={["ToolCard", "CatalogCard", "DirectoryCard"]} note="PageCard filled from database card fields: card name, description, icon key, and art key." source="app/ui/cards/" title="Tool, catalog, and directory cards">
<Example components={["DirectoryCard", "CatalogCard", "PageCard", "PageCardCounts"]} note="The large size for homepage collections. DirectoryCard shows counts. CatalogCard is the same card with a tool count. Without approved art, the icon shows large." source="app/ui/cards/" title="Feature card">
<div className={styles.cardGrid}>
<ToolCard tool={{ slug: "dns-lookup", card_name: "DNS lookup", card_description: "Look up any DNS record from several public resolvers at once.", card_icon_key: "file-search", card_art_key: "dns-lookup" }} />
<CatalogCard catalog={exampleCatalog} />
<DirectoryCard
counts={[{ label: "apps", value: 1240 }, { label: "categories", value: 34 }]}
page={{ slug: "self-host-apps", card_name: "Self-host apps", card_description: "Find apps you can run on your own server, with RAM and storage estimates.", card_icon_key: "self-host-apps", card_art_key: "self-host-apps" }}
/>
<PageCard
art={null}
count={<PageCardCounts counts={[{ label: "tools", value: 12 }]} label="Example catalog stats" />}
description="Without approved art, the card shows its icon large."
href="/storage-tools"
icon={HardDrive}
name="Card without art"
/>
</div>
</Example>

<Example components={["AppFactCardContent", "DirectoryAppCard"]} note="A record card with an image, categories, a summary, and labeled facts. DirectoryAppCard fills it from one app record." source="app/ui/directories/app-fact-card.tsx, app/ui/directories/directory-app-card.tsx" title="Record card">
<Example components={["DirectoryAppCard", "AppFactCardContent"]} note="A record card with an image, categories, a summary, and labeled facts. DirectoryAppCard fills AppFactCardContent from one app record." source="app/ui/directories/directory-app-card.tsx, app/ui/directories/app-fact-card.tsx" title="Record card">
<div className={styles.cardGrid}>
<Link className={appFactCardClassName} href="/self-host-apps">
<AppFactCardContent
categories={[{ name: "Home automation" }, { name: "Utilities" }]}
facts={[
{ label: "Source", value: "open source", icon: Code2 },
{ label: "Pricing", value: "free to self-host", icon: WalletCards },
{ label: "Est. RAM", value: "2 to 4 GB", icon: MemoryStick },
{ label: "Est. Storage", value: "low", icon: HardDrive },
{ label: "Last update", value: "not recorded", icon: CalendarDays },
]}
headingLevel={3}
image={<span aria-hidden="true" className={styles.cardLogo}>H</span>}
name="Example home server app"
summary="Run a home dashboard and connect devices on your own server."
/>
</Link>
<DirectoryAppCard app={exampleApp} href="/self-host-apps" />
</div>
</Example>
Expand Down
154 changes: 90 additions & 64 deletions app/(site)/(components-ui)/components-ui/examples/content.tsx
Original file line number Diff line number Diff line change
@@ -1,94 +1,120 @@
import { Callout, Note, Warning } from "@/app/ui/callout";
import { Note, Tip, Warning } from "@/app/ui/callout";
import { ContentBlock } from "@/app/ui/content-block";
import { ContentMarkdown } from "@/app/ui/content-markdown";
import { Term, Terms } from "@/app/ui/content-elements";
import { Faq } from "@/app/ui/faq";
import { CodeBlock } from "@/app/ui/site/code-block";
import { proseClassName, proseFlowClassName } from "@/app/ui/site/prose";
import { Example } from "../gallery";
import styles from "../components-ui.module.css";

// Demos sit in the same prose flow as page content, so they look the way a page shows them.
const flow = `${proseClassName} ${proseFlowClassName} ${styles.reading}`;

export function ContentExamples() {
return (
<>
<Example components={["Callout", "Note", "Warning"]} note="Note is context to keep in mind. Warning is something that can cost money, data, or time. Recommend is a suggested choice." source="app/ui/callout.tsx" title="Callouts">
<div className={styles.twoColumns}>
<Example
components={["Note", "Warning", "Tip", "Callout"]}
note="A note is context to keep in mind. A warning is something that can cost money, data, or time, and it is the only one whose title takes a color. A tip is a recommendation. Without a title, each one uses its kind as the title."
source="app/ui/callout.tsx"
title="Callouts"
usage={{ label: "MDX", code: `<Note title="Check the source">
Use the provider's current documentation when a setting can change.
</Note>

<Warning title="Back up before changing disks">
Changing a storage layout can erase the disks you select.
</Warning>

<Tip title="Start with a mirror">
Two matching drives are the simplest layout to restore.
</Tip>` }}
>
<div className={flow}>
<Note title="Check the source"><p>Use the provider&apos;s current documentation when a setting can change.</p></Note>
<Warning title="Back up before changing disks"><p>Changing a storage layout can erase the disks you select.</p></Warning>
<Callout title="Start with a mirror" tone="recommend"><p>Two matching drives are the simplest layout to restore.</p></Callout>
<Tip title="Start with a mirror"><p>Two matching drives are the simplest layout to restore.</p></Tip>
</div>
</Example>

<Example components={["ContentBlock"]} note="A formula box holds the rule. An example box works it through with real numbers." source="app/ui/content-block.tsx" title="Formula and example">
<div className={styles.twoColumns}>
<ContentBlock kind="formula"><p><code>usable space = raw space × efficiency</code></p><p>Keep both values in the same unit.</p></ContentBlock>
<ContentBlock kind="example"><p>Six 8 TB drives contain 48 TB of raw capacity before parity and overhead.</p></ContentBlock>
<Example
components={["ContentBlock"]}
note="A formula holds the rule once. An example works it through with real numbers. Code inside a formula reads as plain math, without code chips."
source="app/ui/content-block.tsx"
title="Formula and example"
usage={{ label: "MDX", code: `<Formula title="Payback months">

\`payback months = CAC ÷ (monthly ARPA × gross margin)\`

Use gross margin as a decimal, so 80% is 0.8.

</Formula>

<Example title="A $1,200 customer">

A customer costs $1,200 to win and pays $100 a month at an 80% gross margin.
Each month returns $80, so the cost comes back in 15 months.

</Example>` }}
>
<div className={flow}>
<ContentBlock kind="formula" title="Payback months">
<p><code>payback months = CAC ÷ (monthly ARPA × gross margin)</code></p>
<p>Use gross margin as a decimal, so 80% is 0.8.</p>
</ContentBlock>
<ContentBlock kind="example" title="A $1,200 customer">
<p>A customer costs $1,200 to win and pays $100 a month at an 80% gross margin. Each month returns $80, so the cost comes back in 15 months.</p>
</ContentBlock>
</div>
</Example>

<Example components={["Faq"]} source="app/ui/faq.tsx" title="Questions">
<Example
components={["Terms", "Term"]}
note="Words a reader may not know, each with its meaning."
source="app/ui/content-elements.tsx"
title="Terms"
usage={{ label: "MDX", code: `<Terms>
<Term name="TTL">How long a resolver may keep a record before it asks again.</Term>
<Term name="Resolver">The DNS server that looks up names for your device.</Term>
</Terms>` }}
>
<div className={flow}>
<Terms>
<Term name="TTL">How long a resolver may keep a record before it asks again.</Term>
<Term name="Resolver">The DNS server that looks up names for your device.</Term>
</Terms>
</div>
</Example>

<Example
components={["Faq"]}
note={<>Questions stay open, one under another. Course pages use this. In MDX, write each question as a <code>###</code> heading under a <code>##</code> heading instead.</>}
source="app/ui/faq.tsx"
title="Questions"
usage={{ label: "TSX", code: `<Faq items={[
{ question: "When should I use a chart?", answer: "When measured values are easier to compare visually." },
{ question: "Can a directory use a tool component?", answer: "Yes. Every shared component works on every page type." },
]} />` }}
>
<Faq items={[
{ question: "When should I use a chart?", answer: "Use one when measured values are easier to compare visually." },
{ question: "Can a directory use a tool component?", answer: "Yes. Every shared component works on every page type." },
{ question: "When should I use a chart?", answer: <p>When measured values are easier to compare visually than to read.</p> },
{ question: "Can a directory use a tool component?", answer: <p>Yes. Every shared component works on every page type.</p> },
]} />
</Example>

<Example components={["CodeBlock"]} note="A command or file with a label and a copy button." source="app/ui/site/code-block.tsx" title="Code block">
<Example
components={["CodeBlock"]}
note="A fenced block with a language or file name shows it in the header bar. Without one, the copy button sits beside the code."
source="app/ui/site/code-block.tsx"
title="Code block"
usage={{ label: "MDX", code: "```docker-compose.yml\nservices:\n app:\n image: ghcr.io/example/app:latest\n```\n\n```\nssh user@192.168.1.50\n```" }}
>
<CodeBlock label="docker-compose.yml">{`services:
app:
image: ghcr.io/example/app:latest
ports:
- "8080:80"`}</CodeBlock>
</Example>

<Example components={["proseClassName", "proseFlowClassName"]} note="Reading text uses the shared prose classes for headings, lists, links, and spacing." source="app/ui/site/prose.ts" title="Prose">
<div className={`${proseClassName} ${proseFlowClassName}`}>
<h3>What is a reverse proxy?</h3>
<p>A reverse proxy sits in front of your apps and sends each request to the right one. It also handles <a href="/nginx-proxy-pass-calculator">HTTPS certificates</a> in one place.</p>
<ul>
<li>One address for many apps</li>
<li>One place to renew certificates</li>
</ul>
</div>
</Example>

<Example components={["ContentMarkdown"]} note="Markdown on any page uses this syntax for callouts, formulas, examples, tables, code, and charts." source="app/ui/content-markdown.tsx" title="Markdown">
<ContentMarkdown markdown={[
"> [!NOTE] About this result",
"> Values come from a single sample.",
"",
"> [!FORMULA] Percentage used",
"> `used ÷ total × 100`",
"",
"> [!WARNING] Keep a backup",
"> A disk change can remove data.",
"",
"> [!TIP] Start small",
"> Two drives in a mirror are enough to begin.",
"",
"> [!EXAMPLE] Small pool",
"> 5 TB used out of 20 TB is 25% used.",
"",
"| Service | Requests |",
"| --- | ---: |",
"| API | 50 |",
"| Worker | 30 |",
"",
"```bash",
"df -h",
"```",
"",
"```chart-bar",
'{"title":"Requests by service","caption":"Same 15-minute sample.","data":[{"id":"api","label":"API","value":50},{"id":"worker","label":"Worker","value":30}]}',
"```",
"",
"```chart-line",
'{"title":"Storage over time","valueLabel":"Used TB","data":[{"id":"jan","label":"January","xLabel":"Jan","value":5},{"id":"feb","label":"February","xLabel":"Feb","value":7},{"id":"mar","label":"March","xLabel":"Mar","value":8.5},{"id":"apr","label":"April","xLabel":"Apr","value":11}]}',
"```",
"",
"```chart-donut",
'{"title":"Work split","data":[{"id":"api","label":"API","value":50},{"id":"worker","label":"Worker","value":30}]}',
"```",
].join("\n")} />
<CodeBlock>ssh user@192.168.1.50</CodeBlock>
</Example>
</>
);
Expand Down
17 changes: 17 additions & 0 deletions app/(site)/(components-ui)/components-ui/examples/navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Globe, HardDrive, Keyboard } from "lucide-react";
import { Pagination } from "@/app/ui/pagination";
import { Breadcrumbs } from "@/app/ui/site/breadcrumbs";
import { SidebarList } from "@/app/ui/site/sidebar-list";
import { Example } from "../gallery";
import styles from "../components-ui.module.css";
Expand All @@ -9,6 +10,22 @@ const here = "/components-ui/navigation";
export function NavigationExamples() {
return (
<>
<Example
components={["Breadcrumbs", "getBreadcrumbs", "getToolBreadcrumbs"]}
note="Above every page title but home's. Sections show lowercase, like the main menu; the page keeps its own name. A tool sits under its primary catalog. Pass the trail to PageHeader."
source="app/ui/site/breadcrumbs.tsx, lib/breadcrumbs.ts"
title="Breadcrumbs"
usage={{ label: "TSX", code: `<PageHeader breadcrumbs={await getBreadcrumbs("keyboard-shortcuts/claude-code", app.name)}>
<h1>…</h1>
</PageHeader>` }}
>
<div className={styles.stack}>
<Breadcrumbs items={[{ name: "stack127", href: "/", section: true }, { name: "domain tools", href: "/domain-tools", section: true }, { name: "Domain age checker", href: "/domain-age", section: false }]} />
<Breadcrumbs items={[{ name: "stack127", href: "/", section: true }, { name: "Keyboard shortcuts", href: "/keyboard-shortcuts", section: true }, { name: "Claude Code", href: "/keyboard-shortcuts/claude-code", section: false }]} />
<Breadcrumbs items={[{ name: "stack127", href: "/", section: true }, { name: "self-host apps", href: "/self-host-apps", section: true }]} />
</div>
</Example>

<Example components={["Pagination"]} note="The page builds each link, so filters and sorting stay in the URL. Page one uses the clean URL." source="app/ui/pagination.tsx" title="Pagination">
<Pagination ariaLabel="Example pages" currentPage={4} hrefForPage={(page) => page === 1 ? here : `${here}?page=${page}`} totalPages={12} />
</Example>
Expand Down
Loading
Loading