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
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import {
|
||||
Card, CardContent, PageHeader, PageHeaderContent, PageHeaderTitle, PageHeaderDescription,
|
||||
Badge,
|
||||
Button,
|
||||
Combobox,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
useDataTable,
|
||||
type DataTableColumnDef,
|
||||
} from "@hm-design/ui"
|
||||
import "../list-screen.css"
|
||||
import { MoreHorizontalIcon } from "lucide-react"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { formatDate, sameDay, sampleUsers, statusLabel, type SampleUser } from "./sample-users"
|
||||
@@ -99,83 +101,44 @@ const statusOptions = [
|
||||
const departmentOptions = [...new Set(sampleUsers.map((u) => u.department))].map((d) => ({ value: d, label: d }))
|
||||
|
||||
// Filter Bar → filter state → Data Table: search is the TanStack global filter, status / date are column filters.
|
||||
function WithFilterBar() {
|
||||
function WithFilterBar({ extended = true }: { extended?: boolean }) {
|
||||
const data = useMemo(() => sampleUsers, [])
|
||||
const table = useDataTable({ columns, data, getRowId: (row) => row.id })
|
||||
const [search, setSearch] = useState("")
|
||||
const [status, setStatus] = useState("all")
|
||||
const [date, setDate] = useState<Date | undefined>()
|
||||
const [department, setDepartment] = useState<string | undefined>()
|
||||
|
||||
const setSearchValue = (value: string) => {
|
||||
setSearch(value)
|
||||
table.setGlobalFilter(value || undefined)
|
||||
const apply = () => {
|
||||
table.setGlobalFilter(search || undefined)
|
||||
table.getColumn("status")?.setFilterValue(status === "all" ? undefined : status)
|
||||
table.getColumn("department")?.setFilterValue(department)
|
||||
table.getColumn("createdAt")?.setFilterValue(date)
|
||||
}
|
||||
const reset = () => {
|
||||
setSearch("")
|
||||
setStatus("all")
|
||||
setDate(undefined)
|
||||
setDepartment(undefined)
|
||||
table.resetGlobalFilter(true)
|
||||
table.resetColumnFilters(true)
|
||||
setSearch(""); setStatus("all"); setDate(undefined); setDepartment(undefined)
|
||||
table.resetGlobalFilter(true); table.resetColumnFilters(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid w-full gap-4">
|
||||
<FilterBar>
|
||||
<FilterBarSearch value={search} onValueChange={setSearchValue} placeholder="이름·부서 검색" />
|
||||
<Select
|
||||
value={status}
|
||||
onValueChange={(value) => {
|
||||
setStatus(value)
|
||||
table.getColumn("status")?.setFilterValue(value === "all" ? undefined : value)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger size="medium" aria-label="상태">
|
||||
<SelectValue placeholder="상태" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">전체 상태</SelectItem>
|
||||
{statusOptions.map((o) => (
|
||||
<SelectItem key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Combobox
|
||||
controlled
|
||||
size="medium"
|
||||
aria-label="부서"
|
||||
options={departmentOptions}
|
||||
value={department}
|
||||
placeholder="부서"
|
||||
onValueChange={(value) => {
|
||||
setDepartment(value)
|
||||
setSearchValue(value)
|
||||
}}
|
||||
/>
|
||||
<DatePicker
|
||||
controlled
|
||||
size="medium"
|
||||
aria-label="등록일"
|
||||
placeholder="등록일"
|
||||
value={date}
|
||||
onValueChange={(value) => {
|
||||
setDate(value)
|
||||
table.getColumn("createdAt")?.setFilterValue(value)
|
||||
}}
|
||||
/>
|
||||
<FilterBarActions>
|
||||
<Button type="button" variant="cancel" appearance="outline" onClick={reset}>
|
||||
초기화
|
||||
</Button>
|
||||
<DataTableViewOptions table={table} labels={columnLabels} />
|
||||
</FilterBarActions>
|
||||
</FilterBar>
|
||||
<DataTable table={table} emptyMessage="조건에 맞는 사용자가 없습니다." />
|
||||
</div>
|
||||
)
|
||||
return <div className="grid min-w-0 gap-4">
|
||||
<PageHeader><PageHeaderContent><PageHeaderTitle>사용자 목록</PageHeaderTitle><PageHeaderDescription>조건을 입력한 뒤 조회합니다.</PageHeaderDescription></PageHeaderContent></PageHeader>
|
||||
<Card className="min-w-0"><CardContent>
|
||||
<div className="grid min-w-0 gap-4">
|
||||
<form onSubmit={event => { event.preventDefault(); apply() }}>
|
||||
<FilterBar className={extended ? "hm-list-search-extended" : "hm-list-search-basic"}>
|
||||
<Select value={status} onValueChange={setStatus}>
|
||||
<SelectTrigger size="medium" aria-label="상태"><SelectValue placeholder="상태" /></SelectTrigger>
|
||||
<SelectContent><SelectItem value="all">전체 상태</SelectItem>{statusOptions.map(option => <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
{extended && <><Combobox controlled size="medium" aria-label="부서" options={departmentOptions} value={department} placeholder="부서" onValueChange={setDepartment} />
|
||||
<DatePicker controlled size="medium" aria-label="등록일" placeholder="등록일" value={date} onValueChange={setDate} /></>}
|
||||
<FilterBarSearch value={search} onValueChange={setSearch} placeholder="이름·부서 검색" />
|
||||
<FilterBarActions><Button type="submit" variant="primary" appearance="solid">조회</Button><Button type="button" variant="cancel" appearance="outline" onClick={reset}>초기화</Button></FilterBarActions>
|
||||
</FilterBar>
|
||||
</form>
|
||||
<div className="flex justify-end"><DataTableViewOptions table={table} labels={columnLabels} /></div>
|
||||
<DataTable table={table} emptyMessage="조건에 맞는 사용자가 없습니다." />
|
||||
</div>
|
||||
</CardContent></Card>
|
||||
</div>
|
||||
}
|
||||
|
||||
const meta = { title: "Patterns/Data/Data Table", parameters: { layout: "padded", controls: { disable: true } } } satisfies Meta
|
||||
@@ -196,6 +159,8 @@ export const All: Story = {
|
||||
<StorySection title="Overview" description="행 선택(헤더 혼합 상태), 정렬, 열 표시, 행 작업, 페이지 크기와 페이지 이동을 실제로 조작합니다.">
|
||||
<Basic />
|
||||
</StorySection>
|
||||
<StorySection title="기본 목록 검색" description="조건·검색어·조회·초기화를 8px 간격으로 가운데 정렬합니다. 제목은 Card 밖에 배치합니다."><WithFilterBar extended={false} /></StorySection>
|
||||
<StorySection title="추가 조건 목록 검색" description="부서·등록일을 제공하는 구성은 입력값과 관계없이 왼쪽 정렬합니다. 모바일 버튼 행은 우측 정렬합니다."><WithFilterBar /></StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState } from "react"
|
||||
import { useId, useState } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import {
|
||||
Button,
|
||||
Combobox,
|
||||
DatePicker,
|
||||
Label,
|
||||
FilterBar,
|
||||
FilterBarActions,
|
||||
FilterBarSearch,
|
||||
@@ -24,15 +25,16 @@ const categories = [
|
||||
]
|
||||
|
||||
function Full() {
|
||||
const searchId = useId()
|
||||
const [search, setSearch] = useState("")
|
||||
const [status, setStatus] = useState("all")
|
||||
const [category, setCategory] = useState<string | undefined>()
|
||||
const [date, setDate] = useState<Date | undefined>()
|
||||
const [applied, setApplied] = useState(0)
|
||||
const active = [search, status !== "all" && status, category, date].filter(Boolean).length
|
||||
return (
|
||||
<div className="grid w-full gap-3">
|
||||
<FilterBar>
|
||||
<FilterBarSearch value={search} onValueChange={setSearch} placeholder="검색어 입력" />
|
||||
<FilterBar layout="list-extended">
|
||||
<Select value={status} onValueChange={setStatus}>
|
||||
<SelectTrigger size="medium" aria-label="상태">
|
||||
<SelectValue />
|
||||
@@ -45,7 +47,10 @@ function Full() {
|
||||
</Select>
|
||||
<Combobox controlled size="medium" aria-label="분류" options={categories} value={category} onValueChange={setCategory} placeholder="분류" />
|
||||
<DatePicker controlled size="medium" aria-label="날짜" value={date} onValueChange={setDate} placeholder="날짜" />
|
||||
<Label htmlFor={searchId} className="sr-only">검색어</Label>
|
||||
<FilterBarSearch id={searchId} value={search} onValueChange={setSearch} placeholder="검색어 입력" />
|
||||
<FilterBarActions>
|
||||
<Button type="button" variant="primary" appearance="solid" onClick={() => setApplied(active)}>조회</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="cancel"
|
||||
@@ -55,17 +60,15 @@ function Full() {
|
||||
setStatus("all")
|
||||
setCategory(undefined)
|
||||
setDate(undefined)
|
||||
setApplied(0)
|
||||
}}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
<Button type="button" variant="primary" appearance="outline">
|
||||
조회
|
||||
</Button>
|
||||
</FilterBarActions>
|
||||
</FilterBar>
|
||||
<p className="text-sm text-muted-foreground" role="status">
|
||||
적용된 조건 {active}개
|
||||
입력한 조건 {active}개 · 조회 적용 조건 {applied}개
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
@@ -89,6 +92,12 @@ export const All: Story = {
|
||||
<StorySection title="Overview" description="검색 · Select · Combobox · Date Picker와 초기화 / 조회 버튼입니다. 폭이 좁으면 줄바꿈됩니다.">
|
||||
<Full />
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="추가 조건은 list-extended, 기본 조건은 list-basic. 기본 layout은 기존 행 끝 정렬을 유지합니다. 390px에서는 검색과 액션이 줄바꿈됩니다.">
|
||||
<p>입력은 draft로 유지하고 조회에만 적용합니다. 초기화는 입력과 적용값을 함께 비웁니다. 목록은 PageHeader 밖의 흰 Card 하나에 검색과 결과를 함께 배치합니다.</p>
|
||||
</StorySection>
|
||||
<StorySection title="Do & Don't" description="요구한 조건과 액션만 구성합니다.">
|
||||
<p>Do: 공개 Label과 control ID 연결, Solid Green 조회와 Outline Gray 초기화. Don't: Story에 보이는 분류·날짜·메뉴·CSV를 요구사항 없이 추가하거나 임의 토큰으로 색을 지정하지 않습니다.</p>
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState, type CSSProperties, type MouseEvent, type ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { BellIcon, ChartColumnIcon, LayoutGridIcon, SearchIcon, SettingsIcon, type LucideIcon } from "lucide-react"
|
||||
import { BellIcon, ChartColumnIcon, SearchIcon, SettingsIcon, type LucideIcon } from "lucide-react"
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
@@ -52,6 +52,7 @@ import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/compon
|
||||
// the content, White 2-depth panel, text-only profile); every part inside is an HM component. The menu data, the user and the current
|
||||
// location below are local sample data for this Story only (no network, no auth, no router). The desktop / mobile switch follows the
|
||||
// viewport (1024px): narrow the Story to 390px to see the hamburger and the Sheet menu.
|
||||
// GNBUser's default accessible name includes both the visible name and description; explicit labels must preserve them.
|
||||
|
||||
type Destination = { id: string; label: string }
|
||||
type NavItem = { id: string; label: string; children?: Destination[] }
|
||||
@@ -339,7 +340,7 @@ function StickyPage() {
|
||||
// Consumer-owned data and route state; every menu face/disclosure is an existing public LNB part.
|
||||
type SidebarNode = { id: string; label: string; icon?: LucideIcon; children?: SidebarNode[] }
|
||||
const sidebarNodes: SidebarNode[] = [
|
||||
{ id: "dashboard", label: "대시보드", icon: LayoutGridIcon },
|
||||
{ id: "dashboard", label: "대시보드" },
|
||||
{
|
||||
id: "projects", label: "프로젝트 관리", children: [
|
||||
{ id: "projects/design", label: "디자인 관리", children: [
|
||||
@@ -366,7 +367,7 @@ const sidebarGnbItems: NavItem[] = navItems.map(item => item.id === "projects" ?
|
||||
],
|
||||
} : item)
|
||||
|
||||
function SidebarPage({ fullHeight = false }: { fullHeight?: boolean }) {
|
||||
function SidebarPage({ fullHeight = false, color = "green" }: { fullHeight?: boolean; color?: "green" | "brown" }) {
|
||||
const [current, setCurrent] = useState("projects/design/tokens")
|
||||
const [groups, setGroups] = useState<Record<string, boolean>>({ projects: true, "projects/design": true })
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
@@ -400,7 +401,7 @@ function SidebarPage({ fullHeight = false }: { fullHeight?: boolean }) {
|
||||
<LNBMobileLink key={node.id} href={`#${node.id}`} active={current === node.id} onClick={go(node.id)}>{node.label}</LNBMobileLink>
|
||||
))
|
||||
return (
|
||||
<div data-hm-gnb-layout="sidebar" style={{ height: fullHeight ? "100dvh" : 560 }}>
|
||||
<div data-hm-gnb-layout="sidebar" data-hm-color={color} style={{ height: fullHeight ? "100dvh" : 560 }}>
|
||||
<Bar items={sidebarGnbItems} current={current} onCurrent={setCurrent}
|
||||
mobileMenuOpen={mobileOpen} onMobileMenuOpenChange={setMobileOpen}
|
||||
mobileChildren={<div data-slot="lnb-mobile-nav">{mobileMenu(sidebarNodes)}</div>} />
|
||||
@@ -485,9 +486,10 @@ export const All: Story = {
|
||||
<StickyPage />
|
||||
</StorySection>
|
||||
<StorySection title="Usage · GNB Sidebar"
|
||||
description="GNB ONLY 내부 Sidebar입니다. 기존 LNB 메뉴를 그대로 사용하고 외곽 배경만 Green Light 2로 바꿉니다. 모바일은 GNB의 단일 Sheet에 같은 메뉴를 표시합니다.">
|
||||
description="기존 Reference 전용 콘텐츠 LNB입니다. 본문과 같은 배경, 세로 구분선 하나, 아이콘 기본 생략·화살표 유지. 신규 AI GNB_ONLY 화면에는 Sidebar를 생성하지 않습니다.">
|
||||
<SidebarPage />
|
||||
</StorySection>
|
||||
<StorySection title="Usage · Brown 콘텐츠 LNB" description="기존 Reference의 Brown 선택·Hover를 비교합니다. Green 선택색을 혼용하지 않습니다."><SidebarPage color="brown" /></StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Fragment, useState, type CSSProperties, type MouseEvent, type ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
// LNBProfile's default accessible name includes the visible name and description; explicit labels must preserve them.
|
||||
import { ChartColumnIcon, LayoutGridIcon, ListChecksIcon, SettingsIcon, type LucideIcon } from "lucide-react"
|
||||
import {
|
||||
GNB,
|
||||
@@ -33,7 +34,7 @@ import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// LNB is a composition pattern. Layout visual reference is the ui-viz LNB (272px green column, 18/700 items, Beige current item joined
|
||||
// to the content, Black 10% hover, Black 20% open group). The menu data and the current location are local sample data for this Story
|
||||
// to the content, Black 10% hover, Black 8% open group). The menu data and the current location are local sample data for this Story
|
||||
// (no router). The content next to the column is a positioned surface with `--hm-lnb-join` rounded left corners: the LNB Mode page
|
||||
// owns it, here it only shows the connection. The column is desktop only (hidden below 1024px).
|
||||
|
||||
@@ -651,7 +652,7 @@ export const All: Story = {
|
||||
min={560}
|
||||
states={[
|
||||
{ id: "default", label: "Default · 기존 LNB", note: "선택 없음, 2뎁스 그룹 접힘. 항목에 마우스를 올리면 Black 10%가 콘텐츠 경계까지 닿습니다.", children: <ResponsiveSpecimen nodes={simpleNav()} initial={null} initialOpen={{ tasks: false }} /> },
|
||||
{ id: "expanded", label: "Expanded · 2뎁스 펼침", note: "하위 선택 없이 펼친 그룹: Black 20% 면 + 1px 초록 선.", children: <ResponsiveSpecimen nodes={simpleNav()} initial={null} initialOpen={{ tasks: true }} /> },
|
||||
{ id: "expanded", label: "Expanded · 2뎁스 펼침", note: "하위 선택 없이 펼친 그룹: Black 8% 면 + 1px 초록 선.", children: <ResponsiveSpecimen nodes={simpleNav()} initial={null} initialOpen={{ tasks: true }} /> },
|
||||
{ id: "selected-child", label: "Selected Child · 2뎁스 선택", note: "하위 선택: 상위 이름·화살표가 노란색, 하위는 Beige.", children: <ResponsiveSpecimen nodes={simpleNav()} initial="tasks/list" initialOpen={{ tasks: true }} /> },
|
||||
{ id: "collapsed-with-selection", label: "Collapsed With Selection · 접힘", note: "하위가 선택된 채 접힌 그룹도 상위가 노란색을 유지합니다.", children: <ResponsiveSpecimen nodes={simpleNav()} initial="tasks/list" initialOpen={{ tasks: false }} /> },
|
||||
{ id: "third-second-collapsed", label: "3뎁스 메뉴 · 2뎁스 접힘", note: "디자인 관리를 접어도 선택된 경로의 1·2뎁스 표시를 유지합니다. 하위 메뉴와 연결선이 함께 숨겨집니다.", children: <ResponsiveSpecimen nodes={thirdLevelNav()} initial="projects/design/tokens" initialOpen={{ projects: true, "projects/design": false }} /> },
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
PageHeaderContent,
|
||||
PageHeaderDescription,
|
||||
PageHeaderTitle,
|
||||
Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow,
|
||||
Tabs, TabsContent, TabsList, TabsTrigger,
|
||||
} from "@hm-design/ui"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
@@ -32,9 +34,12 @@ export const All: Story = {
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.patterns}
|
||||
title="Page Header"
|
||||
description="화면 콘텐츠의 제목 영역입니다. Breadcrumb, 제목, 설명과 대표 Action을 조합합니다."
|
||||
description="기본형은 Breadcrumb 없이 제목 중심입니다. 설명·액션·Breadcrumb는 화면의 필요에 따라 선택 적용합니다."
|
||||
/>
|
||||
<StorySection title="Overview" description="단일 Brand Action을 사용하는 기본 조합입니다.">
|
||||
<StorySection title="Overview" description="제목 중심 기본형입니다. Breadcrumb는 생략하고 필요한 설명·액션을 선택 적용합니다.">
|
||||
<PageHeader><PageHeaderContent><PageHeaderTitle>업무 현황</PageHeaderTitle></PageHeaderContent></PageHeader>
|
||||
</StorySection>
|
||||
<StorySection title="Breadcrumb 포함형" description="Breadcrumb·설명·단일 Brand Action과 Primary Tabs 조합입니다. 제목→설명 8px, Header→Tabs 20px을 유지합니다.">
|
||||
<PageHeader>
|
||||
<PageHeaderContent>
|
||||
<Breadcrumb>
|
||||
@@ -61,6 +66,29 @@ export const All: Story = {
|
||||
</Button>
|
||||
</PageHeaderActions>
|
||||
</PageHeader>
|
||||
<Tabs defaultValue="mine" className="min-w-0">
|
||||
<TabsList aria-label="업무 목록 보기">
|
||||
<TabsTrigger value="mine">내 업무</TabsTrigger>
|
||||
<TabsTrigger value="completed">완료 업무</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="mine" className="px-0 py-6">
|
||||
<Table>
|
||||
<TableCaption>내 업무 일정 예시</TableCaption>
|
||||
<TableHeader><TableRow><TableHead scope="col">업무</TableHead><TableHead scope="col">마감일</TableHead><TableHead scope="col">상태</TableHead></TableRow></TableHeader>
|
||||
<TableBody>
|
||||
<TableRow><TableCell>화면 설계서 검토</TableCell><TableCell>10월 14일</TableCell><TableCell>검토</TableCell></TableRow>
|
||||
<TableRow><TableCell>고객 요구사항 정리</TableCell><TableCell>10월 16일</TableCell><TableCell>진행</TableCell></TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
<TabsContent value="completed" className="px-0 py-6">
|
||||
<Table>
|
||||
<TableCaption>완료 업무 예시</TableCaption>
|
||||
<TableHeader><TableRow><TableHead scope="col">업무</TableHead><TableHead scope="col">완료일</TableHead><TableHead scope="col">상태</TableHead></TableRow></TableHeader>
|
||||
<TableBody><TableRow><TableCell>프로젝트 기본 정보 등록</TableCell><TableCell>10월 9일</TableCell><TableCell>완료</TableCell></TableRow></TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user