diff --git a/kit/README.md b/kit/README.md index b42a274..01fde50 100644 --- a/kit/README.md +++ b/kit/README.md @@ -66,7 +66,7 @@ const returnTo = safeReturnTo(url.searchParams.get("return_to")); // Build absolute login URL (bypasses React Router basename prepend) const loginUrl = buildLoginRedirect(returnTo, url.origin); -// → "https://ampl.tools/auth/login?return_to=%2Fpalaeography" +// → "https://ampl.tools/auth/login?return_to=%2Fpaleography" ``` ### Non-refresh note @@ -118,26 +118,26 @@ import { AmplHeader, DEFAULT_TOOLS } from "@ampl/kit/ui"; // Compact signed-in header (standard in-app use) } account={{ name: user.name, avatarUrl: user.avatar_url }} nav={[ - { label: "Manuscripts", href: "/palaeography/manuscripts", active: true }, - { label: "Groups", href: "/palaeography/groups" }, + { label: "Manuscripts", href: "/paleography/manuscripts", active: true }, + { label: "Groups", href: "/paleography/groups" }, ]} /> // Full signed-out front door (size="full" only on the front door) } - signInHref="/auth/login?return_to=/palaeography" + signInHref="/auth/login?return_to=/paleography" /> ``` -The tool switcher defaults to `DEFAULT_TOOLS` (Palaeography + Scheduling); pass +The tool switcher defaults to `DEFAULT_TOOLS` (Paleography + Scheduling); pass a custom `tools` array to override. `SiteHeader` is deprecated as of v0.3.0 in favour of `AmplHeader`. See `CONSUMING.md` for the full prop reference and CSP requirements. diff --git a/kit/auth/index.ts b/kit/auth/index.ts index bfb7eb0..fb72e45 100644 --- a/kit/auth/index.ts +++ b/kit/auth/index.ts @@ -135,7 +135,7 @@ export async function validateSession( * - backslash (handles `\\evil` and `/path\\evil`) * - embedded schemes (javascript:, http://, etc.) via URL parser * - * NOTE: returns the full apex pathname (e.g. /palaeography/x) + * NOTE: returns the full apex pathname (e.g. /paleography/x) * — NOT basename-stripped. The callback's absolute-URL redirect handles * routing without double-prefix. */ @@ -158,8 +158,8 @@ export function safeReturnTo(value: string | null): string { * prepend. Consumer tools call this to redirect unauthenticated users. * * Example: - * buildLoginRedirect("/palaeography", "https://ampl.tools") - * // → "https://ampl.tools/auth/login?return_to=%2Fpalaeography" + * buildLoginRedirect("/paleography", "https://ampl.tools") + * // → "https://ampl.tools/auth/login?return_to=%2Fpaleography" * * The `returnTo` value should already be validated by `safeReturnTo`. */ @@ -184,8 +184,8 @@ export function buildLoginRedirect( * basename, same absolute URL output via `new URL(...)`. * * Example: - * buildLogoutHref("/palaeography", "https://ampl.tools") - * // → "https://ampl.tools/auth/logout?return_to=%2Fpalaeography" + * buildLogoutHref("/paleography", "https://ampl.tools") + * // → "https://ampl.tools/auth/logout?return_to=%2Fpaleography" * * The `returnTo` value should already be validated by `safeReturnTo`. */ diff --git a/kit/ui/ampl-header/AmplHeader.tsx b/kit/ui/ampl-header/AmplHeader.tsx index e0c5e83..6bbdcfe 100644 --- a/kit/ui/ampl-header/AmplHeader.tsx +++ b/kit/ui/ampl-header/AmplHeader.tsx @@ -5,7 +5,10 @@ * sheet; holds the single mobile-sheet open/close state. All tool variation * comes through props. The public entry point for the component. * - * @version v0.3.0 + * v0.3.2: the cross-tool switcher moved from the WORKSHOP band into the + * institutional band; the mobile sheet gains a "switch tool" section. + * + * @version v0.3.2 */ import { useState } from "react"; import { InstitutionalBand } from "./InstitutionalBand"; @@ -43,17 +46,17 @@ export function AmplHeader({ labHome={labHome} menuOpen={menuOpen} onMenuToggle={() => setMenuOpen((v) => !v)} - /> - + ); diff --git a/kit/ui/ampl-header/InstitutionalBand.tsx b/kit/ui/ampl-header/InstitutionalBand.tsx index e0cc1e1..acca129 100644 --- a/kit/ui/ampl-header/InstitutionalBand.tsx +++ b/kit/ui/ampl-header/InstitutionalBand.tsx @@ -1,25 +1,37 @@ /** * Institutional band — the white top band: AMPL logo lockup (links to the lab - * home) + lab-site nav, scaling between full and compact; plus the mobile - * hamburger that toggles the sheet. Owned by the kit (single AMPL identity). + * home) + the cross-tool WORKSHOP switcher + lab-site nav, scaling between full + * and compact; plus the mobile hamburger that toggles the sheet. Owned by the + * kit (single AMPL identity). * - * @version v0.3.0 + * v0.3.2: the WORKSHOP tool switcher moved here (first nav item) from the plum + * band, so its dropdown opens at page-top with nothing below to clip it. + * + * @version v0.3.2 */ import { useTranslation } from "react-i18next"; import amplLogo from "../../assets/ampl-logo.svg"; import { LAB_NAV } from "./lab-nav"; -import type { HeaderSize } from "./types"; +import { ToolSwitcher } from "./ToolSwitcher"; +import type { HeaderSize, ToolId, ToolLink } from "./types"; export function InstitutionalBand({ size, labHome, menuOpen, onMenuToggle, + tool, + toolName, + tools, }: { size: HeaderSize; labHome: string; menuOpen: boolean; onMenuToggle: () => void; + /** v0.3.2: the cross-tool switcher now lives here, as the first nav item. */ + tool: ToolId; + toolName: string; + tools: ToolLink[]; }) { const { t } = useTranslation("kit"); const full = size === "full"; @@ -39,13 +51,15 @@ export function InstitutionalBand({ - {/* Lab nav — desktop only; mobile uses the sheet. */} + {/* Switcher + lab nav — desktop only; mobile uses the sheet. The + WORKSHOP switcher is first (left of the lab links). */} + {LAB_NAV.map((item) => ( tl.id === tool); + const otherTools = tools.filter((tl) => tl.id !== tool); return ( + {/* Workshop switcher — current tool + switch-to links */} + + + {t("switcher.current")} + + + {currentTool?.name ?? toolName} + + {otherTools.length > 0 && ( + <> + + {t("switcher.switchTo")} + + {otherTools.map((tl) => ( + + {tl.name} + + ))} + > + )} + + {/* Lab nav */} - + {LAB_NAV.map((item) => ( {t(`nav.${item.key}`)} diff --git a/kit/ui/ampl-header/ToolSwitcher.tsx b/kit/ui/ampl-header/ToolSwitcher.tsx index 7b4491d..727b835 100644 --- a/kit/ui/ampl-header/ToolSwitcher.tsx +++ b/kit/ui/ampl-header/ToolSwitcher.tsx @@ -1,39 +1,47 @@ /** - * Tool switcher — the `Workshop · {tool} ▾` cluster on the WORKSHOP band, a - * native disclosure. The panel surfaces the current tool in a banner, - * then lists the other AMPL tools you can switch to (current-banner + others). + * Tool switcher — the `WORKSHOP ▾` cluster, a native disclosure. Lives + * in the white INSTITUTIONAL band as the first nav item (v0.3.2 relocation), so + * the trigger is styled like the lab-nav links (dark, uppercase title) and scales + * with the band `size`. The panel itself is the dark plum popover: the current + * tool in a banner, then the other AMPL tools you can switch to. * - * @version v0.3.1 + * @version v0.3.2 */ import { useTranslation } from "react-i18next"; -import type { ToolId, ToolLink } from "./types"; +import type { HeaderSize, ToolId, ToolLink } from "./types"; export function ToolSwitcher({ tool, toolName, tools, + size = "compact", }: { tool: ToolId; toolName: string; tools: ToolLink[]; + /** Institutional-band scale — matches the lab-nav link sizing. */ + size?: HeaderSize; }) { const { t } = useTranslation("kit"); const current = tools.find((tl) => tl.id === tool); const others = tools.filter((tl) => tl.id !== tool); + const full = size === "full"; return ( - Workshop - · - {toolName} - ▾ + {/* Literal "Workshop" (as the pre-v0.3.2 trigger was) — ES branding of + this label is a pending decision, deliberately not invented here. */} + Workshop + ▾ - + {/* Current tool — where you are now */} diff --git a/kit/ui/ampl-header/WorkshopBand.tsx b/kit/ui/ampl-header/WorkshopBand.tsx index dabef38..417dc5b 100644 --- a/kit/ui/ampl-header/WorkshopBand.tsx +++ b/kit/ui/ampl-header/WorkshopBand.tsx @@ -1,35 +1,31 @@ /** - * WORKSHOP band — the deep-plum band: tool switcher + contextual in-app nav - * (active/disabled states) + the EN/ES switcher and account-chip-or-sign-in cluster. + * WORKSHOP band — the deep-plum band: contextual in-app nav (active/disabled + * states) + the EN/ES switcher and account-chip-or-sign-in cluster. * - * @version v0.3.0 + * v0.3.2: the cross-tool WORKSHOP switcher moved UP into the institutional band; + * this band no longer owns it. + * + * @version v0.3.2 */ import type { ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import { ToolSwitcher } from "./ToolSwitcher"; import { AccountMenu } from "./AccountMenu"; -import type { AccountInfo, NavItem, ToolId, ToolLink } from "./types"; +import type { AccountInfo, NavItem } from "./types"; export function WorkshopBand({ - tool, - toolName, nav, context, localeSwitcher, account, signInHref, signInLabel, - tools, }: { - tool: ToolId; - toolName: string; nav?: NavItem[]; context?: ReactNode; localeSwitcher: ReactNode; account?: AccountInfo | null; signInHref?: string; signInLabel?: ReactNode; - tools: ToolLink[]; }) { const { t } = useTranslation("kit"); const hasNav = (nav && nav.length > 0) || context; @@ -37,8 +33,6 @@ export function WorkshopBand({ return ( - - {/* Contextual tool nav — desktop only; mobile uses the sheet. */} {hasNav && ( diff --git a/kit/ui/ampl-header/tools.ts b/kit/ui/ampl-header/tools.ts index 5cc6692..676d547 100644 --- a/kit/ui/ampl-header/tools.ts +++ b/kit/ui/ampl-header/tools.ts @@ -1,13 +1,13 @@ import type { ToolLink } from "./types"; /** - * The live AMPL Workshop tool registry. Display names are public ("Palaeography" + * The live AMPL Workshop tool registry. Display names are public ("Paleography" * for the calamus codebase); descriptors are English fallbacks localised at * render via `switcher.tagline.`. Data-driven so future tools slot in. * - * @version v0.3.0 + * @version v0.3.2 */ export const DEFAULT_TOOLS: ToolLink[] = [ - { id: "calamus", name: "Palaeography", descriptor: "Practice reading manuscripts", href: "https://ampl.tools/palaeography" }, + { id: "calamus", name: "Paleography", descriptor: "Practice reading manuscripts", href: "https://ampl.tools/paleography" }, { id: "scheduling", name: "Scheduling", descriptor: "booking & polls", href: "https://ampl.tools/scheduling" }, ]; diff --git a/kit/ui/ampl-header/types.ts b/kit/ui/ampl-header/types.ts index c982669..23a33ec 100644 --- a/kit/ui/ampl-header/types.ts +++ b/kit/ui/ampl-header/types.ts @@ -44,7 +44,7 @@ export type HeaderSize = "full" | "compact"; export interface AmplHeaderProps { /** Internal tool id — drives the switcher "current" highlight. */ tool: ToolId; - /** Public display name shown in the WORKSHOP band (e.g. "Palaeography"). */ + /** Public display name shown in the WORKSHOP band (e.g. "Paleography"). */ toolName: string; /** Institutional-band scale. Default "compact"; "full" only on the signed-out front door. */ size?: HeaderSize; diff --git a/package.json b/package.json index 0390521..e982bd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ampl/kit", - "version": "0.3.1", + "version": "0.3.2", "private": true, "type": "module", "description": "Shared foundation for the AMPL tools suite: the ampl-auth Worker (ampl.tools/auth) + the @ampl/kit design system, surfaces, and session-validation helper consumed by every tool.", diff --git a/tests/kit/AmplHeader.test.ts b/tests/kit/AmplHeader.test.ts index 6059891..1d2568e 100644 --- a/tests/kit/AmplHeader.test.ts +++ b/tests/kit/AmplHeader.test.ts @@ -20,10 +20,10 @@ const SWITCHER = React.createElement("span", { "data-testid": "ls" }, "EN/ES"); describe("AmplHeader", () => { it("signed-in: renders the account menu (POST sign-out form), no sign-in link", async () => { const html = await render({ - tool: "calamus", toolName: "Palaeography", localeSwitcher: SWITCHER, + tool: "calamus", toolName: "Paleography", localeSwitcher: SWITCHER, account: { name: "Juan Cobo", handle: "juan", avatarUrl: null, signOutHref: "/auth/logout", returnTo: "/palaeography" }, }); - expect(html).toContain("Palaeography"); + expect(html).toContain("Paleography"); expect(html).toContain('method="post"'); expect(html).toContain('action="/auth/logout?return_to=%2Fpalaeography"'); expect(html).not.toContain("header.signIn"); // no sign-in link rendered @@ -46,16 +46,17 @@ describe("AmplHeader", () => { expect(html).toContain("Host sign in"); }); - it("switcher lists the default registry and marks the current tool", async () => { - const html = await render({ tool: "calamus", toolName: "Palaeography", localeSwitcher: SWITCHER, account: null }); - expect(html).toContain("Palaeography"); - expect(html).toContain("Scheduling"); + it("switcher (v0.3.2 in the institutional band) shows the Workshop trigger, lists the registry, marks the current tool", async () => { + const html = await render({ tool: "calamus", toolName: "Paleography", localeSwitcher: SWITCHER, account: null }); + expect(html).toContain("Workshop"); // the relocated trigger label + expect(html).toContain("Paleography"); // current-tool banner + expect(html).toContain("Scheduling"); // switch-to entry expect(html).toContain('aria-current="true"'); }); it("active nav item gets the white underline (after: + text-white)", async () => { const html = await render({ - tool: "calamus", toolName: "Palaeography", localeSwitcher: SWITCHER, account: null, + tool: "calamus", toolName: "Paleography", localeSwitcher: SWITCHER, account: null, nav: [ { label: "Dashboard", href: "/d", active: false }, { label: "Library", href: "/l", active: true }, @@ -69,14 +70,14 @@ describe("AmplHeader", () => { }); it("full size uses the 220px logo; compact uses 132px", async () => { - const full = await render({ tool: "calamus", toolName: "Palaeography", localeSwitcher: SWITCHER, account: null, size: "full" }); + const full = await render({ tool: "calamus", toolName: "Paleography", localeSwitcher: SWITCHER, account: null, size: "full" }); expect(full).toContain("w-[220px]"); - const compact = await render({ tool: "calamus", toolName: "Palaeography", localeSwitcher: SWITCHER, account: null }); + const compact = await render({ tool: "calamus", toolName: "Paleography", localeSwitcher: SWITCHER, account: null }); expect(compact).toContain("w-[132px]"); }); it("renders the deep-plum WORKSHOP band and the mobile sheet (hidden at rest)", async () => { - const html = await render({ tool: "calamus", toolName: "Palaeography", localeSwitcher: SWITCHER, account: null }); + const html = await render({ tool: "calamus", toolName: "Paleography", localeSwitcher: SWITCHER, account: null }); expect(html).toContain("bg-accent-deep"); // The mobile sheet is always in the DOM but hidden until the hamburger // toggles it; SSR (menuOpen=false) must emit the `hidden` attribute. @@ -85,7 +86,7 @@ describe("AmplHeader", () => { it("account takes precedence over signInHref (no sign-in link when signed in)", async () => { const html = await render({ - tool: "calamus", toolName: "Palaeography", localeSwitcher: SWITCHER, + tool: "calamus", toolName: "Paleography", localeSwitcher: SWITCHER, account: { name: "Juan Cobo", handle: "juan", avatarUrl: null }, signInHref: "/auth/login", });