feat: integrate latest HMDesign design system updates
This commit is contained in:
1 parent
99faec189e
commit
1c4e0bb9a1
209 files changed
+8845
-552
No files matched your search
@@ -0,0 +1,14 @@
|
||||
import { useMemo, useState } from "react"
|
||||
import { DashboardLayout, DashboardHeader, DashboardMain, DashboardContent, Card, CardHeader, CardTitle, CardContent, Progress, Tabs, TabsList, TabsTrigger, TabsContent, DataTable, useDataTable, type DataTableColumnDef } from "@hm-design/ui"
|
||||
import { Heading } from "./Heading"
|
||||
|
||||
type Summary = { category: string; total: number; completed: number }
|
||||
const columns: DataTableColumnDef<Summary>[] = [{ accessorKey: "category", header: "업무 분류" }, { accessorKey: "total", header: "전체 건수" }, { accessorKey: "completed", header: "완료 건수" }]
|
||||
|
||||
export function Dashboard({ statistics = false }: { statistics?: boolean }) {
|
||||
const [period, setPeriod] = useState("week")
|
||||
const data = useMemo(() => ["설계", "시공", "안전"].map((category, i) => ({ category, total: (period === "week" ? 12 : 48) + i, completed: (period === "week" ? 8 : 32) + i })), [period])
|
||||
const table = useDataTable({ columns, data })
|
||||
const title = statistics ? "통계 Dashboard" : "업무 Dashboard"
|
||||
return <DashboardLayout header={<DashboardHeader><strong>{title}</strong></DashboardHeader>}><DashboardMain><DashboardContent><Heading title={title} description="기간별 건수와 진행률을 확인합니다."/><Tabs value={period} onValueChange={setPeriod}><TabsList aria-label="조회 기간"><TabsTrigger value="week">이번 주</TabsTrigger><TabsTrigger value="month">이번 달</TabsTrigger></TabsList>{["week", "month"].map(p => <TabsContent key={p} value={p}><div className="quality-metrics">{["진행", "대기", "완료"].map((label, i) => <Card key={label}><CardHeader><CardTitle>{label}</CardTitle></CardHeader><CardContent><p>{(p === "week" ? 12 : 48) + i}건</p><Progress value={40 + i * 20} aria-label={`${label} 진행률`}/></CardContent></Card>)}</div></TabsContent>)}</Tabs>{statistics && <Card><CardHeader><CardTitle>업무 분류별 집계</CardTitle></CardHeader><CardContent><DataTable table={table}/></CardContent></Card>}</DashboardContent></DashboardMain></DashboardLayout>
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ReactNode } from "react"
|
||||
import { GNB, GNBBrand, GNBMobileMenu, GNBMobileLink } from "@hm-design/ui"
|
||||
|
||||
export function GNBFrame({ title, children }: { title: string; children: ReactNode }) {
|
||||
return <><GNB><GNBMobileMenu title={`${title} 메뉴`}><GNBMobileLink href="#content" active>{title}</GNBMobileLink></GNBMobileMenu><GNBBrand href="#content"><span><small>HMDesign</small><strong>{title}</strong></span></GNBBrand></GNB><main id="content" className="quality-main">{children}</main></>
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PageHeader, PageHeaderContent, PageHeaderTitle, PageHeaderDescription } from "@hm-design/ui"
|
||||
|
||||
export function Heading({ title, description }: { title: string; description: string }) {
|
||||
return <PageHeader><PageHeaderContent><PageHeaderTitle>{title}</PageHeaderTitle><PageHeaderDescription>{description}</PageHeaderDescription></PageHeaderContent></PageHeader>
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ReactNode } from "react"
|
||||
import { LNB, LNBBrand, LNBNav, LNBItem, LNBMobileHeader, LNBMobileBrand, LNBMobileMenu, LNBMobileLink } from "@hm-design/ui"
|
||||
|
||||
export function LNBFrame({ title, children }: { title: string; children: ReactNode }) {
|
||||
return <div className="quality-lnb"><LNB><LNBBrand><span><small>HMDesign</small><strong>{title}</strong></span></LNBBrand><LNBNav aria-label={`${title} 메뉴`}><LNBItem href="#content" active>{title}</LNBItem></LNBNav></LNB><div className="quality-lnb-content"><div className="quality-mobile-header"><LNBMobileHeader><LNBMobileMenu title={`${title} 메뉴`}><LNBMobileLink href="#content" active>{title}</LNBMobileLink></LNBMobileMenu><LNBMobileBrand href="#content">{title}</LNBMobileBrand></LNBMobileHeader></div><main id="content" className="quality-main">{children}</main></div></div>
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, Button, Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, DrawerFooter, DrawerClose } from "@hm-design/ui"
|
||||
import { Heading } from "./Heading"
|
||||
|
||||
export function OverlayWorkflow() {
|
||||
const [status, setStatus] = useState("변경 전")
|
||||
return <><Heading title="작업 변경" description="Drawer에서 작업 변경을 확인하고 적용합니다."/><Card><CardContent><p role="status">{status}</p><Drawer><DrawerTrigger asChild><Button>작업 변경 열기</Button></DrawerTrigger><DrawerContent><DrawerHeader><DrawerTitle>작업 변경</DrawerTitle><DrawerDescription>확인 후 적용하면 변경 상태를 갱신합니다.</DrawerDescription></DrawerHeader><DrawerFooter><DrawerClose asChild><Button variant="cancel" appearance="solid">취소</Button></DrawerClose><DrawerClose asChild><Button onClick={() => setStatus("변경 적용 완료")}>적용</Button></DrawerClose></DrawerFooter></DrawerContent></Drawer></CardContent></Card></>
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useId, useState } from "react"
|
||||
import { Card, CardContent, Button, Label, Input, Checkbox, Switch } from "@hm-design/ui"
|
||||
import { Heading } from "./Heading"
|
||||
|
||||
export function Permissions() {
|
||||
const id = useId()
|
||||
const [edit, setEdit] = useState(false)
|
||||
const [notice, setNotice] = useState(true)
|
||||
const [status, setStatus] = useState("")
|
||||
return <><Heading title="권한 설정" description="편집 권한과 알림 설정을 저장합니다."/><Card><CardContent><div className="quality-fields"><Label htmlFor={`${id}-account`}>계정</Label><Input id={`${id}-account`} readOnly value="hm-design"/>
|
||||
<div className="flex items-center gap-2"><Checkbox id={`${id}-edit`} checked={edit} onCheckedChange={v => setEdit(v === true)}/><Label htmlFor={`${id}-edit`}>편집 권한</Label></div>
|
||||
<div className="flex items-center gap-2"><Switch id={`${id}-notice`} checked={notice} onCheckedChange={setNotice}/><Label htmlFor={`${id}-notice`}>알림 수신</Label></div><Button onClick={() => setStatus(`저장 완료 · 편집 ${edit ? "허용" : "불가"} · 알림 ${notice ? "켜짐" : "꺼짐"}`)}>저장</Button><p role="status">{status}</p>
|
||||
</div></CardContent></Card></>
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useId, useRef, useState } from "react"
|
||||
import { Card, CardContent, Button, Label, Input, Textarea, Checkbox } from "@hm-design/ui"
|
||||
import { Heading } from "./Heading"
|
||||
|
||||
export function RequestForm() {
|
||||
const id = useId()
|
||||
const ref = useRef<HTMLInputElement>(null)
|
||||
const [name, setName] = useState("")
|
||||
const [description, setDescription] = useState("")
|
||||
const [agreed, setAgreed] = useState(false)
|
||||
const [status, setStatus] = useState("")
|
||||
return <><Heading title="업무 요청" description="필수 이름과 동의를 확인한 뒤 제출합니다."/><Card><CardContent><form className="quality-fields" noValidate onSubmit={e => { e.preventDefault(); if (!name.trim()) { setStatus("이름을 입력하세요."); ref.current?.focus(); return } if (!agreed) { setStatus("동의가 필요합니다."); document.getElementById(`${id}-agree`)?.focus(); return } setStatus("요청이 제출되었습니다.") }}>
|
||||
<Label htmlFor={`${id}-name`}>요청자 이름</Label><Input id={`${id}-name`} ref={ref} required aria-invalid={status === "이름을 입력하세요."} aria-describedby={`${id}-status`} value={name} onChange={e => setName(e.target.value)}/>
|
||||
<Label htmlFor={`${id}-description`}>요청 내용</Label><Textarea id={`${id}-description`} value={description} onChange={e => setDescription(e.target.value)}/>
|
||||
<div className="flex items-center gap-2"><Checkbox id={`${id}-agree`} checked={agreed} onCheckedChange={v => setAgreed(v === true)}/><Label htmlFor={`${id}-agree`}>처리에 동의합니다</Label></div>
|
||||
<p id={`${id}-status`} role="status">{status}</p><Button type="submit">제출</Button>
|
||||
</form></CardContent></Card></>
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useId, useMemo, useState } from "react"
|
||||
import { Card, CardHeader, CardTitle, CardContent, Button, Label, FilterBar, FilterBarSearch, FilterBarActions, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, DataTable, useDataTable, type DataTableColumnDef, Badge, Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose, Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, SheetClose } from "@hm-design/ui"
|
||||
import { Heading } from "./Heading"
|
||||
|
||||
type Row = { id: string; name: string; group: string; status: string }
|
||||
const records: Row[] = [{ id: "1", name: "김한맥", group: "설계", status: "진행" }, { id: "2", name: "이한맥", group: "시공", status: "대기" }, { id: "3", name: "박한맥", group: "설계", status: "완료" }]
|
||||
const columns: DataTableColumnDef<Row>[] = [{ accessorKey: "name", header: "이름" }, { accessorKey: "group", header: "부서" }, { accessorKey: "status", header: "상태" }]
|
||||
export function SearchList({ title, cards = false, detail = "none" }: { title: string; cards?: boolean; detail?: "none" | "dialog" | "sheet" }) {
|
||||
const [draft, setDraft] = useState("")
|
||||
const [group, setGroup] = useState("all")
|
||||
const [applied, setApplied] = useState({ query: "", group: "all" })
|
||||
const [confirmed, setConfirmed] = useState(false)
|
||||
const rows = useMemo(() => records.filter(r => r.name.includes(applied.query) && (applied.group === "all" || r.group === applied.group)), [applied])
|
||||
const table = useDataTable({ columns, data: rows, getRowId: row => row.id })
|
||||
const id = useId()
|
||||
return <><Heading title={title} description="부서와 이름 조건은 조회할 때 적용됩니다."/><Card><CardContent><div className="grid min-w-0 gap-4"><form onSubmit={e => { e.preventDefault(); setApplied({ query: draft, group }); table.setPageIndex(0) }}><FilterBar layout="list-basic">
|
||||
<Select value={group} onValueChange={setGroup}><SelectTrigger size="medium" aria-label="부서"><SelectValue/></SelectTrigger><SelectContent><SelectItem value="all">전체 부서</SelectItem><SelectItem value="설계">설계</SelectItem><SelectItem value="시공">시공</SelectItem></SelectContent></Select>
|
||||
<Label htmlFor={id} className="sr-only">이름 검색</Label><FilterBarSearch id={id} value={draft} onValueChange={setDraft} placeholder="이름 검색"/>
|
||||
<FilterBarActions><Button type="submit" variant="primary" appearance="solid">조회</Button><Button type="button" variant="cancel" appearance="outline" onClick={() => { setDraft(""); setGroup("all"); setApplied({ query: "", group: "all" }); table.setPageIndex(0) }}>초기화</Button></FilterBarActions>
|
||||
</FilterBar></form><p role="status">조회 결과 {rows.length}건{confirmed ? " · 처리 완료" : ""}</p>
|
||||
{cards ? <div className="grid gap-3">{rows.map(row => <Card key={row.id}><CardHeader><CardTitle>{row.name}</CardTitle></CardHeader><CardContent>{row.group} · <Badge variant="outline">{row.status}</Badge></CardContent></Card>)}{!rows.length && <p>조건에 맞는 업무가 없습니다.</p>}</div> : <DataTable table={table} emptyMessage="조건에 맞는 항목이 없습니다."/>}
|
||||
{detail === "dialog" && <Dialog><DialogTrigger asChild><Button>결재 확인</Button></DialogTrigger><DialogContent><DialogHeader><DialogTitle>결재 확인</DialogTitle><DialogDescription>현재 조회한 결재를 승인합니다.</DialogDescription></DialogHeader><DialogFooter><DialogClose asChild><Button variant="cancel" appearance="solid">취소</Button></DialogClose><DialogClose asChild><Button onClick={() => setConfirmed(true)}>승인</Button></DialogClose></DialogFooter></DialogContent></Dialog>}
|
||||
{detail === "sheet" && <Sheet><SheetTrigger asChild><Button>채용 상세</Button></SheetTrigger><SheetContent><SheetHeader><SheetTitle>채용 상세</SheetTitle><SheetDescription>공고의 상세 내용과 확인 상태입니다.</SheetDescription></SheetHeader><SheetBody>설계 부문 채용 · 서류 검토</SheetBody><SheetFooter><SheetClose asChild><Button onClick={() => setConfirmed(true)}>확인</Button></SheetClose></SheetFooter></SheetContent></Sheet>}
|
||||
</div></CardContent></Card></>
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, Button, Alert, AlertTitle, AlertDescription, Skeleton } from "@hm-design/ui"
|
||||
import { Heading } from "./Heading"
|
||||
|
||||
export function StateWorkflow() {
|
||||
const [state, setState] = useState("loading")
|
||||
return <><Heading title="처리 상태" description="로딩, 오류, 빈 결과를 확인하는 작업입니다."/><Card><CardContent><div className="grid gap-4"><div className="flex flex-wrap gap-2"><Button onClick={() => setState("loading")}>로딩</Button><Button onClick={() => setState("error")}>오류</Button><Button onClick={() => setState("empty")}>빈 결과</Button></div>
|
||||
{state === "loading" && <div role="status" aria-label="처리 중"><Skeleton className="h-10 w-full"/></div>}
|
||||
{state === "error" && <Alert variant="destructive"><AlertTitle>처리 실패</AlertTitle><AlertDescription>다시 시도하세요.</AlertDescription><Button onClick={() => setState("empty")}>다시 시도</Button></Alert>}
|
||||
{state === "empty" && <p role="status">등록된 항목이 없습니다.</p>}
|
||||
</div></CardContent></Card></>
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
@import "@hm-design/ui/theme.css";
|
||||
@import "@hm-design/ui/dashboard-status.css";
|
||||
|
||||
html, body { margin: 0; font-family: var(--hm-font-family-base); background: var(--hm-sem-bg-content); color: var(--hm-sem-fg-primary); }
|
||||
.quality-main { max-width: 1180px; min-width: 0; margin: auto; padding: 24px; display: grid; gap: 20px; }
|
||||
.quality-fields { display: grid; gap: 8px; max-width: 480px; }
|
||||
.quality-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
|
||||
.quality-lnb { display: flex; min-height: 100vh; background: var(--hm-sem-bg-brand); }
|
||||
.quality-lnb-content { flex: 1; min-width: 0; background: var(--hm-sem-bg-content); border-radius: var(--hm-lnb-join) 0 0 var(--hm-lnb-join); }
|
||||
.quality-mobile-header { display: none; }
|
||||
@media (max-width: 1023px) { .quality-mobile-header { display: block; } .quality-lnb-content { border-radius: 0; } }
|
||||
@media (max-width: 640px) { .quality-main { padding: 16px; } .quality-metrics { grid-template-columns: 1fr; } }
|
||||
@@ -0,0 +1,2 @@
|
||||
@import "tailwindcss" source("../");
|
||||
@import "@hm-design/ui/tailwind.css";
|
||||
Reference in new issue
Block a user