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
2 changes: 1 addition & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 2 additions & 0 deletions app/_docs/demos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -59,6 +60,7 @@ export const demos: Record<string, ComponentType> = {
"form-dialog": FormDialogDemo,
"empty-state": EmptyStateDemo,
"page-header": PageHeaderDemo,
section: SectionDemo,
"list-skeleton": ListSkeletonDemo,
"member-combobox": MemberComboboxDemo,
"field-list": FieldListDemo,
Expand Down
23 changes: 23 additions & 0 deletions app/_docs/demos/section.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex w-full max-w-2xl flex-col gap-12 text-left">
<Section
title="外部 API 額度"
actions={<Button variant="outline">調整</Button>}
>
<p className="text-title font-medium tabular-nums">US$ 31.58</p>
<Progress value={37} aria-label="已用額度" />
<p className="text-muted-foreground tabular-nums">
已用 US$ 18.42 / 50,11 月 1 日重置
</p>
</Section>
<Section title="自架模型用量">
<p className="text-muted-foreground">本月 3,902 次請求,不計費。</p>
</Section>
</div>
)
}
7 changes: 7 additions & 0 deletions app/_docs/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ export const docs: Doc[] = [
description: "清單的一列原地展開細節,不換頁也不開對話框。",
item: "expand-row",
},
{
group: "區塊",
slug: "section",
title: "區塊分組",
description: "頁面上的一組內容:上方一條分隔線與標題,不用卡片。",
item: "section",
},
{
group: "區塊",
slug: "page-header",
Expand Down
18 changes: 18 additions & 0 deletions registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
41 changes: 41 additions & 0 deletions registry/winlab/blocks/section/section.tsx
Original file line number Diff line number Diff line change
@@ -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<React.ComponentProps<"section">, "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 (
<section
data-slot="section"
aria-labelledby={title ? id : undefined}
className={cn("flex min-w-0 flex-col gap-4 border-t pt-6", className)}
{...props}
>
{title && (
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<h2 id={id} className="min-w-0 font-semibold">
{title}
</h2>
{actions && <div className="flex shrink-0 gap-3">{actions}</div>}
</div>
)}
{children}
</section>
)
}

export { Section }
Loading