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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Reporting:
- Directory source lives in `app/(site)/(directories)/`, including stable fact pages such as `/tld-directory`.
- Course source lives in `app/(site)/(courses)/`.
- Home, about, privacy, terms, and labs live in `app/(site)/(site-pages)/`.
- Tool source lives in `app/(site)/(tools)/`. DNS, domain, and storage tools use a shared route-group layout before `<slug>/`.
- Tool source lives in `app/(site)/(tools)/<slug>/`. Every tool uses the same flat folder. Code shared by a tool family lives in `app/ui/tools/`.
- `public.stack127_pages` is the only publication registry.
- Exact published slugs are indexable and enter the sitemap. Unpublished routes return 404.
- Do not add route-owned metadata, robots, canonicals, sitemap dates, or JSON-LD to page files.
Expand Down
7 changes: 0 additions & 7 deletions app/(site)/(catalogs)/layout.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ const fullLayouts = [
{ href: "/about", label: "Site page", detail: "InfoPage with a reading-width article" },
];

const sidebarModes = [
{ href: "/components-ui/sidebar-modes/left", label: "Left", detail: "sidebarPosition=\"left\", the default. Filters and lessons. A drawer on small screens." },
{ href: "/components-ui/sidebar-modes/right", label: "Right", detail: "sidebarPosition=\"right\". Related pages and section links. Below the content on small screens." },
{ href: "/components-ui/sidebar-modes/none", label: "None", detail: "sidebarPosition=\"none\". Content only. Reading sections center." },
];

