diff --git a/DESIGN.md b/DESIGN.md index c6b0737..0f22dab 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -4,12 +4,12 @@ These rules hold for every WinLab app. `bun run tokens:check` enforces the parts ## App shell -Every WinLab app is framed by four fixed corners, 24px in from the viewport. `app-shell` draws them. By default each corner has the job below; an app that needs something else in a corner replaces it through `corners`, built from `CornerLink` and `CornerTip` so it keeps the corner's type, color and tips. +Every WinLab app is framed by four fixed corners, 24px in from the viewport. `app-shell` draws them. By default each corner has the job below; an app that needs something else in a corner replaces it through `corners`, built from `CornerLink`, `CornerTip` and `cornerLinkClassName` (for a corner item that is not a link) so it keeps the corner's type, color and tips. | Corner | Default job | Default content | |--------|-----|---------| | Top left | Where you are | Breadcrumb: the lab (portal home), then the app (its home) | -| Top right | Where you can go | The app's pages, the current one in `foreground`; last, the account: "登入" when signed out, the member's name when signed in | +| Top right | Where you can go | The app's pages, the current one in `foreground`; last, the account: "登入" when signed out, the member's name when signed in (a link to their page, or a menu with items such as 登出) | | Bottom left | Which build | The short commit of this build, linking to its CD run, with the build time (Asia/Taipei) as its tip | | Bottom right | Whose it is | © year; its tip says NYCU WinLab | @@ -20,7 +20,7 @@ Every WinLab app is framed by four fixed corners, 24px in from the viewport. `ap - Every corner item has a tip (`tip` is required in `app-shell`, so a missing one fails the type check) that says what its label leaves out (where a link goes, what a page holds, who owns the site). A tip never repeats the label. Tips open at once, toward the page, lined up with the corner's outer edge; touch never opens them, so nothing depends on one. - Corner text is `text-body` in `muted-foreground`, links turn `foreground` on hover and when current. - Content that scrolls under the corners fades into a 64px band of frosted glass at the top and bottom edges. -- No menu button: an app keeps its pages few enough to fit beside the breadcrumb on a phone. +- Below `md` the pages fold into one menu button (頁面選單) holding the same links; the breadcrumb and the account stay. Keep an app to about five pages per area, so the wide layout still fits on one line. An app with a second area (such as `/admin`) shows that area's pages there instead, with one more breadcrumb. ## Page layouts diff --git a/README.md b/README.md index 8f20623..7173b96 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ npx shadcn@latest add @winlab/button | `number-ticker` | `registry:ui` | A whole number whose digits roll to their new value | | `progress` | `registry:ui` | How much of a whole is used: an 8px muted track with a primary fill | | `chart` | `registry:ui` | Recharts in WinLab colors: series take `primary`, `muted-foreground` or a state color; inverted tooltip and a legend | -| `app-shell` | `registry:block` | The four corners of every WinLab app and the page layouts `column`, `spotlight` and `wide`; set `NEXT_PUBLIC_BUILD_SHA`, `_TIME` and `_URL` at build for the version corner | +| `app-shell` | `registry:block` | The four corners of every WinLab app (pages fold into a menu on narrow screens; the account can be a link or a menu) and the page layouts `column`, `spotlight` and `wide`; set `NEXT_PUBLIC_BUILD_SHA`, `_TIME` and `_URL` at build for the version corner | | `confirm-dialog` | `registry:block` | Asks before an action runs; the confirm button names the verb, both buttons lock while it runs, and it closes only when the action finishes | | `form-dialog` | `registry:block` | A short form in a dialog; `FormField` puts a label over its control, fields and buttons lock while it submits, and it closes only when the submit finishes | | `empty-state` | `registry:block` | What a list shows with no rows: "還沒有{noun}" or "找不到符合「{query}」的{noun}", with the next action; `TableEmpty` is the same sentence as a table row | diff --git a/app/_docs/demos/app-shell.tsx b/app/_docs/demos/app-shell.tsx index 6434723..af02195 100644 --- a/app/_docs/demos/app-shell.tsx +++ b/app/_docs/demos/app-shell.tsx @@ -1,7 +1,7 @@ export function Demo() { return (
- 左上麵包屑、右上導覽與帳號、左下版本、右下版權。 + 左上麵包屑、右上導覽與帳號(窄螢幕收成選單)、左下版本、右下版權。
) } diff --git a/registry.json b/registry.json index 6c46353..9533669 100644 --- a/registry.json +++ b/registry.json @@ -960,13 +960,16 @@ "name": "app-shell", "type": "registry:block", "title": "App Shell", - "description": "The four corners of a WinLab app: breadcrumb, page nav and account, build version, copyright year.", + "description": "The four corners of a WinLab app: breadcrumb, page nav (a menu on narrow screens) and account, build version, copyright year.", "dependencies": [ - "cn" + "cn", + "lucide-react" ], "registryDependencies": [ "https://ui.winlab.tw/r/utils.json", - "https://ui.winlab.tw/r/tooltip.json" + "https://ui.winlab.tw/r/tooltip.json", + "https://ui.winlab.tw/r/popover.json", + "https://ui.winlab.tw/r/dropdown-menu.json" ], "files": [ { diff --git a/registry/winlab/blocks/app-shell/app-shell.tsx b/registry/winlab/blocks/app-shell/app-shell.tsx index ee7ea1b..5ff5b0e 100644 --- a/registry/winlab/blocks/app-shell/app-shell.tsx +++ b/registry/winlab/blocks/app-shell/app-shell.tsx @@ -3,8 +3,20 @@ import * as React from "react" import Link from "next/link" import { usePathname } from "next/navigation" +import { MenuIcon } from "lucide-react" import { cn } from "@/registry/winlab/lib/utils" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/registry/winlab/ui/dropdown-menu" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/registry/winlab/ui/popover" + import { Tooltip, TooltipContent, @@ -17,6 +29,9 @@ import { // it is); it never repeats the label. type Crumb = { label: string; href: string; tip: string } type NavItem = { label: string; href: string; tip: string } +// Signed in, the account can open a menu instead of a page: the member's +// name, then items such as 登出 (DropdownMenuItem children). +type AccountMenu = { label: string; tip: string; menu: React.ReactNode } // The chrome of every WinLab app is its four corners. By default each corner // has one job, filled from the props below; an app that needs something else @@ -24,7 +39,8 @@ type NavItem = { label: string; href: string; tip: string } // it keeps the corner's look. // top left where you are: breadcrumb back to the lab and the app // top right where you can go: this app's pages, then the account (登入 -// when signed out, the member's name when signed in) +// when signed out, the member's name when signed in). Below +// md the pages fold into one menu button; the account stays. // bottom left which build: the commit this build came from // bottom right the copyright year; the tip names the owner // Every item can carry a tip, opening toward the page and lined up with the @@ -45,9 +61,9 @@ function AppShell({ }: { breadcrumb?: Crumb[] nav?: NavItem[] - /** Last in the top right: { label: "登入", href: "/sign-in" } signed out, - * the member's name linking to their page signed in. */ - account?: NavItem + /** Last in the top right: { label: "登入", href: "/sign-in" } signed out; + * signed in, the member's name linking to their page, or opening `menu`. */ + account?: NavItem | AccountMenu corners?: Partial< Record< "topLeft" | "topRight" | "bottomLeft" | "bottomRight", @@ -90,7 +106,10 @@ function AppShell({ href={crumb.href} prefetch aria-current={last ? "page" : undefined} - className={cn(cornerLink, last && "text-foreground")} + className={cn( + cornerLinkClassName, + last && "text-foreground" + )} > {crumb.label} @@ -103,30 +122,12 @@ function AppShell({