diff --git a/DESIGN.md b/DESIGN.md index 0f22dab..4ab4f8f 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -55,7 +55,7 @@ The interface is flat and has exactly two layers. | 1. Page | Everything that is on the page when it loads: titles, text, lists, tables, long forms | None. No borders around groups, no shadows, no card backgrounds | | 2. Overlay | Anything called up on top of the page: `dialog`, `alert-dialog`, the `select` menu, popovers and menus | No fill: frosted glass (`bg-transparent backdrop-blur-md`, 12px), a border, a shadow, `rounded-surface` or `rounded-menu`. The dialog overlay is the same frosted glass with no tint | -- Group page content with spacing and, where a line helps, a divider (`border-t` / `border-b`). Never wrap a group in a frame. +- Group page content with spacing and, where a line helps, a divider (`border-t` / `border-b`). Never wrap a group in a frame. Where another kit would use a card, use `section`: a divider above, the group's title and its actions. - Every pick-a-value control (select, combobox) looks like an input when closed: one shared trigger class. Never use a button as a field. - Controls keep their own outline: inputs, selects and outline buttons are controls, not containers. A segmented tab list may use the `muted` fill: it is one control. - Tooltips are the one inverted overlay (`bg-foreground text-background`): a few words over any content must stay legible, so they are not frosted. diff --git a/README.md b/README.md index 7173b96..d2b5db6 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ npx shadcn@latest add @winlab/button | `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, and the page's own actions on the right (under the title on a phone); `SectionHeader` is the same one level down | +| `section` | `registry:block` | One group on a page instead of a card: a `border-t` divider, a title (h2) and the group's actions | | `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 | diff --git a/app/_docs/demos/index.ts b/app/_docs/demos/index.ts index d7207e7..1272f8f 100644 --- a/app/_docs/demos/index.ts +++ b/app/_docs/demos/index.ts @@ -6,6 +6,7 @@ import { Demo as ConfirmDialog } from "@/app/_docs/demos/confirm-dialog" import { Demo as FormDialogDemo } from "@/app/_docs/demos/form-dialog" import { Demo as EmptyStateDemo } from "@/app/_docs/demos/empty-state" import { Demo as PageHeaderDemo } from "@/app/_docs/demos/page-header" +import { Demo as SectionDemo } from "@/app/_docs/demos/section" import { Demo as ListSkeletonDemo } from "@/app/_docs/demos/list-skeleton" import { Demo as MemberComboboxDemo } from "@/app/_docs/demos/member-combobox" import { Demo as FieldListDemo } from "@/app/_docs/demos/field-list" @@ -59,6 +60,7 @@ export const demos: Record = { "form-dialog": FormDialogDemo, "empty-state": EmptyStateDemo, "page-header": PageHeaderDemo, + section: SectionDemo, "list-skeleton": ListSkeletonDemo, "member-combobox": MemberComboboxDemo, "field-list": FieldListDemo, diff --git a/app/_docs/demos/section.tsx b/app/_docs/demos/section.tsx new file mode 100644 index 0000000..2606445 --- /dev/null +++ b/app/_docs/demos/section.tsx @@ -0,0 +1,23 @@ +import { Section } from "@/registry/winlab/blocks/section/section" +import { Button } from "@/registry/winlab/ui/button" +import { Progress } from "@/registry/winlab/ui/progress" + +export function Demo() { + return ( +
+
調整} + > +

US$ 31.58

+ +

+ 已用 US$ 18.42 / 50,11 月 1 日重置 +

+
+
+

本月 3,902 次請求,不計費。

+
+
+ ) +} diff --git a/app/_docs/docs.ts b/app/_docs/docs.ts index 4b77cc6..73d75ce 100644 --- a/app/_docs/docs.ts +++ b/app/_docs/docs.ts @@ -82,6 +82,13 @@ export const docs: Doc[] = [ description: "清單的一列原地展開細節,不換頁也不開對話框。", item: "expand-row", }, + { + group: "區塊", + slug: "section", + title: "區塊分組", + description: "頁面上的一組內容:上方一條分隔線與標題,不用卡片。", + item: "section", + }, { group: "區塊", slug: "page-header", diff --git a/registry.json b/registry.json index 9533669..fbc4548 100644 --- a/registry.json +++ b/registry.json @@ -1043,6 +1043,24 @@ } ] }, + { + "name": "section", + "type": "registry:block", + "title": "Section", + "description": "One group on a page instead of a card: a divider above it, a title and the group's actions.", + "dependencies": [ + "cn" + ], + "registryDependencies": [ + "https://ui.winlab.tw/r/utils.json" + ], + "files": [ + { + "path": "registry/winlab/blocks/section/section.tsx", + "type": "registry:component" + } + ] + }, { "name": "list-skeleton", "type": "registry:block", diff --git a/registry/winlab/blocks/section/section.tsx b/registry/winlab/blocks/section/section.tsx new file mode 100644 index 0000000..549e323 --- /dev/null +++ b/registry/winlab/blocks/section/section.tsx @@ -0,0 +1,41 @@ +import * as React from "react" +import { cn } from "@/registry/winlab/lib/utils" + +// One group on a page, where other kits would use a card. Layer 1 has no +// surface, so the group is a divider above it and space around it; the title +// and its actions sit on one row, like SectionHeader in page-header. No +// subtitle: put what the group needs to say in its content. +function Section({ + title, + actions, + className, + children, + ...props +}: Omit, "title"> & { + /** The group's name; becomes an h2 and labels the section. */ + title?: React.ReactNode + /** Buttons that act on this group only. */ + actions?: React.ReactNode +}) { + const id = React.useId() + return ( +
+ {title && ( +
+

+ {title} +

+ {actions &&
{actions}
} +
+ )} + {children} +
+ ) +} + +export { Section }