@ihabdevteam/core 0.136.0 → 0.138.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 +98 -0
- package/dist/components.cjs.js +177 -177
- package/dist/components.esm.css +2 -2
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core.cjs.js +182 -182
- package/dist/ihabdevteam-core.d.ts +20 -5
- package/dist/packages/core/src/components/Breadcrumb.js +1 -0
- package/dist/packages/core/src/components/Button.js +10 -1
- package/dist/packages/core/src/components/Effect/EffectPartyPopper.js +3 -2
- package/dist/packages/core/src/components/Icon.js +4 -1
- package/dist/packages/core/src/components/LandingHeader.d.ts +1 -1
- package/dist/packages/core/src/components/LandingHeader.js +5 -1
- package/dist/packages/core/src/components/PageTitle.js +1 -0
- package/dist/packages/core/src/components/Pagination.js +1 -1
- package/dist/packages/core/src/components/Panel.css +3 -0
- package/dist/packages/core/src/components/Panel.d.ts +11 -1
- package/dist/packages/core/src/components/Panel.js +3 -3
- package/dist/packages/core/src/components/PanelRow.js +8 -1
- package/dist/packages/core/src/components/PassCard/PassCard.js +1 -1
- package/dist/packages/core/src/components/PassCard/PassCardBgIHAB.js +1 -0
- package/dist/packages/core/src/components/PassCard/PassCardBgLeftColorGrid.js +1 -0
- package/dist/packages/core/src/components/PassCard/PassCardBgSoundMove.js +1 -0
- package/dist/packages/core/src/components/PassCard/PassCardBgSoundMoveV1.js +1 -0
- package/dist/packages/core/src/components/PassCard/PassCardBgTalk.js +1 -0
- package/dist/packages/core/src/components/PassCard/PassCardBgTalkV1.js +1 -0
- package/dist/packages/core/src/components/PassCard/PassCardBgTwoCircle.js +1 -0
- package/dist/packages/core/src/components/Popup/DeviceSideTable.js +8 -1
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +3 -1
- package/dist/packages/core/src/components/Popup/Popup.css +10 -0
- package/dist/packages/core/src/components/Popup/Popup.d.ts +5 -0
- package/dist/packages/core/src/components/Popup/Popup.js +5 -4
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +6 -1
- package/dist/packages/core/src/components/ProgressIndicator.js +1 -0
- package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgDark.js +1 -0
- package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgDeep.js +1 -0
- package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgDiagonal.js +1 -0
- package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgDots.js +1 -0
- package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgInstagram.js +1 -0
- package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgMain.js +1 -0
- package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgNaver.js +1 -0
- package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgOutline.js +1 -0
- package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgRadial.js +1 -0
- package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgSoft.js +1 -0
- package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgSoftSub.js +1 -0
- package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgSub.js +1 -0
- package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgSuccess.js +1 -0
- package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgWarm.js +1 -0
- package/dist/packages/core/src/components/Table.js +4 -1
- package/dist/packages/core/src/components/TextField.js +3 -1
- package/dist/packages/core/src/components/Tooltip/DescriptionTooltip.js +1 -0
- package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +23 -26
- package/dist/packages/core/src/components/Training/TrainingBlock.js +20 -3
- package/dist/packages/core/src/components/Training/TrainingBlockButton.js +10 -4
- package/dist/packages/core/src/components/Training/TrainingCardGroup.js +2 -1
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +20 -5
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +60 -3
- package/dist/packages/core/src/components/Training/TrainingStepCard.js +16 -1
- package/dist/packages/core/src/components/Training/TrainingStepConfirm.js +2 -1
- package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +26 -6
- package/dist/packages/core/src/components/Training/TrainingStepOrderList.d.ts +0 -1
- package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +16 -1
- package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +18 -3
- package/dist/packages/core/src/components/Training/TrainingWordCard.js +28 -5
- package/dist/packages/core/src/components/Training/currentSound.d.ts +5 -0
- package/dist/packages/core/src/components/Training/currentSound.js +13 -0
- package/dist/packages/core/src/components/Training/trainingPopupUtils.js +12 -1
- package/dist/packages/core/src/components/Training/useNoiseFade.js +7 -1
- package/dist/packages/core/src/components/Training/useTTSPlayback.js +5 -1
- package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +3 -1
- package/dist/packages/core/src/components/WordTable.js +25 -7
- package/dist/packages/core/src/hooks/useEditableList.js +13 -5
- package/dist/packages/core/src/hooks/useFontScale.js +13 -4
- package/dist/packages/core/src/hooks/useIcon.js +3 -0
- package/dist/packages/core/src/hooks/useLucideIcon.js +3 -0
- package/dist/packages/core/src/hooks/useMediaQuery.d.ts +2 -1
- package/dist/packages/core/src/hooks/useMediaQuery.js +37 -8
- package/dist/packages/core/src/hooks/usePagedList.js +1 -0
- package/dist/packages/core/src/hooks/usePasswordChange.js +7 -0
- package/dist/packages/core/src/hooks/useTrainingTypeLabels.js +1 -0
- package/dist/packages/core/src/hooks/useVisitHistory.js +1 -0
- package/dist/packages/core/src/hooks/useWordTrainingDataGeneration.js +18 -3
- package/dist/packages/core/src/i18n/locales/en.json +41 -4
- package/dist/packages/core/src/i18n/locales/ko.json +41 -4
- package/dist/packages/core/src/utils/errorMessage.js +7 -1
- package/dist/packages/core/src/utils/wordPairIndex.js +11 -3
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +2 -2
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ import { JSX } from 'react';
|
|
|
12
12
|
import { KeyboardEvent as KeyboardEvent_2 } from 'react';
|
|
13
13
|
import { MouseEvent as MouseEvent_2 } from 'react';
|
|
14
14
|
import { default as React_2 } from 'react';
|
|
15
|
+
import * as React_3 from 'react';
|
|
15
16
|
import { ReactNode } from 'react';
|
|
16
17
|
import { ReactPortal } from 'react';
|
|
17
18
|
import { Ref } from 'react';
|
|
@@ -1375,7 +1376,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1375
1376
|
*
|
|
1376
1377
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1377
1378
|
*/
|
|
1378
|
-
export declare const CORE_VERSION = "0.
|
|
1379
|
+
export declare const CORE_VERSION = "0.138.0";
|
|
1379
1380
|
|
|
1380
1381
|
/** @public */
|
|
1381
1382
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -2699,7 +2700,7 @@ export declare interface LandingBottomStickyProps {
|
|
|
2699
2700
|
*
|
|
2700
2701
|
* @public
|
|
2701
2702
|
*/
|
|
2702
|
-
export declare function LandingHeader({ logo, logoAlt, logoHref, links, actions, className, containerClassName, renderLink, menuLabel, }: LandingHeaderProps): JSX.Element;
|
|
2703
|
+
export declare function LandingHeader({ logo, logoAlt, logoHref, links, actions, className, containerClassName, renderLink, menuLabel: menuLabelProp, }: LandingHeaderProps): JSX.Element;
|
|
2703
2704
|
|
|
2704
2705
|
/**
|
|
2705
2706
|
* 가운데 내비 링크 한 개.
|
|
@@ -2935,7 +2936,7 @@ export declare interface MediaQueryOverride {
|
|
|
2935
2936
|
}
|
|
2936
2937
|
|
|
2937
2938
|
/** @public */
|
|
2938
|
-
export declare const MediaQueryOverrideContext: Context<MediaQueryOverride | null>;
|
|
2939
|
+
export declare const MediaQueryOverrideContext: React_3.Context<MediaQueryOverride | null>;
|
|
2939
2940
|
|
|
2940
2941
|
/** @public */
|
|
2941
2942
|
export declare function MenuTooltip({ columns, onClose, className, size }: {
|
|
@@ -3350,7 +3351,7 @@ export declare interface PairStats {
|
|
|
3350
3351
|
total: number;
|
|
3351
3352
|
}
|
|
3352
3353
|
|
|
3353
|
-
export declare function Panel({ title, icon, disc, actions, size, minHeight, maxWidth, className, scrollable, useBorder, useBg, useRowBorder, labelWidth, form, loading, empty, children }: PanelProps): JSX.Element;
|
|
3354
|
+
export declare function Panel({ title, icon, disc, actions, size, minHeight, maxWidth, className, scrollable, scrollbarGutter, compactRows, useBorder, useBg, useRowBorder, labelWidth, form, loading, empty, children }: PanelProps): JSX.Element;
|
|
3354
3355
|
|
|
3355
3356
|
/** @public */
|
|
3356
3357
|
export declare type PanelElement = HTMLDivElement & {
|
|
@@ -3391,6 +3392,16 @@ declare interface PanelProps {
|
|
|
3391
3392
|
maxWidth?: number | string;
|
|
3392
3393
|
className?: string;
|
|
3393
3394
|
scrollable?: boolean;
|
|
3395
|
+
/**
|
|
3396
|
+
* `scrollable` 본문의 스크롤바 자리. `'stable'` 이면 스크롤바가 없어도 자리를 비워 두어,
|
|
3397
|
+
* 내용이 늘어 스크롤바가 생길 때 폭이 튀지 않는다. 기본 `'auto'`(생길 때만 자리를 차지).
|
|
3398
|
+
*/
|
|
3399
|
+
scrollbarGutter?: 'auto' | 'stable';
|
|
3400
|
+
/**
|
|
3401
|
+
* 행을 조금 낮춘다 — 최소 높이 46px·위아래 여백 3px. `size="small"` 에 겹쳐 쓰면 행 하나짜리 머리 없는 패널의
|
|
3402
|
+
* **테두리까지 바깥 높이가 48px** 이라 옆의 48px 단추와 줄이 맞는다(공유 링크 칸 등). 머리 높이·라벨 급은 그대로.
|
|
3403
|
+
*/
|
|
3404
|
+
compactRows?: boolean;
|
|
3394
3405
|
/** 외곽선 표시 여부. 기본 true */
|
|
3395
3406
|
useBorder?: boolean;
|
|
3396
3407
|
/** 배경(카드 흰 배경) 표시 여부. 기본 true. false면 투명 배경으로 페이지에 녹아든다 */
|
|
@@ -4175,6 +4186,11 @@ export declare interface PopupProps {
|
|
|
4175
4186
|
* 그대로 걸리므로 창이 화면 밖으로 나가지 않는다. 넘치는 내용은 본문이 스스로 굴린다.
|
|
4176
4187
|
*/
|
|
4177
4188
|
height?: number | string;
|
|
4189
|
+
/**
|
|
4190
|
+
* 본문이 패널 높이를 채운다 — 본문 자신은 굴리지 않고, 안쪽을 세로 flex(높이 100%)로 넘겨 **안의 한 칸만** 굴리게 한다.
|
|
4191
|
+
* `height` 로 창 높이를 박고 가운데 목록(`Panel scrollable` 등)만 굴러가야 하는 창에 쓴다.
|
|
4192
|
+
*/
|
|
4193
|
+
fillBody?: boolean;
|
|
4178
4194
|
/**
|
|
4179
4195
|
* 이 값이 바뀌면 본문(.popup-content) 스크롤을 맨 위로 되돌린다.
|
|
4180
4196
|
* 탭/모드 전환처럼 내부 콘텐츠가 통째로 바뀌는 팝업에서, 스크롤을 내린 채
|
|
@@ -7534,7 +7550,6 @@ export declare type TrainingStepListenProps = {
|
|
|
7534
7550
|
showNoiseWaveForm?: boolean;
|
|
7535
7551
|
};
|
|
7536
7552
|
|
|
7537
|
-
/** @public */
|
|
7538
7553
|
export declare function TrainingStepOrderList({ index, stepIndex, isLast, title, onDone, onChange, context, }: TrainingStepOrderListProps): JSX.Element;
|
|
7539
7554
|
|
|
7540
7555
|
/** @public */
|
|
@@ -68,7 +68,16 @@ export default function Button(props) {
|
|
|
68
68
|
useEffect(() => {
|
|
69
69
|
if (prevDisabledRef.current && !disabledProp && btnRef.current) {
|
|
70
70
|
const btn = btnRef.current;
|
|
71
|
-
|
|
71
|
+
/* 마우스로 잡힌 초점만 푼다 — 키보드로 누른 단추(모두 듣기 등)가 막혔다 풀리는 순간 무조건 blur 해 초점이 body 로 빠졌다 */
|
|
72
|
+
let 마우스초점 = false;
|
|
73
|
+
try {
|
|
74
|
+
마우스초점 = btn.matches(':focus:not(:focus-visible)');
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
마우스초점 = false;
|
|
78
|
+
}
|
|
79
|
+
if (마우스초점)
|
|
80
|
+
btn.blur();
|
|
72
81
|
btn.style.pointerEvents = 'none';
|
|
73
82
|
const id = requestAnimationFrame(() => {
|
|
74
83
|
requestAnimationFrame(() => {
|
|
@@ -111,8 +111,9 @@ onComplete, trigger = 0, }) {
|
|
|
111
111
|
// 움직임 최소화를 켠 사용자에게는 화면을 가로지르는 물리 애니메이션을 재생하지 않는다.
|
|
112
112
|
// CSS의 전역 reduce 규칙(styles.css)은 requestAnimationFrame 기반인 이 효과를 막지 못한다.
|
|
113
113
|
if (typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
// 곧바로 부르지 않고 한 틱 미룬다 — effect 안에서 바로 부르면 StrictMode 의 두 번 실행에서 onComplete 가 두 번
|
|
115
|
+
const id = setTimeout(() => onCompleteRef.current?.(), 0);
|
|
116
|
+
return () => clearTimeout(id);
|
|
116
117
|
}
|
|
117
118
|
// production: no debug logging
|
|
118
119
|
const gravity = 1200; // px/s^2 (pulls downward) - reduced for longer arcs
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
3
|
import { createElement } from 'react';
|
|
3
4
|
import useLucideIcon from '../hooks/useLucideIcon';
|
|
@@ -20,7 +21,9 @@ export default function Icon(props) {
|
|
|
20
21
|
const { iconToUse, mode, svgText, blobUrl } = useIcon(iconStr);
|
|
21
22
|
// ────────────────────────────────────────────────────────────────────────────
|
|
22
23
|
const createWrapper = (children, extraProps = {}) => (_jsx("span", { className: wrapperClass, style: wrapperStyle, ...extraProps, children: children }));
|
|
23
|
-
const createImage = (src, extraProps = {}) => (
|
|
24
|
+
const createImage = (src, extraProps = {}) => (
|
|
25
|
+
// 글자가 아닌 label(요소)은 alt 로 못 쓴다 — String() 하면 "[object Object]" 가 읽혔다
|
|
26
|
+
_jsx("img", { src: src, alt: typeof label === 'string' ? label : (typeof label === 'number' ? String(label) : ''), className: defaultImgClass, ...extraProps }));
|
|
24
27
|
if (!icon)
|
|
25
28
|
return null;
|
|
26
29
|
if (typeof icon !== 'string')
|
|
@@ -64,4 +64,4 @@ export interface LandingHeaderProps {
|
|
|
64
64
|
*
|
|
65
65
|
* @public
|
|
66
66
|
*/
|
|
67
|
-
export default function LandingHeader({ logo, logoAlt, logoHref, links, actions, className, containerClassName, renderLink, menuLabel, }: LandingHeaderProps): import("react").JSX.Element;
|
|
67
|
+
export default function LandingHeader({ logo, logoAlt, logoHref, links, actions, className, containerClassName, renderLink, menuLabel: menuLabelProp, }: LandingHeaderProps): import("react").JSX.Element;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useId, useState } from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
4
5
|
import { pushEsc } from '../hooks/escStack';
|
|
5
6
|
import './LandingHeader.css';
|
|
6
7
|
/**
|
|
@@ -18,7 +19,10 @@ import './LandingHeader.css';
|
|
|
18
19
|
*
|
|
19
20
|
* @public
|
|
20
21
|
*/
|
|
21
|
-
export default function LandingHeader({ logo, logoAlt = '', logoHref, links = [], actions, className, containerClassName, renderLink, menuLabel
|
|
22
|
+
export default function LandingHeader({ logo, logoAlt = '', logoHref, links = [], actions, className, containerClassName, renderLink, menuLabel: menuLabelProp, }) {
|
|
23
|
+
const { t } = useTranslation();
|
|
24
|
+
// 기본 이름표도 지금 언어로 — 박힌 '메뉴 열기' 가 영어 화면 햄버거 이름으로 읽혔다
|
|
25
|
+
const menuLabel = menuLabelProp ?? { open: t('header.menuOpen', '메뉴 열기'), close: t('header.menuClose', '메뉴 닫기') };
|
|
22
26
|
const [open, setOpen] = useState(false);
|
|
23
27
|
// 서랍 id 를 고정 문자열로 두면 한 화면에 헤더가 둘일 때(미리보기·놀이터 견본) id 가 겹쳐
|
|
24
28
|
// 햄버거의 aria-controls 가 엉뚱한 서랍을 가리킨다 — 헤더마다 따로 짓는다.
|
|
@@ -19,5 +19,5 @@ export default function Pagination({ page, totalPages, onChange, prevLabel, next
|
|
|
19
19
|
const 마지막 = Math.max(1, totalPages);
|
|
20
20
|
const handlePrev = () => onChange(Math.max(1, Math.min(마지막, page - 1)));
|
|
21
21
|
const handleNext = () => onChange(Math.min(마지막, page + 1));
|
|
22
|
-
return (_jsxs("div", { className: cx('pagination', className), children: [_jsx(Button, { htmlType: "button", size: 32, type: "dot", shape: "rounded", icon: "chevron-left", ariaLabel: resolvedPrev, disabled: page <= 1, onClick: handlePrev }), _jsxs("span", { className: "pagination__info body-3", children: [page, " / ",
|
|
22
|
+
return (_jsxs("div", { className: cx('pagination', className), children: [_jsx(Button, { htmlType: "button", size: 32, type: "dot", shape: "rounded", icon: "chevron-left", ariaLabel: resolvedPrev, disabled: page <= 1, onClick: handlePrev }), _jsxs("span", { className: "pagination__info body-3", children: [Math.min(Math.max(1, page), 마지막), " / ", 마지막] }), _jsx(Button, { htmlType: "button", size: 32, type: "dot", shape: "rounded", icon: "chevron-right", ariaLabel: resolvedNext, disabled: page >= totalPages, onClick: handleNext })] }));
|
|
23
23
|
}
|
|
@@ -104,6 +104,8 @@
|
|
|
104
104
|
.panel--small .panel-row { min-height: 48px; }
|
|
105
105
|
.panel--medium .panel-row { min-height: 56px; }
|
|
106
106
|
.panel--large .panel-row { min-height: 64px; }
|
|
107
|
+
/* compactRows — 크기 규칙 **뒤에** 둬야 같은 특이도로 이긴다 */
|
|
108
|
+
.panel--compact-rows .panel-row { min-height: 46px; padding-top: 3px; padding-bottom: 3px; }
|
|
107
109
|
/* 인접한 row 사이에만 구분선 (첫/끝 처리 불필요) */
|
|
108
110
|
.panel-row + .panel-row {
|
|
109
111
|
border-top: 1px solid var(--color-darkWhite);
|
|
@@ -156,6 +158,7 @@
|
|
|
156
158
|
overflow-y: auto;
|
|
157
159
|
overscroll-behavior-y: contain;
|
|
158
160
|
}
|
|
161
|
+
.panel__body--gutter-stable { scrollbar-gutter: stable; }
|
|
159
162
|
|
|
160
163
|
/* ── SectionRow variants ──────────────────────────────────────────────── */
|
|
161
164
|
/* content: 단순 패딩 콘텐츠 영역 (min-height·flex 오버라이드) */
|
|
@@ -28,6 +28,16 @@ interface PanelProps {
|
|
|
28
28
|
maxWidth?: number | string;
|
|
29
29
|
className?: string;
|
|
30
30
|
scrollable?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* `scrollable` 본문의 스크롤바 자리. `'stable'` 이면 스크롤바가 없어도 자리를 비워 두어,
|
|
33
|
+
* 내용이 늘어 스크롤바가 생길 때 폭이 튀지 않는다. 기본 `'auto'`(생길 때만 자리를 차지).
|
|
34
|
+
*/
|
|
35
|
+
scrollbarGutter?: 'auto' | 'stable';
|
|
36
|
+
/**
|
|
37
|
+
* 행을 조금 낮춘다 — 최소 높이 46px·위아래 여백 3px. `size="small"` 에 겹쳐 쓰면 행 하나짜리 머리 없는 패널의
|
|
38
|
+
* **테두리까지 바깥 높이가 48px** 이라 옆의 48px 단추와 줄이 맞는다(공유 링크 칸 등). 머리 높이·라벨 급은 그대로.
|
|
39
|
+
*/
|
|
40
|
+
compactRows?: boolean;
|
|
31
41
|
/** 외곽선 표시 여부. 기본 true */
|
|
32
42
|
useBorder?: boolean;
|
|
33
43
|
/** 배경(카드 흰 배경) 표시 여부. 기본 true. false면 투명 배경으로 페이지에 녹아든다 */
|
|
@@ -44,7 +54,7 @@ interface PanelProps {
|
|
|
44
54
|
empty?: ReactNode;
|
|
45
55
|
children?: ReactNode;
|
|
46
56
|
}
|
|
47
|
-
export default function Panel({ title, icon, disc, actions, size, minHeight, maxWidth, className, scrollable, useBorder, useBg, useRowBorder, labelWidth, form, loading, empty, children }: PanelProps): import("react").JSX.Element;
|
|
57
|
+
export default function Panel({ title, icon, disc, actions, size, minHeight, maxWidth, className, scrollable, scrollbarGutter, compactRows, useBorder, useBg, useRowBorder, labelWidth, form, loading, empty, children }: PanelProps): import("react").JSX.Element;
|
|
48
58
|
/** @public */
|
|
49
59
|
export declare function PanelGrid({ cols, children }: {
|
|
50
60
|
cols?: number;
|
|
@@ -5,10 +5,10 @@ import { PanelRow } from './PanelRow';
|
|
|
5
5
|
import { Children } from 'react';
|
|
6
6
|
import Icon from './Icon';
|
|
7
7
|
import './Panel.css';
|
|
8
|
-
export default function Panel({ title, icon, disc, actions, size = 'medium', minHeight, maxWidth, className, scrollable, useBorder = true, useBg = true, useRowBorder = true, labelWidth, form = false, loading, empty, children }) {
|
|
8
|
+
export default function Panel({ title, icon, disc, actions, size = 'medium', minHeight, maxWidth, className, scrollable, scrollbarGutter = 'auto', compactRows = false, useBorder = true, useBg = true, useRowBorder = true, labelWidth, form = false, loading, empty, children }) {
|
|
9
9
|
const cls = cx('panel', `panel--${size}`, form ? 'panel--form' : '',
|
|
10
10
|
/* minHeight 패널은 마지막 행 아래 빈 공간이 생길 수 있으므로 행 하단 구분선 사용 */
|
|
11
|
-
minHeight !== undefined ? 'panel--fill' : '', !useBorder ? 'panel--no-border' : '', !useBg ? 'panel--no-bg' : '', !useRowBorder ? 'panel--no-row-border' : '', className);
|
|
11
|
+
minHeight !== undefined ? 'panel--fill' : '', !useBorder ? 'panel--no-border' : '', !useBg ? 'panel--no-bg' : '', !useRowBorder ? 'panel--no-row-border' : '', compactRows ? 'panel--compact-rows' : '', className);
|
|
12
12
|
const style = {};
|
|
13
13
|
if (minHeight !== undefined)
|
|
14
14
|
style.minHeight = minHeight;
|
|
@@ -18,7 +18,7 @@ export default function Panel({ title, icon, disc, actions, size = 'medium', min
|
|
|
18
18
|
style['--panel-label-width'] = typeof labelWidth === 'number' ? `${labelWidth}px` : labelWidth;
|
|
19
19
|
return (_jsxs("div", { className: cls, style: Object.keys(style).length ? style : undefined, children: [(title != null || disc != null) && (_jsxs("div", { className: cx('panel__header', disc != null ? 'panel__header--with-disc' : ''), children: [icon && (_jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "panel__icon" })), _jsxs("div", { className: "panel__title-group", children: [title != null && (_jsx("h2", { className: `panel__title ${disc != null ? 'body-3b' : 'h5b'}`, children: title })), disc != null && _jsx("p", { className: "panel__disc caption-1", children: disc })] }), actions && _jsx("div", { className: "panel__actions", children: actions })] })), loading ? (_jsx(PanelRow, { variant: "loading" })
|
|
20
20
|
/* 비었는지는 React 가 그리는 것으로 센다 — `{조건 && 행들}` 이 false 면 children 이 false 라 빈 상태 문구가 안 떴다 */
|
|
21
|
-
) : empty != null && Children.toArray(children).length === 0 ? (_jsx(PanelRow, { variant: "empty", children: empty })) : scrollable ? (_jsx("div", { className:
|
|
21
|
+
) : empty != null && Children.toArray(children).length === 0 ? (_jsx(PanelRow, { variant: "empty", children: empty })) : scrollable ? (_jsx("div", { className: cx('panel__body--scroll', scrollbarGutter === 'stable' && 'panel__body--gutter-stable'), children: children })) : children] }));
|
|
22
22
|
}
|
|
23
23
|
// ── PanelGrid ──────────────────────────────────────────────────────────────
|
|
24
24
|
/** @public */
|
|
@@ -9,6 +9,13 @@ import Dropdown from './Dropdown';
|
|
|
9
9
|
import Button from './Button';
|
|
10
10
|
import Badge from './Badge';
|
|
11
11
|
import { deviceKindVariant } from '../utils/deviceKind';
|
|
12
|
+
/* 기기 종류 배지 이름은 지금 언어로 — 값은 저장된 한국어(보청기·인공와우)라 그대로 보이면 영어 화면에도 한국어였다.
|
|
13
|
+
색(deviceKindVariant)은 저장값으로 가른다. 모르는 값은 그대로 */
|
|
14
|
+
const 기기종류열쇠 = { '보청기': 'hearingAid', '인공와우': 'cochlearImplant' };
|
|
15
|
+
function 기기종류이름(device, t) {
|
|
16
|
+
const k = 기기종류열쇠[device];
|
|
17
|
+
return k ? t(`device.type.${k}`, { defaultValue: device }) : device;
|
|
18
|
+
}
|
|
12
19
|
import TextField from './TextField';
|
|
13
20
|
import Loader from './Loader';
|
|
14
21
|
import './Panel.css';
|
|
@@ -143,6 +150,6 @@ export function PanelRowDevice({ devices, onRelease, emptyLabel }) {
|
|
|
143
150
|
/* 표의 줄 끝 단추와 같은 모양·같은 말 — 하는 일도 같다.
|
|
144
151
|
이력에서 지우는 것이 아니라 '지금 끼고 있음' 만 내린다
|
|
145
152
|
(잠시 안 씀·수리 보냄 같은 경우가 실제로 있다). */
|
|
146
|
-
_jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "x", ariaLabel: t('customers.device.unwear', '착용 해제'), showTooltipInfo: t('customers.device.unwear', '착용 해제'), onClick: () => onRelease(side) }))] }), d ? (_jsxs("div", { className: "panel-row__device-info", children: [_jsxs("span", { className: "panel-row__device-kind", children: [d.device && _jsx(Badge, { size: "small", variant: deviceKindVariant(d.device), fill: "soft", label: d.device }), 메타(d) && _jsx("span", { className: "panel-row__device-detail body-3", children: 메타(d) })] }), _jsx("span", { className: "panel-row__device-name body-2b", children: d.productName || d.brand }), _jsx("span", { className: "panel-row__device-date caption-1", children: t('customers.device.wornSince', { date: `${d.wornYear}.${d.wornMonth}.${d.wornDay}` }) })] })) : (_jsx("span", { className: "panel-row__device-empty body-3", children: emptyLabel ?? t('customers.device.notRegistered', '미등록') }))] }, side));
|
|
153
|
+
_jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "x", ariaLabel: t('customers.device.unwear', '착용 해제'), showTooltipInfo: t('customers.device.unwear', '착용 해제'), onClick: () => onRelease(side) }))] }), d ? (_jsxs("div", { className: "panel-row__device-info", children: [_jsxs("span", { className: "panel-row__device-kind", children: [d.device && _jsx(Badge, { size: "small", variant: deviceKindVariant(d.device), fill: "soft", label: 기기종류이름(d.device, t) }), 메타(d) && _jsx("span", { className: "panel-row__device-detail body-3", children: 메타(d) })] }), _jsx("span", { className: "panel-row__device-name body-2b", children: d.productName || d.brand }), _jsx("span", { className: "panel-row__device-date caption-1", children: t('customers.device.wornSince', { date: `${d.wornYear}.${d.wornMonth}.${d.wornDay}` }) })] })) : (_jsx("span", { className: "panel-row__device-empty body-3", children: emptyLabel ?? t('customers.device.notRegistered', '미등록') }))] }, side));
|
|
147
154
|
}) }));
|
|
148
155
|
}
|
|
@@ -303,7 +303,7 @@ export default function PassCard({ product, showPrice = false, plan, type, endDa
|
|
|
303
303
|
...(onDelete ? [{ label: t('passCard.menu.delete', '삭제하기'), icon: 'trash-2', onClick: () => { onDelete(); setMenuAnchor(null); }, closeOnSelect: true }] : []),
|
|
304
304
|
],
|
|
305
305
|
}];
|
|
306
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "pass-card-frame", children: _jsxs("div", { ref: cardRef, className: `pass-card pass-card--${state}${hasMenu ? ' pass-card--clickable' : ''}`, style: cardStyle, onMouseEnter: handleMouseEnter, onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave, onClick: handleCardClick, onKeyDown: hasMenu ? handleCardKeyDown : undefined, role: hasMenu ? 'button' : undefined, tabIndex: hasMenu ? 0 : undefined, children: [bg ?? getDefaultBg(rType, rPlan, rLabel), !bgOnly && (_jsxs(_Fragment, { children: [_jsx("div", { className: "pass-card__top", children: _jsxs("div", { className: "pass-card__top-left", children: [_jsx("img", { src: resolvedLogoSrc ?? LOGO[rType], className: "pass-card__logo", alt: rType === 'manager'
|
|
306
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "pass-card-frame", children: _jsxs("div", { ref: cardRef, className: `pass-card pass-card--${state}${hasMenu ? ' pass-card--clickable' : ''}`, style: cardStyle, onMouseEnter: handleMouseEnter, onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave, onClick: handleCardClick, onKeyDown: hasMenu ? handleCardKeyDown : undefined, role: hasMenu ? 'button' : undefined, tabIndex: hasMenu ? 0 : undefined, "aria-haspopup": hasMenu ? 'menu' : undefined, "aria-expanded": hasMenu ? !!menuAnchor : undefined, children: [bg ?? getDefaultBg(rType, rPlan, rLabel), !bgOnly && (_jsxs(_Fragment, { children: [_jsx("div", { className: "pass-card__top", children: _jsxs("div", { className: "pass-card__top-left", children: [_jsx("img", { src: resolvedLogoSrc ?? LOGO[rType], className: "pass-card__logo", alt: rType === 'manager'
|
|
307
307
|
? t('header.logoManager', '말귀매니저 로고')
|
|
308
308
|
: t('header.logoMalgwi', '말귀 로고') }), _jsxs("div", { className: "pass-card__plan", children: [_jsx("div", { children: planDisplay }), _jsx("span", { className: "h5b pass-card__plan-suffix", children: rLabel ? t('passCard.labeledPlan', { label: rLabel }) : t('passCard.planSuffix', '이용권') })] })] }) }), rPrice
|
|
309
309
|
? (_jsxs("div", { className: "pass-card__price-area", children: [rDiscountLabel && _jsx("span", { className: "pass-card__discount body-3", children: rDiscountLabel }), _jsxs("div", { className: "pass-card__price", children: [rOriginalPrice && _jsx("span", { className: "pass-card__price-original body-3", children: toWonSymbol(rOriginalPrice) }), priceDisplay] })] }))
|
|
@@ -8,6 +8,13 @@ import Button from '../Button';
|
|
|
8
8
|
import Checkbox from '../Checkbox';
|
|
9
9
|
import Badge from '../Badge';
|
|
10
10
|
import { deviceKindVariant } from '../../utils/deviceKind';
|
|
11
|
+
/* 기기 종류 배지 이름은 지금 언어로 — 값은 저장된 한국어(보청기·인공와우)라 그대로 보이면 영어 화면에도 한국어였다.
|
|
12
|
+
색(deviceKindVariant)은 저장값으로 가른다. 모르는 값은 그대로 */
|
|
13
|
+
const 기기종류열쇠 = { '보청기': 'hearingAid', '인공와우': 'cochlearImplant' };
|
|
14
|
+
function 기기종류이름(device, t) {
|
|
15
|
+
const k = 기기종류열쇠[device];
|
|
16
|
+
return k ? t(`device.type.${k}`, { defaultValue: device }) : device;
|
|
17
|
+
}
|
|
11
18
|
import { useTablePaging, useTableSort } from '../../hooks/useTablePaging';
|
|
12
19
|
import './DeviceListPopup.css';
|
|
13
20
|
/**
|
|
@@ -72,7 +79,7 @@ export default function DeviceSideTable({ rows, onSelect, onDelete, onWear, onRe
|
|
|
72
79
|
/* 종류는 위 패널의 종류 배지와 같은 모양으로 — 같은 값을 두 곳에서 다르게
|
|
73
80
|
그리면 같은 것인 줄 모른다. 값이 비면 배지도 세우지 않는다(빈 배지는
|
|
74
81
|
'무언가 있다'는 거짓 신호다). */
|
|
75
|
-
{ content: row.device ? _jsx(Badge, { size: "small", variant: deviceKindVariant(row.device), fill: "soft", label: row.device }) : _jsx("span", {}) },
|
|
82
|
+
{ content: row.device ? _jsx(Badge, { size: "small", variant: deviceKindVariant(row.device), fill: "soft", label: 기기종류이름(row.device, t) }) : _jsx("span", {}) },
|
|
76
83
|
{ content: _jsx("span", { children: row.brand }) },
|
|
77
84
|
{ content: _jsx("span", { children: row.fitType || '-' }) },
|
|
78
85
|
{ content: _jsx("span", { children: row.productName || '-' }) },
|
|
@@ -82,7 +82,9 @@ export default function LoadTablePopup({ onLoad, onCancel, tables: tablesProp, o
|
|
|
82
82
|
animateThen(() => onLoad(table));
|
|
83
83
|
}, [tables, selected, onLoad]);
|
|
84
84
|
const headerActions = useMemo(() => tables.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Dropdown, { options: VIEW_OPTIONS, value: viewMode, onChange: handleSetViewMode }), _jsx(Dropdown, { options: SORT_OPTIONS, value: sortKey, onChange: handleSetSortKey })] })) : undefined, [tables.length, viewMode, sortKey, handleSetViewMode, handleSetSortKey, VIEW_OPTIONS, SORT_OPTIONS]);
|
|
85
|
-
return (_jsx(Popup, { className: "popup--load-table", title: t('wordTable.loadTable.title', '저장된 단어 표 불러오기'), headerActions: headerActions, onClose: onCancel, footer: ({ animateThen, closeWithAnimation }) => (_jsxs("div", { className: "load-table-popup__footer-actions", children: [_jsx(Button, { htmlType: "button", size: 48, type: "bar", label: t('common.cancel', '취소'), onClick: closeWithAnimation, width: "fit-content" }), _jsx(Button, { htmlType: "button", size: 48, type: "bar", variant: "main", label: t('wordTable.loadTable.loadButton', '불러오기'), width: "fit-content",
|
|
85
|
+
return (_jsx(Popup, { className: "popup--load-table", title: t('wordTable.loadTable.title', '저장된 단어 표 불러오기'), headerActions: headerActions, onClose: onCancel, footer: ({ animateThen, closeWithAnimation }) => (_jsxs("div", { className: "load-table-popup__footer-actions", children: [_jsx(Button, { htmlType: "button", size: 48, type: "bar", label: t('common.cancel', '취소'), onClick: closeWithAnimation, width: "fit-content" }), _jsx(Button, { htmlType: "button", size: 48, type: "bar", variant: "main", label: t('wordTable.loadTable.loadButton', '불러오기'), width: "fit-content",
|
|
86
|
+
// 고른 표가 지금 목록에 있을 때만 — 부모 목록에서 빠진 표를 가리키면 눌러도 아무 일이 없었다
|
|
87
|
+
disabled: !selected || !tables.some(t => t.id === selected), onClick: () => handleLoadSelected(animateThen) })] })), children: _jsx("div", { className: "load-table-popup__body", children: tables.length === 0 ? (_jsx("p", { className: "load-table-popup__empty body-2", children: t('wordTable.loadTable.empty', '저장된 단어 표가 없습니다.') })) : (_jsxs(_Fragment, { children: [viewMode === 'grid' && (_jsx("ul", { className: "load-table-popup__grid", children: sorted.map(table => (_jsxs("li", { className: `load-table-popup__card${selected === table.id ? ' load-table-popup__card--selected' : ''}`, onClick: () => setSelected(table.id), ...pressable(() => setSelected(table.id)), children: [_jsx(DeleteButton, { onClick: e => handleDelete(table.id, e), ariaLabel: t('wordTable.loadTable.deleteAriaLabel', '삭제') }), _jsx(TableThumbnail, { table: table }), _jsxs("div", { className: "load-table-popup__card-info", children: [_jsx("span", { className: "load-table-popup__card-name body-2", children: table.name }), _jsxs("span", { className: "load-table-popup__card-meta caption-1", children: [formatDate(table.savedAt), " \u00B7 ", t('wordTable.loadTable.itemCount', { count: table.items.length, defaultValue: '{{count}}개' })] })] })] }, table.id))) })), viewMode === 'list' && (_jsx("ul", { className: "load-table-popup__list", children: sorted.map(table => (_jsxs("li", { className: `load-table-popup__row${selected === table.id ? ' load-table-popup__row--selected' : ''}`, onClick: () => setSelected(table.id), ...pressable(() => setSelected(table.id)), children: [_jsx("span", { className: "load-table-popup__row-check", children: selected === table.id && (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", children: _jsx("polyline", { points: "4 13 9 18 20 6", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }) })) }), _jsxs("div", { className: "load-table-popup__row-main", children: [_jsx("span", { className: "load-table-popup__row-name body-2", children: table.name }), _jsx("div", { className: "load-table-popup__row-words", children: table.items.map((item, i) => (_jsx("span", { className: "load-table-popup__row-word caption-1", children: (item.words ?? []).length >= 2
|
|
86
88
|
? `${item.words[0].label} · ${item.words[1].label}`
|
|
87
89
|
: item.words?.[0]?.label ?? item.id }, i))) })] }), _jsx("span", { className: "load-table-popup__row-count caption-1", children: t('wordTable.loadTable.itemCount', { count: table.items.length, defaultValue: '{{count}}개' }) }), _jsx("span", { className: "load-table-popup__row-date caption-1", children: formatDate(table.savedAt) }), _jsx(DeleteButton, { onClick: e => handleDelete(table.id, e), ariaLabel: t('wordTable.loadTable.deleteAriaLabel', '삭제') })] }, table.id))) }))] })) }) }));
|
|
88
90
|
}
|
|
@@ -35,6 +35,16 @@
|
|
|
35
35
|
.popup-panel--medium { max-width: 680px; }
|
|
36
36
|
.popup-panel--large { max-width: 1080px; }
|
|
37
37
|
.popup-panel--xlarge { width: calc(100% - 80px); max-width: 1320px; }
|
|
38
|
+
/* height prop — 변수로 받는다. 클래스 하나라 앱이 `.my-popup .popup-panel { height }` 로 (미디어별로) 이길 수 있다 */
|
|
39
|
+
.popup-panel--has-height { height: var(--popup-height); }
|
|
40
|
+
/* fillBody — 본문은 굴리지 않고 안쪽이 높이를 물려받는다(안의 한 칸만 굴린다) */
|
|
41
|
+
.popup-panel--fill-body .popup-content { overflow: hidden; }
|
|
42
|
+
.popup-panel--fill-body .popup-content__inner {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
min-height: 0;
|
|
46
|
+
height: 100%;
|
|
47
|
+
}
|
|
38
48
|
/* fit — 내용 폭. 최소·최대 모두 화면 폭 - 40px 을 넘지 않는다(좁은 화면에서 480px 로 잘리지 않게) */
|
|
39
49
|
.popup-panel--fit {
|
|
40
50
|
width: fit-content;
|
|
@@ -89,6 +89,11 @@ export interface PopupProps {
|
|
|
89
89
|
* 그대로 걸리므로 창이 화면 밖으로 나가지 않는다. 넘치는 내용은 본문이 스스로 굴린다.
|
|
90
90
|
*/
|
|
91
91
|
height?: number | string;
|
|
92
|
+
/**
|
|
93
|
+
* 본문이 패널 높이를 채운다 — 본문 자신은 굴리지 않고, 안쪽을 세로 flex(높이 100%)로 넘겨 **안의 한 칸만** 굴리게 한다.
|
|
94
|
+
* `height` 로 창 높이를 박고 가운데 목록(`Panel scrollable` 등)만 굴러가야 하는 창에 쓴다.
|
|
95
|
+
*/
|
|
96
|
+
fillBody?: boolean;
|
|
92
97
|
/**
|
|
93
98
|
* 이 값이 바뀌면 본문(.popup-content) 스크롤을 맨 위로 되돌린다.
|
|
94
99
|
* 탭/모드 전환처럼 내부 콘텐츠가 통째로 바뀌는 팝업에서, 스크롤을 내린 채
|
|
@@ -13,7 +13,7 @@ const ANIM_MS = 260;
|
|
|
13
13
|
/** @public */
|
|
14
14
|
export default function Popup(props) {
|
|
15
15
|
const { t } = useTranslation();
|
|
16
|
-
const { title, subtitle, headerActions, children, buttons, footer, footerClassName = "", onClose, closeDisabled = false, beforeClose, closeOnBackdrop = true, className = "", ariaLabel, useTitle = true, inline = false, size, layer = 'modal', height, resetScrollKey } = props || {};
|
|
16
|
+
const { title, subtitle, headerActions, children, buttons, footer, footerClassName = "", onClose, closeDisabled = false, beforeClose, closeOnBackdrop = true, className = "", ariaLabel, useTitle = true, inline = false, size, layer = 'modal', height, fillBody = false, resetScrollKey } = props || {};
|
|
17
17
|
const [closing, setClosing] = useState(false);
|
|
18
18
|
// 닫힘 애니메이션 중(260ms)엔 버튼이 시각적으로만 흐려질 뿐 클릭은 계속 가능해,
|
|
19
19
|
// 그 사이 같은 버튼을 연타하면 cb(확인/완료 콜백)가 여러 번 예약되어 중복 실행된다.
|
|
@@ -159,9 +159,10 @@ export default function Popup(props) {
|
|
|
159
159
|
거절)을 누르면 포커스가 body 로 떨어져, 이어서 누른 Tab 이 뒤 화면으로 나갔다(가둠은 판 안의 키만 듣는다).
|
|
160
160
|
mousedown 의 기본 동작(포커스 옮김)만 막고 click 은 그대로 온다. */
|
|
161
161
|
onMouseDown: inline ? undefined : (e) => { if (e.target === e.currentTarget)
|
|
162
|
-
e.preventDefault(); }, children: _jsxs("div", { ref: panelRef, className: `popup-panel${size ? ` popup-panel--${size}` : ''}`,
|
|
163
|
-
/*
|
|
164
|
-
|
|
162
|
+
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' : ''}`,
|
|
163
|
+
/* 높이는 **CSS 변수로** 넘긴다 — 인라인 height 로 박으면 앱의 미디어 규칙(좁은 화면만 다른 높이)이 못 이겼다.
|
|
164
|
+
max-height(CSS)가 height 보다 이기므로 화면보다 크게 줘도 화면에 맞춰진다 */
|
|
165
|
+
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 && title ? titleId : undefined, "aria-label": !inline && !title && ariaLabel ? ariaLabel : 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", 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${footerClassName ? ` ${footerClassName}` : ''}`, children: typeof footer === 'function' ? footer({ animateThen, closeWithAnimation }) : footer })) : null] }) }));
|
|
165
166
|
if (inline || typeof document === 'undefined')
|
|
166
167
|
return el;
|
|
167
168
|
return createPortal(el, document.body);
|
|
@@ -64,8 +64,13 @@ export default function TrainingResultPopup(props) {
|
|
|
64
64
|
useEffect(() => {
|
|
65
65
|
const total = contextsLength ?? 0;
|
|
66
66
|
if (score != null && total > 0 && score / total > 0.5) {
|
|
67
|
-
|
|
67
|
+
// 한 틱 미뤄 보내고 떠나면 걷는다 — StrictMode 의 두 번 실행에서 폭죽 신호가 두 번 나갔다
|
|
68
|
+
const id = window.setTimeout(() => {
|
|
69
|
+
window.dispatchEvent(new CustomEvent('party:trigger', { detail: { count: 140, strength: 1.6, duration: 2200 } }));
|
|
70
|
+
}, 0);
|
|
71
|
+
return () => window.clearTimeout(id);
|
|
68
72
|
}
|
|
73
|
+
return undefined;
|
|
69
74
|
}, [score, contextsLength]);
|
|
70
75
|
const getJudge = useCallback((it) => {
|
|
71
76
|
const decision = resolveDecision(it);
|
|
@@ -299,7 +299,10 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
299
299
|
? { '--table-grid-cols': effectiveGrid }
|
|
300
300
|
: undefined;
|
|
301
301
|
const hasHeader = title !== undefined;
|
|
302
|
-
return (_jsx("div", { className: `table-wrap table-wrap--${size}${폭조절 ? ' table-wrap--resizable' : ''}${dragOver ? ' table-wrap--dragover' : ''}${collapsed ? ' table-wrap--collapsed' : ''}`, style: gridStyle, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop, children: _jsxs("div", { className: "table body-2", children: [hasHeader && (_jsxs("div", { className: "table-header", onClick: onToggleCollapse, role: onToggleCollapse ? 'button' : undefined, tabIndex: onToggleCollapse ? 0 : undefined, "aria-expanded": onToggleCollapse ? !collapsed : undefined,
|
|
302
|
+
return (_jsx("div", { className: `table-wrap table-wrap--${size}${폭조절 ? ' table-wrap--resizable' : ''}${dragOver ? ' table-wrap--dragover' : ''}${collapsed ? ' table-wrap--collapsed' : ''}`, style: gridStyle, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop, children: _jsxs("div", { className: "table body-2", children: [hasHeader && (_jsxs("div", { className: "table-header", onClick: onToggleCollapse, role: onToggleCollapse ? 'button' : undefined, tabIndex: onToggleCollapse ? 0 : undefined, "aria-expanded": onToggleCollapse ? !collapsed : undefined,
|
|
303
|
+
// 머리 안 단추(headerActions)에서 누른 키는 그 단추 몫이다 — 가로채면 표가 접히고 단추 동작은 막혔다(TableRow 와 같게)
|
|
304
|
+
onKeyDown: onToggleCollapse ? (e) => { if (e.target !== e.currentTarget)
|
|
305
|
+
return; if (e.key === 'Enter' || e.key === ' ') {
|
|
303
306
|
e.preventDefault();
|
|
304
307
|
onToggleCollapse();
|
|
305
308
|
} } : undefined, children: [onToggleCollapse && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-right' : 'chevron-down', ariaLabel: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'), className: "table-header__chevron" })), icon && (_jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "table-header__icon" })), _jsx("span", { className: "table-header__title h5b", children: title }), count !== undefined && _jsx("span", { className: "table-header__count body-3b", children: count }), headerActions && (_jsx("span", { className: "table-header__actions", onClick: e => e.stopPropagation(), children: headerActions }))] })), !collapsed && (_jsxs(_Fragment, { children: [headerGroups && headerGroups.length > 0 && (_jsx("div", { className: "table-fields table-fields--groups", children: headerGroups.map((g, i) => (_jsx("div", { className: cx('table-field body-3', g.className), style: { gridColumn: `span ${g.span}` }, children: g.label }, i))) })), _jsxs("div", { className: cx('table-fields-strip', 폭조절 && 'table-fields-strip--resizable'), children: [_jsx("div", { className: "table-fields", ref: fieldsRef, children: fields.map((f, i) => (_jsx(TableField, { onClick: f.onClick, className: f.className, sortDir: f.sortDir,
|