@ihabdevteam/core 0.181.0 → 0.183.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/CHANGELOG.md +36 -0
- package/dist/components.cjs.js +14 -14
- package/dist/components.esm.css +2 -2
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +1 -1
- package/dist/ihabdevteam-core.cjs.js +17 -17
- package/dist/ihabdevteam-core.d.ts +33 -12
- package/dist/packages/core/src/components/EmptyState.css +11 -9
- package/dist/packages/core/src/components/EmptyState.d.ts +9 -8
- package/dist/packages/core/src/components/EmptyState.js +4 -5
- package/dist/packages/core/src/components/ErrorState.css +3 -0
- package/dist/packages/core/src/components/ErrorState.d.ts +3 -1
- package/dist/packages/core/src/components/ErrorState.js +1 -1
- package/dist/packages/core/src/components/Popup/Popup.css +3 -0
- package/dist/packages/core/src/components/Popup/Popup.d.ts +14 -0
- package/dist/packages/core/src/components/Popup/Popup.js +3 -2
- package/dist/packages/core/src/components/SegmentedControl.d.ts +6 -2
- package/dist/packages/core/src/components/SegmentedControl.js +1 -1
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +1 -1
- package/package.json +1 -1
|
@@ -1600,7 +1600,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1600
1600
|
*
|
|
1601
1601
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1602
1602
|
*/
|
|
1603
|
-
export declare const CORE_VERSION = "0.
|
|
1603
|
+
export declare const CORE_VERSION = "0.183.0";
|
|
1604
1604
|
|
|
1605
1605
|
/** @public */
|
|
1606
1606
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -2145,29 +2145,30 @@ export declare function EffectPartyPopperHost({ trigger, active }: {
|
|
|
2145
2145
|
/** @public 아무것도 안 고른 상태 — 시험이 기준값으로 쓴다. */
|
|
2146
2146
|
export declare const EMPTY_COMPOSITION: WordComposition;
|
|
2147
2147
|
|
|
2148
|
-
export declare function EmptyState({ icon,
|
|
2148
|
+
export declare function EmptyState({ icon, title, message, action, className }: EmptyStateProps): JSX.Element;
|
|
2149
2149
|
|
|
2150
2150
|
/** @public */
|
|
2151
2151
|
export declare interface EmptyStateProps {
|
|
2152
|
+
/** 위에 서는 아이콘 — **lucide 이름만** 받는다(`Icon` 과 같은 규약).
|
|
2153
|
+
* 그림문자(emoji)를 받던 `emoji` prop 은 0.182.0 에서 걷었다: 같은 자리에 두 갈래가 있으면
|
|
2154
|
+
* 제품마다 다른 것이 서고, 그림문자는 기기·글꼴마다 다르게 그려지며 색·크기를 토큰으로 못 맞춘다
|
|
2155
|
+
* (사용자 지시 2026-09-21 "아이콘은 항상 루시드 아이콘만"). */
|
|
2152
2156
|
icon?: string;
|
|
2153
|
-
emoji?: string;
|
|
2154
2157
|
title?: string;
|
|
2155
2158
|
message?: string;
|
|
2156
|
-
/** 메시지 아래 보조 안내(선택). message와 별도 문단으로 렌더 — 문자열 접합 오용 방지. */
|
|
2157
|
-
hint?: string;
|
|
2158
2159
|
/**
|
|
2159
2160
|
* 메시지 아래 단추(다시 시도 등). `onClick` 이 **약속(Promise)을 돌려주면 끝날 때까지(성공·실패 모두) 단추를 막는다** —
|
|
2160
2161
|
* 두 번 눌러 요청이 두 번 나가지 않게.
|
|
2161
2162
|
*/
|
|
2163
|
+
/** 메시지 아래 단추 — **크기는 늘 40 이다.**
|
|
2164
|
+
* 0.181.0 에서 잠깐 `size`(40·48·56)를 받았는데, 빈 화면마다 단추 크기가 달라지면 같은 제품 안에서
|
|
2165
|
+
* "이 단추가 더 중요한가" 로 읽힌다. 빈 화면의 단추는 하나뿐이라 크기로 위계를 줄 것이 없다
|
|
2166
|
+
* (사용자 지시 2026-09-21). */
|
|
2162
2167
|
action?: {
|
|
2163
2168
|
label: string;
|
|
2164
2169
|
icon?: string;
|
|
2165
2170
|
onClick: () => void | Promise<unknown>;
|
|
2166
|
-
/** 단추 크기 — 기본 40. 빈 화면이 한 쪽을 통째로 차지하면 48·56 이 어울린다(말귀 고객관리 2026-09-21). */
|
|
2167
|
-
size?: 40 | 48 | 56;
|
|
2168
2171
|
};
|
|
2169
|
-
/** 표 안의 빈 행처럼 좁은 자리에 넣을 때 위아래 여백을 줄인다. */
|
|
2170
|
-
compact?: boolean;
|
|
2171
2172
|
className?: string;
|
|
2172
2173
|
}
|
|
2173
2174
|
|
|
@@ -2185,7 +2186,9 @@ export declare function ErrorState({ message, hint, icon, onRetry, retryLabel, c
|
|
|
2185
2186
|
export declare interface ErrorStateProps {
|
|
2186
2187
|
/** 에러 메시지(미지정 시 공용 기본 문구). */
|
|
2187
2188
|
message?: string;
|
|
2188
|
-
/** 보조 안내(선택)
|
|
2189
|
+
/** 보조 안내(선택) — "잠시 뒤 다시 시도해 보세요" 처럼 **다음에 할 일**을 적는다.
|
|
2190
|
+
* `EmptyState` 의 같은 줄은 0.182.0 에서 걷었지만(빈 화면은 글자 줄이 적어야 한다),
|
|
2191
|
+
* 실패 화면은 까닭과 다음 걸음이 갈리므로 여기서는 지킨다. */
|
|
2189
2192
|
hint?: string;
|
|
2190
2193
|
icon?: string;
|
|
2191
2194
|
/**
|
|
@@ -4564,6 +4567,20 @@ export declare interface PopupProps {
|
|
|
4564
4567
|
/** 복잡한 커스텀 footer가 필요할 때 사용. buttons가 없을 때만 렌더됩니다.
|
|
4565
4568
|
* buttons와 달리 기본 폭/정렬 제약(.popup-footer--buttons)이 적용되지 않아 자유 배치 가능. */
|
|
4566
4569
|
footer?: ReactNode | ((opts: PopupOptions) => ReactNode);
|
|
4570
|
+
/** 머리 **오른쪽에 서는 보기 고르개** — 창 하나가 갈래 둘(고객정보 · 훈련기록)을 담을 때 쓴다.
|
|
4571
|
+
* 닫기(X)와 `headerActions` **앞**, 제목 오른쪽에 선다. `SegmentedControl size={40}` 그대로이고,
|
|
4572
|
+
* 창이 좁으면 제목 아래로 줄을 바꾼다(제목을 밀어내지 않는다).
|
|
4573
|
+
* 이름이 없으면 낭독기가 '선택 탭' 으로 읽으니 `ariaLabel` 을 준다. */
|
|
4574
|
+
headerSegment?: {
|
|
4575
|
+
value: string;
|
|
4576
|
+
onChange: (key: string) => void;
|
|
4577
|
+
options: {
|
|
4578
|
+
key: string;
|
|
4579
|
+
label: string;
|
|
4580
|
+
disabled?: boolean;
|
|
4581
|
+
}[];
|
|
4582
|
+
ariaLabel?: string;
|
|
4583
|
+
};
|
|
4567
4584
|
/** 커스텀 footer 래퍼에 추가할 클래스 (정렬·간격 override용) */
|
|
4568
4585
|
footerClassName?: string;
|
|
4569
4586
|
/** 바닥 줄의 가로 배치 — `'center'`(기본) · `'fill'` · `'between'`.
|
|
@@ -5858,8 +5875,12 @@ export declare interface SegmentedControlProps {
|
|
|
5858
5875
|
*/
|
|
5859
5876
|
onChange?: (v: string) => void;
|
|
5860
5877
|
className?: string;
|
|
5861
|
-
/** 버튼 높이 — Button 과 같은 숫자 size (24 / 32 / 40 / 48). **겉높이가 곧 size 다**(0.159.0 부터 판 여백 없음) —
|
|
5862
|
-
* 옆에 `Button size={32}` 를 세우면 셈 없이 나란히 선다.
|
|
5878
|
+
/** 버튼 높이 — Button 과 같은 숫자 size (24 / 32 / 40 / 48). **기본 40.** **겉높이가 곧 size 다**(0.159.0 부터 판 여백 없음) —
|
|
5879
|
+
* 옆에 `Button size={32}` 를 세우면 셈 없이 나란히 선다.
|
|
5880
|
+
*
|
|
5881
|
+
* 기본이 48 이던 것을 40 으로 내렸다(사용자 지시 2026-09-21). 코어가 제 안에서 세우는 고르개는 이미 **전부 40**
|
|
5882
|
+
* 이었고(`Toolbar`·`FilterPanel`·`BarChart`·`PanelRow`…), 옆에 서는 `Dropdown`·`TextField` 도 40 이다 —
|
|
5883
|
+
* 기본값만 혼자 48 이라 안 주고 쓴 자리가 한 급 크게 섰다. */
|
|
5863
5884
|
size?: 24 | 32 | 40 | 48;
|
|
5864
5885
|
shape?: 'rounded' | 'pill';
|
|
5865
5886
|
useBG?: boolean;
|
|
@@ -9,15 +9,17 @@
|
|
|
9
9
|
border: 1px solid var(--color-whiteLine);
|
|
10
10
|
border-radius: var(--radius-m);
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
.empty-state__icon { width:
|
|
14
|
-
|
|
12
|
+
/* 빈 화면 아이콘 — 40 은 제목(h4) 옆에서 작았다(사용자 지시 2026-09-21) */
|
|
13
|
+
.empty-state__icon { width: 56px; height: 56px; color: var(--color-lightGray); }
|
|
14
|
+
/* 제목과 본문은 **같은 색** 이다(사용자 지시 2026-09-21) — 전에는 제목이 한 단계 옅어(lightGray)
|
|
15
|
+
제목이 본문보다 뒤로 물러나 보였다. 위계는 글자 등급(h4 · body-3)이 준다.
|
|
16
|
+
같은 색으로 모을 때 gray 를 고른 것은 lightGray 가 흰 지면에서 AA(4.5:1)에 못 미치기 때문이다. */
|
|
17
|
+
.empty-state__title { color: var(--color-gray); margin: 0; }
|
|
15
18
|
.empty-state__message { color: var(--color-gray); margin: 0; }
|
|
16
|
-
|
|
19
|
+
/* 제목과 본문은 **한 덩어리로 읽힌다** — 그 사이만 좁힌다(사용자 지시 2026-09-21).
|
|
20
|
+
통의 gap(10px)은 아이콘·단추 사이에 그대로 남는다. */
|
|
21
|
+
.empty-state__title + .empty-state__message { margin-top: -6px; }
|
|
17
22
|
.empty-state__action { margin-top: var(--spacing-xs); min-width: 120px; }
|
|
18
23
|
|
|
19
|
-
/*
|
|
20
|
-
|
|
21
|
-
padding: var(--spacing-page) var(--spacing-m);
|
|
22
|
-
padding-block: 56px;
|
|
23
|
-
}
|
|
24
|
+
/* `compact` 는 0.183.0 에서 걷었다 — 표·패널의 빈 자리는 **줄(row)로** 나타내기로 했다(사용자 지시 2026-09-21).
|
|
25
|
+
좁은 자리용 변종을 두면 같은 빈 상태가 두 꼴로 살고, 그 둘이 따로 늙는다. */
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
import './EmptyState.css';
|
|
2
2
|
/** @public */
|
|
3
3
|
export interface EmptyStateProps {
|
|
4
|
+
/** 위에 서는 아이콘 — **lucide 이름만** 받는다(`Icon` 과 같은 규약).
|
|
5
|
+
* 그림문자(emoji)를 받던 `emoji` prop 은 0.182.0 에서 걷었다: 같은 자리에 두 갈래가 있으면
|
|
6
|
+
* 제품마다 다른 것이 서고, 그림문자는 기기·글꼴마다 다르게 그려지며 색·크기를 토큰으로 못 맞춘다
|
|
7
|
+
* (사용자 지시 2026-09-21 "아이콘은 항상 루시드 아이콘만"). */
|
|
4
8
|
icon?: string;
|
|
5
|
-
emoji?: string;
|
|
6
9
|
title?: string;
|
|
7
10
|
message?: string;
|
|
8
|
-
/** 메시지 아래 보조 안내(선택). message와 별도 문단으로 렌더 — 문자열 접합 오용 방지. */
|
|
9
|
-
hint?: string;
|
|
10
11
|
/**
|
|
11
12
|
* 메시지 아래 단추(다시 시도 등). `onClick` 이 **약속(Promise)을 돌려주면 끝날 때까지(성공·실패 모두) 단추를 막는다** —
|
|
12
13
|
* 두 번 눌러 요청이 두 번 나가지 않게.
|
|
13
14
|
*/
|
|
15
|
+
/** 메시지 아래 단추 — **크기는 늘 40 이다.**
|
|
16
|
+
* 0.181.0 에서 잠깐 `size`(40·48·56)를 받았는데, 빈 화면마다 단추 크기가 달라지면 같은 제품 안에서
|
|
17
|
+
* "이 단추가 더 중요한가" 로 읽힌다. 빈 화면의 단추는 하나뿐이라 크기로 위계를 줄 것이 없다
|
|
18
|
+
* (사용자 지시 2026-09-21). */
|
|
14
19
|
action?: {
|
|
15
20
|
label: string;
|
|
16
21
|
icon?: string;
|
|
17
22
|
onClick: () => void | Promise<unknown>;
|
|
18
|
-
/** 단추 크기 — 기본 40. 빈 화면이 한 쪽을 통째로 차지하면 48·56 이 어울린다(말귀 고객관리 2026-09-21). */
|
|
19
|
-
size?: 40 | 48 | 56;
|
|
20
23
|
};
|
|
21
|
-
/** 표 안의 빈 행처럼 좁은 자리에 넣을 때 위아래 여백을 줄인다. */
|
|
22
|
-
compact?: boolean;
|
|
23
24
|
className?: string;
|
|
24
25
|
}
|
|
25
|
-
export default function EmptyState({ icon,
|
|
26
|
+
export default function EmptyState({ icon, title, message, action, className }: EmptyStateProps): import("react").JSX.Element;
|
|
@@ -4,12 +4,11 @@ import { cx } from '../utils/cx';
|
|
|
4
4
|
import './EmptyState.css';
|
|
5
5
|
import Icon from './Icon';
|
|
6
6
|
import { usePendingPress } from '../hooks/usePendingPress';
|
|
7
|
-
export default function EmptyState({ icon,
|
|
7
|
+
export default function EmptyState({ icon, title, message, action, className }) {
|
|
8
8
|
const { pending, press } = usePendingPress(action?.onClick);
|
|
9
|
-
return (_jsxs("div", { className: cx('empty-state',
|
|
9
|
+
return (_jsxs("div", { className: cx('empty-state', className), children: [icon && _jsx(Icon, { icon: icon, className: "empty-state__icon" }), title && _jsx("p", { className: "empty-state__title h4", children: title }), message && _jsx("p", { className: "empty-state__message body-3", children: message }), action && (
|
|
10
10
|
// type="button" — 없으면 폼 안에서 누르는 순간 폼이 제출된다
|
|
11
11
|
_jsxs("button", { type: "button",
|
|
12
|
-
/* 코어 `Button` 을 안 쓰고 클래스를 적는 것은 `aria-busy` 때문이다 — 약속이 끝날 때까지 알린다.
|
|
13
|
-
|
|
14
|
-
className: cx('empty-state__action button', `button${action.size ?? 40}`, `button${action.size ?? 40}--bar`, 'button--variant-main button--align-center button--shape-rounded'), onClick: press, disabled: pending, "aria-busy": pending || undefined, children: [action.icon && _jsx(Icon, { icon: action.icon, className: "button__icon" }), _jsx("span", { className: "button__label button-1", children: action.label })] }))] }));
|
|
12
|
+
/* 코어 `Button` 을 안 쓰고 클래스를 적는 것은 `aria-busy` 때문이다 — 약속이 끝날 때까지 알린다. */
|
|
13
|
+
className: "empty-state__action button button40 button40--bar button--variant-main button--align-center button--shape-rounded", onClick: press, disabled: pending, "aria-busy": pending || undefined, children: [action.icon && _jsx(Icon, { icon: action.icon, className: "button__icon" }), _jsx("span", { className: "button__label button-1", children: action.label })] }))] }));
|
|
15
14
|
}
|
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
/* ErrorState — EmptyState 레이아웃(empty-state) 재사용, 에러 톤만 오버라이드 */
|
|
2
2
|
.error-state__icon { color: var(--color-error); }
|
|
3
|
+
/* 보조 안내 — EmptyState 가 제 hint 줄을 걷으면서(0.182.0) 이 꼴을 여기로 옮겼다.
|
|
4
|
+
그때까지는 `.empty-state__hint` 를 함께 쓰고 있었다. */
|
|
5
|
+
.error-state__hint { color: var(--color-lightGray); margin: 0; }
|
|
@@ -6,7 +6,9 @@ import './ErrorState.css';
|
|
|
6
6
|
export interface ErrorStateProps {
|
|
7
7
|
/** 에러 메시지(미지정 시 공용 기본 문구). */
|
|
8
8
|
message?: string;
|
|
9
|
-
/** 보조 안내(선택)
|
|
9
|
+
/** 보조 안내(선택) — "잠시 뒤 다시 시도해 보세요" 처럼 **다음에 할 일**을 적는다.
|
|
10
|
+
* `EmptyState` 의 같은 줄은 0.182.0 에서 걷었지만(빈 화면은 글자 줄이 적어야 한다),
|
|
11
|
+
* 실패 화면은 까닭과 다음 걸음이 갈리므로 여기서는 지킨다. */
|
|
10
12
|
hint?: string;
|
|
11
13
|
icon?: string;
|
|
12
14
|
/**
|
|
@@ -9,7 +9,7 @@ import { usePendingPress } from '../hooks/usePendingPress';
|
|
|
9
9
|
export default function ErrorState({ message, hint, icon = 'triangle-alert', onRetry, retryLabel, className }) {
|
|
10
10
|
const { t } = useTranslation();
|
|
11
11
|
const { pending, press } = usePendingPress(onRetry);
|
|
12
|
-
return (_jsxs("div", { className: cx('empty-state', 'error-state', className), role: "alert", children: [_jsx(Icon, { icon: icon, className: "empty-state__icon error-state__icon" }), _jsx("p", { className: "empty-state__message body-3", children: message ?? t('common.errorState.message', { defaultValue: '불러오지 못했습니다.' }) }), hint && _jsx("p", { className: "
|
|
12
|
+
return (_jsxs("div", { className: cx('empty-state', 'error-state', className), role: "alert", children: [_jsx(Icon, { icon: icon, className: "empty-state__icon error-state__icon" }), _jsx("p", { className: "empty-state__message body-3", children: message ?? t('common.errorState.message', { defaultValue: '불러오지 못했습니다.' }) }), hint && _jsx("p", { className: "error-state__hint caption-1", children: hint }), onRetry && (_jsxs("button", {
|
|
13
13
|
// type="button" — 없으면 폼 안에서 다시 시도를 누르는 순간 폼이 제출된다
|
|
14
14
|
type: "button", className: "empty-state__action button button40 button40--bar button--variant-main button--align-center button--shape-rounded", onClick: press, disabled: pending, "aria-busy": pending || undefined, children: [_jsx(Icon, { icon: "rotate-cw", className: "button__icon" }), _jsx("span", { className: "button__label button-1", children: retryLabel ?? t('common.errorState.retry', { defaultValue: '다시 시도' }) })] }))] }));
|
|
15
15
|
}
|
|
@@ -70,6 +70,9 @@
|
|
|
70
70
|
.popup-header-main .popup-title--left { flex: none; }
|
|
71
71
|
.popup-header .popup-title--left { margin: 0; }
|
|
72
72
|
.popup-subtitle { text-align: left; color: var(--color-gray); margin: 0; }
|
|
73
|
+
/* 머리 오른쪽 고르개(headerSegment) — 닫기·headerActions 앞. 좁으면 제목 아래로 줄을 바꾸고
|
|
74
|
+
제목을 밀어내지 않는다(제목 칸이 flex:1 로 먼저 줄어든다). */
|
|
75
|
+
.popup-header-segment { flex-shrink: 0; }
|
|
73
76
|
.popup-header-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
|
|
74
77
|
.popup-content {
|
|
75
78
|
padding: 20px 20px 0;
|
|
@@ -36,6 +36,20 @@ export interface PopupProps {
|
|
|
36
36
|
/** 복잡한 커스텀 footer가 필요할 때 사용. buttons가 없을 때만 렌더됩니다.
|
|
37
37
|
* buttons와 달리 기본 폭/정렬 제약(.popup-footer--buttons)이 적용되지 않아 자유 배치 가능. */
|
|
38
38
|
footer?: ReactNode | ((opts: PopupOptions) => ReactNode);
|
|
39
|
+
/** 머리 **오른쪽에 서는 보기 고르개** — 창 하나가 갈래 둘(고객정보 · 훈련기록)을 담을 때 쓴다.
|
|
40
|
+
* 닫기(X)와 `headerActions` **앞**, 제목 오른쪽에 선다. `SegmentedControl size={40}` 그대로이고,
|
|
41
|
+
* 창이 좁으면 제목 아래로 줄을 바꾼다(제목을 밀어내지 않는다).
|
|
42
|
+
* 이름이 없으면 낭독기가 '선택 탭' 으로 읽으니 `ariaLabel` 을 준다. */
|
|
43
|
+
headerSegment?: {
|
|
44
|
+
value: string;
|
|
45
|
+
onChange: (key: string) => void;
|
|
46
|
+
options: {
|
|
47
|
+
key: string;
|
|
48
|
+
label: string;
|
|
49
|
+
disabled?: boolean;
|
|
50
|
+
}[];
|
|
51
|
+
ariaLabel?: string;
|
|
52
|
+
};
|
|
39
53
|
/** 커스텀 footer 래퍼에 추가할 클래스 (정렬·간격 override용) */
|
|
40
54
|
footerClassName?: string;
|
|
41
55
|
/** 바닥 줄의 가로 배치 — `'center'`(기본) · `'fill'` · `'between'`.
|
|
@@ -4,6 +4,7 @@ import { useCallback, useState, useEffect, useRef, useId } from "react";
|
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
import { useTranslation } from "react-i18next";
|
|
6
6
|
import Button from "../Button";
|
|
7
|
+
import SegmentedControl from "../SegmentedControl";
|
|
7
8
|
import { pushEsc } from "../../hooks/escStack";
|
|
8
9
|
import { lockBodyScroll } from "../../hooks/bodyScrollLock";
|
|
9
10
|
import "./Popup.css";
|
|
@@ -15,7 +16,7 @@ const ANIM_MS = 260;
|
|
|
15
16
|
/** @public */
|
|
16
17
|
export default function Popup(props) {
|
|
17
18
|
const { t } = useTranslation();
|
|
18
|
-
const { title, subtitle, headerActions, children, buttons, footer, footerClassName = "", footerLayout = 'center', onClose, closeDisabled = false, beforeClose, closeOnBackdrop = true, className = "", ariaLabel, useTitle = true, inline = false, size, layer = 'modal', height, fillBody = false, resetScrollKey } = props || {};
|
|
19
|
+
const { title, subtitle, headerActions, headerSegment, children, buttons, footer, footerClassName = "", footerLayout = 'center', onClose, closeDisabled = false, beforeClose, closeOnBackdrop = true, className = "", ariaLabel, useTitle = true, inline = false, size, layer = 'modal', height, fillBody = false, resetScrollKey } = props || {};
|
|
19
20
|
const [closing, setClosing] = useState(false);
|
|
20
21
|
// 닫힘 애니메이션 중(260ms)엔 버튼이 시각적으로만 흐려질 뿐 클릭은 계속 가능해,
|
|
21
22
|
// 그 사이 같은 버튼을 연타하면 cb(확인/완료 콜백)가 여러 번 예약되어 중복 실행된다.
|
|
@@ -211,7 +212,7 @@ export default function Popup(props) {
|
|
|
211
212
|
e.preventDefault(); }, children: _jsxs("div", { ref: panelRef, className: `popup-panel${size ? ` popup-panel--${size}` : ''}${height != null ? ' popup-panel--has-height' : ''}${fillBody ? ' popup-panel--fill-body' : ''}`,
|
|
212
213
|
/* 높이는 **CSS 변수로** 넘긴다 — 인라인 height 로 박으면 앱의 미디어 규칙(좁은 화면만 다른 높이)이 못 이겼다.
|
|
213
214
|
max-height(CSS)가 height 보다 이기므로 화면보다 크게 줘도 화면에 맞춰진다 */
|
|
214
|
-
style: height != null ? { '--popup-height': typeof height === 'number' ? `${height}px` : height } : undefined, tabIndex: -1, role: inline ? undefined : 'dialog', "aria-modal": inline ? undefined : true, "aria-labelledby": !inline && useTitle && title ? titleId : undefined, "aria-label": !inline && !(useTitle && title) ? (ariaLabel ?? (typeof title === 'string' ? title : undefined)) : undefined, children: [!useTitle && (headerActions || onClose) ? (_jsxs("div", { className: "popup-close-float", children: [headerActions, onClose && _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, onClick: closeWithAnimation, disabled: closeDisabled, ariaLabel: t('common.close', '닫기') })] })) : (title || headerActions || onClose) ? (_jsxs("div", { className: `popup-header${subtitle ? ' popup-header--with-subtitle' : ''}`, children: [_jsxs("div", { className: "popup-header-main", children: [_jsx("span", { id: titleId, className: "popup-title popup-title--left h2", children: title }), subtitle && _jsx("span", { className: "popup-subtitle body-3", children: subtitle })] }), _jsxs("div", { className: "popup-header-actions", children: [headerActions, onClose && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, onClick: closeWithAnimation, disabled: closeDisabled, ariaLabel: t('common.close', '닫기') }))] })] })) : null, _jsx("div", { className: "popup-content", ref: contentRef, tabIndex: contentScrollable ? 0 : undefined, children: _jsx("div", { className: "popup-content__inner", children: typeof children === 'function' ? children({ animateThen, closeWithAnimation }) : children }) }), buttons?.length ? (_jsx("div", { className: `popup-footer popup-footer--buttons${footerLayout !== 'center' ? ` popup-footer--${footerLayout}` : ''}`, children: buttons.map((btn, i) => (_jsx(Button, { htmlType: "button", size: 48, variant: btn.variant ?? 'default', label: btn.label, disabled: btn.disabled, onClick: () => btn.onClick?.({ animateThen, closeWithAnimation }) }, i))) })) : footer ? (_jsx("div", { className: `popup-footer${footerLayout !== 'center' ? ` popup-footer--${footerLayout}` : ''}${footerClassName ? ` ${footerClassName}` : ''}`, children: typeof footer === 'function' ? footer({ animateThen, closeWithAnimation }) : footer })) : null] }) }));
|
|
215
|
+
style: height != null ? { '--popup-height': typeof height === 'number' ? `${height}px` : height } : undefined, tabIndex: -1, role: inline ? undefined : 'dialog', "aria-modal": inline ? undefined : true, "aria-labelledby": !inline && useTitle && title ? titleId : undefined, "aria-label": !inline && !(useTitle && title) ? (ariaLabel ?? (typeof title === 'string' ? title : undefined)) : undefined, children: [!useTitle && (headerActions || onClose) ? (_jsxs("div", { className: "popup-close-float", children: [headerActions, onClose && _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, onClick: closeWithAnimation, disabled: closeDisabled, ariaLabel: t('common.close', '닫기') })] })) : (title || headerActions || onClose) ? (_jsxs("div", { className: `popup-header${subtitle ? ' popup-header--with-subtitle' : ''}`, children: [_jsxs("div", { className: "popup-header-main", children: [_jsx("span", { id: titleId, className: "popup-title popup-title--left h2", children: title }), subtitle && _jsx("span", { className: "popup-subtitle body-3", children: subtitle })] }), headerSegment && (_jsx("div", { className: "popup-header-segment", children: _jsx(SegmentedControl, { size: 40, ariaLabel: headerSegment.ariaLabel, options: headerSegment.options.map(o => ({ key: o.key, label: o.label, disabled: o.disabled })), value: headerSegment.value, onChange: headerSegment.onChange }) })), _jsxs("div", { className: "popup-header-actions", children: [headerActions, onClose && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, onClick: closeWithAnimation, disabled: closeDisabled, ariaLabel: t('common.close', '닫기') }))] })] })) : null, _jsx("div", { className: "popup-content", ref: contentRef, tabIndex: contentScrollable ? 0 : undefined, children: _jsx("div", { className: "popup-content__inner", children: typeof children === 'function' ? children({ animateThen, closeWithAnimation }) : children }) }), buttons?.length ? (_jsx("div", { className: `popup-footer popup-footer--buttons${footerLayout !== 'center' ? ` popup-footer--${footerLayout}` : ''}`, children: buttons.map((btn, i) => (_jsx(Button, { htmlType: "button", size: 48, variant: btn.variant ?? 'default', label: btn.label, disabled: btn.disabled, onClick: () => btn.onClick?.({ animateThen, closeWithAnimation }) }, i))) })) : footer ? (_jsx("div", { className: `popup-footer${footerLayout !== 'center' ? ` popup-footer--${footerLayout}` : ''}${footerClassName ? ` ${footerClassName}` : ''}`, children: typeof footer === 'function' ? footer({ animateThen, closeWithAnimation }) : footer })) : null] }) }));
|
|
215
216
|
if (inline || typeof document === 'undefined')
|
|
216
217
|
return el;
|
|
217
218
|
return createPortal(el, document.body);
|
|
@@ -17,8 +17,12 @@ export interface SegmentedControlProps {
|
|
|
17
17
|
*/
|
|
18
18
|
onChange?: (v: string) => void;
|
|
19
19
|
className?: string;
|
|
20
|
-
/** 버튼 높이 — Button 과 같은 숫자 size (24 / 32 / 40 / 48). **겉높이가 곧 size 다**(0.159.0 부터 판 여백 없음) —
|
|
21
|
-
* 옆에 `Button size={32}` 를 세우면 셈 없이 나란히 선다.
|
|
20
|
+
/** 버튼 높이 — Button 과 같은 숫자 size (24 / 32 / 40 / 48). **기본 40.** **겉높이가 곧 size 다**(0.159.0 부터 판 여백 없음) —
|
|
21
|
+
* 옆에 `Button size={32}` 를 세우면 셈 없이 나란히 선다.
|
|
22
|
+
*
|
|
23
|
+
* 기본이 48 이던 것을 40 으로 내렸다(사용자 지시 2026-09-21). 코어가 제 안에서 세우는 고르개는 이미 **전부 40**
|
|
24
|
+
* 이었고(`Toolbar`·`FilterPanel`·`BarChart`·`PanelRow`…), 옆에 서는 `Dropdown`·`TextField` 도 40 이다 —
|
|
25
|
+
* 기본값만 혼자 48 이라 안 주고 쓴 자리가 한 급 크게 섰다. */
|
|
22
26
|
size?: 24 | 32 | 40 | 48;
|
|
23
27
|
shape?: 'rounded' | 'pill';
|
|
24
28
|
useBG?: boolean;
|
|
@@ -10,7 +10,7 @@ export default function SegmentedControl(props) {
|
|
|
10
10
|
const { t } = useTranslation();
|
|
11
11
|
const { options = [],
|
|
12
12
|
// 안 주면 **켜진** 첫 칸 — 꺼진 첫 칸이 골라져 있었다(Tabs 와 같게)
|
|
13
|
-
value = options.find((o) => !o.disabled)?.key ?? options[0]?.key, onChange = (_v) => { }, className = '', size =
|
|
13
|
+
value = options.find((o) => !o.disabled)?.key ?? options[0]?.key, onChange = (_v) => { }, className = '', size = 40, shape = 'pill', useBG = true, fullWidth = false, minButtonWidth, ariaLabel, } = props || {};
|
|
14
14
|
/* **라디오 무리는 Tab 자리 하나 + 화살표**가 ARIA 규칙이다.
|
|
15
15
|
`role="radiogroup"`·`role="radio"` 만 붙여 놓고 키를 안 만들면 **안 붙인 것보다
|
|
16
16
|
나쁘다** — 낭독기가 "라디오 그룹, 3 중 2" 라고 읽어 주니 사람은 화살표를 누르는데
|