diff --git a/.trellis/spec/frontend/components.md b/.trellis/spec/frontend/components.md index 294df68..f36fe27 100644 --- a/.trellis/spec/frontend/components.md +++ b/.trellis/spec/frontend/components.md @@ -97,6 +97,26 @@ export function InteractiveWidget({ initialData }: { initialData: Data }) { ## Semantic HTML +## Kumo UI Convention + +Use `components/ui/*` as the project-owned adapter layer for Cloudflare Kumo components. +Feature modules and route files should import `Button`, `Input`, `Select`, `Textarea`, `Table`, +`Checkbox`, `Badge`, `Card`, and `Dialog` from `@/components/ui/...` instead of importing +`@cloudflare/kumo` directly. + +```typescript +// Good: project adapter keeps Kumo API differences local +import { Select } from "@/components/ui/select"; +import { Table } from "@/components/ui/table"; + +// Avoid: feature code should not hand-style native controls + : null} + + ); +} diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx index 4b8a64e..a6506e1 100644 --- a/components/ui/dialog.tsx +++ b/components/ui/dialog.tsx @@ -1,6 +1,10 @@ +"use client"; + import * as React from "react"; import { X } from "lucide-react"; +import { Dialog as KumoDialog } from "@cloudflare/kumo/components/dialog"; +import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; type DialogProps = { @@ -21,38 +25,35 @@ export function Dialog({ footer, onClose, className, -}: DialogProps): React.ReactElement | null { - if (!open) { - return null; - } - +}: DialogProps): React.ReactElement { return ( -
-
{ + if (!nextOpen) { + onClose(); + } + }} + open={open} + > + -
+
-

{title}

