diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49bc4a3..a09c370 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,6 +123,8 @@ jobs: grep -q 'toDateString' components/date-picker.tsx bunx shadcn@latest add http://localhost:8099/r/action-panel.json -y -o grep -q 'doneLabel' components/action-panel.tsx + bunx shadcn@latest add http://localhost:8099/r/attachment.json -y -o + grep -q 'rounded-menu' components/ui/attachment.tsx bun run build - if: always() run: docker compose logs diff --git a/README.md b/README.md index f9f9562..9580953 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ npx shadcn@latest add @winlab/button | `combobox` | `registry:ui` | `ComboboxTrigger` (same field look as `select`) and `ComboboxContent` (as wide as the trigger) around a `command` | | `dropdown-menu` | `registry:ui` | Action menu: items, labels, separators and submenus | | `calendar` | `registry:ui` | One month of 40px days to pick a single date; weeks start on Monday, captions read 2026 年 10 月 | +| `attachment` | `registry:ui` | One file per row: 40px icon or thumbnail, name, a detail line and actions; states `idle` (dashed), `uploading`, `error`, `done` | | `checkbox` | `registry:ui` | Round 20px checkbox | | `switch` | `registry:ui` | On / off toggle, 40 x 24 | | `tabs` | `registry:ui` | Segmented tabs | diff --git a/app/_docs/demos/attachment.tsx b/app/_docs/demos/attachment.tsx new file mode 100644 index 0000000..fe05fa5 --- /dev/null +++ b/app/_docs/demos/attachment.tsx @@ -0,0 +1,58 @@ +import { FileTextIcon, ImageIcon, RotateCwIcon, XIcon } from "lucide-react" + +import { + Attachment, + AttachmentAction, + AttachmentActions, + AttachmentContent, + AttachmentDescription, + AttachmentGroup, + AttachmentMedia, + AttachmentTitle, + AttachmentTrigger, +} from "@/registry/winlab/ui/attachment" + +export function Demo() { + return ( + + + + + + + + 收據-文具.pdf + PDF · 240 KB + + + + + + + + + + + + + 發票照片.jpg + 上傳中 · 62% + + + + + + + + 出差報告.docx + 不支援這種檔案 + + + + + + + + + ) +} diff --git a/app/_docs/demos/index.ts b/app/_docs/demos/index.ts index 12ee655..9ab1d70 100644 --- a/app/_docs/demos/index.ts +++ b/app/_docs/demos/index.ts @@ -21,6 +21,7 @@ import { Demo as Label } from "@/app/_docs/demos/label" import { Demo as Select } from "@/app/_docs/demos/select" import { Demo as Combobox } from "@/app/_docs/demos/combobox" import { Demo as Calendar } from "@/app/_docs/demos/calendar" +import { Demo as Attachment } from "@/app/_docs/demos/attachment" import { Demo as Checkbox } from "@/app/_docs/demos/checkbox" import { Demo as Switch } from "@/app/_docs/demos/switch" import { Demo as Table } from "@/app/_docs/demos/table" @@ -59,6 +60,7 @@ export const demos: Record = { select: Select, combobox: Combobox, calendar: Calendar, + attachment: Attachment, checkbox: Checkbox, switch: Switch, table: Table, diff --git a/app/_docs/docs.ts b/app/_docs/docs.ts index fbfe869..1486461 100644 --- a/app/_docs/docs.ts +++ b/app/_docs/docs.ts @@ -160,6 +160,13 @@ export const docs: Doc[] = [ description: "從月曆選一天,週一開頭。", item: "calendar", }, + { + group: "元件", + slug: "attachment", + title: "附件", + description: "一個檔案的圖示、檔名與狀態,可以移除或重試。", + item: "attachment", + }, { group: "元件", slug: "checkbox", diff --git a/registry.json b/registry.json index 6229f91..0d46048 100644 --- a/registry.json +++ b/registry.json @@ -656,6 +656,26 @@ } ] }, + { + "name": "attachment", + "type": "registry:ui", + "title": "Attachment", + "description": "One file as a row: icon or thumbnail, name, detail line and actions, with idle, uploading, error and done states.", + "dependencies": [ + "@base-ui/react", + "cn" + ], + "registryDependencies": [ + "https://ui.winlab.tw/r/utils.json", + "https://ui.winlab.tw/r/button.json" + ], + "files": [ + { + "path": "registry/winlab/ui/attachment.tsx", + "type": "registry:ui" + } + ] + }, { "name": "app-shell", "type": "registry:block", diff --git a/registry/winlab/ui/attachment.tsx b/registry/winlab/ui/attachment.tsx new file mode 100644 index 0000000..b6582ab --- /dev/null +++ b/registry/winlab/ui/attachment.tsx @@ -0,0 +1,166 @@ +import * as React from "react" +import { mergeProps } from "@base-ui/react/merge-props" +import { useRender } from "@base-ui/react/use-render" + +import { cn } from "@/registry/winlab/lib/utils" +import { Button } from "@/registry/winlab/ui/button" + +type AttachmentState = "idle" | "uploading" | "error" | "done" + +// One file: a 40px icon or thumbnail, its name and a detail line, then +// actions. It keeps its own outline like an input, since it is a control; +// rounded-menu with p-1 around the control-radius media (concentric). +// idle is dashed (not sent yet), uploading pulses, error turns destructive. +function Attachment({ + className, + state = "done", + ...props +}: React.ComponentProps<"div"> & { state?: AttachmentState }) { + return ( +
[data-slot=attachment-trigger]]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed", + className + )} + {...props} + /> + ) +} + +function AttachmentMedia({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AttachmentContent({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AttachmentTitle({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function AttachmentDescription({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +// Sits above the trigger, so its buttons stay clickable. +function AttachmentActions({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +// A ghost icon button; give it an aria-label naming the verb. +function AttachmentAction({ + className, + ...props +}: React.ComponentProps) { + return ( +