@ihabdevteam/core 0.135.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 +128 -0
- package/dist/components.cjs.js +177 -177
- package/dist/components.esm.css +3 -3
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core.cjs.js +181 -181
- package/dist/ihabdevteam-core.d.ts +38 -7
- package/dist/packages/core/src/components/Alert.js +1 -1
- package/dist/packages/core/src/components/Breadcrumb.js +1 -0
- package/dist/packages/core/src/components/Button.css +5 -0
- package/dist/packages/core/src/components/Button.js +3 -3
- package/dist/packages/core/src/components/Checkbox.js +10 -2
- package/dist/packages/core/src/components/CodeBox.js +2 -1
- package/dist/packages/core/src/components/Effect/EffectPartyPopper.js +5 -2
- package/dist/packages/core/src/components/Icon.js +4 -1
- package/dist/packages/core/src/components/IconGridMenu.js +25 -22
- 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/ListPopup.js +3 -1
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +6 -5
- package/dist/packages/core/src/components/Popup/Popup.css +19 -0
- package/dist/packages/core/src/components/Popup/Popup.d.ts +14 -1
- package/dist/packages/core/src/components/Popup/Popup.js +6 -5
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +6 -1
- package/dist/packages/core/src/components/Popup/customerDetailUtils.js +5 -3
- package/dist/packages/core/src/components/ProgressBar.js +5 -2
- package/dist/packages/core/src/components/ProgressIndicator.js +7 -1
- 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/Rating.js +3 -1
- package/dist/packages/core/src/components/SideNav.js +6 -3
- package/dist/packages/core/src/components/SplashLogo.js +1 -2
- package/dist/packages/core/src/components/StatChart.js +1 -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/TrainingTooltip.js +6 -5
- package/dist/packages/core/src/components/Training/TrainingBlockButton.css +57 -0
- package/dist/packages/core/src/components/Training/TrainingBlockButton.js +15 -7
- package/dist/packages/core/src/components/Training/TrainingCardGroup.js +1 -0
- package/dist/packages/core/src/components/Training/TrainingPagePopupTitle.js +2 -1
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +28 -1
- package/dist/packages/core/src/components/Training/TrainingStepCard.js +10 -3
- package/dist/packages/core/src/components/Training/TrainingStepCardOrder.js +36 -26
- 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 +7 -4
- package/dist/packages/core/src/components/Training/TrainingStepTitle.js +4 -1
- package/dist/packages/core/src/components/Training/TrainingWordCard.js +21 -4
- package/dist/packages/core/src/components/Training/WordTablePopup.js +9 -3
- 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/trainingBlockButtonTypes.d.ts +3 -1
- package/dist/packages/core/src/components/Training/trainingMissStats.js +9 -2
- 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/Word/WordTableBlock.js +5 -2
- package/dist/packages/core/src/components/Word/WordTableButton.js +3 -0
- package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +5 -1
- package/dist/packages/core/src/components/Word/wordScoreStats.js +7 -2
- package/dist/packages/core/src/components/Word/wordTablePrint.js +15 -2
- package/dist/packages/core/src/components/WordTable.js +19 -5
- package/dist/packages/core/src/components/buttonTypes.d.ts +6 -0
- package/dist/packages/core/src/components/useCalendarMonth.js +15 -17
- package/dist/packages/core/src/components/useToastLifecycle.js +7 -2
- package/dist/packages/core/src/context/I18nProvider.js +12 -3
- 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/useSoundCompassExam.js +13 -2
- 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 +42 -4
- package/dist/packages/core/src/i18n/locales/ko.json +42 -4
- package/dist/packages/core/src/utils/date.js +26 -5
- package/dist/packages/core/src/utils/errorMessage.js +7 -1
- package/dist/packages/core/src/utils/speak.js +23 -3
- package/dist/packages/core/src/utils/textSort.js +12 -1
- package/dist/packages/core/src/utils/ttsConfig.js +10 -8
- 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';
|
|
@@ -773,6 +774,12 @@ export declare interface ButtonProps {
|
|
|
773
774
|
title?: string;
|
|
774
775
|
items: string[];
|
|
775
776
|
} | null;
|
|
777
|
+
/**
|
|
778
|
+
* 누르는 단추가 아니라 **툴팁을 보여 주는 칩** 이다 — 커서를 손가락이 아닌 기본 화살표로 둔다.
|
|
779
|
+
* 모양(바탕·hover)과 포커스·툴팁은 그대로다. `onClick` 없이 `showTooltipInfo`·`tooltipDescription` 만 다는
|
|
780
|
+
* 자리(점수 칩 등)에 쓴다. 막힌 단추(`disabled`)와는 다르다 — 흐려지지 않는다.
|
|
781
|
+
*/
|
|
782
|
+
tooltipOnly?: boolean;
|
|
776
783
|
iconSize?: 'small' | 'medium' | 'large';
|
|
777
784
|
iconColor?: string;
|
|
778
785
|
/**
|
|
@@ -1369,7 +1376,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1369
1376
|
*
|
|
1370
1377
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1371
1378
|
*/
|
|
1372
|
-
export declare const CORE_VERSION = "0.
|
|
1379
|
+
export declare const CORE_VERSION = "0.137.0";
|
|
1373
1380
|
|
|
1374
1381
|
/** @public */
|
|
1375
1382
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -2693,7 +2700,7 @@ export declare interface LandingBottomStickyProps {
|
|
|
2693
2700
|
*
|
|
2694
2701
|
* @public
|
|
2695
2702
|
*/
|
|
2696
|
-
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;
|
|
2697
2704
|
|
|
2698
2705
|
/**
|
|
2699
2706
|
* 가운데 내비 링크 한 개.
|
|
@@ -2929,7 +2936,7 @@ export declare interface MediaQueryOverride {
|
|
|
2929
2936
|
}
|
|
2930
2937
|
|
|
2931
2938
|
/** @public */
|
|
2932
|
-
export declare const MediaQueryOverrideContext: Context<MediaQueryOverride | null>;
|
|
2939
|
+
export declare const MediaQueryOverrideContext: React_3.Context<MediaQueryOverride | null>;
|
|
2933
2940
|
|
|
2934
2941
|
/** @public */
|
|
2935
2942
|
export declare function MenuTooltip({ columns, onClose, className, size }: {
|
|
@@ -3344,7 +3351,7 @@ export declare interface PairStats {
|
|
|
3344
3351
|
total: number;
|
|
3345
3352
|
}
|
|
3346
3353
|
|
|
3347
|
-
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;
|
|
3348
3355
|
|
|
3349
3356
|
/** @public */
|
|
3350
3357
|
export declare type PanelElement = HTMLDivElement & {
|
|
@@ -3385,6 +3392,16 @@ declare interface PanelProps {
|
|
|
3385
3392
|
maxWidth?: number | string;
|
|
3386
3393
|
className?: string;
|
|
3387
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;
|
|
3388
3405
|
/** 외곽선 표시 여부. 기본 true */
|
|
3389
3406
|
useBorder?: boolean;
|
|
3390
3407
|
/** 배경(카드 흰 배경) 표시 여부. 기본 true. false면 투명 배경으로 페이지에 녹아든다 */
|
|
@@ -4150,9 +4167,17 @@ export declare interface PopupProps {
|
|
|
4150
4167
|
* - `small` 480px (확인/알림류)
|
|
4151
4168
|
* - `medium` 680px (입력 폼)
|
|
4152
4169
|
* - `large` 1080px (리스트/다중 콘텐츠)
|
|
4170
|
+
* - `xlarge` 1320px, 폭은 화면 - 80px(좌우 40px) — 훈련 카드 만들기처럼 화면을 넓게 쓰는 창
|
|
4171
|
+
* - `fit` 내용 폭에 맞춘다 — 최소 480px·최대 1140px(둘 다 화면 폭 - 40px 을 넘지 않는다).
|
|
4172
|
+
* 한 줄에 카드가 몇 장 들어가느냐로 폭이 정해지는 목록 창에 쓴다.
|
|
4153
4173
|
* 더 넓거나 무제한이 필요하면 className으로 `.popup-panel` max-width를 직접 override.
|
|
4154
4174
|
*/
|
|
4155
|
-
size?: 'small' | 'medium' | 'large';
|
|
4175
|
+
size?: 'small' | 'medium' | 'large' | 'xlarge' | 'fit';
|
|
4176
|
+
/**
|
|
4177
|
+
* 쌓이는 층. 기본 `modal`(`--z-modal`) 은 훈련 페이지 팝업(`--z-page-popup`) 위에 뜬다.
|
|
4178
|
+
* `belowPagePopup` 은 그 **바로 아래** 에 깐다 — 이 창에서 연 훈련 팝업이 이 창을 덮어야 할 때(실행 창 등).
|
|
4179
|
+
*/
|
|
4180
|
+
layer?: 'modal' | 'belowPagePopup';
|
|
4156
4181
|
/**
|
|
4157
4182
|
* 패널 높이를 못 박는다(숫자는 px, 문자열은 CSS 길이 그대로). 미지정 시 내용만큼(기본).
|
|
4158
4183
|
*
|
|
@@ -4161,6 +4186,11 @@ export declare interface PopupProps {
|
|
|
4161
4186
|
* 그대로 걸리므로 창이 화면 밖으로 나가지 않는다. 넘치는 내용은 본문이 스스로 굴린다.
|
|
4162
4187
|
*/
|
|
4163
4188
|
height?: number | string;
|
|
4189
|
+
/**
|
|
4190
|
+
* 본문이 패널 높이를 채운다 — 본문 자신은 굴리지 않고, 안쪽을 세로 flex(높이 100%)로 넘겨 **안의 한 칸만** 굴리게 한다.
|
|
4191
|
+
* `height` 로 창 높이를 박고 가운데 목록(`Panel scrollable` 등)만 굴러가야 하는 창에 쓴다.
|
|
4192
|
+
*/
|
|
4193
|
+
fillBody?: boolean;
|
|
4164
4194
|
/**
|
|
4165
4195
|
* 이 값이 바뀌면 본문(.popup-content) 스크롤을 맨 위로 되돌린다.
|
|
4166
4196
|
* 탭/모드 전환처럼 내부 콘텐츠가 통째로 바뀌는 팝업에서, 스크롤을 내린 채
|
|
@@ -6863,8 +6893,10 @@ export declare interface TrainingBlockButtonProps {
|
|
|
6863
6893
|
* - `small`: 120×120px 축소 카드 (행 펼치기 영역 등 좁은 공간용)
|
|
6864
6894
|
* - `list`: 너비 200px · 최소 높이 68px 가로형 항목
|
|
6865
6895
|
* - `compact`: 툴팁 등 좁은 공간용 소형 리스트 항목 (전체 너비 × 높이 40px, 작은 아이콘)
|
|
6896
|
+
* - `row`: 전체 너비 가로 행 — [아이콘][유형·제목][문항수]. 미리 듣기 단추는 없다.
|
|
6897
|
+
* 좁은 화면에서 정사각형 카드 대신 쓴다(카드는 폭이 줄면 내용이 눌린다). 유형 줄은 `showTypeLabel` 일 때.
|
|
6866
6898
|
*/
|
|
6867
|
-
size?: 'square' | 'small' | 'list' | 'compact';
|
|
6899
|
+
size?: 'square' | 'small' | 'list' | 'compact' | 'row';
|
|
6868
6900
|
renderPopup?: (props: TrainingBlockButtonPopupProps) => ReactNode;
|
|
6869
6901
|
completedIds?: Set<string>;
|
|
6870
6902
|
/** 선택 상태 ID Set. 포함된 항목(완료가 아닌 것)에 체크 배지를 표시 — 완료와 같은 배지이고, `hideCheckBadge` 면 숨는다. */
|
|
@@ -7518,7 +7550,6 @@ export declare type TrainingStepListenProps = {
|
|
|
7518
7550
|
showNoiseWaveForm?: boolean;
|
|
7519
7551
|
};
|
|
7520
7552
|
|
|
7521
|
-
/** @public */
|
|
7522
7553
|
export declare function TrainingStepOrderList({ index, stepIndex, isLast, title, onDone, onChange, context, }: TrainingStepOrderListProps): JSX.Element;
|
|
7523
7554
|
|
|
7524
7555
|
/** @public */
|
|
@@ -24,5 +24,5 @@ export default function Alert({ tone = 'info', title, message, children, dismiss
|
|
|
24
24
|
const cls = cx('alert', `alert--${tone}`, className);
|
|
25
25
|
const hasTitle = Boolean(title);
|
|
26
26
|
const hasMessage = Boolean(message || children);
|
|
27
|
-
return (_jsxs("div", { className: cls, role: "alert", children: [_jsxs("div", { className: "alert__body", children: [_jsxs("div", { className: "alert__header", children: [_jsx(Icon, { icon: TONE_ICON[tone], size: 20, className: "alert__icon" }), hasTitle && _jsx("span", { className: "alert__title body-2", children: title }), !hasTitle && hasMessage && _jsx("div", { className: "alert__message body-3", children: message
|
|
27
|
+
return (_jsxs("div", { className: cls, role: "alert", children: [_jsxs("div", { className: "alert__body", children: [_jsxs("div", { className: "alert__header", children: [_jsx(Icon, { icon: TONE_ICON[tone], size: 20, className: "alert__icon" }), hasTitle && _jsx("span", { className: "alert__title body-2", children: title }), !hasTitle && hasMessage && _jsx("div", { className: "alert__message body-3", children: message || children })] }), hasTitle && hasMessage && (_jsx("div", { className: "alert__message alert__message--indented body-3", children: message || children }))] }), dismissible && (_jsx("button", { type: "button", className: "alert__dismiss", onClick: handleDismiss, "aria-label": t('common.close', '닫기'), children: "\u00D7" }))] }));
|
|
28
28
|
}
|
|
@@ -8,7 +8,7 @@ import InfoTooltip from "./Tooltip/InfoTooltip";
|
|
|
8
8
|
import DescriptionTooltip from "./Tooltip/DescriptionTooltip";
|
|
9
9
|
/** @public */
|
|
10
10
|
export default function Button(props) {
|
|
11
|
-
const { size = 48, type: explicitType, htmlType, href, target, rel, icon, rightIcon, label: labelProp, count, subLabel, children, ariaLabel, variant = 'default', align = 'center', selected, toggleable = false, disabled = false, useLabel = true, useBorder = true, useBG = true, useDisabledCovered = false, useDisabledInvisible = false, width, minWidth, onClick, onDisabledClick, showTooltipInfo = false, tooltipInfoText = null, tooltipDescription = null, iconSize = 'medium', iconColor, shape = 'rounded', color, className, ...rest } = props;
|
|
11
|
+
const { size = 48, type: explicitType, htmlType, href, target, rel, icon, rightIcon, label: labelProp, count, subLabel, children, ariaLabel, variant = 'default', align = 'center', selected, toggleable = false, disabled = false, useLabel = true, useBorder = true, useBG = true, useDisabledCovered = false, useDisabledInvisible = false, width, minWidth, onClick, onDisabledClick, showTooltipInfo = false, tooltipInfoText = null, tooltipDescription = null, tooltipOnly = false, iconSize = 'medium', iconColor, shape = 'rounded', color, className, ...rest } = props;
|
|
12
12
|
const sizeKey = typeof size === 'string' && isNaN(Number(size))
|
|
13
13
|
? size.toLowerCase()
|
|
14
14
|
: String(Number(size) || 48);
|
|
@@ -85,7 +85,7 @@ export default function Button(props) {
|
|
|
85
85
|
const ariaPressed = (toggleable || typeof selected === 'boolean')
|
|
86
86
|
? (isSelected ? 'true' : 'false')
|
|
87
87
|
: undefined;
|
|
88
|
-
const classes = cx('button', sizeBase, sizeTypeClass, variantClass, alignClass, shapeClass, isSelected ? 'is-selected' : null, !useBG ? 'button--no-bg' : null, className);
|
|
88
|
+
const classes = cx('button', sizeBase, sizeTypeClass, variantClass, alignClass, shapeClass, isSelected ? 'is-selected' : null, !useBG ? 'button--no-bg' : null, tooltipOnly ? 'button--tooltip-only' : null, className);
|
|
89
89
|
const iconClassName = `button__icon button__icon--${iconSize}`;
|
|
90
90
|
const subLabels = Array.isArray(subLabel) ? subLabel : (subLabel !== undefined ? [subLabel] : []);
|
|
91
91
|
const renderLabelContent = () => {
|
|
@@ -180,7 +180,7 @@ export default function Button(props) {
|
|
|
180
180
|
const Root = (isLink ? 'a' : 'button');
|
|
181
181
|
return (_jsxs(_Fragment, { children: [_jsx(Root, { ...restProps, ref: btnRef, ...(isLink
|
|
182
182
|
? { href, target, rel: rel ?? (target === '_blank' ? 'noreferrer' : undefined) }
|
|
183
|
-
: { type: htmlType, disabled: disabledProp && !onDisabledClick }), className: classes, "aria-pressed": ariaPressed, "aria-label": ariaLabelString, "aria-disabled":
|
|
183
|
+
: { type: htmlType, disabled: disabledProp && !onDisabledClick }), className: classes, "aria-pressed": ariaPressed, "aria-label": ariaLabelString, "aria-disabled": disabledProp && (isLink || !!onDisabledClick) ? true : undefined, style: style, onClick: handleClickInternal, onMouseLeave: handleMouseLeave, onPointerEnter: (e) => {
|
|
184
184
|
showTooltipFn();
|
|
185
185
|
if (!disabledProp && typeof rest.onPointerEnter === 'function')
|
|
186
186
|
rest.onPointerEnter(e);
|
|
@@ -7,9 +7,17 @@ import { useEffect, useId, useRef } from 'react';
|
|
|
7
7
|
export default function Checkbox({ checked = false, onChange, disabled, indeterminate, label, size = 'medium', className, title, ariaLabel, }) {
|
|
8
8
|
const ref = useRef(null);
|
|
9
9
|
const id = useId();
|
|
10
|
+
/* 중간 상태는 DOM 속성이라 누르는 순간 브라우저가 false 로 바꾼다. React 는 checked 만 되돌리므로 prop 이 그대로
|
|
11
|
+
true 여도(비활성 행이 있어 전체 선택이 안 되는 등) 빈 칸으로 남았다 — 누른 뒤와 다시 그릴 때마다 다시 넣는다. */
|
|
12
|
+
const indeterminateRef = useRef(!!indeterminate);
|
|
13
|
+
indeterminateRef.current = !!indeterminate;
|
|
10
14
|
useEffect(() => {
|
|
11
15
|
if (ref.current)
|
|
12
16
|
ref.current.indeterminate = !!indeterminate;
|
|
13
|
-
}
|
|
14
|
-
return (_jsxs("label", { className: cx('checkbox', size === 'small' && 'checkbox--small', size === 'large' && 'checkbox--large', disabled && 'checkbox--disabled', className), htmlFor: id, title: title, children: [_jsx("input", { ref: ref, id: id, type: "checkbox", className: "checkbox__input", checked: checked, onChange: e =>
|
|
17
|
+
});
|
|
18
|
+
return (_jsxs("label", { className: cx('checkbox', size === 'small' && 'checkbox--small', size === 'large' && 'checkbox--large', disabled && 'checkbox--disabled', className), htmlFor: id, title: title, children: [_jsx("input", { ref: ref, id: id, type: "checkbox", className: "checkbox__input", checked: checked, onChange: e => {
|
|
19
|
+
onChange?.(e.target.checked);
|
|
20
|
+
if (ref.current)
|
|
21
|
+
ref.current.indeterminate = indeterminateRef.current;
|
|
22
|
+
}, disabled: disabled, "aria-label": !label && ariaLabel ? ariaLabel : undefined }), _jsx("span", { className: "checkbox__box" }), label && _jsx("span", { className: "checkbox__label body-3", children: label })] }));
|
|
15
23
|
}
|
|
@@ -32,7 +32,8 @@ const KEYWORDS = /^(?:const|let|var|function|return|if|else|for|while|switch|cas
|
|
|
32
32
|
function tokenize(code, lang) {
|
|
33
33
|
let s = code;
|
|
34
34
|
const tokens = [];
|
|
35
|
-
|
|
35
|
+
// 이름이 **정확히** 셸일 때만 — 부분 일치라 csharp 도 셸로 읽혀 `//` 주석은 놓치고 `#region` 이 주석이 됐다
|
|
36
|
+
const isBash = /^(?:sh|bash|zsh|shell|console)$/.test(lang.toLowerCase());
|
|
36
37
|
const push = (type, content) => tokens.push({ type, content });
|
|
37
38
|
while (s.length > 0) {
|
|
38
39
|
const ws = s.match(/^\s+/);
|
|
@@ -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
|
|
@@ -176,6 +177,8 @@ onComplete, trigger = 0, }) {
|
|
|
176
177
|
if (now < p.startTime) {
|
|
177
178
|
// waiting to start: keep hidden
|
|
178
179
|
node.style.display = "none";
|
|
180
|
+
// 출발 전 조각도 살아 있다 — 안 세면 첫 틀이 모든 지연보다 이를 때 곧바로 끝나 한 조각도 안 났다
|
|
181
|
+
alive = true;
|
|
179
182
|
continue;
|
|
180
183
|
}
|
|
181
184
|
// initialize origin positioning once when particle starts
|
|
@@ -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')
|
|
@@ -16,33 +16,36 @@ function IconGridMenuButton(props) {
|
|
|
16
16
|
/** @public */
|
|
17
17
|
export default function IconGridMenu(props) {
|
|
18
18
|
const { categories = [], selected, onSelect, size = 'medium' } = props || {};
|
|
19
|
-
/* `null` 은 "고르지 않음" 이라는 제어 값이다 — 첫 카테고리로 갈음하지
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
19
|
+
/* 제어 모드면 **prop 을 그대로** 그린다. `null` 은 "고르지 않음" 이라는 제어 값이다 — 첫 카테고리로 갈음하지 않는다.
|
|
20
|
+
전에는 누르는 순간 안쪽 상태를 바꿔, 부모가 값을 안 바꾸면(같은 selected 를 다시 넘기면) 화면만 다른 단추로 남았다. */
|
|
21
|
+
const isControlled = selected !== undefined;
|
|
22
|
+
const [innerKey, setInnerKey] = useState(() => (isControlled ? null : ((categories && categories[0]?.key) ?? null)));
|
|
23
|
+
const selectedKey = isControlled ? selected : innerKey;
|
|
24
|
+
/* 비제어 모드의 첫 선택 — 목록이 **처음 생겼을 때** 한 번 고르고 부모에 알린다. 마운트 때만 보면 카테고리가 늦게
|
|
25
|
+
오는 경우(`[]` 뒤 목록) 아무것도 안 골라졌고, StrictMode 의 두 번 실행에서는 알림이 두 번 갔다.
|
|
26
|
+
제어 모드에서는 부모가 이미 값을 가지므로 에코하지 않는다(부모 상태를 덮어쓸 수 있다). */
|
|
27
|
+
const onSelectRef = useRef(onSelect);
|
|
28
|
+
useEffect(() => { onSelectRef.current = onSelect; });
|
|
29
|
+
const firstPickDoneRef = useRef(false);
|
|
30
|
+
const firstKey = (categories && categories[0]?.key) ?? null;
|
|
23
31
|
/* eslint-disable react-hooks/set-state-in-effect */
|
|
24
32
|
useEffect(() => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
if (isControlled || firstPickDoneRef.current || !firstKey)
|
|
34
|
+
return;
|
|
35
|
+
firstPickDoneRef.current = true;
|
|
36
|
+
const key = innerKey ?? firstKey;
|
|
37
|
+
if (innerKey == null)
|
|
38
|
+
setInnerKey(key);
|
|
39
|
+
onSelectRef.current?.(key);
|
|
40
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
41
|
+
}, [isControlled, firstKey]);
|
|
31
42
|
/* eslint-enable react-hooks/set-state-in-effect */
|
|
32
|
-
const initialOnSelectRef = useRef(onSelect);
|
|
33
|
-
const initialKeyRef = useRef(selectedKey);
|
|
34
|
-
const isControlledRef = useRef(selected !== undefined);
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
// 비제어(uncontrolled) 모드에서만 초기 선택을 부모에 알림.
|
|
37
|
-
// 제어 모드(selected 전달)에서는 부모가 이미 값을 가지므로 에코하면 의도치 않게 부모 상태를 덮어쓸 수 있음.
|
|
38
|
-
if (!isControlledRef.current && initialKeyRef.current && typeof initialOnSelectRef.current === 'function') {
|
|
39
|
-
initialOnSelectRef.current(initialKeyRef.current);
|
|
40
|
-
}
|
|
41
|
-
}, []);
|
|
42
43
|
const handleClick = useCallback((key) => {
|
|
43
44
|
const next = selectedKey === key ? null : key;
|
|
44
|
-
|
|
45
|
+
firstPickDoneRef.current = true;
|
|
46
|
+
if (!isControlled)
|
|
47
|
+
setInnerKey(next);
|
|
45
48
|
onSelect?.(next);
|
|
46
|
-
}, [selectedKey, onSelect]);
|
|
49
|
+
}, [selectedKey, isControlled, onSelect]);
|
|
47
50
|
return (_jsx("div", { className: `icon-grid-menu icon-grid-menu--${size}`, children: (categories || []).map((t) => (_jsx(IconGridMenuButton, { category: t, isSelected: selectedKey === t.key, onClick: () => handleClick(t.key) }, t.key))) }));
|
|
48
51
|
}
|
|
@@ -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 || '-' }) },
|
|
@@ -7,6 +7,7 @@ import Table from '../Table';
|
|
|
7
7
|
import Toolbar from '../Toolbar';
|
|
8
8
|
import './ListPopup.css';
|
|
9
9
|
import { 칸순 } from '../../utils/textSort';
|
|
10
|
+
import { cx } from '../../utils/cx';
|
|
10
11
|
/** @public */
|
|
11
12
|
export default function ListPopup(props) {
|
|
12
13
|
const { title, items, columns, gridTemplateColumns, searchPlaceholder, searchFn, defaultSortKey, defaultSortAsc = true, selectedId, selectedIds, onSelect, onClose, pageSizeOptions = [10, 25, 50, 100], initialPageSize, emptyText, countLabel, topContent, className = '', } = props;
|
|
@@ -104,7 +105,8 @@ export default function ListPopup(props) {
|
|
|
104
105
|
content: (col.render
|
|
105
106
|
? col.render(it)
|
|
106
107
|
: (it[col.key] ?? '')),
|
|
107
|
-
className
|
|
108
|
+
// 고른 행에서도 열 className(정렬·폭 등)을 지킨다 — 전에는 고른 표시가 그 자리를 덮어 고른 행만 칸 모양이 달라졌다
|
|
109
|
+
className: cx(col.className, isSelected && 'popup-list__row--selected'),
|
|
108
110
|
}));
|
|
109
111
|
}, empty: resolvedEmptyText })] }));
|
|
110
112
|
}
|
|
@@ -31,7 +31,7 @@ function TableThumbnail({ table }) {
|
|
|
31
31
|
const item = table.items[i];
|
|
32
32
|
if (!item)
|
|
33
33
|
return _jsx("div", { className: "load-table-popup__thumb-cell load-table-popup__thumb-cell--empty" }, i);
|
|
34
|
-
return (_jsx("div", { className: "load-table-popup__thumb-cell", children: item.words.length >= 2 ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "load-table-popup__thumb-word body-3", children: item.words[0].label }), _jsx("span", { className: "load-table-popup__thumb-divider" }), _jsx("span", { className: "load-table-popup__thumb-word body-3", children: item.words[1].label })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "load-table-popup__thumb-word body-3", children: item.words[0]?.label ?? item.id }), item.words[0]?.subLabel && (_jsx("span", { className: "load-table-popup__thumb-sub", children: item.words[0].subLabel }))] })) }, i));
|
|
34
|
+
return (_jsx("div", { className: "load-table-popup__thumb-cell", children: (item.words ?? []).length >= 2 ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "load-table-popup__thumb-word body-3", children: item.words[0].label }), _jsx("span", { className: "load-table-popup__thumb-divider" }), _jsx("span", { className: "load-table-popup__thumb-word body-3", children: item.words[1].label })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "load-table-popup__thumb-word body-3", children: item.words?.[0]?.label ?? item.id }), item.words?.[0]?.subLabel && (_jsx("span", { className: "load-table-popup__thumb-sub", children: item.words[0].subLabel }))] })) }, i));
|
|
35
35
|
}) }));
|
|
36
36
|
}
|
|
37
37
|
const DeleteButton = ({ onClick, ariaLabel }) => {
|
|
@@ -51,10 +51,11 @@ export default function LoadTablePopup({ onLoad, onCancel, tables: tablesProp, o
|
|
|
51
51
|
const [viewMode, setViewMode] = useState('grid');
|
|
52
52
|
const sorted = useMemo(() => {
|
|
53
53
|
const copy = [...tables];
|
|
54
|
+
// 저장소에서 온 표는 어긋나 있을 수 있다(savedAt 없음) — 한 표 때문에 팝업 전체가 렌더 중에 터지던 것
|
|
54
55
|
if (sortKey === 'newest')
|
|
55
|
-
return copy.sort((a, b) => b.savedAt.localeCompare(a.savedAt));
|
|
56
|
+
return copy.sort((a, b) => String(b.savedAt ?? '').localeCompare(String(a.savedAt ?? '')));
|
|
56
57
|
if (sortKey === 'oldest')
|
|
57
|
-
return copy.sort((a, b) => a.savedAt.localeCompare(b.savedAt));
|
|
58
|
+
return copy.sort((a, b) => String(a.savedAt ?? '').localeCompare(String(b.savedAt ?? '')));
|
|
58
59
|
if (sortKey === 'name')
|
|
59
60
|
return copy.sort((a, b) => a.name.localeCompare(b.name, 'ko'));
|
|
60
61
|
if (sortKey === 'count')
|
|
@@ -81,7 +82,7 @@ export default function LoadTablePopup({ onLoad, onCancel, tables: tablesProp, o
|
|
|
81
82
|
animateThen(() => onLoad(table));
|
|
82
83
|
}, [tables, selected, onLoad]);
|
|
83
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]);
|
|
84
|
-
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", disabled: !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 ", table.items.length,
|
|
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", disabled: !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
|
|
85
86
|
? `${item.words[0].label} · ${item.words[1].label}`
|
|
86
|
-
: item.words[0]?.label ?? item.id }, i))) })] }),
|
|
87
|
+
: 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))) }))] })) }) }));
|
|
87
88
|
}
|