diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fca5c98..d0308eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,6 +95,7 @@ jobs: test -f lib/field.ts bunx shadcn@latest add http://localhost:8099/r/app-shell.json -y -o grep -q 'NYCU WinLab' components/app-shell.tsx + grep -q 'layout === "wide"' components/app-shell.tsx bunx shadcn@latest add http://localhost:8099/r/confirm-dialog.json -y -o grep -q 'useTransition' components/confirm-dialog.tsx bunx shadcn@latest add http://localhost:8099/r/form-dialog.json -y -o diff --git a/DESIGN.md b/DESIGN.md index cf787dc..0399675 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -24,13 +24,16 @@ Every WinLab app is framed by four fixed corners, 24px in from the viewport. `ap ## Page layouts -Two, chosen per page with `app-shell`'s `layout`: +Three, chosen per page with `app-shell`'s `layout`: | Layout | For | Shape | |--------|-----|-------| | `column` | Pages read top to bottom: home, lists, tables, long forms | One centered column (`max-w-4xl`) starting under the top corners | +| `wide` | A grid that needs the whole screen: a two-week timetable, a room schedule | The full width between the corners' outer edges (24px in from the viewport), starting under the top corners | | `spotlight` | Pages about one thing: a single item, sign-in, a result, an empty state, a not-found, a component's doc page | The content centered horizontally and vertically between the corners (`max-w-2xl`); taller content scrolls as a column | +Use `wide` only when the content is a grid that loses meaning when squeezed; text and lists stay in `column`, so lines never run across a whole monitor. + In a spotlight, the title and description center above the focal content; text that is the focus itself stays left-aligned when it runs to more than one line. ## Two layers diff --git a/README.md b/README.md index 44ccce0..aa4cfee 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ npx shadcn@latest add @winlab/button | `separator` | `registry:ui` | Hairline divider | | `collapsible` | `registry:ui` | Unstyled show / hide region | | `sonner` | `registry:ui` | Toasts: frosted surface, status-colored icons, action and cancel buttons | -| `app-shell` | `registry:block` | The four corners of every WinLab app; 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 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/[slug]/page.tsx b/app/[slug]/page.tsx index 0f1bcb6..28577dd 100644 --- a/app/[slug]/page.tsx +++ b/app/[slug]/page.tsx @@ -36,9 +36,9 @@ export default async function DocPage({ return ( -
+

{doc.title}

{doc.description}

diff --git a/app/_docs/demos/index.ts b/app/_docs/demos/index.ts index 3a70fc2..5954157 100644 --- a/app/_docs/demos/index.ts +++ b/app/_docs/demos/index.ts @@ -11,6 +11,7 @@ import { Demo as MemberComboboxDemo } from "@/app/_docs/demos/member-combobox" import { Demo as FieldListDemo } from "@/app/_docs/demos/field-list" import { Demo as StatusPageDemo } from "@/app/_docs/demos/status-page" import { Demo as DatePickerDemo } from "@/app/_docs/demos/date-picker" +import { Demo as Wide } from "@/app/_docs/demos/wide" import { Demo as Button } from "@/app/_docs/demos/button" import { Demo as Badge } from "@/app/_docs/demos/badge" import { Demo as Input } from "@/app/_docs/demos/input" @@ -38,6 +39,7 @@ import { Demo as Sonner } from "@/app/_docs/demos/sonner" export const demos: Record = { colors: Colors, "app-shell": AppShell, + wide: Wide, "confirm-dialog": ConfirmDialog, "form-dialog": FormDialogDemo, "empty-state": EmptyStateDemo, diff --git a/app/_docs/demos/wide.tsx b/app/_docs/demos/wide.tsx new file mode 100644 index 0000000..9da07b5 --- /dev/null +++ b/app/_docs/demos/wide.tsx @@ -0,0 +1,36 @@ +// Fourteen days of room slots: the grid that wide exists for. +const days = Array.from({ length: 14 }, (_, index) => index + 5) +const hours = ["09", "10", "11", "13", "14", "15", "16"] +const booked = new Set(["7-10", "8-14", "12-09", "15-15", "16-11", "18-13"]) + +export function Demo() { + return ( +
+
+ + {days.map((day) => ( + + 10/{day} + + ))} + {hours.map((hour) => ( +
+ + {hour}:00 + + {days.map((day) => ( + + ))} +
+ ))} +
+
+ ) +} diff --git a/app/_docs/docs.ts b/app/_docs/docs.ts index a381a9d..4c5e78e 100644 --- a/app/_docs/docs.ts +++ b/app/_docs/docs.ts @@ -7,6 +7,8 @@ export type Doc = { description: string /** Registry item to install, if the page is about one. */ item?: string + /** The page's app-shell layout; doc pages default to spotlight. */ + layout?: "spotlight" | "wide" } export const docs: Doc[] = [ @@ -24,6 +26,13 @@ export const docs: Doc[] = [ "每個 WinLab app 的四個角,預設是麵包屑、導覽、使用者與版本、版權,也可以換成別的內容。", item: "app-shell", }, + { + group: "版面", + slug: "wide", + title: "寬版面", + description: "時段表這類格子用滿四角之間的寬度,文字和清單不用。", + layout: "wide", + }, { group: "區塊", slug: "confirm-dialog", diff --git a/app/_docs/site-shell.tsx b/app/_docs/site-shell.tsx index 374d994..6184ec6 100644 --- a/app/_docs/site-shell.tsx +++ b/app/_docs/site-shell.tsx @@ -7,7 +7,7 @@ export function SiteShell({ children, }: { page?: { label: string; href: string } - layout?: "column" | "spotlight" + layout?: "column" | "spotlight" | "wide" children: React.ReactNode }) { return ( diff --git a/registry/winlab/blocks/app-shell/app-shell.tsx b/registry/winlab/blocks/app-shell/app-shell.tsx index 5247b54..b3e9fe6 100644 --- a/registry/winlab/blocks/app-shell/app-shell.tsx +++ b/registry/winlab/blocks/app-shell/app-shell.tsx @@ -34,10 +34,12 @@ type User = { name: string; href: string; image?: string; tip?: string } // 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 // corner's outer edge so it never leaves the viewport. -// Two page layouts: +// Three page layouts: // column read top to bottom: home, lists, tables, long forms // spotlight one thing, centered between the corners: a detail, sign-in, // a result, an empty state; taller content scrolls as a column +// wide a grid that needs every pixel, such as a two-week timetable: +// the full width between the corners' outer edges function AppShell({ breadcrumb = [], nav = [], @@ -55,7 +57,7 @@ function AppShell({ React.ReactNode > > - layout?: "column" | "spotlight" + layout?: "column" | "spotlight" | "wide" children: React.ReactNode }) { const pathname = usePathname() @@ -157,6 +159,8 @@ function AppShell({
{children}
+ ) : layout === "wide" ? ( +
{children}
) : (
{children}