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
9 changes: 9 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ Color means state: something that changes as work moves on. A category (a kind,
- Each app maps its status values to these variants in one place, next to the status labels, and every page reads that map.
- A status that only applies to some rows shows nothing on the others; do not add a "normal" badge.

## Copy

Say it once, in as few words as the thing needs. No sentence explains what the screen already shows.

- Buttons are the verb: "登入", "刪除", "允許". While it runs, the verb + "中…".
- Titles name the thing or ask the question: "收據", "刪除這張收據?", "找不到這個頁面". There is no subtitle; blocks have no description slot, so a page cannot grow one.
- Toasts are the outcome or the reason, a few words: "已送出", "檔案超過 10 MB".
- Field labels carry what a field needs; there is no helper text (see Two layers).

## Type

Two sizes, by role: `text-title` (24px) for page and dialog titles, `text-body` (16px) for everything else. Weight and the muted color carry the rest: section titles `font-semibold`, labels and buttons `font-medium`, secondary text `text-muted-foreground`.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ npx shadcn@latest add @winlab/button
| `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 |
| `page-header` | `registry:block` | The top of a page: `text-title` title, one line on what it holds, and the page's own actions on the right (under the title on a phone); `SectionHeader` is the same one level down |
| `page-header` | `registry:block` | The top of a page: `text-title` title, and the page's own actions on the right (under the title on a phone); `SectionHeader` is the same one level down |
| `list-skeleton` | `registry:block` | Placeholder rows for a list (`ListSkeleton`) or a table body (`TableSkeleton`) on first load only, split by the same dividers as the real rows |
| `member-combobox` | `registry:block` | Pick lab members by name or email; one member closes on choice, `multiple` keeps the menu open and toggles |
| `field-list` | `registry:block` | One record's fields as a `dl`: muted names in a left column, values beside them (under them on a phone), split by dividers |
Expand Down
2 changes: 0 additions & 2 deletions app/_docs/demos/confirm-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@ export function Demo() {
</Button>
}
title="刪除這張收據?"
description="刪除後無法復原。"
confirmLabel="刪除"
onConfirm={wait}
/>
<ConfirmDialog
trigger={<Button variant="outline">關閉訂單</Button>}
title="關閉這筆訂單?"
description="關閉後成員就不能再點餐,之後可以重新開啟。"
confirmLabel="關閉"
variant="default"
onConfirm={wait}
Expand Down
6 changes: 1 addition & 5 deletions app/_docs/demos/page-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ import { Button } from "@/registry/winlab/ui/button"
export function Demo() {
return (
<div className="flex w-full max-w-2xl flex-col gap-12 text-left">
<PageHeader
title="收據"
description="上傳的收據會送給簽核人審核。"
actions={<Button>上傳收據</Button>}
/>
<PageHeader title="收據" actions={<Button>上傳收據</Button>} />
<SectionHeader
title="審核中"
actions={<Button variant="outline">全部下載</Button>}
Expand Down
19 changes: 4 additions & 15 deletions app/_docs/demos/sonner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,23 @@ import { Button } from "@/registry/winlab/ui/button"
export function Demo() {
return (
<div className="flex flex-wrap justify-center gap-3">
<Button
variant="outline"
onClick={() =>
toast.success("已送出申請", {
description: "簽核人會收到通知。",
})
}
>
<Button variant="outline" onClick={() => toast.success("已送出")}>
成功
</Button>
<Button
variant="outline"
onClick={() =>
toast.error("上傳失敗", {
description: "檔案超過 10 MB。",
toast.error("檔案超過 10 MB", {
action: { label: "重試", onClick: () => {} },
})
}
>
錯誤
</Button>
<Button
variant="outline"
onClick={() => toast.warning("發票缺少統編,請在 3 天內補件。")}
>
<Button variant="outline" onClick={() => toast.warning("缺少統編")}>
警告
</Button>
<Button variant="outline" onClick={() => toast.message("已複製信箱")}>
<Button variant="outline" onClick={() => toast.message("已複製")}>
訊息
</Button>
</div>
Expand Down
1 change: 0 additions & 1 deletion app/_docs/demos/status-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export function Demo() {
return (
<StatusPage
title="這個頁面出了問題"
description="資料沒有載入成功,可以再試一次。"
action={<Button onClick={() => window.location.reload()}>重試</Button>}
/>
)
Expand Down
4 changes: 2 additions & 2 deletions app/_docs/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const docs: Doc[] = [
group: "區塊",
slug: "page-header",
title: "頁首",
description: "頁面標題、一句說明,與這頁的動作按鈕。",
description: "頁面標題與這頁的動作按鈕。",
item: "page-header",
},
{
Expand Down Expand Up @@ -86,7 +86,7 @@ export const docs: Doc[] = [
group: "區塊",
slug: "status-page",
title: "狀態頁",
description: "找不到、出錯、沒有權限時,說明發生什麼事和下一步。",
description: "找不到、出錯、沒有權限時的標題與下一步。",
item: "status-page",
},
{
Expand Down
1 change: 0 additions & 1 deletion app/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export default function NotFound() {
<SiteShell layout="spotlight">
<StatusPage
title="找不到這個頁面"
description="網址可能打錯了,或這個頁面已經移走。"
action={
<Link href="/" className={buttonVariants()}>
回到首頁
Expand Down
2 changes: 1 addition & 1 deletion registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,7 @@
"name": "page-header",
"type": "registry:block",
"title": "Page Header",
"description": "The top of a page: title, one line on what it holds, and the page's own actions.",
"description": "The top of a page: its title and the page's own actions.",
"dependencies": [],
"files": [
{
Expand Down
4 changes: 0 additions & 4 deletions registry/winlab/blocks/confirm-dialog/confirm-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
Expand All @@ -21,7 +20,6 @@ type ConfirmDialogProps = {
open?: boolean
onOpenChange?: (open: boolean) => void
title: string
description: React.ReactNode
/** The verb on the confirm button: "刪除", "撤回". Never "確定". */
confirmLabel: string
/** Shown while onConfirm runs; defaults to the verb + "中…". */
Expand All @@ -40,7 +38,6 @@ function ConfirmDialog({
open: openProp,
onOpenChange,
title,
description,
confirmLabel,
pendingLabel = `${confirmLabel}中…`,
variant = "destructive",
Expand Down Expand Up @@ -77,7 +74,6 @@ function ConfirmDialog({
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={pending}>取消</AlertDialogCancel>
Expand Down
6 changes: 0 additions & 6 deletions registry/winlab/blocks/form-dialog/form-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
Expand All @@ -22,7 +21,6 @@ type FormDialogProps = {
open?: boolean
onOpenChange?: (open: boolean) => void
title: string
description?: React.ReactNode
size?: "default" | "wide"
/** The verb on the submit button: "新增", "儲存". */
submitLabel: string
Expand All @@ -43,7 +41,6 @@ function FormDialog({
open: openProp,
onOpenChange,
title,
description,
size,
submitLabel,
pendingLabel = `${submitLabel}中…`,
Expand Down Expand Up @@ -84,9 +81,6 @@ function FormDialog({
<form onSubmit={submit} className="flex flex-col gap-6">
<DialogHeader className="pr-0">
<DialogTitle>{title}</DialogTitle>
{description && (
<DialogDescription>{description}</DialogDescription>
)}
</DialogHeader>
<fieldset disabled={pending} className="flex min-w-0 flex-col gap-4">
{children}
Expand Down
21 changes: 7 additions & 14 deletions registry/winlab/blocks/page-header/page-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,34 @@ import * as React from "react"

type HeaderProps = {
title: React.ReactNode
/** One line under the title saying what the page holds. */
description?: React.ReactNode
/** Buttons that act on this page's content, such as 新增. Navigation goes
* in the app shell's corners, not here. */
actions?: React.ReactNode
}

// The top of a page: its title and what it holds on the left, the page's
// own actions on the right. On a phone the actions drop under the title.
function PageHeader({ title, description, actions }: HeaderProps) {
// The top of a page: its title on the left, the page's own actions on the
// right. No subtitle: the title says what the page is. On a phone the
// actions drop under the title.
function PageHeader({ title, actions }: HeaderProps) {
return (
<header
data-slot="page-header"
className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"
>
<div className="flex min-w-0 flex-col gap-2">
<h1 className="text-title font-medium">{title}</h1>
{description && <p className="text-muted-foreground">{description}</p>}
</div>
<h1 className="min-w-0 text-title font-medium">{title}</h1>
{actions && <div className="flex shrink-0 gap-3">{actions}</div>}
</header>
)
}

// A section title inside a page, one step below the page title by weight.
function SectionHeader({ title, description, actions }: HeaderProps) {
function SectionHeader({ title, actions }: HeaderProps) {
return (
<div
data-slot="section-header"
className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"
>
<div className="flex min-w-0 flex-col gap-2">
<h2 className="font-semibold">{title}</h2>
{description && <p className="text-muted-foreground">{description}</p>}
</div>
<h2 className="min-w-0 font-semibold">{title}</h2>
{actions && <div className="flex shrink-0 gap-3">{actions}</div>}
</div>
)
Expand Down
16 changes: 4 additions & 12 deletions registry/winlab/blocks/status-page/status-page.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
import * as React from "react"

// A page that only says what happened and what to do next: not found, an
// error, no access, signed out. Put it in app-shell's spotlight layout.
// Say it in Chinese, as a sentence: "找不到這個頁面", "這個頁面出了問題",
// "你沒有這個頁面的權限".
// A page that only says what happened and the way out: not found, an error,
// no access, signed out. Put it in app-shell's spotlight layout. The title
// says it all ("找不到這個頁面"); the action is a verb ("回到首頁", "重試").
function StatusPage({
title,
description,
action,
}: {
title: string
/** Why it happened, in one sentence. */
description: React.ReactNode
/** The way out: 回到首頁, 重試, 登入. */
action?: React.ReactNode
}) {
return (
<div
data-slot="status-page"
className="flex flex-col items-center gap-12 text-center"
>
<header className="flex flex-col items-center gap-2">
<h1 className="text-title font-medium">{title}</h1>
<p className="text-muted-foreground">{description}</p>
</header>
<h1 className="text-title font-medium">{title}</h1>
{action}
</div>
)
Expand Down
Loading