- {description ?

{description}

: null} + {title} + {description ? {description} : null}
- +
{children}
- {footer ? : null} -
-
+ {footer ? : null} + + ); } diff --git a/components/ui/input.tsx b/components/ui/input.tsx index 28b6f5a..ffe15da 100644 --- a/components/ui/input.tsx +++ b/components/ui/input.tsx @@ -1,18 +1 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -export type InputProps = React.InputHTMLAttributes; - -export function Input({ className, type, ...props }: InputProps): React.ReactElement { - return ( - - ); -} +export { Input, Textarea, type InputProps, type InputAreaProps as TextareaProps } from "@cloudflare/kumo/components/input"; diff --git a/components/ui/select.tsx b/components/ui/select.tsx new file mode 100644 index 0000000..27fe176 --- /dev/null +++ b/components/ui/select.tsx @@ -0,0 +1,49 @@ +"use client"; + +import * as React from "react"; +import { Select as KumoSelect } from "@cloudflare/kumo/components/select"; + +export type SelectOption = { + value: string; + label: React.ReactNode; + disabled?: boolean; +}; + +type SelectProps = { + "aria-label"?: string; + className?: string; + defaultValue?: string; + disabled?: boolean; + label?: React.ReactNode; + name?: string; + onValueChange?: (value: string) => void; + options: SelectOption[]; + placeholder?: string; + required?: boolean; + value?: string; +}; + +export function Select({ + onValueChange, + options, + placeholder, + value, + defaultValue, + ...props +}: SelectProps): React.ReactElement { + return ( + + defaultValue={defaultValue} + onValueChange={(nextValue) => onValueChange?.(String(nextValue))} + placeholder={placeholder} + value={value} + {...props} + > + {options.map((option) => ( + + {option.label} + + ))} + + ); +} diff --git a/components/ui/table.tsx b/components/ui/table.tsx new file mode 100644 index 0000000..df3d47f --- /dev/null +++ b/components/ui/table.tsx @@ -0,0 +1 @@ +export { Table, type KumoTableLayout, type KumoTableRowVariant } from "@cloudflare/kumo/components/table"; diff --git a/modules/auth/components/AuthPanel.tsx b/modules/auth/components/AuthPanel.tsx index 5a06962..df5c834 100644 --- a/modules/auth/components/AuthPanel.tsx +++ b/modules/auth/components/AuthPanel.tsx @@ -9,6 +9,7 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; +import { Select } from "@/components/ui/select"; import type { ApiResult } from "@/modules/core/server/api"; import type { Organization } from "@/modules/core/types"; @@ -206,17 +207,14 @@ function AuthPanelContent({ mode }: AuthPanelProps): React.ReactElement { {mode === "register" && !invitationToken && organizations.length > 0 ? ( ) : null} diff --git a/modules/auth/components/PendingAssignment.tsx b/modules/auth/components/PendingAssignment.tsx index 8616491..91ee231 100644 --- a/modules/auth/components/PendingAssignment.tsx +++ b/modules/auth/components/PendingAssignment.tsx @@ -1,8 +1,7 @@ -import Link from "next/link"; import { Clock3, MailCheck, ShieldCheck } from "lucide-react"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; +import { LinkButton } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { SignOutButton } from "@/modules/auth/components/SignOutButton"; import type { PublicAccount } from "@/modules/core/types"; @@ -52,9 +51,9 @@ export function PendingAssignment({ account }: PendingAssignmentProps): React.Re

分配角色后重新进入即可使用。

- + + 刷新状态 + diff --git a/modules/dashboard/components/DashboardHome.tsx b/modules/dashboard/components/DashboardHome.tsx index 2abebd4..8397271 100644 --- a/modules/dashboard/components/DashboardHome.tsx +++ b/modules/dashboard/components/DashboardHome.tsx @@ -13,6 +13,7 @@ import type { LucideIcon } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Table } from "@/components/ui/table"; type Metric = { label: string; @@ -112,30 +113,30 @@ export function DashboardHome(): React.ReactElement {
- - - - - - - - - - +
任务负责状态截止
+ + + 任务 + 负责 + 状态 + 截止 + + + {taskRows.map((task) => ( - - - - - - + + {task.due} + ))} - -
{task.name}{task.owner} + + {task.name} + {task.owner} + {task.status} - {task.due}
+ +
diff --git a/modules/elders/components/EldersClient.tsx b/modules/elders/components/EldersClient.tsx index f8ff0e7..b0d9828 100644 --- a/modules/elders/components/EldersClient.tsx +++ b/modules/elders/components/EldersClient.tsx @@ -7,7 +7,9 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog } from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; +import { Input, Textarea } from "@/components/ui/input"; +import { Select } from "@/components/ui/select"; +import { Table } from "@/components/ui/table"; import type { ApiResult } from "@/modules/core/server/api"; import type { Permission } from "@/modules/core/types"; import type { CareLevel, Elder, ElderInput, ElderStatus } from "@/modules/elders/types"; @@ -66,6 +68,20 @@ function getRiskLabel(elder: Elder): { label: string; variant: "danger" | "warni return { label: "绿色稳定", variant: "success" }; } +const statusOptions = [ + { value: ALL_STATUS, label: "全部状态" }, + ...Object.entries(ELDER_STATUS_LABELS).map(([value, label]) => ({ value, label })), +]; + +const careLevelOptions = [ + { value: ALL_CARE_LEVELS, label: "全部照护" }, + ...Object.entries(CARE_LEVEL_LABELS).map(([value, label]) => ({ value, label })), +]; + +const genderOptions = Object.entries(GENDER_LABELS).map(([value, label]) => ({ value, label })); +const formCareLevelOptions = Object.entries(CARE_LEVEL_LABELS).map(([value, label]) => ({ value, label })); +const formStatusOptions = Object.entries(ELDER_STATUS_LABELS).map(([value, label]) => ({ value, label })); + function matchesFilters( elder: Elder, query: string, @@ -291,33 +307,23 @@ export function EldersClient({ initialElders, permissions }: EldersClientProps): value={query} /> -