@ihabdevteam/core 0.136.0 → 0.137.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 +53 -0
- package/dist/components.cjs.js +176 -176
- 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/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/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/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/TextField.js +3 -1
- package/dist/packages/core/src/components/Tooltip/DescriptionTooltip.js +1 -0
- package/dist/packages/core/src/components/Training/TrainingBlockButton.js +10 -4
- package/dist/packages/core/src/components/Training/TrainingCardGroup.js +1 -0
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +28 -1
- package/dist/packages/core/src/components/Training/TrainingStepCard.js +7 -1
- 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/TrainingWordCard.js +21 -4
- 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 +4 -0
- package/dist/packages/core/src/components/Training/useTTSPlayback.js +5 -1
- package/dist/packages/core/src/components/WordTable.js +19 -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/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/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.137.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 */
|
|
@@ -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
|
}
|
|
@@ -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 || '-' }) },
|
|
@@ -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);
|
|
@@ -8,6 +8,8 @@ import Icon from './Icon';
|
|
|
8
8
|
import Button from './Button';
|
|
9
9
|
import { useComboboxSuggestions } from './useComboboxSuggestions';
|
|
10
10
|
import './TextField.css';
|
|
11
|
+
/* 서버 렌더에서는 layout effect 가 돌지 않고 React 18 은 경고를 찍는다 — 브라우저에서만 layout, 서버에서는 effect */
|
|
12
|
+
const useIsoLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
|
11
13
|
/** @public */
|
|
12
14
|
export default function TextField(props) {
|
|
13
15
|
const { value, defaultValue, name, inputRef, onChange, label, ariaLabel, required, placeholder, type = 'text', disabled = false, readOnly = false, error, helperText, maxLength, size = 48, variant = 'input', fullWidth = false, newline = false, multiline = false, rows = 3, leftIcon, rightIcon, clearable = false, list, showCount = true, className = '', onFocus, onBlur, onKeyDown, onEnter, inputMode, autoComplete, autoFocus, suggestions, openOnFocus = false, } = props;
|
|
@@ -61,7 +63,7 @@ export default function TextField(props) {
|
|
|
61
63
|
돌아, 부모가 같은 값을 다시 두면(공백을 걷는 등) 화면 칸에 친 글자가 남았다. React 가 일반 칸에
|
|
62
64
|
해 주는 되돌림(restoreControlledState)을 onChange 뒤 한 박자에 흉내 낸다. */
|
|
63
65
|
const valueRef = useRef(value);
|
|
64
|
-
|
|
66
|
+
useIsoLayoutEffect(() => { valueRef.current = value; });
|
|
65
67
|
/*
|
|
66
68
|
* React 는 controlled input 의 값을 node.defaultValue(= HTML value 속성)에 동기화한다.
|
|
67
69
|
* 일반 입력에서는 무해하지만 비밀번호에서는 그 속성이 outerHTML 직렬화 경로
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { getTopicTypes, getTrainingTypes } from '../../utils/categories';
|
|
6
|
+
import { TRAINING_TYPE_I18N_KEY } from '../../utils/CategoryData';
|
|
6
7
|
import Icon from '../Icon';
|
|
7
8
|
import Button from '../Button';
|
|
8
9
|
import CategoryIcon from './CategoryIcon';
|
|
@@ -93,9 +94,11 @@ export default function TrainingBlockButton(props) {
|
|
|
93
94
|
const c = topic.iconColor;
|
|
94
95
|
wrapperStyle = { ['--t-item-fg']: c };
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
+
// 카테고리 이름도 지금 언어로(topic.* · topic.sub.*, 사전에 없으면 등록된 이름)
|
|
98
|
+
mainName = parent ? t(`topic.${parent.key}`, { defaultValue: parent.label || '' }) : '';
|
|
97
99
|
try {
|
|
98
|
-
|
|
100
|
+
const sub = parent?.subs?.find((s) => s.key === subKey);
|
|
101
|
+
subName = sub ? t(`topic.sub.${sub.key}`, { defaultValue: sub.label || '' }) : '';
|
|
99
102
|
}
|
|
100
103
|
catch (_) { }
|
|
101
104
|
}
|
|
@@ -172,8 +175,11 @@ export default function TrainingBlockButton(props) {
|
|
|
172
175
|
const isList = size === 'list' || isRow;
|
|
173
176
|
const isCompact = size === 'compact';
|
|
174
177
|
const isSmall = size === 'small';
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
// 유형 이름은 지금 언어로 — 등록된 원본의 한국어 라벨을 써 영어 화면 계획 바구니에 '소음 하 문장 듣기' 가 떴다
|
|
179
|
+
const 유형원본 = isSmall && type?.key ? getTrainingTypes().find(x => x.key === type.key) : undefined;
|
|
180
|
+
const 유형열쇠 = 유형원본 ? TRAINING_TYPE_I18N_KEY[유형원본.key] : undefined;
|
|
181
|
+
const trainingTypeName = 유형원본
|
|
182
|
+
? (유형열쇠 ? t(`trainingType.${유형열쇠}.title`, { defaultValue: 유형원본.label }) : (유형원본.label ?? ''))
|
|
177
183
|
: '';
|
|
178
184
|
// 타입 라벨이 실제로 렌더되는지(타이틀과 같으면 숨김) — 레이아웃 여백 정리에 사용.
|
|
179
185
|
const typeLabelRendered = (!!showTypeLabel && !!type?.label && type.label !== String(itemTitle)) ||
|