moru-krds-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/accordion/index.d.ts +63 -0
- package/dist/accordion/index.d.ts.map +1 -0
- package/dist/accordion/index.js +145 -0
- package/dist/accordion/index.js.map +1 -0
- package/dist/alert/index.d.ts +44 -0
- package/dist/alert/index.d.ts.map +1 -0
- package/dist/alert/index.js +66 -0
- package/dist/alert/index.js.map +1 -0
- package/dist/avatar/index.d.ts +102 -0
- package/dist/avatar/index.d.ts.map +1 -0
- package/dist/avatar/index.js +146 -0
- package/dist/avatar/index.js.map +1 -0
- package/dist/badge/index.d.ts +49 -0
- package/dist/badge/index.d.ts.map +1 -0
- package/dist/badge/index.js +41 -0
- package/dist/badge/index.js.map +1 -0
- package/dist/breadcrumb/index.d.ts +43 -0
- package/dist/breadcrumb/index.d.ts.map +1 -0
- package/dist/breadcrumb/index.js +43 -0
- package/dist/breadcrumb/index.js.map +1 -0
- package/dist/button/index.d.ts +78 -0
- package/dist/button/index.d.ts.map +1 -0
- package/dist/button/index.js +83 -0
- package/dist/button/index.js.map +1 -0
- package/dist/checkbox/index.d.ts +97 -0
- package/dist/checkbox/index.d.ts.map +1 -0
- package/dist/checkbox/index.js +110 -0
- package/dist/checkbox/index.js.map +1 -0
- package/dist/combobox/index.d.ts +135 -0
- package/dist/combobox/index.d.ts.map +1 -0
- package/dist/combobox/index.js +416 -0
- package/dist/combobox/index.js.map +1 -0
- package/dist/context-menu/index.d.ts +94 -0
- package/dist/context-menu/index.d.ts.map +1 -0
- package/dist/context-menu/index.js +399 -0
- package/dist/context-menu/index.js.map +1 -0
- package/dist/critical-alert/index.d.ts +39 -0
- package/dist/critical-alert/index.d.ts.map +1 -0
- package/dist/critical-alert/index.js +50 -0
- package/dist/critical-alert/index.js.map +1 -0
- package/dist/data-table/index.d.ts +94 -0
- package/dist/data-table/index.d.ts.map +1 -0
- package/dist/data-table/index.js +128 -0
- package/dist/data-table/index.js.map +1 -0
- package/dist/disclosure/index.d.ts +35 -0
- package/dist/disclosure/index.d.ts.map +1 -0
- package/dist/disclosure/index.js +54 -0
- package/dist/disclosure/index.js.map +1 -0
- package/dist/drawer/index.d.ts +106 -0
- package/dist/drawer/index.d.ts.map +1 -0
- package/dist/drawer/index.js +352 -0
- package/dist/drawer/index.js.map +1 -0
- package/dist/empty-state/index.d.ts +86 -0
- package/dist/empty-state/index.d.ts.map +1 -0
- package/dist/empty-state/index.js +138 -0
- package/dist/empty-state/index.js.map +1 -0
- package/dist/env.d.js +1 -0
- package/dist/env.d.js.map +1 -0
- package/dist/field/index.d.ts +149 -0
- package/dist/field/index.d.ts.map +1 -0
- package/dist/field/index.js +121 -0
- package/dist/field/index.js.map +1 -0
- package/dist/file-upload/index.d.ts +140 -0
- package/dist/file-upload/index.d.ts.map +1 -0
- package/dist/file-upload/index.js +249 -0
- package/dist/file-upload/index.js.map +1 -0
- package/dist/footer/index.d.ts +155 -0
- package/dist/footer/index.d.ts.map +1 -0
- package/dist/footer/index.js +186 -0
- package/dist/footer/index.js.map +1 -0
- package/dist/header/index.d.ts +170 -0
- package/dist/header/index.d.ts.map +1 -0
- package/dist/header/index.js +258 -0
- package/dist/header/index.js.map +1 -0
- package/dist/identifier/index.d.ts +38 -0
- package/dist/identifier/index.d.ts.map +1 -0
- package/dist/identifier/index.js +55 -0
- package/dist/identifier/index.js.map +1 -0
- package/dist/in-page-navigation/index.d.ts +71 -0
- package/dist/in-page-navigation/index.d.ts.map +1 -0
- package/dist/in-page-navigation/index.js +137 -0
- package/dist/in-page-navigation/index.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/input-group/index.d.ts +66 -0
- package/dist/input-group/index.d.ts.map +1 -0
- package/dist/input-group/index.js +85 -0
- package/dist/input-group/index.js.map +1 -0
- package/dist/lib/krds.d.ts +45 -0
- package/dist/lib/krds.d.ts.map +1 -0
- package/dist/lib/krds.js +11 -0
- package/dist/lib/krds.js.map +1 -0
- package/dist/lib/link.d.ts +34 -0
- package/dist/lib/link.d.ts.map +1 -0
- package/dist/lib/link.js +8 -0
- package/dist/lib/link.js.map +1 -0
- package/dist/main-menu/index.d.ts +123 -0
- package/dist/main-menu/index.d.ts.map +1 -0
- package/dist/main-menu/index.js +613 -0
- package/dist/main-menu/index.js.map +1 -0
- package/dist/masthead/index.d.ts +25 -0
- package/dist/masthead/index.d.ts.map +1 -0
- package/dist/masthead/index.js +21 -0
- package/dist/masthead/index.js.map +1 -0
- package/dist/modal/index.d.ts +105 -0
- package/dist/modal/index.d.ts.map +1 -0
- package/dist/modal/index.js +285 -0
- package/dist/modal/index.js.map +1 -0
- package/dist/pagination/index.d.ts +58 -0
- package/dist/pagination/index.d.ts.map +1 -0
- package/dist/pagination/index.js +82 -0
- package/dist/pagination/index.js.map +1 -0
- package/dist/progress/index.d.ts +58 -0
- package/dist/progress/index.d.ts.map +1 -0
- package/dist/progress/index.js +125 -0
- package/dist/progress/index.js.map +1 -0
- package/dist/radio/index.d.ts +111 -0
- package/dist/radio/index.d.ts.map +1 -0
- package/dist/radio/index.js +136 -0
- package/dist/radio/index.js.map +1 -0
- package/dist/resize/index.d.ts +118 -0
- package/dist/resize/index.d.ts.map +1 -0
- package/dist/resize/index.js +342 -0
- package/dist/resize/index.js.map +1 -0
- package/dist/scroll-area/index.d.ts +40 -0
- package/dist/scroll-area/index.d.ts.map +1 -0
- package/dist/scroll-area/index.js +52 -0
- package/dist/scroll-area/index.js.map +1 -0
- package/dist/select/index.d.ts +67 -0
- package/dist/select/index.d.ts.map +1 -0
- package/dist/select/index.js +61 -0
- package/dist/select/index.js.map +1 -0
- package/dist/separator/index.d.ts +47 -0
- package/dist/separator/index.d.ts.map +1 -0
- package/dist/separator/index.js +61 -0
- package/dist/separator/index.js.map +1 -0
- package/dist/side-navigation/index.d.ts +78 -0
- package/dist/side-navigation/index.d.ts.map +1 -0
- package/dist/side-navigation/index.js +188 -0
- package/dist/side-navigation/index.js.map +1 -0
- package/dist/skeleton/index.d.ts +76 -0
- package/dist/skeleton/index.d.ts.map +1 -0
- package/dist/skeleton/index.js +78 -0
- package/dist/skeleton/index.js.map +1 -0
- package/dist/skip-link/index.d.ts +46 -0
- package/dist/skip-link/index.d.ts.map +1 -0
- package/dist/skip-link/index.js +37 -0
- package/dist/skip-link/index.js.map +1 -0
- package/dist/spinner/index.d.ts +58 -0
- package/dist/spinner/index.d.ts.map +1 -0
- package/dist/spinner/index.js +36 -0
- package/dist/spinner/index.js.map +1 -0
- package/dist/step-indicator/index.d.ts +41 -0
- package/dist/step-indicator/index.d.ts.map +1 -0
- package/dist/step-indicator/index.js +63 -0
- package/dist/step-indicator/index.js.map +1 -0
- package/dist/structured-list/index.d.ts +150 -0
- package/dist/structured-list/index.d.ts.map +1 -0
- package/dist/structured-list/index.js +194 -0
- package/dist/structured-list/index.js.map +1 -0
- package/dist/tab/index.d.ts +108 -0
- package/dist/tab/index.d.ts.map +1 -0
- package/dist/tab/index.js +198 -0
- package/dist/tab/index.js.map +1 -0
- package/dist/table/index.d.ts +75 -0
- package/dist/table/index.d.ts.map +1 -0
- package/dist/table/index.js +91 -0
- package/dist/table/index.js.map +1 -0
- package/dist/tag/index.d.ts +71 -0
- package/dist/tag/index.d.ts.map +1 -0
- package/dist/tag/index.js +45 -0
- package/dist/tag/index.js.map +1 -0
- package/dist/text-input/index.d.ts +69 -0
- package/dist/text-input/index.d.ts.map +1 -0
- package/dist/text-input/index.js +70 -0
- package/dist/text-input/index.js.map +1 -0
- package/dist/text-list/index.d.ts +71 -0
- package/dist/text-list/index.d.ts.map +1 -0
- package/dist/text-list/index.js +79 -0
- package/dist/text-list/index.js.map +1 -0
- package/dist/textarea/index.d.ts +29 -0
- package/dist/textarea/index.d.ts.map +1 -0
- package/dist/textarea/index.js +69 -0
- package/dist/textarea/index.js.map +1 -0
- package/dist/toast/index.d.ts +76 -0
- package/dist/toast/index.d.ts.map +1 -0
- package/dist/toast/index.js +183 -0
- package/dist/toast/index.js.map +1 -0
- package/dist/tooltip/index.d.ts +84 -0
- package/dist/tooltip/index.d.ts.map +1 -0
- package/dist/tooltip/index.js +329 -0
- package/dist/tooltip/index.js.map +1 -0
- package/package.json +57 -0
- package/src/accordion/index.tsx +251 -0
- package/src/alert/index.tsx +111 -0
- package/src/avatar/index.tsx +272 -0
- package/src/badge/index.tsx +91 -0
- package/src/breadcrumb/index.tsx +125 -0
- package/src/button/index.tsx +190 -0
- package/src/checkbox/index.tsx +247 -0
- package/src/combobox/index.tsx +700 -0
- package/src/context-menu/index.tsx +623 -0
- package/src/critical-alert/index.tsx +102 -0
- package/src/data-table/index.tsx +289 -0
- package/src/disclosure/index.tsx +87 -0
- package/src/drawer/index.tsx +516 -0
- package/src/empty-state/index.tsx +256 -0
- package/src/env.d.ts +10 -0
- package/src/field/index.tsx +307 -0
- package/src/file-upload/index.tsx +492 -0
- package/src/footer/index.tsx +393 -0
- package/src/header/index.tsx +498 -0
- package/src/identifier/index.tsx +96 -0
- package/src/in-page-navigation/index.tsx +258 -0
- package/src/index.ts +72 -0
- package/src/input-group/index.tsx +197 -0
- package/src/lib/krds.ts +75 -0
- package/src/lib/link.tsx +35 -0
- package/src/main-menu/index.tsx +912 -0
- package/src/masthead/index.tsx +50 -0
- package/src/modal/index.tsx +457 -0
- package/src/pagination/index.tsx +198 -0
- package/src/progress/index.tsx +198 -0
- package/src/radio/index.tsx +283 -0
- package/src/resize/index.tsx +541 -0
- package/src/scroll-area/index.tsx +96 -0
- package/src/select/index.tsx +136 -0
- package/src/separator/index.tsx +107 -0
- package/src/side-navigation/index.tsx +336 -0
- package/src/skeleton/index.tsx +172 -0
- package/src/skip-link/index.tsx +89 -0
- package/src/spinner/index.tsx +98 -0
- package/src/step-indicator/index.tsx +122 -0
- package/src/structured-list/index.tsx +395 -0
- package/src/tab/index.tsx +348 -0
- package/src/table/index.tsx +186 -0
- package/src/tag/index.tsx +127 -0
- package/src/text-input/index.tsx +156 -0
- package/src/text-list/index.tsx +177 -0
- package/src/textarea/index.tsx +113 -0
- package/src/toast/index.tsx +311 -0
- package/src/tooltip/index.tsx +496 -0
- package/styles/krds-tailwind.css +335 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/empty-state/index.tsx"],"sourcesContent":["import { UiLink } from '../lib/link';\nimport type { ComponentPropsWithRef, ReactNode } from 'react';\nimport { cx, type KrdsIcon, type KrdsTone } from '../lib/krds';\n\n/**\n * 빈 상태 상자 — KRDS 에 없다. shadcn `Empty` 형태에 KRDS 토큰을 입힌 보충 부품이다.\n *\n * 「보여 줄 것이 없다」를 알린다 — 등록된 것이 없음 · 조건에 맞는 것이 없음 · 권한 없음 · 불러오기 실패.\n * 「알아 둘 것이 있다」는 `Alert`, 표 안 한 행짜리 빈 칸은 `ui/table` 의 `TableEmpty` 다.\n * **막다른 골목을 만들지 마라** — `hints` 나 `actions` 중 하나는 준다.\n * 검색 결과 없음은 규격이 문구와 대안 4개까지 못 박았으므로 {@link SearchEmptyState} 를 쓴다.\n *\n * @example\n * <EmptyState reason=\"no-results\" actions={<Button onClick={reset}>조건 초기화</Button>} />\n * <EmptyState reason=\"forbidden\" layout=\"inline\" />\n * <SearchEmptyState query={q} count={0} helpHref=\"/help/search\" />\n *\n * 자세히: docs/krds/09-부품-노트.md#빈상태\n */\n\n/** 왜 비었는가. 색·아이콘·기본 문구가 여기서 갈린다. */\nexport type EmptyStateReason =\n /** 아직 등록된 것이 없다(조건 문제가 아니다) */\n | 'empty'\n /** 검색어·필터에 맞는 것이 없다 */\n | 'no-results'\n /** 볼 권한이 없다 */\n | 'forbidden'\n /** 불러오지 못했다 */\n | 'error';\n\n/** 색. `KrdsTone` 에서 이 상자가 쓰는 넷만 좁힌 것이다. */\nexport type EmptyStateTone = Extract<KrdsTone, 'gray' | 'information' | 'warning' | 'danger'>;\n\n/**\n * 배치. `panel` 은 목록이 있던 넓은 자리(가운데 정렬), `inline` 은 폼 옆·카드 안처럼 좁은 자리다.\n * 테두리는 둘 다 같은 사방 점선이고 정렬과 여백만 다르다.\n */\nexport type EmptyStateLayout = 'panel' | 'inline';\n\n/** 사유별 기본값. `forbidden` 에 아이콘이 없는 것은 일부러다 — 킷에 자물쇠·경고 아이콘이 없다. */\nconst REASON: Record<EmptyStateReason, { tone: EmptyStateTone; icon: KrdsIcon | null; title: string }> = {\n empty: { tone: 'gray', icon: 'file', title: '등록된 자료가 없습니다.' },\n 'no-results': { tone: 'gray', icon: 'sch', title: '조건에 맞는 결과가 없습니다.' },\n forbidden: { tone: 'warning', icon: null, title: '이 내용을 볼 권한이 없습니다.' },\n error: { tone: 'danger', icon: 'error-fill', title: '불러오지 못했습니다.' },\n};\n\n/** 색조별 유틸리티. 선 `border-*-line` · 면 `bg-*-surface` · mask 아이콘 색 `tint`. */\nconst TONE: Record<EmptyStateTone, { line: string; surface: string; tint: string }> = {\n gray: { line: 'border-line', surface: 'bg-surface-subtler', tint: 'bg-fg-disabled' },\n information: { line: 'border-information-line', surface: 'bg-information-surface', tint: 'bg-information' },\n warning: { line: 'border-warning-line', surface: 'bg-warning-surface', tint: 'bg-warning' },\n danger: { line: 'border-danger-line', surface: 'bg-danger-surface', tint: 'bg-danger' },\n};\n\nexport type EmptyStateProps = Omit<ComponentPropsWithRef<'div'>, 'title'> & {\n /** 기본 `empty`. 색·아이콘·기본 문구가 여기서 정해진다. */\n reason?: EmptyStateReason;\n /** 사유의 기본 색을 덮는다. 굳이 필요할 때만. */\n tone?: EmptyStateTone;\n /** 기본 `panel`. 좁은 자리에는 `inline`. */\n layout?: EmptyStateLayout;\n /** 킷 아이콘 이름(`ico-` 뺀 것). 사유 기본값을 덮는다. `null` 이면 그리지 않는다. */\n icon?: KrdsIcon | null;\n /** 제목 한 줄. 생략하면 사유의 기본 문구. */\n title?: ReactNode;\n /** 제목 위에 오는 줄. 검색 결과 수처럼 제목보다 먼저 와야 하는 것이 여기 온다. */\n lead?: ReactNode;\n /** 제목 아래 보충 설명. */\n description?: ReactNode;\n /** 대안 행동 목록. 결과 없음에는 반드시 채운다 — 막다른 골목을 만들지 않는다. */\n hints?: ReactNode[];\n /** 아래쪽 단추 자리(`ui/button` 의 Button·LinkButton 을 넣는다). */\n actions?: ReactNode;\n /**\n * `role=\"status\"` 를 붙인다. 클라이언트에서 목록이 바뀌는 화면에서만 켠다 —\n * 이때 상자는 내용이 바뀌기 전부터 DOM 에 있어야 낭독된다.\n */\n live?: boolean;\n children?: ReactNode;\n};\n\nexport function EmptyState({\n reason = 'empty',\n tone,\n layout = 'panel',\n icon,\n title,\n lead,\n description,\n hints,\n actions,\n live,\n className,\n children,\n ...rest\n}: EmptyStateProps) {\n const preset = REASON[reason];\n const t = TONE[tone ?? preset.tone];\n const panel = layout === 'panel';\n\n // `undefined` 는 「기본값을 쓴다」, `null` 은 「아이콘을 그리지 않는다」. 둘을 구분해야 한다.\n const iconName = icon === undefined ? preset.icon : icon;\n // `-fill` 아이콘은 background-image 라 색을 못 바꾼다 — 색 유틸리티를 붙이지 않는다(함정 1).\n const iconNode = iconName ? (\n <i\n className={cx(\n 'svg-icon',\n `ico-${iconName}`,\n panel ? 'size-12' : 'size-6 shrink-0',\n !iconName.endsWith('-fill') && t.tint,\n )}\n aria-hidden=\"true\"\n />\n ) : null;\n\n const hintList =\n hints && hints.length > 0 ? (\n // 킷 전역 `ol,ul{list-style:none}` 때문에 목록 의미가 떨어진다 — `ui/text-list` 와 같이 role 을 명시한다.\n <ul role=\"list\" className={cx('krds-info-list dash', panel && 'w-fit text-left')}>\n {hints.map((hint, i) => (\n <li key={i} role=\"listitem\">\n {hint}\n </li>\n ))}\n </ul>\n ) : null;\n\n // shadcn `EmptyHeader` 자리 — 아이콘·제목·설명이 한 덩이로 `gap-2`(8px) 씩 벌어진다.\n // panel 은 아이콘이 글 **위**에 서고(shadcn `EmptyMedia`), inline 은 좁은 자리라 글 **왼쪽**에 선다.\n const header = (\n <div className={cx('flex flex-col gap-2', panel && 'items-center text-center')}>\n {panel && iconNode}\n {lead && <p className=\"m-0 text-sm text-fg-subtle\">{lead}</p>}\n <p className={cx('m-0 font-bold text-fg', panel ? 'text-heading-xs' : 'text-sm')}>{title ?? preset.title}</p>\n {description && <p className=\"m-0 text-sm text-fg-subtle\">{description}</p>}\n </div>\n );\n\n const body = (\n <>\n {header}\n {hintList}\n {children}\n {/* shadcn `EmptyContent` 자리 — 단추 줄. */}\n {actions && <div className={cx('flex flex-wrap items-center gap-2', panel && 'justify-center')}>{actions}</div>}\n </>\n );\n\n return (\n <div\n role={live ? 'status' : undefined}\n className={cx(\n // shadcn `Empty` — 점선 사방 테두리 + 둥글기. `border-solid` 가 아니라 `border-dashed` 라야\n // 선이 그려진다(preflight 가 없어 border-style 기본값이 none 이다. `ui/separator` 함정 2).\n 'rounded-lg border border-dashed',\n panel\n ? // 가운데 정렬 + 세로 흐름 `gap-6`(24px). 위아래 40px 은 킷 `.no-results` 실측(`--krds-padding-10`).\n 'flex flex-col items-center gap-6 px-6 py-10 text-center'\n : // 좁은 자리 — 아이콘을 왼쪽에 세우고 글은 왼쪽 정렬. 여백 24/16px.\n 'flex items-start gap-2 px-6 py-4 text-left',\n t.line,\n t.surface,\n className,\n )}\n {...rest}\n >\n {panel ? (\n body\n ) : (\n <>\n {iconNode}\n <div className=\"flex min-w-0 flex-1 flex-col gap-4\">{body}</div>\n </>\n )}\n </div>\n );\n}\n\n/**\n * 검색 결과 없음 — KRDS 가 문구와 대안까지 못 박은 화면이다.\n *\n * 결과 수 0 · 규격 문구 · 대안 4개(철자 확인 · 다른 검색어 · 더 일반적인 검색어 · 검색 도움말)를\n * 기본값으로 담는다. `<SearchEmptyState query={q} />` 만으로 규격을 채운다.\n *\n * **`helpHref` 를 주지 않으면 대안 ④가 갈 곳 없는 문장으로 남는다** —\n * 도움말 문서가 아직 없다면 `hints` 를 직접 넘겨 그 화면에서 할 수 있는 행동으로 바꿔라.\n */\n\n/** 규격이 든 안내 문구 그대로. 바꾸지 마라. */\nexport const SEARCH_NO_RESULT_MESSAGE = '검색어와 일치하는 결과를 찾을 수 없습니다.';\n\n/** 규격 대안 4개 중 링크가 필요 없는 셋. ④는 `helpHref` 에 따라 아래에서 만든다. */\nexport const SEARCH_NO_RESULT_HINTS: readonly string[] = [\n '검색어의 철자가 정확한지 확인해 보세요.',\n '다른 검색어로 다시 검색해 보세요.',\n '더 일반적인 검색어를 사용해 보세요.',\n];\n\nexport type SearchEmptyStateProps = Omit<EmptyStateProps, 'reason' | 'lead'> & {\n /** 사용자가 넣은 검색어. 결과 수 줄에 굵게 되짚어 준다. */\n query?: string;\n /** 결과 수. 규격상 0을 반드시 표시한다. 화면 위쪽에서 이미 세고 있으면 `null` 로 숨긴다. */\n count?: number | null;\n /** 검색 도움말 문서 주소. 규격 대안 ④가 링크가 되려면 있어야 한다. */\n helpHref?: string;\n /** 도움말 링크 글자. 기본 「검색 도움말」 — 바꾸면 뒤따르는 조사가 어긋날 수 있다. */\n helpLabel?: string;\n};\n\nexport function SearchEmptyState({\n query,\n count = 0,\n helpHref,\n helpLabel = '검색 도움말',\n hints,\n title = SEARCH_NO_RESULT_MESSAGE,\n ...rest\n}: SearchEmptyStateProps) {\n // 킷에 문장 속 텍스트 링크 클래스가 없어(그쪽은 높이가 박힌 버튼이다) 유틸리티로 보충한다.\n const helpLink = helpHref ? (\n helpHref.startsWith('/') ? (\n <UiLink href={helpHref} className=\"text-link underline hover:text-link-hover\">\n {helpLabel}\n </UiLink>\n ) : (\n <a href={helpHref} className=\"text-link underline hover:text-link-hover\">\n {helpLabel}\n </a>\n )\n ) : null;\n\n const specHints: ReactNode[] = [\n ...SEARCH_NO_RESULT_HINTS,\n helpLink ? <>{helpLink}을 참고하세요.</> : `${helpLabel}을 참고하세요.`,\n ];\n\n return (\n <EmptyState\n reason=\"no-results\"\n title={title}\n // 규격 순서: 결과 수 → 안내 메시지 → 대안. 그래서 결과 수는 제목 위(`lead`)에 온다.\n lead={\n count === null ? undefined : (\n <>\n {query ? <>「<b className=\"text-fg\">{query}</b>」에 대한 검색 결과 </> : '검색 결과 '}\n <b className=\"text-fg\">{count.toLocaleString('ko-KR')}</b>건\n </>\n )\n }\n hints={hints ?? specHints}\n {...rest}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/env.d.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=env.d.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef, ReactNode } from 'react';
|
|
2
|
+
import { type KrdsFieldState } from '../lib/krds';
|
|
3
|
+
/**
|
|
4
|
+
* 폼 필드 골격 — KRDS `fieldset` · `form-group` · `form-tit` · `form-conts` · `form-hint`.
|
|
5
|
+
*
|
|
6
|
+
* 레이블 · 컨트롤 · 도움말 · 상태 메시지를 한 골격으로 묶고 접근성 속성을 이어 준다.
|
|
7
|
+
* 컨트롤은 `TextInput` · `Select` · `Textarea` 가 스스로 그린다 — 여기서 겹쳐 그리지 않는다.
|
|
8
|
+
* **`hint` 는 상태와 무관한 입력 규칙 안내**(회색)이고, 파란 「정보」 메시지는
|
|
9
|
+
* `state="information"` + `message` 다. 둘을 바꿔 쓰면 평범한 안내가 상태 메시지로 보인다.
|
|
10
|
+
* 레이블 달린 텍스트 한 줄이면 `ui/text-input` 의 `TextField` 가 이 조립을 대신한다.
|
|
11
|
+
* 서버 컴포넌트라 `id` 가 필수다.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* <Fieldset>
|
|
15
|
+
* <Field id="user_id" label="아이디" hint="4~30자" required>
|
|
16
|
+
* {(a) => <TextInput {...a} placeholder="아이디" />}
|
|
17
|
+
* </Field>
|
|
18
|
+
* <Field id="user_pw" label="비밀번호" state="error" message="8자 이상이어야 합니다."
|
|
19
|
+
* action={<Button iconOnly label="비밀번호 보기" icon="pw-visible" />}>
|
|
20
|
+
* {(a) => <TextInput {...a} type="password" />}
|
|
21
|
+
* </Field>
|
|
22
|
+
* </Fieldset>
|
|
23
|
+
*
|
|
24
|
+
* 자세히: docs/krds/09-부품-노트.md#폼필드
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* `<div class="fieldset">` — 폼그룹 여럿을 세로로 묶는다.
|
|
28
|
+
* 필드 사이 세로 간격이 여기서 나온다 — `mt-*` 를 손으로 붙이지 마라.
|
|
29
|
+
*/
|
|
30
|
+
export declare function Fieldset({ className, children, ...rest }: ComponentPropsWithRef<'div'>): import("react").JSX.Element;
|
|
31
|
+
/**
|
|
32
|
+
* `<div class="form-col-group">` — 폼그룹 여럿을 가로로 늘어놓는다(성/이름, 시작일/종료일).
|
|
33
|
+
* 좁은 화면(767px 이하)에서는 킷이 알아서 세로로 접는다.
|
|
34
|
+
*
|
|
35
|
+
* 컨트롤 하나에 딸린 버튼·기호를 묶는 것은 `ui/input-group` 의 `InputGroup` 이다.
|
|
36
|
+
*/
|
|
37
|
+
export declare function FieldRow({ className, children, ...rest }: ComponentPropsWithRef<'div'>): import("react").JSX.Element;
|
|
38
|
+
/**
|
|
39
|
+
* `<div class="form-group">` — 필드 하나.
|
|
40
|
+
* 도움말 선택자와 상태 토큰이 전부 여기 매여 있다 — 이 감싸개 없이 조각만 쓰면 모양이 나오지 않는다.
|
|
41
|
+
*/
|
|
42
|
+
export declare function FormGroup({ className, children, ...rest }: ComponentPropsWithRef<'div'>): import("react").JSX.Element;
|
|
43
|
+
export type FieldLabelProps = Omit<ComponentPropsWithRef<'label'>, 'htmlFor'> & {
|
|
44
|
+
/** 이을 컨트롤의 id. `<label for>` 라 필수다. */
|
|
45
|
+
htmlFor: string;
|
|
46
|
+
/** 필수 입력 표시(`*`). 킷에 클래스가 없어 보충한 부분이다. */
|
|
47
|
+
required?: boolean;
|
|
48
|
+
/** 눈에서만 감춘다(`sr-only`). 이름 자체는 남는다 — 검색창처럼 레이블을 못 두는 자리에. */
|
|
49
|
+
srOnly?: boolean;
|
|
50
|
+
/** `.form-tit` 감싸개에 붙는다. `className` 은 `<label>` 로 간다. */
|
|
51
|
+
boxClassName?: string;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* 레이블 — `<div class="form-tit"><label for>…</label></div>`.
|
|
55
|
+
* 라디오·체크박스 묶음의 **소제목**은 이것이 아니라 `.form-conts` 안의 `.form-label` 자리다.
|
|
56
|
+
*/
|
|
57
|
+
export declare function FieldLabel({ htmlFor, required, srOnly, boxClassName, className, children, ...rest }: FieldLabelProps): import("react").JSX.Element;
|
|
58
|
+
/** `data-*` 속성. React 타입에 자리가 없어 객체 리터럴로 넘기면 `TS2353` 이 나므로 열어 둔다. */
|
|
59
|
+
export type KrdsDataAttrs = {
|
|
60
|
+
[key: `data-${string}`]: string | number | boolean | undefined;
|
|
61
|
+
};
|
|
62
|
+
export type FieldControlProps = ComponentPropsWithRef<'div'> & KrdsDataAttrs & {
|
|
63
|
+
/** 필드 상태. 킷의 `is-*` 클래스가 이 감싸개에 붙는다. */
|
|
64
|
+
state?: KrdsFieldState;
|
|
65
|
+
/**
|
|
66
|
+
* 컨트롤 오른쪽에 겹쳐 놓을 아이콘 버튼(비밀번호 보기, 내용 지우기 등).
|
|
67
|
+
* 킷 클래스 `btn-ico-wrap` 은 부품이 붙인다. 둘 이상이면 `<div class="btn-group">` 으로 묶어 넘긴다.
|
|
68
|
+
*/
|
|
69
|
+
action?: ReactNode;
|
|
70
|
+
/**
|
|
71
|
+
* 「내용 지우기」 버튼을 킷 CSS 로 여닫는다. **`placeholder` 가 없으면 영영 보이지 않는다.**
|
|
72
|
+
* 버튼 자체는 `action` 으로 넘겨 입력칸의 형제가 되게 한다.
|
|
73
|
+
*/
|
|
74
|
+
deletable?: boolean;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* `<div class="form-conts">` — 컨트롤 감싸개. **상태 클래스가 붙는 자리다.**
|
|
78
|
+
*
|
|
79
|
+
* 킷이 여기 얹어 쓰는 클래스가 더 있다 — 날짜 입력 `calendar-conts`,
|
|
80
|
+
* 검색 키워드 입력 `keyword-sch`. 필요하면 `className` 으로 더한다.
|
|
81
|
+
*/
|
|
82
|
+
export declare function FieldControl({ state, action, deletable, className, children, ...rest }: FieldControlProps): import("react").JSX.Element;
|
|
83
|
+
/** 메시지 클래스 4종. `state` 를 생략하면 중립(`form-hint`, 회색)이다. */
|
|
84
|
+
export declare const FIELD_HINT_CLASS: Record<KrdsFieldState | 'neutral', string>;
|
|
85
|
+
/** 상태 → 메시지 클래스. `undefined` 는 중립이다. */
|
|
86
|
+
export declare function fieldHintClass(state?: KrdsFieldState): string;
|
|
87
|
+
export type FieldHintProps = ComponentPropsWithRef<'p'> & {
|
|
88
|
+
/** 생략하면 중립 도움말(`form-hint`). 상태를 주면 그 상태의 메시지가 된다. */
|
|
89
|
+
state?: KrdsFieldState;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* `<p class="form-hint…">` — 도움말·상태 메시지. `.form-group` 안에 두어야 모양을 받는다.
|
|
93
|
+
* 아이콘은 킷이 `::before` 로 붙인다 — 따로 넣지 마라.
|
|
94
|
+
*/
|
|
95
|
+
export declare function FieldHint({ state, className, children, ...rest }: FieldHintProps): import("react").JSX.Element;
|
|
96
|
+
/** 컨트롤에 그대로 펼쳐 넣는 속성. `<input {...control} />` 처럼 쓴다. */
|
|
97
|
+
export type FieldControlAttrs = {
|
|
98
|
+
id: string;
|
|
99
|
+
/** 도움말·상태 메시지의 id 를 잇는다(둘 다 있으면 공백으로 이어 붙인다). */
|
|
100
|
+
'aria-describedby'?: string;
|
|
101
|
+
'aria-invalid'?: true;
|
|
102
|
+
'aria-required'?: true;
|
|
103
|
+
required?: boolean;
|
|
104
|
+
};
|
|
105
|
+
export type FieldProps = Omit<ComponentPropsWithRef<'div'>, 'children'> & {
|
|
106
|
+
/** 컨트롤의 id. `<label for>` 와 `aria-describedby` 가 여기서 갈라져 나온다. 서버 컴포넌트라 필수다. */
|
|
107
|
+
id: string;
|
|
108
|
+
/** 레이블 글자. */
|
|
109
|
+
label: ReactNode;
|
|
110
|
+
/** 레이블을 눈에서만 감춘다. 이름은 남는다. */
|
|
111
|
+
hideLabel?: boolean;
|
|
112
|
+
/** 중립 도움말(회색). 상태와 무관한 입력 규칙 안내는 여기에 쓴다. */
|
|
113
|
+
hint?: ReactNode;
|
|
114
|
+
/** 필드 상태. `is-*` 는 `.form-conts` 에, 색은 `message` 에 걸린다. */
|
|
115
|
+
state?: KrdsFieldState;
|
|
116
|
+
/** 상태 메시지. `state` 와 짝으로 쓴다. */
|
|
117
|
+
message?: ReactNode;
|
|
118
|
+
/** 오류 문구 지름길. `state="error"` + `message` 와 같다(직접 준 쪽이 이긴다). */
|
|
119
|
+
error?: ReactNode;
|
|
120
|
+
/** 필수 입력. 레이블에 `*`, 컨트롤에 `required`·`aria-required` 를 함께 붙인다. */
|
|
121
|
+
required?: boolean;
|
|
122
|
+
/** 이미 있는 설명 요소를 더 잇고 싶을 때(공백으로 이어진 id 목록). */
|
|
123
|
+
describedBy?: string;
|
|
124
|
+
/** 컨트롤 오른쪽에 겹쳐 놓을 아이콘 버튼. {@link FieldControlProps.action} 참고. */
|
|
125
|
+
action?: ReactNode;
|
|
126
|
+
/** 「내용 지우기」 버튼을 연다. {@link FieldControlProps.deletable} 참고. */
|
|
127
|
+
deletable?: boolean;
|
|
128
|
+
/** `.form-conts` 에 그대로 흘릴 속성(`className="calendar-conts"` · `data-*` 등). */
|
|
129
|
+
contentProps?: Omit<ComponentPropsWithRef<'div'>, 'children'> & KrdsDataAttrs;
|
|
130
|
+
/** 이 필드 하나만 놓일 때 `<div class="fieldset">` 로 감싼다. 여럿이면 바깥에서 한 번만 감싼다. */
|
|
131
|
+
standalone?: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* 컨트롤. 함수로 주면 접근성 속성을 받는다 — **반드시 펼쳐 넣어야** 레이블·도움말이 이어진다.
|
|
134
|
+
* 킷 클래스는 여기서 넘기지 않는다(컨트롤 부품이 스스로 붙인다).
|
|
135
|
+
*/
|
|
136
|
+
children: ReactNode | ((control: FieldControlAttrs) => ReactNode);
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* 레이블 · 컨트롤 · 도움말 · 상태 메시지를 KRDS 골격 하나로 묶는다.
|
|
140
|
+
*
|
|
141
|
+
* 레이블 달린 텍스트 한 줄이면 `ui/text-input` 의 `TextField` 가 이 조립을 대신한다.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* <Field id="user_id" label="아이디" hint="4~30자" required>
|
|
145
|
+
* {(a) => <TextInput {...a} placeholder="아이디" />}
|
|
146
|
+
* </Field>
|
|
147
|
+
*/
|
|
148
|
+
export declare function Field({ id, label, hideLabel, hint, state, message, error, required, describedBy, action, deletable, contentProps, standalone, className, children, ...rest }: FieldProps): import("react").JSX.Element;
|
|
149
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/field/index.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx, stateClass } from "../lib/krds.js";
|
|
3
|
+
function Fieldset({ className, children, ...rest }) {
|
|
4
|
+
return /* @__PURE__ */ jsx("div", { className: cx("fieldset", className), ...rest, children });
|
|
5
|
+
}
|
|
6
|
+
function FieldRow({ className, children, ...rest }) {
|
|
7
|
+
return /* @__PURE__ */ jsx("div", { className: cx("form-col-group", className), ...rest, children });
|
|
8
|
+
}
|
|
9
|
+
function FormGroup({ className, children, ...rest }) {
|
|
10
|
+
return /* @__PURE__ */ jsx("div", { className: cx("form-group", className), ...rest, children });
|
|
11
|
+
}
|
|
12
|
+
function FieldLabel({
|
|
13
|
+
htmlFor,
|
|
14
|
+
required,
|
|
15
|
+
srOnly,
|
|
16
|
+
boxClassName,
|
|
17
|
+
className,
|
|
18
|
+
children,
|
|
19
|
+
...rest
|
|
20
|
+
}) {
|
|
21
|
+
const label = /* @__PURE__ */ jsxs("label", { htmlFor, className: cx(srOnly && "sr-only", className), ...rest, children: [
|
|
22
|
+
children,
|
|
23
|
+
required && // 킷에 필수 표시 클래스가 없어 보충한 부분. 소리로는 컨트롤의 required/aria-required 가 알린다.
|
|
24
|
+
/* @__PURE__ */ jsx("em", { className: "not-italic text-danger ml-1", "aria-hidden": "true", children: "*" })
|
|
25
|
+
] });
|
|
26
|
+
return srOnly ? label : /* @__PURE__ */ jsx("div", { className: cx("form-tit", boxClassName), children: label });
|
|
27
|
+
}
|
|
28
|
+
function FieldControl({
|
|
29
|
+
state,
|
|
30
|
+
action,
|
|
31
|
+
deletable,
|
|
32
|
+
className,
|
|
33
|
+
children,
|
|
34
|
+
...rest
|
|
35
|
+
}) {
|
|
36
|
+
return /* @__PURE__ */ jsxs(
|
|
37
|
+
"div",
|
|
38
|
+
{
|
|
39
|
+
className: cx("form-conts", stateClass(state), action ? "btn-ico-wrap" : void 0, className),
|
|
40
|
+
"data-delete": deletable ? "true" : void 0,
|
|
41
|
+
...rest,
|
|
42
|
+
children: [
|
|
43
|
+
children,
|
|
44
|
+
action
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const FIELD_HINT_CLASS = {
|
|
50
|
+
neutral: "form-hint",
|
|
51
|
+
error: "form-hint-invalid",
|
|
52
|
+
success: "form-hint-success",
|
|
53
|
+
information: "form-hint-information"
|
|
54
|
+
};
|
|
55
|
+
function fieldHintClass(state) {
|
|
56
|
+
return FIELD_HINT_CLASS[state ?? "neutral"];
|
|
57
|
+
}
|
|
58
|
+
function FieldHint({ state, className, children, ...rest }) {
|
|
59
|
+
return /* @__PURE__ */ jsx("p", { className: cx(fieldHintClass(state), className), ...rest, children });
|
|
60
|
+
}
|
|
61
|
+
function Field({
|
|
62
|
+
id,
|
|
63
|
+
label,
|
|
64
|
+
hideLabel,
|
|
65
|
+
hint,
|
|
66
|
+
state,
|
|
67
|
+
message,
|
|
68
|
+
error,
|
|
69
|
+
required,
|
|
70
|
+
describedBy,
|
|
71
|
+
action,
|
|
72
|
+
deletable,
|
|
73
|
+
contentProps,
|
|
74
|
+
standalone,
|
|
75
|
+
className,
|
|
76
|
+
children,
|
|
77
|
+
...rest
|
|
78
|
+
}) {
|
|
79
|
+
const finalState = state ?? (error ? "error" : void 0);
|
|
80
|
+
const finalMessage = message || error;
|
|
81
|
+
const hintId = hint ? `${id}-hint` : void 0;
|
|
82
|
+
const messageId = finalMessage ? `${id}-message` : void 0;
|
|
83
|
+
const control = { id };
|
|
84
|
+
const described = cx(describedBy, hintId, messageId);
|
|
85
|
+
if (described) control["aria-describedby"] = described;
|
|
86
|
+
if (finalState === "error") control["aria-invalid"] = true;
|
|
87
|
+
if (required) {
|
|
88
|
+
control.required = true;
|
|
89
|
+
control["aria-required"] = true;
|
|
90
|
+
}
|
|
91
|
+
const { className: contentClassName, ...contentRest } = contentProps ?? {};
|
|
92
|
+
const group = /* @__PURE__ */ jsxs(FormGroup, { className, ...rest, children: [
|
|
93
|
+
/* @__PURE__ */ jsx(FieldLabel, { htmlFor: id, required, srOnly: hideLabel, children: label }),
|
|
94
|
+
/* @__PURE__ */ jsx(
|
|
95
|
+
FieldControl,
|
|
96
|
+
{
|
|
97
|
+
state: finalState,
|
|
98
|
+
action,
|
|
99
|
+
deletable,
|
|
100
|
+
className: contentClassName,
|
|
101
|
+
...contentRest,
|
|
102
|
+
children: typeof children === "function" ? children(control) : children
|
|
103
|
+
}
|
|
104
|
+
),
|
|
105
|
+
hint && /* @__PURE__ */ jsx(FieldHint, { id: hintId, children: hint }),
|
|
106
|
+
finalMessage && /* @__PURE__ */ jsx(FieldHint, { id: messageId, state: finalState, children: finalMessage })
|
|
107
|
+
] });
|
|
108
|
+
return standalone ? /* @__PURE__ */ jsx(Fieldset, { children: group }) : group;
|
|
109
|
+
}
|
|
110
|
+
export {
|
|
111
|
+
FIELD_HINT_CLASS,
|
|
112
|
+
Field,
|
|
113
|
+
FieldControl,
|
|
114
|
+
FieldHint,
|
|
115
|
+
FieldLabel,
|
|
116
|
+
FieldRow,
|
|
117
|
+
Fieldset,
|
|
118
|
+
FormGroup,
|
|
119
|
+
fieldHintClass
|
|
120
|
+
};
|
|
121
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/field/index.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from 'react';\nimport { cx, stateClass, type KrdsFieldState } from '../lib/krds';\n\n/**\n * 폼 필드 골격 — KRDS `fieldset` · `form-group` · `form-tit` · `form-conts` · `form-hint`.\n *\n * 레이블 · 컨트롤 · 도움말 · 상태 메시지를 한 골격으로 묶고 접근성 속성을 이어 준다.\n * 컨트롤은 `TextInput` · `Select` · `Textarea` 가 스스로 그린다 — 여기서 겹쳐 그리지 않는다.\n * **`hint` 는 상태와 무관한 입력 규칙 안내**(회색)이고, 파란 「정보」 메시지는\n * `state=\"information\"` + `message` 다. 둘을 바꿔 쓰면 평범한 안내가 상태 메시지로 보인다.\n * 레이블 달린 텍스트 한 줄이면 `ui/text-input` 의 `TextField` 가 이 조립을 대신한다.\n * 서버 컴포넌트라 `id` 가 필수다.\n *\n * @example\n * <Fieldset>\n * <Field id=\"user_id\" label=\"아이디\" hint=\"4~30자\" required>\n * {(a) => <TextInput {...a} placeholder=\"아이디\" />}\n * </Field>\n * <Field id=\"user_pw\" label=\"비밀번호\" state=\"error\" message=\"8자 이상이어야 합니다.\"\n * action={<Button iconOnly label=\"비밀번호 보기\" icon=\"pw-visible\" />}>\n * {(a) => <TextInput {...a} type=\"password\" />}\n * </Field>\n * </Fieldset>\n *\n * 자세히: docs/krds/09-부품-노트.md#폼필드\n */\n\n/* ────────────────────────────── 골격 조각 ────────────────────────────── */\n\n/**\n * `<div class=\"fieldset\">` — 폼그룹 여럿을 세로로 묶는다.\n * 필드 사이 세로 간격이 여기서 나온다 — `mt-*` 를 손으로 붙이지 마라.\n */\nexport function Fieldset({ className, children, ...rest }: ComponentPropsWithRef<'div'>) {\n return (\n <div className={cx('fieldset', className)} {...rest}>\n {children}\n </div>\n );\n}\n\n/**\n * `<div class=\"form-col-group\">` — 폼그룹 여럿을 가로로 늘어놓는다(성/이름, 시작일/종료일).\n * 좁은 화면(767px 이하)에서는 킷이 알아서 세로로 접는다.\n *\n * 컨트롤 하나에 딸린 버튼·기호를 묶는 것은 `ui/input-group` 의 `InputGroup` 이다.\n */\nexport function FieldRow({ className, children, ...rest }: ComponentPropsWithRef<'div'>) {\n return (\n <div className={cx('form-col-group', className)} {...rest}>\n {children}\n </div>\n );\n}\n\n/**\n * `<div class=\"form-group\">` — 필드 하나.\n * 도움말 선택자와 상태 토큰이 전부 여기 매여 있다 — 이 감싸개 없이 조각만 쓰면 모양이 나오지 않는다.\n */\nexport function FormGroup({ className, children, ...rest }: ComponentPropsWithRef<'div'>) {\n return (\n <div className={cx('form-group', className)} {...rest}>\n {children}\n </div>\n );\n}\n\nexport type FieldLabelProps = Omit<ComponentPropsWithRef<'label'>, 'htmlFor'> & {\n /** 이을 컨트롤의 id. `<label for>` 라 필수다. */\n htmlFor: string;\n /** 필수 입력 표시(`*`). 킷에 클래스가 없어 보충한 부분이다. */\n required?: boolean;\n /** 눈에서만 감춘다(`sr-only`). 이름 자체는 남는다 — 검색창처럼 레이블을 못 두는 자리에. */\n srOnly?: boolean;\n /** `.form-tit` 감싸개에 붙는다. `className` 은 `<label>` 로 간다. */\n boxClassName?: string;\n};\n\n/**\n * 레이블 — `<div class=\"form-tit\"><label for>…</label></div>`.\n * 라디오·체크박스 묶음의 **소제목**은 이것이 아니라 `.form-conts` 안의 `.form-label` 자리다.\n */\nexport function FieldLabel({\n htmlFor,\n required,\n srOnly,\n boxClassName,\n className,\n children,\n ...rest\n}: FieldLabelProps) {\n const label = (\n <label htmlFor={htmlFor} className={cx(srOnly && 'sr-only', className)} {...rest}>\n {children}\n {required && (\n // 킷에 필수 표시 클래스가 없어 보충한 부분. 소리로는 컨트롤의 required/aria-required 가 알린다.\n <em className=\"not-italic text-danger ml-1\" aria-hidden=\"true\">\n *\n </em>\n )}\n </label>\n );\n\n // 감춘 레이블은 `.form-tit` 로 감싸지 않는다 — 감싸개만 남아 gap 이 한 칸 더 벌어진다.\n return srOnly ? label : <div className={cx('form-tit', boxClassName)}>{label}</div>;\n}\n\n/** `data-*` 속성. React 타입에 자리가 없어 객체 리터럴로 넘기면 `TS2353` 이 나므로 열어 둔다. */\nexport type KrdsDataAttrs = {\n [key: `data-${string}`]: string | number | boolean | undefined;\n};\n\nexport type FieldControlProps = ComponentPropsWithRef<'div'> &\n KrdsDataAttrs & {\n /** 필드 상태. 킷의 `is-*` 클래스가 이 감싸개에 붙는다. */\n state?: KrdsFieldState;\n /**\n * 컨트롤 오른쪽에 겹쳐 놓을 아이콘 버튼(비밀번호 보기, 내용 지우기 등).\n * 킷 클래스 `btn-ico-wrap` 은 부품이 붙인다. 둘 이상이면 `<div class=\"btn-group\">` 으로 묶어 넘긴다.\n */\n action?: ReactNode;\n /**\n * 「내용 지우기」 버튼을 킷 CSS 로 여닫는다. **`placeholder` 가 없으면 영영 보이지 않는다.**\n * 버튼 자체는 `action` 으로 넘겨 입력칸의 형제가 되게 한다.\n */\n deletable?: boolean;\n };\n\n/**\n * `<div class=\"form-conts\">` — 컨트롤 감싸개. **상태 클래스가 붙는 자리다.**\n *\n * 킷이 여기 얹어 쓰는 클래스가 더 있다 — 날짜 입력 `calendar-conts`,\n * 검색 키워드 입력 `keyword-sch`. 필요하면 `className` 으로 더한다.\n */\nexport function FieldControl({\n state,\n action,\n deletable,\n className,\n children,\n ...rest\n}: FieldControlProps) {\n return (\n <div\n className={cx('form-conts', stateClass(state), action ? 'btn-ico-wrap' : undefined, className)}\n // 킷 선택자가 `.form-conts[data-delete=true]` 라 문자열 \"true\" 여야 한다.\n data-delete={deletable ? 'true' : undefined}\n {...rest}\n >\n {children}\n {action}\n </div>\n );\n}\n\n/** 메시지 클래스 4종. `state` 를 생략하면 중립(`form-hint`, 회색)이다. */\nexport const FIELD_HINT_CLASS: Record<KrdsFieldState | 'neutral', string> = {\n neutral: 'form-hint',\n error: 'form-hint-invalid',\n success: 'form-hint-success',\n information: 'form-hint-information',\n};\n\n/** 상태 → 메시지 클래스. `undefined` 는 중립이다. */\nexport function fieldHintClass(state?: KrdsFieldState): string {\n return FIELD_HINT_CLASS[state ?? 'neutral'];\n}\n\nexport type FieldHintProps = ComponentPropsWithRef<'p'> & {\n /** 생략하면 중립 도움말(`form-hint`). 상태를 주면 그 상태의 메시지가 된다. */\n state?: KrdsFieldState;\n};\n\n/**\n * `<p class=\"form-hint…\">` — 도움말·상태 메시지. `.form-group` 안에 두어야 모양을 받는다.\n * 아이콘은 킷이 `::before` 로 붙인다 — 따로 넣지 마라.\n */\nexport function FieldHint({ state, className, children, ...rest }: FieldHintProps) {\n return (\n <p className={cx(fieldHintClass(state), className)} {...rest}>\n {children}\n </p>\n );\n}\n\n/* ────────────────────────────── 완성형 ────────────────────────────── */\n\n/** 컨트롤에 그대로 펼쳐 넣는 속성. `<input {...control} />` 처럼 쓴다. */\nexport type FieldControlAttrs = {\n id: string;\n /** 도움말·상태 메시지의 id 를 잇는다(둘 다 있으면 공백으로 이어 붙인다). */\n 'aria-describedby'?: string;\n 'aria-invalid'?: true;\n 'aria-required'?: true;\n required?: boolean;\n};\n\nexport type FieldProps = Omit<ComponentPropsWithRef<'div'>, 'children'> & {\n /** 컨트롤의 id. `<label for>` 와 `aria-describedby` 가 여기서 갈라져 나온다. 서버 컴포넌트라 필수다. */\n id: string;\n /** 레이블 글자. */\n label: ReactNode;\n /** 레이블을 눈에서만 감춘다. 이름은 남는다. */\n hideLabel?: boolean;\n /** 중립 도움말(회색). 상태와 무관한 입력 규칙 안내는 여기에 쓴다. */\n hint?: ReactNode;\n /** 필드 상태. `is-*` 는 `.form-conts` 에, 색은 `message` 에 걸린다. */\n state?: KrdsFieldState;\n /** 상태 메시지. `state` 와 짝으로 쓴다. */\n message?: ReactNode;\n /** 오류 문구 지름길. `state=\"error\"` + `message` 와 같다(직접 준 쪽이 이긴다). */\n error?: ReactNode;\n /** 필수 입력. 레이블에 `*`, 컨트롤에 `required`·`aria-required` 를 함께 붙인다. */\n required?: boolean;\n /** 이미 있는 설명 요소를 더 잇고 싶을 때(공백으로 이어진 id 목록). */\n describedBy?: string;\n /** 컨트롤 오른쪽에 겹쳐 놓을 아이콘 버튼. {@link FieldControlProps.action} 참고. */\n action?: ReactNode;\n /** 「내용 지우기」 버튼을 연다. {@link FieldControlProps.deletable} 참고. */\n deletable?: boolean;\n /** `.form-conts` 에 그대로 흘릴 속성(`className=\"calendar-conts\"` · `data-*` 등). */\n contentProps?: Omit<ComponentPropsWithRef<'div'>, 'children'> & KrdsDataAttrs;\n /** 이 필드 하나만 놓일 때 `<div class=\"fieldset\">` 로 감싼다. 여럿이면 바깥에서 한 번만 감싼다. */\n standalone?: boolean;\n /**\n * 컨트롤. 함수로 주면 접근성 속성을 받는다 — **반드시 펼쳐 넣어야** 레이블·도움말이 이어진다.\n * 킷 클래스는 여기서 넘기지 않는다(컨트롤 부품이 스스로 붙인다).\n */\n children: ReactNode | ((control: FieldControlAttrs) => ReactNode);\n};\n\n/**\n * 레이블 · 컨트롤 · 도움말 · 상태 메시지를 KRDS 골격 하나로 묶는다.\n *\n * 레이블 달린 텍스트 한 줄이면 `ui/text-input` 의 `TextField` 가 이 조립을 대신한다.\n *\n * @example\n * <Field id=\"user_id\" label=\"아이디\" hint=\"4~30자\" required>\n * {(a) => <TextInput {...a} placeholder=\"아이디\" />}\n * </Field>\n */\nexport function Field({\n id,\n label,\n hideLabel,\n hint,\n state,\n message,\n error,\n required,\n describedBy,\n action,\n deletable,\n contentProps,\n standalone,\n className,\n children,\n ...rest\n}: FieldProps) {\n // `error` 지름길. 직접 준 state·message 가 이긴다.\n // 빈 값(undefined·null·'' ·false)은 「메시지 없음」으로 본다 — 빈 <p> 가 남으면 gap 만 벌어진다.\n const finalState: KrdsFieldState | undefined = state ?? (error ? 'error' : undefined);\n const finalMessage = message || error;\n\n const hintId = hint ? `${id}-hint` : undefined;\n const messageId = finalMessage ? `${id}-message` : undefined;\n\n const control: FieldControlAttrs = { id };\n // KRDS 는 규정하지 않지만 WAI-ARIA 상 옳다 — 초과 준수로 붙인다.\n const described = cx(describedBy, hintId, messageId);\n if (described) control['aria-describedby'] = described;\n if (finalState === 'error') control['aria-invalid'] = true;\n if (required) {\n control.required = true;\n control['aria-required'] = true;\n }\n\n const { className: contentClassName, ...contentRest } = contentProps ?? {};\n\n const group = (\n <FormGroup className={className} {...rest}>\n <FieldLabel htmlFor={id} required={required} srOnly={hideLabel}>\n {label}\n </FieldLabel>\n\n <FieldControl\n state={finalState}\n action={action}\n deletable={deletable}\n className={contentClassName}\n {...contentRest}\n >\n {typeof children === 'function' ? children(control) : children}\n </FieldControl>\n\n {/* 중립 도움말. 상태 메시지와 **다른 줄**이다 — 규칙 안내는 오류가 떠도 그대로 남는다. */}\n {hint && <FieldHint id={hintId}>{hint}</FieldHint>}\n {finalMessage && (\n <FieldHint id={messageId} state={finalState}>\n {finalMessage}\n </FieldHint>\n )}\n </FormGroup>\n );\n\n return standalone ? <Fieldset>{group}</Fieldset> : group;\n}\n"],"mappings":"AAmCI,cAyDA,YAzDA;AAlCJ,SAAS,IAAI,kBAAuC;AAgC7C,SAAS,SAAS,EAAE,WAAW,UAAU,GAAG,KAAK,GAAiC;AACvF,SACE,oBAAC,SAAI,WAAW,GAAG,YAAY,SAAS,GAAI,GAAG,MAC5C,UACH;AAEJ;AAQO,SAAS,SAAS,EAAE,WAAW,UAAU,GAAG,KAAK,GAAiC;AACvF,SACE,oBAAC,SAAI,WAAW,GAAG,kBAAkB,SAAS,GAAI,GAAG,MAClD,UACH;AAEJ;AAMO,SAAS,UAAU,EAAE,WAAW,UAAU,GAAG,KAAK,GAAiC;AACxF,SACE,oBAAC,SAAI,WAAW,GAAG,cAAc,SAAS,GAAI,GAAG,MAC9C,UACH;AAEJ;AAiBO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,QACJ,qBAAC,WAAM,SAAkB,WAAW,GAAG,UAAU,WAAW,SAAS,GAAI,GAAG,MACzE;AAAA;AAAA,IACA;AAAA,IAEC,oBAAC,QAAG,WAAU,+BAA8B,eAAY,QAAO,eAE/D;AAAA,KAEJ;AAIF,SAAO,SAAS,QAAQ,oBAAC,SAAI,WAAW,GAAG,YAAY,YAAY,GAAI,iBAAM;AAC/E;AA6BO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAsB;AACpB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,cAAc,WAAW,KAAK,GAAG,SAAS,iBAAiB,QAAW,SAAS;AAAA,MAE7F,eAAa,YAAY,SAAS;AAAA,MACjC,GAAG;AAAA,MAEH;AAAA;AAAA,QACA;AAAA;AAAA;AAAA,EACH;AAEJ;AAGO,MAAM,mBAA+D;AAAA,EAC1E,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS;AAAA,EACT,aAAa;AACf;AAGO,SAAS,eAAe,OAAgC;AAC7D,SAAO,iBAAiB,SAAS,SAAS;AAC5C;AAWO,SAAS,UAAU,EAAE,OAAO,WAAW,UAAU,GAAG,KAAK,GAAmB;AACjF,SACE,oBAAC,OAAE,WAAW,GAAG,eAAe,KAAK,GAAG,SAAS,GAAI,GAAG,MACrD,UACH;AAEJ;AA0DO,SAAS,MAAM;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAe;AAGb,QAAM,aAAyC,UAAU,QAAQ,UAAU;AAC3E,QAAM,eAAe,WAAW;AAEhC,QAAM,SAAS,OAAO,GAAG,EAAE,UAAU;AACrC,QAAM,YAAY,eAAe,GAAG,EAAE,aAAa;AAEnD,QAAM,UAA6B,EAAE,GAAG;AAExC,QAAM,YAAY,GAAG,aAAa,QAAQ,SAAS;AACnD,MAAI,UAAW,SAAQ,kBAAkB,IAAI;AAC7C,MAAI,eAAe,QAAS,SAAQ,cAAc,IAAI;AACtD,MAAI,UAAU;AACZ,YAAQ,WAAW;AACnB,YAAQ,eAAe,IAAI;AAAA,EAC7B;AAEA,QAAM,EAAE,WAAW,kBAAkB,GAAG,YAAY,IAAI,gBAAgB,CAAC;AAEzE,QAAM,QACJ,qBAAC,aAAU,WAAuB,GAAG,MACnC;AAAA,wBAAC,cAAW,SAAS,IAAI,UAAoB,QAAQ,WAClD,iBACH;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,QACP;AAAA,QACA;AAAA,QACA,WAAW;AAAA,QACV,GAAG;AAAA,QAEH,iBAAO,aAAa,aAAa,SAAS,OAAO,IAAI;AAAA;AAAA,IACxD;AAAA,IAGC,QAAQ,oBAAC,aAAU,IAAI,QAAS,gBAAK;AAAA,IACrC,gBACC,oBAAC,aAAU,IAAI,WAAW,OAAO,YAC9B,wBACH;AAAA,KAEJ;AAGF,SAAO,aAAa,oBAAC,YAAU,iBAAM,IAAc;AACrD;","names":[]}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { type ChangeEventHandler, type ComponentPropsWithRef, type ReactNode, type Ref } from 'react';
|
|
2
|
+
import { type KrdsSize } from '../lib/krds';
|
|
3
|
+
/**
|
|
4
|
+
* 파일 크기 표기 — `17KB` · `1.2MB` 꼴(공백 없음). 1KB 미만도 `1KB` 로 올린다.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* formatFileSize(17408) // '17KB'
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatFileSize(bytes: number): string;
|
|
10
|
+
/**
|
|
11
|
+
* KRDS 파일 표기 — `이름 [유형, 크기]`. `type` 을 생략하면 이름 끝의 확장자에서 뽑아 이름에서 뗀다.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* formatFileLabel({ name: '위임장.hwp', size: 17408 }) // '위임장 [hwp, 17KB]'
|
|
15
|
+
*/
|
|
16
|
+
export declare function formatFileLabel(file: {
|
|
17
|
+
name: string;
|
|
18
|
+
type?: string;
|
|
19
|
+
size?: number;
|
|
20
|
+
}): string;
|
|
21
|
+
export type FileUploadProps = Omit<ComponentPropsWithRef<'div'>, 'title'> & {
|
|
22
|
+
/** 테두리형. 다른 폼과 구분해 한 덩어리로 보일 때. */
|
|
23
|
+
bordered?: boolean;
|
|
24
|
+
/** 제목. */
|
|
25
|
+
title?: ReactNode;
|
|
26
|
+
/** 제목 태그. 기본 `h3` — 화면의 제목 위계에 맞춘다. */
|
|
27
|
+
titleAs?: 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
28
|
+
/** 제목 아래 설명. **파일 유형·크기·개수 제한을 여기에 적는다**(가이드 요구). */
|
|
29
|
+
description?: ReactNode;
|
|
30
|
+
/** 설명 영역의 `id`. {@link FileDropzone} 의 `describedBy` 와 짝을 맞춘다. */
|
|
31
|
+
descriptionId?: string;
|
|
32
|
+
};
|
|
33
|
+
export declare function FileUpload({ bordered, title, titleAs: Tit, description, descriptionId, className, children, ...rest }: FileUploadProps): import("react").JSX.Element;
|
|
34
|
+
export type FileDropzoneProps = Omit<ComponentPropsWithRef<'div'>, 'onChange' | 'onDrop' | 'onDragEnter' | 'onDragLeave' | 'onDragOver'> & {
|
|
35
|
+
/** `<input type="file">` 의 id. 생략하면 `useId` 로 만든다. */
|
|
36
|
+
id?: string;
|
|
37
|
+
/** 폼 전송에 쓸 이름. */
|
|
38
|
+
name?: string;
|
|
39
|
+
/** 파일 선택창의 필터. **드롭으로 들어온 파일은 이걸로 걸러지지 않는다.** */
|
|
40
|
+
accept?: string;
|
|
41
|
+
/** 여러 개를 고를 수 있다. */
|
|
42
|
+
multiple?: boolean;
|
|
43
|
+
/** 비활성. 드롭도 받지 않는다. */
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
/** 필수 입력. */
|
|
46
|
+
required?: boolean;
|
|
47
|
+
/** 파일칸을 직접 다뤄야 할 때(값 비우기 등). */
|
|
48
|
+
inputRef?: Ref<HTMLInputElement>;
|
|
49
|
+
/** 파일칸에 걸 `aria-describedby`. {@link FileUpload} 의 `descriptionId` 와 짝이다. */
|
|
50
|
+
describedBy?: string;
|
|
51
|
+
/** 드롭 영역 안내 문구. 기본값은 킷 샘플 문구 그대로. */
|
|
52
|
+
text?: ReactNode;
|
|
53
|
+
/** 파일 선택 버튼 글자. */
|
|
54
|
+
buttonLabel?: ReactNode;
|
|
55
|
+
/** 기본 medium(48px). 같은 줄의 폼 요소와 같은 값을 준다. */
|
|
56
|
+
buttonSize?: KrdsSize;
|
|
57
|
+
/** 파일 선택 버튼의 위계. `primary` 는 제출 버튼과 강조가 충돌해 두지 않았다. */
|
|
58
|
+
buttonVariant?: 'secondary' | 'tertiary' | 'text';
|
|
59
|
+
/** 선택·드롭 공통 진입점. `multiple` 이 아니면 첫 파일 하나만 넘긴다. */
|
|
60
|
+
onFiles?: (files: File[]) => void;
|
|
61
|
+
/** 원래의 `change` 도 필요하면 받는다. `onFiles` 보다 먼저 불린다. */
|
|
62
|
+
onChange?: ChangeEventHandler<HTMLInputElement>;
|
|
63
|
+
/** 넘긴 뒤 파일칸을 비워 같은 파일 재선택을 살린다. **폼 제출로 파일을 보낸다면 켜지 마라.** */
|
|
64
|
+
clearAfterSelect?: boolean;
|
|
65
|
+
};
|
|
66
|
+
export declare function FileDropzone({ id, name, accept, multiple, disabled, required, inputRef, describedBy, text, buttonLabel, buttonSize, buttonVariant, onFiles, onChange, clearAfterSelect, className, children, ...rest }: FileDropzoneProps): import("react").JSX.Element;
|
|
67
|
+
type FileListBaseProps = ComponentPropsWithRef<'ul'> & {
|
|
68
|
+
/** 올릴 수 있는 최대 파일 수. */
|
|
69
|
+
max?: number;
|
|
70
|
+
/** 세는 단위. 기본 `개`. */
|
|
71
|
+
unit?: string;
|
|
72
|
+
/** 전체 삭제 버튼 글자. */
|
|
73
|
+
deleteAllLabel?: ReactNode;
|
|
74
|
+
/** `.file-list` 에 붙일 클래스. `className` 은 `<ul class="upload-list">` 로 간다. */
|
|
75
|
+
wrapClassName?: string;
|
|
76
|
+
};
|
|
77
|
+
/** 카운터를 그리는 두 방법. `count`(부품이 `3개 / 10개` 를 그린다) 또는 `counter`(직접 그린다). */
|
|
78
|
+
type FileListCounterProps = {
|
|
79
|
+
count: number;
|
|
80
|
+
counter?: ReactNode;
|
|
81
|
+
} | {
|
|
82
|
+
count?: number;
|
|
83
|
+
counter: NonNullable<ReactNode>;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* **전체 삭제 버튼은 카운터 없이 둘 수 없다** — 킷이 그 버튼을 절대 배치해서 카운터가 없으면
|
|
87
|
+
* 목록 첫 항목 위에 겹친다. 타입으로 막아 둔다.
|
|
88
|
+
*/
|
|
89
|
+
export type FileListProps = FileListBaseProps & (({
|
|
90
|
+
/** 전체 삭제. 카운터(`count` 또는 `counter`)가 함께 필요하다. 조건부로 `undefined` 도 받는다. */
|
|
91
|
+
onDeleteAll: (() => void) | undefined;
|
|
92
|
+
} & FileListCounterProps) | {
|
|
93
|
+
onDeleteAll?: never;
|
|
94
|
+
/** 올라간 파일 수. `max` 와 함께 주면 `3개 / 10개` 카운터를 그린다. */
|
|
95
|
+
count?: number;
|
|
96
|
+
/** 카운터를 직접 그릴 때. 주면 `count`·`max` 는 무시한다. */
|
|
97
|
+
counter?: ReactNode;
|
|
98
|
+
});
|
|
99
|
+
export declare function FileList(props: FileListProps): import("react").JSX.Element;
|
|
100
|
+
/** 업로드 상태 아이콘. `uploading` 스피너 · `complete` 완료 표시. */
|
|
101
|
+
export type FileItemStatus = 'uploading' | 'complete';
|
|
102
|
+
type FileItemBaseProps = Omit<ComponentPropsWithRef<'li'>, 'children'> & {
|
|
103
|
+
/** 파일 이름. `type` 을 안 주면 여기서 확장자를 뽑아 이름에서 뗀다({@link formatFileLabel}). */
|
|
104
|
+
name: string;
|
|
105
|
+
/** 파일 유형. 예: `hwp`. */
|
|
106
|
+
type?: string;
|
|
107
|
+
/** 바이트 수. `328KB` 꼴로 바꿔 이름 옆 대괄호에 넣는다. */
|
|
108
|
+
size?: number;
|
|
109
|
+
/** 삭제 단추를 그린다. */
|
|
110
|
+
onDelete?: () => void;
|
|
111
|
+
/** 삭제 단추 글자. */
|
|
112
|
+
deleteLabel?: ReactNode;
|
|
113
|
+
/** 내려받기 주소. 「다운로드」와 「바로보기」는 규격상 따로 둔다. */
|
|
114
|
+
downloadHref?: string;
|
|
115
|
+
/** 내려받기 단추 글자. */
|
|
116
|
+
downloadLabel?: ReactNode;
|
|
117
|
+
/** 바로보기(미리보기) 주소. */
|
|
118
|
+
viewHref?: string;
|
|
119
|
+
/** 바로보기 단추 글자. */
|
|
120
|
+
viewLabel?: ReactNode;
|
|
121
|
+
/** 항목 버튼 크기. 킷 샘플은 medium. */
|
|
122
|
+
actionSize?: KrdsSize;
|
|
123
|
+
/** 좁은 화면에서 이름과 버튼을 위아래로 놓는다. 버튼이 둘 이상일 때 쓴다. */
|
|
124
|
+
stackOnMobile?: boolean;
|
|
125
|
+
/** 버튼 영역을 통째로 대신 그린다. */
|
|
126
|
+
children?: ReactNode;
|
|
127
|
+
};
|
|
128
|
+
/** 업로드 상태와 오류는 배타다 — 등록되지 않은 파일에 스피너·완료 아이콘이 같이 뜨면 안 된다. */
|
|
129
|
+
export type FileItemProps = FileItemBaseProps & ({
|
|
130
|
+
/** 업로드 진행 표시. 완료 아이콘은 잠시 뒤 삭제 버튼으로 바꾼다. */
|
|
131
|
+
status?: FileItemStatus;
|
|
132
|
+
error?: never;
|
|
133
|
+
} | {
|
|
134
|
+
status?: never;
|
|
135
|
+
/** 오류 문구. 「형식이 올바르지 않습니다」처럼 **무엇이 틀렸는지** 밝힌다(가이드 요구). */
|
|
136
|
+
error: ReactNode;
|
|
137
|
+
});
|
|
138
|
+
export declare function FileItem(props: FileItemProps): import("react").JSX.Element;
|
|
139
|
+
export {};
|
|
140
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/file-upload/index.tsx"],"names":[],"mappings":"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"}
|