export function PageLayoutExamples() {
return (
<>
Expand Down Expand Up @@ -82,7 +88,15 @@ export function PageLayoutExamples() {
</ToolWorkbench>
</Example>

<Example components={["AppShell", "PageCanvas"]} note="Every page uses the shared shell with a sidebar, and PageCanvas for each content section. This gallery uses both. See full layouts on their live pages." source="app/ui/site/app-shell.tsx, app/ui/site/page-canvas.tsx" title="Full page layouts">
<Example components={["AppShell"]} note="Every page uses the shared shell. Pick where its sidebar goes with sidebarPosition: left for options people use, right for secondary links, or none when the page only needs its content. Each link opens the real shell." source="app/ui/site/app-shell.tsx" title="Sidebar positions" usage={{ label: "TSX", code: '<AppShell sidebar={<RelatedPages />} sidebarPosition="right">\n <PageCanvas>…</PageCanvas>\n</AppShell>' }}>
<ul className={styles.layoutLinks}>
{sidebarModes.map((mode) => (
<li key={mode.href}><Link href={mode.href}>{mode.label}</Link><span>{mode.detail}</span></li>
))}
</ul>
</Example>

<Example components={["AppShell", "PageCanvas"]} note="PageCanvas wraps each content section. This gallery uses both. See full layouts on their live pages." source="app/ui/site/app-shell.tsx, app/ui/site/page-canvas.tsx" title="Full page layouts">
<ul className={styles.layoutLinks}>
{fullLayouts.map((layout) => (
<li key={layout.href}><Link href={layout.href}>{layout.label}</Link><span>{layout.detail}</span></li>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { registryLayout } from "@/lib/registry-layout";

const registry = registryLayout("/components-ui/sidebar-modes/[position]");
export const generateMetadata = registry.generateMetadata;
export function generateStaticParams() { return []; }
export const revalidate = false;
export default registry.Layout;
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { ReactNode } from "react";
import Link from "next/link";
import { notFound } from "next/navigation";
import { LayoutGrid, PanelLeft, PanelRight, Square } from "lucide-react";
import { AppShell, SidebarLabel, type SidebarPosition } from "@/app/ui/site/app-shell";
import { PageCanvas } from "@/app/ui/site/page-canvas";
import { PageHeader } from "@/app/ui/site/page-header";
import { PageTitleIcon } from "@/app/ui/site/page-title-icon";
import { proseClassName } from "@/app/ui/site/prose";
import { PathnameSidebarList } from "@/app/ui/site/pathname-sidebar-list";
import { sidebarPanelClassName, sidebarPanelHeadingClassName } from "@/app/ui/site/sidebar-panel";

const modes: Record<SidebarPosition, { title: ReactNode; lede: string; icon: typeof PanelLeft }> = {
left: {
title: <>Sidebar on the <mark>left</mark></>,
lede: "For options people use to work the page, such as directory filters or course lessons. On small screens it opens from the drawer.",
icon: PanelLeft,
},
right: {
title: <>Sidebar on the <mark>right</mark></>,
lede: "For secondary help, such as related pages or links to this page's sections. On small screens it sits below the content.",
icon: PanelRight,
},
none: {
title: <>No <mark>sidebar</mark></>,
lede: "For pages that only need their content. Wide sections fill the page. Reading sections center at the reading width.",
icon: Square,
},
};

const sidebarItems = [
{ href: "/components-ui/sidebar-modes/left", label: "Sidebar on the left", icon: <PanelLeft size={15} strokeWidth={2.2} /> },
{ href: "/components-ui/sidebar-modes/right", label: "Sidebar on the right", icon: <PanelRight size={15} strokeWidth={2.2} /> },
{ href: "/components-ui/sidebar-modes/none", label: "No sidebar", icon: <Square size={15} strokeWidth={2.2} /> },
{ href: "/components-ui/page-layout", label: "Back to page layout", icon: <LayoutGrid size={15} strokeWidth={2.2} /> },
];

function isSidebarPosition(value: string): value is SidebarPosition {
return value === "left" || value === "right" || value === "none";
}

// Development only: shows each AppShell sidebar position with the real shell, outside the gallery's own shell.
export default async function SidebarModePage({ params }: { params: Promise<{ position: string }> }) {
if (process.env.NODE_ENV !== "development") notFound();
const { position } = await params;
if (!isSidebarPosition(position)) notFound();
const mode = modes[position];

return (
<AppShell
mainLabel="Sidebar modes"
menuLabel="Modes"
sidebar={(
<div className={sidebarPanelClassName}>
<div className={sidebarPanelHeadingClassName}><SidebarLabel>Sidebar modes</SidebarLabel></div>
<nav aria-label="Sidebar modes"><PathnameSidebarList items={sidebarItems} /></nav>
</div>
)}
sidebarLabel="Sidebar modes sidebar"
sidebarPosition={position}
>
<PageCanvas>
<PageHeader bordered>
<h1>
<PageTitleIcon><mode.icon strokeWidth={2.2} /></PageTitleIcon> {mode.title}
</h1>
<p>{mode.lede}</p>
</PageHeader>
<p>
This section uses the wide <code>PageCanvas</code>. It fills the main column, so a table or card grid has room.
</p>
</PageCanvas>
<PageCanvas reading>
<div className={proseClassName}>
<h2>A reading section</h2>
<p>
This section uses <code>PageCanvas reading</code>. Lines stop at the reading width, so long paragraphs stay easy to follow.
With a sidebar, the section starts at the left edge of the main column. With no sidebar, it centers in the page.
</p>
<ul>
{sidebarItems.map((item) => <li key={item.href}><Link href={item.href}>{item.label}</Link></li>)}
</ul>
</div>
</PageCanvas>
</AppShell>
);
}
12 changes: 3 additions & 9 deletions app/(site)/(directories)/epp-status-codes/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import { getRequiredDirectoryPage } from "@/lib/directory-pages";
import { DirectoryPageTitle } from "@/app/ui/directories/directory-page-content";
import { PageMeta } from "@/app/ui/site/page-meta";
import { PageCanvas } from "@/app/ui/site/page-canvas";
import "@/app/(site)/(tools)/(domain)/domain-tools.css";
import "@/app/ui/tools/domain/domain-tools.css";
import { AppShell } from "../../../ui/site/app-shell";
import { DomainToolsSidebar } from "@/app/(site)/(tools)/(domain)/domain-tools-sidebar";
import { STATUS_CODES, STATUS_GROUP_TITLES, type StatusGroup } from "@/lib/domain/status-codes.ts";
import { StatePill } from "@/app/(site)/(tools)/(domain)/domain-lookup-client";
import { StatePill } from "@/app/ui/tools/domain/domain-lookup-client";
import { DirectoryTitleIcon } from "@/app/ui/directories/directory-title-icon";
import { PageHeader } from "../../../ui/site/page-header";
import { proseSectionFlowClassName } from "../../../ui/site/prose";
Expand Down Expand Up @@ -55,12 +54,7 @@ function SourcePathParagraph({ children, ...props }: ComponentPropsWithoutRef<"p
export default async function EppStatusCodesPage() {
const directoryPage = await getRequiredDirectoryPage("epp-status-codes");
return (
<AppShell
mainLabel="Domain status codes"
menuLabel="Domain tools"
sidebar={<DomainToolsSidebar />}
sidebarLabel="Domain tools sidebar"
>
<AppShell mainLabel="Domain status codes" sidebarPosition="none">
<PageCanvas className="domain-tool-main">
<PageHeader breadcrumbs={await getBreadcrumbs("epp-status-codes")}>
<h1><DirectoryTitleIcon iconKey={directoryPage.card_icon_key} />{" "}<DirectoryPageTitle title={directoryPage.title} /></h1>
Expand Down
8 changes: 1 addition & 7 deletions app/(site)/(directories)/keyboard-shortcuts/(index)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { AppShell } from "@/app/ui/site/app-shell";
import { KeyboardShortcutsSidebar } from "@/app/ui/directories/keyboard-shortcuts-sidebar";
import { KeyboardShortcutsDirectory } from "../keyboard-shortcuts-directory";

export default function KeyboardShortcutsDirectoryPage() {
return (
<AppShell
mainLabel="Keyboard shortcuts directory"
menuLabel="Shortcuts"
sidebar={<KeyboardShortcutsSidebar activePage="directory" />}
sidebarLabel="Shortcut pages"
>
<AppShell mainLabel="Keyboard shortcuts directory" sidebarPosition="none">
<KeyboardShortcutsDirectory />
</AppShell>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { getPublishedDirectoryPage, getRequiredDirectoryPage } from "@/lib/direc
import { AppShell } from "@/app/ui/site/app-shell";
import type { ReactNode } from "react";
import type { AppShortcuts } from "@/lib/shortcuts/types";
import { KeyboardShortcutsSidebar } from "@/app/ui/directories/keyboard-shortcuts-sidebar";
import { KeyboardShortcutsReference } from "./keyboard-shortcuts-reference";

export async function KeyboardShortcutsAppPage({ app, intro }: { app: AppShortcuts; intro: ReactNode }) {
Expand All @@ -14,12 +13,7 @@ export async function KeyboardShortcutsAppPage({ app, intro }: { app: AppShortcu
]);
const iconKey = directoryPage.card_icon_key ?? parentPage?.card_icon_key ?? null;
return (
<AppShell
mainLabel={app.name + " keyboard shortcuts"}
menuLabel="Shortcuts"
sidebar={<KeyboardShortcutsSidebar activeSlug={app.slug} groups={app.groups} />}
sidebarLabel="Shortcut pages"
>
<AppShell mainLabel={app.name + " keyboard shortcuts"} sidebarPosition="none">
<KeyboardShortcutsReference app={app} breadcrumbs={breadcrumbs} directoryPage={directoryPage} iconKey={iconKey} intro={intro} />
</AppShell>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { PageMeta } from "@/app/ui/site/page-meta";
import { PageCanvas } from "@/app/ui/site/page-canvas";
import Link from "next/link";
import { appsInCategory, shortcutAppCatalog, shortcutCategories } from "@/lib/shortcuts";
import { platformLabels } from "@/lib/shortcuts/types";
import type { PlatformId } from "@/lib/shortcuts/types";
import { DirectoryTitleIcon } from "@/app/ui/directories/directory-title-icon";
import { PageHeader } from "@/app/ui/site/page-header";
import { EntryHero } from "@/app/ui/site/entry-hero";
Expand All @@ -16,6 +16,9 @@ import Intro from "./(index)/content/intro.mdx";
import ShortcutNotWorking from "./(index)/content/shortcut-not-working.mdx";
import WhereShortcutsComeFrom from "./(index)/content/where-shortcuts-come-from.mdx";

const platformOrder: PlatformId[] = ["windows", "macos", "linux"];
const platformShortLabels: Record<PlatformId, string> = { windows: "Win", macos: "Mac", linux: "Linux" };

function categoryId(category: string) {
return "category-" + category.toLowerCase().replace(/[^a-z0-9]+/g, "-");
}
Expand All @@ -41,32 +44,27 @@ export async function KeyboardShortcutsDirectory() {
return (
<section className={styles.section} aria-labelledby={categoryId(category) + "-title"} id={categoryId(category)} key={category}>
<h2 id={categoryId(category) + "-title"}>{category}</h2>
<div className={styles.tableFrame}>
<table className={styles.table + " " + styles.directoryTable}>
<thead>
<tr>
<th scope="col">App</th>
<th scope="col">Platforms</th>
<th scope="col">Shortcuts</th>
</tr>
</thead>
<tbody>
{apps.map((app) => (
<tr key={app.slug}>
<td data-label="App">
<Link href={"/keyboard-shortcuts/" + app.slug}>{app.name}</Link>
</td>
<td data-label="Platforms">
<ul className={styles.platforms}>
{app.platforms.map((platform) => <li key={platform}>{platformLabels[platform]}</li>)}
</ul>
</td>
<td className={styles.count} data-label="Shortcuts">{app.shortcutCount}</td>
</tr>
))}
</tbody>
</table>
</div>
<ul className={styles.appGrid}>
{apps.map((app) => (
<li key={app.slug}>
<Link className={styles.appCard} href={"/keyboard-shortcuts/" + app.slug}>
<span className={styles.appName}>{app.name}</span>
<span className={styles.appCount}>{app.shortcutCount}<small> shortcuts</small></span>
<span className={styles.appPlatforms}>
{platformOrder.map((platform) => (
<span
aria-hidden={app.platforms.includes(platform) ? undefined : true}
className={app.platforms.includes(platform) ? styles.platform : `${styles.platform} ${styles.platformMissing}`}
key={platform}
>
{platformShortLabels[platform]}
</span>
))}
</span>
</Link>
</li>
))}
</ul>
</section>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,28 @@
.table a { color: var(--color-link); font-weight: var(--weight-semibold); text-decoration: none; text-decoration-color: currentColor; text-underline-offset: 3px; }
.table a:hover { color: var(--color-link); text-decoration-color: currentColor; }
.table a:focus-visible, .nextStep a:focus-visible { outline: 0; box-shadow: var(--focus-ring); }
.platforms { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-5); margin: 0; padding: 0; list-style: none; }
.platforms li { white-space: nowrap; }
.count { color: var(--color-text-muted); font-family: var(--font-mono); white-space: nowrap; }
.appGrid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-5); margin: 0; padding: 0; list-style: none; }
.appCard {
display: grid;
gap: var(--space-2);
height: 100%;
padding: var(--space-7);
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
color: var(--color-text);
text-decoration: none;
transition: border-color var(--motion-base);
}
.appCard:hover { border-color: var(--color-accent); }
.appCard:hover .appName { text-decoration: underline; text-underline-offset: 3px; }
.appCard:focus-visible { outline: var(--focus-outline); outline-offset: 2px; }
.appName { color: var(--color-link); font-family: var(--font-display); font-size: var(--text-h3); font-weight: var(--weight-bold); letter-spacing: var(--tracking-display); line-height: var(--line-heading); }
.appCount { font-family: var(--font-mono); white-space: nowrap; }
.appCount small { color: var(--color-text-muted); font-size: var(--text-small); }
.appPlatforms { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-4); }
.platform { padding: 0 var(--space-2); border: 1px solid var(--color-border-strong); border-radius: var(--radius); font-family: var(--font-mono); font-size: var(--text-small); line-height: var(--line-body); }
.platformMissing { border-color: var(--color-border); border-style: dashed; color: var(--color-text-muted); text-decoration: line-through; opacity: 0.6; }
.referenceTable { table-layout: fixed; }
.referenceTable th:first-child, .referenceTable .action { width: 40%; }
.referenceTable th, .referenceTable td { padding: var(--space-10); }
Expand Down Expand Up @@ -73,11 +92,14 @@
.referenceTable tbody tr td + td { margin-top: var(--space-7); }
.referenceTable .action { width: auto; }
}
@media (max-width: 1100px) {
.appGrid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 760px) {
.directoryTable thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.directoryTable tbody tr { display: grid; grid-template-columns: 1fr auto; padding: var(--space-7); gap: var(--space-3); border-bottom: 1px solid var(--color-border); }
.directoryTable tbody tr:last-child { border-bottom: 0; }
.directoryTable tbody tr td { padding: 0; border: 0; }
.directoryTable tbody tr td:first-child { grid-column: 1 / -1; }
.directoryTable .count::after { content: " shortcuts"; font-family: var(--font-sans); }
.appGrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.appCard { padding: var(--space-5); }
.platform { padding: 0 var(--space-1); }
}
@media (max-width: 360px) {
.appGrid { grid-template-columns: minmax(0, 1fr); }
}
10 changes: 2 additions & 8 deletions app/(site)/(directories)/tld-directory/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import { getBreadcrumbs } from "@/lib/breadcrumbs";
import { DirectoryPageTitle } from "@/app/ui/directories/directory-page-content";
import { PageMeta } from "@/app/ui/site/page-meta";
import { PageCanvas } from "@/app/ui/site/page-canvas";
import "@/app/(site)/(tools)/(domain)/domain-tools.css";
import "@/app/ui/tools/domain/domain-tools.css";
import { AppShell } from "../../../ui/site/app-shell";
import { DomainToolsSidebar } from "@/app/(site)/(tools)/(domain)/domain-tools-sidebar";
import { TldTable, type TldRow } from "./tld-table";
import directory from "@/lib/domain/data/tld-directory.json" with { type: "json" };
import { DirectoryTitleIcon } from "@/app/ui/directories/directory-title-icon";
Expand Down Expand Up @@ -33,12 +32,7 @@ export default async function TldDirectoryPage() {
});

return (
<AppShell
mainLabel="TLD directory"
menuLabel="Domain tools"
sidebar={<DomainToolsSidebar />}
sidebarLabel="Domain tools sidebar"
>
<AppShell mainLabel="TLD directory" sidebarPosition="none">
<PageCanvas className="domain-tool-main">
<PageHeader breadcrumbs={await getBreadcrumbs("tld-directory")}>
<h1><DirectoryTitleIcon iconKey={directoryPage.card_icon_key} />{" "}<DirectoryPageTitle title={directoryPage.title} /></h1>
Expand Down
2 changes: 1 addition & 1 deletion app/(site)/(directories)/tld-directory/tld-table.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useMemo, useState } from "react";
import { StatePill } from "@/app/(site)/(tools)/(domain)/domain-lookup-client";
import { StatePill } from "@/app/ui/tools/domain/domain-lookup-client";

export type TldRow = {
tld: string;
Expand Down
Loading
Loading