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,498 @@
|
|
|
1
|
+
import { UiLink } from '../lib/link';
|
|
2
|
+
import { Children, isValidElement, type ComponentPropsWithRef, type ReactNode } from 'react';
|
|
3
|
+
import { cx, type KrdsIcon } from '../lib/krds';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 헤더 — KRDS `#krds-header`.
|
|
7
|
+
*
|
|
8
|
+
* 조각을 규격 순서대로 조립한다 — 유틸리티 링크 그룹 → 서비스 아이덴티티(로고) → 주요 기능 버튼
|
|
9
|
+
* → 주메뉴. 그 앞의 건너뛰기 링크(`ui/skip-link`)와 공식 배너(`ui/masthead`)는 `<header>` 밖이라
|
|
10
|
+
* 셸에서 순서를 지켜야 한다.
|
|
11
|
+
* **`HeaderNaviButton`·`HeaderNaviLink` 의 `icon` 은 킷에 있는 5종뿐이다** — 다른 이름을 주면
|
|
12
|
+
* 검은 네모가 나온다. 그 밖의 기능은 `ui/button` 의 `Button` 을 쓴다.
|
|
13
|
+
* 드롭다운 열림 상태는 이 부품이 쥐지 않는다(화면 쪽 클라이언트 부품이 `open` 으로 내려 준다).
|
|
14
|
+
* 좁은 화면 숨김·표시는 킷이 처리하므로 미디어쿼리를 따로 쓰지 않는다.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* <Header mobileNav={<MainMenuMobile items={menu} open={open} onClose={close} />}>
|
|
18
|
+
* <HeaderContainer>
|
|
19
|
+
* <HeaderUtility>
|
|
20
|
+
* <HeaderUtilityLink href="/en" icon="global">English</HeaderUtilityLink>
|
|
21
|
+
* </HeaderUtility>
|
|
22
|
+
* <HeaderBranding>
|
|
23
|
+
* <HeaderLogo name="○○기관" src="/site/logo.svg" />
|
|
24
|
+
* <HeaderActions>
|
|
25
|
+
* <HeaderNaviLink icon="login" href="/login">로그인</HeaderNaviLink>
|
|
26
|
+
* <HeaderNaviButton icon="all" onClick={openNav}>전체메뉴</HeaderNaviButton>
|
|
27
|
+
* </HeaderActions>
|
|
28
|
+
* </HeaderBranding>
|
|
29
|
+
* </HeaderContainer>
|
|
30
|
+
* <MainMenu items={menu} />
|
|
31
|
+
* </Header>
|
|
32
|
+
*
|
|
33
|
+
* 자세히: docs/krds/09-부품-노트.md#헤더
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** 유틸리티 링크가 이 수 이상이면 드롭다운으로 묶는다(규격). */
|
|
37
|
+
export const HEADER_UTILITY_DROPDOWN_THRESHOLD = 5;
|
|
38
|
+
|
|
39
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
40
|
+
/* 뿌리 */
|
|
41
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
42
|
+
|
|
43
|
+
export type HeaderProps = Omit<ComponentPropsWithRef<'header'>, 'id'> & {
|
|
44
|
+
/** `.header-in` 안에 들어갈 것 — `HeaderContainer` 와 `MainMenu`. */
|
|
45
|
+
children?: ReactNode;
|
|
46
|
+
/** 모바일 전체메뉴(`MainMenuMobile`). `.header-in` 밖에 놓여야 헤더 띠에 갇히지 않는다. */
|
|
47
|
+
mobileNav?: ReactNode;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function Header({ children, mobileNav, className, ...rest }: HeaderProps) {
|
|
51
|
+
return (
|
|
52
|
+
<header
|
|
53
|
+
// 모양이 `#krds-header` 아이디에 걸려 있어 뿌리에는 킷 클래스가 없다. 빈 class 를 남기지 않는다.
|
|
54
|
+
className={cx(className) || undefined}
|
|
55
|
+
{...rest}
|
|
56
|
+
// 킷 CSS 가 물고 있는 선택자다. rest 로 덮이지 않도록 마지막에 고정한다.
|
|
57
|
+
id="krds-header"
|
|
58
|
+
>
|
|
59
|
+
<div className="header-in">{children}</div>
|
|
60
|
+
{mobileNav}
|
|
61
|
+
</header>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 헤더 상단 줄 — 킷 `.header-container > .inner`. 본문과 같은 최대 너비·좌우 여백을 킷이 준다.
|
|
67
|
+
* `className` 은 바깥(`.header-container`)에 붙는다.
|
|
68
|
+
*/
|
|
69
|
+
export function HeaderContainer({ className, children, ...rest }: ComponentPropsWithRef<'div'>) {
|
|
70
|
+
return (
|
|
71
|
+
<div className={cx('header-container', className)} {...rest}>
|
|
72
|
+
<div className="inner">{children}</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
78
|
+
/* 유틸리티 링크 그룹 */
|
|
79
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 유틸리티 링크 그룹 — 킷 `.header-utility > ul.utility-list`.
|
|
83
|
+
*
|
|
84
|
+
* 자식을 **부품이 `<li>` 로 감싼다** — 직접 `<li>` 를 넣지 마라.
|
|
85
|
+
* **1023px 이하에서 킷이 통째로 감춘다** — 언어 전환처럼 반드시 닿아야 하는 것은
|
|
86
|
+
* 모바일 전체메뉴에도 한 벌 더 둔다. 5개 이상이면 `HeaderDropdown` 으로 묶는다(규격).
|
|
87
|
+
*/
|
|
88
|
+
export type HeaderUtilityProps = ComponentPropsWithRef<'div'> & { children?: ReactNode };
|
|
89
|
+
|
|
90
|
+
export function HeaderUtility({ className, children, ...rest }: HeaderUtilityProps) {
|
|
91
|
+
const items = Children.toArray(children);
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
process.env.NODE_ENV !== 'production' &&
|
|
95
|
+
items.length >= HEADER_UTILITY_DROPDOWN_THRESHOLD
|
|
96
|
+
) {
|
|
97
|
+
// 규격: 「유틸리티 링크가 5개 이상 필요한 경우 드롭다운 메뉴를 사용한다」.
|
|
98
|
+
console.warn(
|
|
99
|
+
`[HeaderUtility] 유틸리티 링크가 ${items.length}개다. ` +
|
|
100
|
+
`${HEADER_UTILITY_DROPDOWN_THRESHOLD}개 이상이면 비슷한 것끼리 HeaderDropdown 으로 묶어라(KRDS 02_03).`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className={cx('header-utility', className)} {...rest}>
|
|
106
|
+
<ul className="utility-list">
|
|
107
|
+
{items.map((child, i) => (
|
|
108
|
+
<li key={(isValidElement(child) && child.key) || i}>{child}</li>
|
|
109
|
+
))}
|
|
110
|
+
</ul>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 유틸리티 링크 한 칸 — 킷 `krds-btn small text`(밑줄 없는 작은 글자 버튼).
|
|
117
|
+
* 여기에 `ui/button` 의 `LinkButton` 을 쓰지 마라 — 그쪽은 밑줄 있는 `link` 변형이다.
|
|
118
|
+
*/
|
|
119
|
+
export type HeaderUtilityLinkProps = Omit<ComponentPropsWithRef<'a'>, 'href'> & {
|
|
120
|
+
href: string;
|
|
121
|
+
/** 글자 앞 아이콘(`ico-` 뺀 이름). 예: 언어 전환의 `global`. */
|
|
122
|
+
icon?: KrdsIcon;
|
|
123
|
+
/** 새 창으로 여는 바깥 주소. `UiLink` 대신 맨 `<a>` 로 그리고 `ico-go` 를 뒤에 붙인다. */
|
|
124
|
+
external?: boolean;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** 새 창 안내 문구. `ui/button` 과 맞춰 하나로 통일한다. */
|
|
128
|
+
const NEW_WINDOW_TITLE = '새 창 열림';
|
|
129
|
+
|
|
130
|
+
export function HeaderUtilityLink({
|
|
131
|
+
href,
|
|
132
|
+
icon,
|
|
133
|
+
external,
|
|
134
|
+
className,
|
|
135
|
+
children,
|
|
136
|
+
title,
|
|
137
|
+
...rest
|
|
138
|
+
}: HeaderUtilityLinkProps) {
|
|
139
|
+
const cls = cx('krds-btn', 'small', 'text', className);
|
|
140
|
+
const inner = (
|
|
141
|
+
<>
|
|
142
|
+
{icon && <i className={`svg-icon ico-${icon}`} aria-hidden="true" />}
|
|
143
|
+
{children}
|
|
144
|
+
{external && <i className="svg-icon ico-go" aria-hidden="true" />}
|
|
145
|
+
</>
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return external ? (
|
|
149
|
+
<a
|
|
150
|
+
className={cls}
|
|
151
|
+
href={href}
|
|
152
|
+
target="_blank"
|
|
153
|
+
rel="noopener noreferrer"
|
|
154
|
+
title={title ?? NEW_WINDOW_TITLE}
|
|
155
|
+
{...rest}
|
|
156
|
+
>
|
|
157
|
+
{inner}
|
|
158
|
+
</a>
|
|
159
|
+
) : (
|
|
160
|
+
<UiLink className={cls} href={href} title={title} {...rest}>
|
|
161
|
+
{inner}
|
|
162
|
+
</UiLink>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 유틸리티 버튼 한 칸 — 화면을 옮기지 않고 레이어·모달을 여는 것(글자·화면 설정 등)에 쓴다. */
|
|
167
|
+
export type HeaderUtilityButtonProps = ComponentPropsWithRef<'button'> & { icon?: KrdsIcon };
|
|
168
|
+
|
|
169
|
+
export function HeaderUtilityButton({
|
|
170
|
+
icon,
|
|
171
|
+
className,
|
|
172
|
+
children,
|
|
173
|
+
type = 'button',
|
|
174
|
+
...rest
|
|
175
|
+
}: HeaderUtilityButtonProps) {
|
|
176
|
+
return (
|
|
177
|
+
<button type={type} className={cx('krds-btn', 'small', 'text', className)} {...rest}>
|
|
178
|
+
{icon && <i className={`svg-icon ico-${icon}`} aria-hidden="true" />}
|
|
179
|
+
{children}
|
|
180
|
+
</button>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
185
|
+
/* 드롭다운 */
|
|
186
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 헤더 드롭다운 — 킷 `.krds-drop-wrap`. 언어 전환·개인 메뉴 자리다.
|
|
190
|
+
*
|
|
191
|
+
* **열림 상태를 쥐지 않는다** — `open` 을 받아 그리기만 한다.
|
|
192
|
+
* 바깥 클릭·Esc 로 닫는 것은 화면 쪽 클라이언트 부품이 맡는다.
|
|
193
|
+
* 표 행 동작처럼 키보드 메뉴 규약이 필요한 자리는 `ui/context-menu` 다.
|
|
194
|
+
*/
|
|
195
|
+
export type HeaderDropdownProps = Omit<ComponentPropsWithRef<'div'>, 'children'> & {
|
|
196
|
+
/** 여는 단추 글자. */
|
|
197
|
+
label: ReactNode;
|
|
198
|
+
/** 열렸는가. 화면 쪽에서 쥔 상태를 내려 준다. */
|
|
199
|
+
open?: boolean;
|
|
200
|
+
/** 여는 단추를 눌렀을 때. */
|
|
201
|
+
onToggle?: () => void;
|
|
202
|
+
/** 단추 모양. `utility` 는 유틸리티 줄, `navi` 는 `.header-actions` 의 개인 메뉴다. */
|
|
203
|
+
trigger?: 'utility' | 'navi';
|
|
204
|
+
/** `utility` 일 때 글자 앞 아이콘. 예: 언어 전환의 `global`. */
|
|
205
|
+
icon?: KrdsIcon;
|
|
206
|
+
/** `navi` 일 때 쓸 `.btn-navi` 아이콘. 기본 `my`. */
|
|
207
|
+
naviIcon?: HeaderNaviIcon;
|
|
208
|
+
/** 메뉴 정렬. 기본은 단추 가운데 정렬. */
|
|
209
|
+
align?: 'left' | 'center' | 'right';
|
|
210
|
+
/** 목록 위 영역 — 개인 메뉴의 이름·남은 시간 등. */
|
|
211
|
+
top?: ReactNode;
|
|
212
|
+
/** 목록 아래 영역 — 로그아웃·초기화 등. */
|
|
213
|
+
bottom?: ReactNode;
|
|
214
|
+
/** `HeaderDropdownItem` 들. */
|
|
215
|
+
children?: ReactNode;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export function HeaderDropdown({
|
|
219
|
+
label,
|
|
220
|
+
open = false,
|
|
221
|
+
onToggle,
|
|
222
|
+
trigger = 'utility',
|
|
223
|
+
icon,
|
|
224
|
+
naviIcon = 'my',
|
|
225
|
+
align = 'center',
|
|
226
|
+
top,
|
|
227
|
+
bottom,
|
|
228
|
+
className,
|
|
229
|
+
children,
|
|
230
|
+
...rest
|
|
231
|
+
}: HeaderDropdownProps) {
|
|
232
|
+
const navi = trigger === 'navi';
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<div
|
|
236
|
+
className={cx(
|
|
237
|
+
'krds-drop-wrap',
|
|
238
|
+
// `.my-drop` 은 1023px 이하에서 킷이 감춘다 — 개인 메뉴는 모바일 전체메뉴 쪽에도 둔다.
|
|
239
|
+
navi && 'my-drop',
|
|
240
|
+
align === 'left' && 'drop-left',
|
|
241
|
+
align === 'right' && 'drop-right',
|
|
242
|
+
className,
|
|
243
|
+
)}
|
|
244
|
+
{...rest}
|
|
245
|
+
>
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
className={
|
|
249
|
+
navi
|
|
250
|
+
? cx('btn-navi', naviIcon, 'drop-btn')
|
|
251
|
+
: cx('krds-btn', 'small', 'text', 'drop-btn')
|
|
252
|
+
}
|
|
253
|
+
aria-expanded={open}
|
|
254
|
+
onClick={onToggle}
|
|
255
|
+
>
|
|
256
|
+
{!navi && icon && <i className={`svg-icon ico-${icon}`} aria-hidden="true" />}
|
|
257
|
+
{label}
|
|
258
|
+
{/* 킷 샘플: 유틸리티 드롭다운만 뒤에 여닫음 화살표를 둔다. `.btn-navi` 는 ::before 아이콘이 이미 있다. */}
|
|
259
|
+
{!navi && <i className="svg-icon ico-toggle" aria-hidden="true" />}
|
|
260
|
+
</button>
|
|
261
|
+
<div className="drop-menu" style={{ display: open ? 'block' : 'none' }}>
|
|
262
|
+
<div className="drop-in">
|
|
263
|
+
{top !== undefined && <div className="drop-top">{top}</div>}
|
|
264
|
+
{children !== undefined && <ul className="drop-list">{children}</ul>}
|
|
265
|
+
{bottom !== undefined && <div className="drop-bottom">{bottom}</div>}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 드롭다운 한 줄 — 킷 `li > .item-link`. `href` 가 없으면 `<button>` 으로 그린다.
|
|
274
|
+
* 고른 항목에는 `.active` 와 함께 「선택됨」 안내가 붙는다(색만으로 알리지 않기 위해).
|
|
275
|
+
*/
|
|
276
|
+
export type HeaderDropdownItemProps = Omit<ComponentPropsWithRef<'a'>, 'href'> & {
|
|
277
|
+
/** 없으면 `<button type="button">` 이 된다. */
|
|
278
|
+
href?: string;
|
|
279
|
+
/** 지금 고른 항목. `.active` + 「선택됨」 안내가 붙는다. */
|
|
280
|
+
active?: boolean;
|
|
281
|
+
/** 새 창으로 여는 바깥 주소. */
|
|
282
|
+
external?: boolean;
|
|
283
|
+
/** 단추일 때의 클릭 처리. */
|
|
284
|
+
onSelect?: () => void;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
export function HeaderDropdownItem({
|
|
288
|
+
href,
|
|
289
|
+
active,
|
|
290
|
+
external,
|
|
291
|
+
onSelect,
|
|
292
|
+
className,
|
|
293
|
+
children,
|
|
294
|
+
title,
|
|
295
|
+
...rest
|
|
296
|
+
}: HeaderDropdownItemProps) {
|
|
297
|
+
const cls = cx('item-link', active && 'active', className);
|
|
298
|
+
const inner = (
|
|
299
|
+
<>
|
|
300
|
+
{children}
|
|
301
|
+
{external && <i className="svg-icon ico-go" aria-hidden="true" />}
|
|
302
|
+
{active && <span className="sr-only">선택됨</span>}
|
|
303
|
+
</>
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
if (!href) {
|
|
307
|
+
return (
|
|
308
|
+
<li>
|
|
309
|
+
<button
|
|
310
|
+
type="button"
|
|
311
|
+
className={cls}
|
|
312
|
+
onClick={onSelect}
|
|
313
|
+
// `<a>` 용 props 를 `<button>` 에 흘리지 않는다 — lang·aria-* 만 쓰이는 자리다.
|
|
314
|
+
{...(rest as ComponentPropsWithRef<'button'>)}
|
|
315
|
+
>
|
|
316
|
+
{inner}
|
|
317
|
+
</button>
|
|
318
|
+
</li>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return (
|
|
323
|
+
<li>
|
|
324
|
+
{external ? (
|
|
325
|
+
<a
|
|
326
|
+
className={cls}
|
|
327
|
+
href={href}
|
|
328
|
+
target="_blank"
|
|
329
|
+
rel="noopener noreferrer"
|
|
330
|
+
title={title ?? NEW_WINDOW_TITLE}
|
|
331
|
+
{...rest}
|
|
332
|
+
>
|
|
333
|
+
{inner}
|
|
334
|
+
</a>
|
|
335
|
+
) : (
|
|
336
|
+
<UiLink className={cls} href={href} title={title} {...rest}>
|
|
337
|
+
{inner}
|
|
338
|
+
</UiLink>
|
|
339
|
+
)}
|
|
340
|
+
</li>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
345
|
+
/* 서비스 아이덴티티 · 주요 기능 */
|
|
346
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
347
|
+
|
|
348
|
+
/** 로고와 주요 기능 버튼을 담는 줄 — 킷 `.header-branding`. */
|
|
349
|
+
export function HeaderBranding({ className, children, ...rest }: ComponentPropsWithRef<'div'>) {
|
|
350
|
+
return (
|
|
351
|
+
<div className={cx('header-branding', className)} {...rest}>
|
|
352
|
+
{children}
|
|
353
|
+
</div>
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 서비스 아이덴티티(로고) — 킷 `.logo > a`. 메인 화면으로 가는 링크다.
|
|
359
|
+
*
|
|
360
|
+
* **`src` 를 주지 않으면 킷의 KRDS 로고가 그대로 뜬다.** 로고 칸은 137×56px 고정이라
|
|
361
|
+
* 비율이 다른 파일은 여백이 생긴다 — 파일 쪽에서 맞춘다.
|
|
362
|
+
* 대체 텍스트에 「로고」라는 단어를 넣지 마라(규격).
|
|
363
|
+
*/
|
|
364
|
+
export type HeaderLogoProps = Omit<ComponentPropsWithRef<'a'>, 'href' | 'children'> & {
|
|
365
|
+
/** 스크린리더가 읽을 기관/서비스 이름. 「로고」라는 단어를 넣지 마라. */
|
|
366
|
+
name: string;
|
|
367
|
+
/** 이동 주소. 규격상 메인 화면. */
|
|
368
|
+
href?: string;
|
|
369
|
+
/** 기관 로고 이미지 주소. 없고 `wordmark` 도 없으면 킷의 KRDS 로고가 그대로 뜬다. */
|
|
370
|
+
src?: string;
|
|
371
|
+
/** 로고 파일이 없을 때 글자로 대신한다. 배경 이미지와 고정 너비를 끈다. */
|
|
372
|
+
wordmark?: ReactNode;
|
|
373
|
+
/** 감쌀 제목 태그. 기본 `h2` — 로고가 그 화면의 최상위 제목이면 `h1`, 목차에 안 넣으려면 `div`. */
|
|
374
|
+
heading?: 'h1' | 'h2' | 'div';
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
export function HeaderLogo({
|
|
378
|
+
name,
|
|
379
|
+
href = '/',
|
|
380
|
+
src,
|
|
381
|
+
wordmark,
|
|
382
|
+
heading = 'h2',
|
|
383
|
+
className,
|
|
384
|
+
style,
|
|
385
|
+
...rest
|
|
386
|
+
}: HeaderLogoProps) {
|
|
387
|
+
const Heading = heading;
|
|
388
|
+
// 글자 로고는 킷의 고정 너비에 갇히면 잘린다. 아이디 선택자를 이기려면 인라인이어야 한다.
|
|
389
|
+
const headingStyle = wordmark ? { width: 'auto' } : undefined;
|
|
390
|
+
const linkStyle = {
|
|
391
|
+
...(src ? { backgroundImage: `url("${src}")` } : null),
|
|
392
|
+
...(wordmark && !src ? { backgroundImage: 'none' } : null),
|
|
393
|
+
...style,
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
return (
|
|
397
|
+
<Heading className={cx('logo', className)} style={headingStyle}>
|
|
398
|
+
<UiLink
|
|
399
|
+
href={href}
|
|
400
|
+
style={Object.keys(linkStyle).length ? linkStyle : undefined}
|
|
401
|
+
// 보이는 글자가 이름에 포함되도록 넘긴다(WCAG 2.5.3 Label in Name).
|
|
402
|
+
aria-label={wordmark ? name : undefined}
|
|
403
|
+
{...rest}
|
|
404
|
+
>
|
|
405
|
+
{wordmark ?? <span className="sr-only">{name}</span>}
|
|
406
|
+
</UiLink>
|
|
407
|
+
</Heading>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** 주요 기능 버튼 묶음 — 킷 `.header-actions`. 킷이 오른쪽에 붙인다. */
|
|
412
|
+
export function HeaderActions({ className, children, ...rest }: ComponentPropsWithRef<'div'>) {
|
|
413
|
+
return (
|
|
414
|
+
<div className={cx('header-actions', className)} {...rest}>
|
|
415
|
+
{children}
|
|
416
|
+
</div>
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** `.btn-navi` 가 아이콘을 가진 5종. 다른 이름을 주면 마스크 없는 검은 네모가 남는다. */
|
|
421
|
+
export type HeaderNaviIcon = 'sch' | 'login' | 'join' | 'my' | 'all';
|
|
422
|
+
|
|
423
|
+
type NaviBase = {
|
|
424
|
+
/** 킷에 있는 5종 중 하나. 필수 — 빠지면 아이콘 자리가 검은 네모가 된다. */
|
|
425
|
+
icon: HeaderNaviIcon;
|
|
426
|
+
/** 가로형 — 아이콘과 글자를 한 줄에. 기본은 세로형이고, 한 헤더 안에서 섞지 마라. */
|
|
427
|
+
row?: boolean;
|
|
428
|
+
className?: string;
|
|
429
|
+
children?: ReactNode;
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* 헤더 주요 기능 버튼 — 킷 `.btn-navi`. 레이어·전체메뉴를 여는 자리다.
|
|
434
|
+
*
|
|
435
|
+
* 좁은 화면에서 `join`·`my` 를, 1024px 이상에서 `all` 을 킷이 감춘다 —
|
|
436
|
+
* 미디어쿼리를 따로 쓰지 마라. `icon="all"` 이면 `aria-controls="mobile-nav"` 가 자동으로 붙는다.
|
|
437
|
+
*/
|
|
438
|
+
export type HeaderNaviButtonProps = Omit<ComponentPropsWithRef<'button'>, 'children'> & NaviBase;
|
|
439
|
+
|
|
440
|
+
export function HeaderNaviButton({
|
|
441
|
+
icon,
|
|
442
|
+
row,
|
|
443
|
+
className,
|
|
444
|
+
children,
|
|
445
|
+
type = 'button',
|
|
446
|
+
'aria-controls': ariaControls,
|
|
447
|
+
...rest
|
|
448
|
+
}: HeaderNaviButtonProps) {
|
|
449
|
+
return (
|
|
450
|
+
<button
|
|
451
|
+
type={type}
|
|
452
|
+
className={cx('btn-navi', icon, row && 'navi-row', className)}
|
|
453
|
+
// 킷 샘플: 전체메뉴 버튼은 모바일 전체메뉴를 가리킨다. 직접 준 값이 있으면 그것을 쓴다.
|
|
454
|
+
aria-controls={ariaControls ?? (icon === 'all' ? 'mobile-nav' : undefined)}
|
|
455
|
+
{...rest}
|
|
456
|
+
>
|
|
457
|
+
{children}
|
|
458
|
+
</button>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** 헤더 주요 기능 링크 — 화면을 옮기는 것(로그인 등)만 링크로 둔다. 레이어는 `HeaderNaviButton`. */
|
|
463
|
+
export type HeaderNaviLinkProps = Omit<ComponentPropsWithRef<'a'>, 'href' | 'children'> &
|
|
464
|
+
NaviBase & {
|
|
465
|
+
href: string;
|
|
466
|
+
/** 새 창으로 여는 바깥 주소. */
|
|
467
|
+
external?: boolean;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
export function HeaderNaviLink({
|
|
471
|
+
icon,
|
|
472
|
+
row,
|
|
473
|
+
href,
|
|
474
|
+
external,
|
|
475
|
+
className,
|
|
476
|
+
children,
|
|
477
|
+
title,
|
|
478
|
+
...rest
|
|
479
|
+
}: HeaderNaviLinkProps) {
|
|
480
|
+
const cls = cx('btn-navi', icon, row && 'navi-row', className);
|
|
481
|
+
|
|
482
|
+
return external ? (
|
|
483
|
+
<a
|
|
484
|
+
className={cls}
|
|
485
|
+
href={href}
|
|
486
|
+
target="_blank"
|
|
487
|
+
rel="noopener noreferrer"
|
|
488
|
+
title={title ?? NEW_WINDOW_TITLE}
|
|
489
|
+
{...rest}
|
|
490
|
+
>
|
|
491
|
+
{children}
|
|
492
|
+
</a>
|
|
493
|
+
) : (
|
|
494
|
+
<UiLink className={cls} href={href} title={title} {...rest}>
|
|
495
|
+
{children}
|
|
496
|
+
</UiLink>
|
|
497
|
+
);
|
|
498
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef, ReactNode } from 'react';
|
|
2
|
+
import { cx } from '../lib/krds';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 운영기관 식별자 — KRDS `krds-identifier`.
|
|
6
|
+
*
|
|
7
|
+
* 「이 누리집은 [기관]에서 운영하는 누리집입니다.」 안내. 푸터 안 **가장 마지막 구획**에 둔다.
|
|
8
|
+
* 여기 들어가는 로고는 서비스 로고가 아니라 **운영 주체 기관**의 로고다(서비스 로고는 `f-logo`).
|
|
9
|
+
* 문구 형식과 요소 배치는 규격이 변형을 금지한다 — `children` 은 번역을 넣는 자리다.
|
|
10
|
+
* 푸터 골격을 조립할 때는 `ui/footer` 의 `FooterIdentifier` 를 쓴다.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* <Identifier org="○○기관" logoSrc="/site/logo-mark.svg" />
|
|
14
|
+
* <Identifier org="○○기관" variant="text" dark />
|
|
15
|
+
*
|
|
16
|
+
* 자세히: docs/krds/09-부품-노트.md#운영기관식별자
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 표현 방식. `card` 는 흰 면 + 둥근 상자(기본), `text` 는 한 줄짜리 킷 전용 변형이다.
|
|
21
|
+
*/
|
|
22
|
+
export type IdentifierVariant = 'card' | 'text';
|
|
23
|
+
|
|
24
|
+
export type IdentifierProps = Omit<ComponentPropsWithRef<'section'>, 'children'> & {
|
|
25
|
+
/** 운영기관명. 안내 문구와 로고 대체 텍스트에 함께 쓰인다. */
|
|
26
|
+
org: string;
|
|
27
|
+
/** 운영기관 로고 이미지 경로. 없으면 `.logo` 를 아예 그리지 않는다(킷 마크가 찍히는 것을 막는다). */
|
|
28
|
+
logoSrc?: string;
|
|
29
|
+
/** 표현 방식. 기본 `card`. */
|
|
30
|
+
variant?: IdentifierVariant;
|
|
31
|
+
/** 어두운 바탕용. 킷에 클래스가 없어 KRDS 반전 토큰으로 그린다. */
|
|
32
|
+
dark?: boolean;
|
|
33
|
+
/** 안내 문구. 다국어 번역을 넣을 때만 쓰고 형식은 바꾸지 않는다. */
|
|
34
|
+
children?: ReactNode;
|
|
35
|
+
/** 구획 이름(`aria-label`). `<section>` 은 이름이 있어야 region 으로 노출된다. */
|
|
36
|
+
label?: string;
|
|
37
|
+
/** 가이드가 허용하는 두 요소. 기본은 `section`. */
|
|
38
|
+
as?: 'section' | 'article';
|
|
39
|
+
className?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function Identifier({
|
|
43
|
+
org,
|
|
44
|
+
logoSrc,
|
|
45
|
+
variant = 'card',
|
|
46
|
+
dark,
|
|
47
|
+
label = '운영기관 식별자',
|
|
48
|
+
as: Tag = 'section',
|
|
49
|
+
className,
|
|
50
|
+
children,
|
|
51
|
+
...rest
|
|
52
|
+
}: IdentifierProps) {
|
|
53
|
+
// 가이드 형식: `(운영기관 로고) 이 누리집은 [운영기관명]에서 운영하는 누리집입니다.`
|
|
54
|
+
const text = children ?? `이 누리집은 ${org}에서 운영하는 누리집입니다.`;
|
|
55
|
+
|
|
56
|
+
const logo = logoSrc ? (
|
|
57
|
+
<span
|
|
58
|
+
className={cx(
|
|
59
|
+
'logo',
|
|
60
|
+
// 킷의 KRDS 마크 배경과 고정 폭을 지운다. 높이는 킷 값을 그대로 쓴다.
|
|
61
|
+
'inline-flex items-center bg-none w-auto',
|
|
62
|
+
// text 변형에는 킷 `.logo` 규칙이 닿지 않아 높이가 없다 — 본문 소자 줄에 맞춘다.
|
|
63
|
+
variant === 'text' && 'h-5',
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
{/* 이름은 옆의 sr-only 가 준다. alt 까지 채우면 두 번 읽힌다. */}
|
|
67
|
+
<img src={logoSrc} alt="" className="h-full w-auto" />
|
|
68
|
+
<span className="sr-only">{org}</span>
|
|
69
|
+
</span>
|
|
70
|
+
) : null;
|
|
71
|
+
|
|
72
|
+
const cls =
|
|
73
|
+
variant === 'text'
|
|
74
|
+
? cx(
|
|
75
|
+
'krds-identifier-txt',
|
|
76
|
+
// 킷 규칙이 `#krds-footer .f-btm` 안에서만 걸린다 — 밖에서도 같은 줄로 서게 보충한다.
|
|
77
|
+
// 킷의 `white-space:nowrap` 은 옮기지 않는다(긴 기관명이 좁은 화면에서 넘친다).
|
|
78
|
+
'flex items-center gap-2 text-sm',
|
|
79
|
+
dark && 'text-fg-inverse',
|
|
80
|
+
className,
|
|
81
|
+
)
|
|
82
|
+
: cx(
|
|
83
|
+
'krds-identifier',
|
|
84
|
+
// 킷의 흰 면(`--krds-light-color-surface-white`)을 반전 토큰으로 덮는다.
|
|
85
|
+
dark && 'bg-surface-inverse text-fg-inverse',
|
|
86
|
+
className,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
// rest 를 뒤에 두어 화면 쪽에서 aria-label 을 덮어쓸 수 있게 한다.
|
|
91
|
+
<Tag aria-label={label} className={cls} {...rest}>
|
|
92
|
+
{logo}
|
|
93
|
+
{variant === 'text' ? <span>{text}</span> : <span className="ban-txt">{text}</span>}
|
|
94
|
+
</Tag>
|
|
95
|
+
);
|
|
96
|
+
}
|